Add semantic analysis classes and context handling for expressions and graphs

This commit is contained in:
Kyler Olsen 2025-02-10 17:29:20 -07:00
parent 3d07510cf7
commit e2f37553df
1 changed files with 305 additions and 0 deletions

View File

@ -2,6 +2,7 @@
# Feb 2024
from enum import Enum
import math
from typing import ClassVar, Sequence
from textwrap import indent
@ -1668,6 +1669,310 @@ def _expression_sa(tokens: list[Token]) -> Expression:
def syntactical_analyzer(tokens: Sequence[Token]) -> File:
return File._sa(list(tokens))
# -- Semantics --
# class SemanticsError(CompilerError):
class SemanticsError(Exception):
_compiler_error_type = "Semantics"
class ContextOperator(Enum):
NoOp = "0"
Identifier = "i"
FunctionCall = "f"
Negate = "n"
Factorial = "!"
Exponential = "^"
Division = "/"
Modulus = "%"
Multiplication = "*"
Subtraction = "-"
Addition = "+"
class ContextExpression:
_context: "Context | ContextFunction"
_value: None | int | float
_name: None | str
_operator: ContextOperator
_expressions: "list[ContextExpression]"
@property
def value(self) -> int | float:
a = 0 if len(self._expressions) > 0 else self._expressions[0].value
b = 0 if len(self._expressions) > 1 else self._expressions[1].value
match self._operator:
case ContextOperator.NoOp:
return self._value or 0
case ContextOperator.Identifier:
if self._name is not None:
return self._context.lookup(self._name)
else: raise Exception
case ContextOperator.FunctionCall:
if self._name is not None:
return self._context.call(self._name, self._expressions)
else: raise Exception
case ContextOperator.Negate:
if len(self._expressions) > 0:
return (0 if self._value is None else -self._value)
else: return -self._expressions[0].value
case ContextOperator.Factorial:
if len(self._expressions) > 0:
if self._value is None: return 1
else: return math.factorial(int(self._value))
else: return math.factorial(int(self._expressions[0].value))
case ContextOperator.Exponential:
return a ** b
case ContextOperator.Division:
return a / b
case ContextOperator.Modulus:
return a % b
case ContextOperator.Multiplication:
return a * b
case ContextOperator.Subtraction:
return a - b
case ContextOperator.Addition:
return a + b
class ContextFunction:
_context: "Context"
_values: "dict[str, ContextAnimation | ContextExpression]"
@property
def value(self) -> int | float: pass
def lookup(self, value: str) -> int | float:
if value in self._values:
return self._values[value].value
else: return self._context.lookup(value)
def call(self, value: str, args: list[ContextExpression]) -> int | float:
return self._context.call(value, args)
class ContextAnimation:
_current: int | float
_range_start: ContextExpression
_range_start_inclusive: bool
_range_end: ContextExpression
_range_end_inclusive: bool
_step: ContextExpression
_direction: AnimationDirection
_reversed: bool
def __iter__(self): return self
def __next__(self):
if self._direction == AnimationDirection.Increase or (
self._direction == AnimationDirection.Bounce and not self._reversed
):
self._current += self._step.value
if self._range_end_inclusive:
if self._current > self._range_end.value:
self._current = self._range_start.value
if self._direction == AnimationDirection.Bounce:
self._reversed = True
if not self._range_start_inclusive:
self._current += self._step.value
else:
if self._current >= self._range_end.value:
self._current = self._range_start.value
if self._direction == AnimationDirection.Bounce:
self._reversed = True
if not self._range_start_inclusive:
self._current += self._step.value
if self._direction == AnimationDirection.Decrease or (
self._direction == AnimationDirection.Bounce and self._reversed
):
self._current -= self._step.value
if self._range_start_inclusive:
if self._current < self._range_start.value:
self._current = self._range_end.value
if self._direction == AnimationDirection.Bounce:
self._reversed = False
if not self._range_end_inclusive:
self._current -= self._step.value
else:
if self._current <= self._range_start.value:
self._current = self._range_end.value
if self._direction == AnimationDirection.Bounce:
self._reversed = False
if not self._range_end_inclusive:
self._current -= self._step.value
return self.value
@property
def value(self) -> int | float: return self._current
class GraphType(Enum):
X_Independent = "x independent"
Y_Independent = "y independent"
Parametric = "parametric"
Polar = "polar"
class ColorSpace(Enum):
Grey = "Grey Scale"
RGB = "RGB"
HSL = "HSL"
class ContextGraph:
_x: None | ContextFunction | ContextAnimation
_y: None | ContextFunction | ContextAnimation
_t: None | ContextAnimation
_r: None | ContextFunction
_theta: None | ContextAnimation
_color_alpha: None | ContextFunction
_color_grey: None | ContextFunction
_color_red: None | ContextFunction
_color_green: None | ContextFunction
_color_blue: None | ContextFunction
_color_hue: None | ContextFunction
_color_saturation: None | ContextFunction
_color_luminosity: None | ContextFunction
def __iter__(self): return self
def __next__(self) -> tuple[
int | float,
int | float,
int | float,
int | float,
tuple[float,float,float,float,],
]:
match self.graph_type:
case GraphType.X_Independent:
x_last = self._x.value # type: ignore
y_last = self._y.value # type: ignore
x = next(self._x) # type: ignore
y = self._y.value # type: ignore
case GraphType.Y_Independent:
y_last = self._y.value # type: ignore
x_last = self._x.value # type: ignore
y = next(self._y) # type: ignore
x = self._x.value # type: ignore
case GraphType.Parametric:
x_last = self._x.value # type: ignore
y_last = self._y.value # type: ignore
next(self._t) # type: ignore
x = self._x.value # type: ignore
y = self._y.value # type: ignore
case GraphType.Polar:
theta_last = self._theta.value # type: ignore
r_last = self._r.value # type: ignore
theta = next(self._theta) # type: ignore
r = self._r.value # type: ignore
return x_last, y_last, x, y, self.color
@property
def graph_type(self) -> GraphType:
if isinstance(self._x, ContextAnimation):
return GraphType.X_Independent
elif isinstance(self._y, ContextAnimation):
return GraphType.Y_Independent
elif isinstance(self._t, ContextAnimation):
return GraphType.Parametric
elif isinstance(self._theta, ContextAnimation):
return GraphType.Polar
else:
raise SemanticsError("Invalid Function Type.")
@property
def color_space(self) -> ColorSpace:
if isinstance(self._color_red, ContextFunction):
return ColorSpace.RGB
elif isinstance(self._color_green, ContextFunction):
return ColorSpace.RGB
elif isinstance(self._color_blue, ContextFunction):
return ColorSpace.RGB
elif isinstance(self._color_hue, ContextFunction):
return ColorSpace.HSL
elif isinstance(self._color_saturation, ContextFunction):
return ColorSpace.HSL
elif isinstance(self._color_luminosity, ContextFunction):
return ColorSpace.HSL
else:
return ColorSpace.Grey
@property
def color(self) -> tuple[float,float,float,float,]:
if self._color_alpha is not None:
a = self._color_alpha.value
else: a = 1
match self.color_space:
case ColorSpace.Grey:
if self._color_grey is not None:
c = self._color_grey.value
else: c = 1
r, g, b = c, c, c
case ColorSpace.RGB:
if self._color_red is not None:
r = self._color_red.value
else: r = 1
if self._color_green is not None:
g = self._color_green.value
else: g = 1
if self._color_blue is not None:
b = self._color_blue.value
else: b = 1
case ColorSpace.HSL:
r,g,b = 1,1,1
return r, g, b, a
@property
def value(self) -> int | float:
match self.graph_type:
case GraphType.X_Independent:
return self._x.value # type: ignore
case GraphType.Y_Independent:
return self._y.value # type: ignore
case GraphType.Parametric:
return self._t.value # type: ignore
case GraphType.Polar:
return self._theta.value # type: ignore
funcs_1 = {
"sin": math.sin,
"asin": math.asin,
"cos": math.cos,
"acos": math.acos,
"tan": math.tan,
"atan": math.atan,
"ln": math.log,
"log": math.log10,
}
class Context:
_constants: dict[str, int | float]
_animations: dict[str, ContextAnimation]
_graphs: list[ContextGraph]
def step(self):
for anim in self._animations.values():
next(anim)
def lookup(self, value: str) -> int | float:
return self._constants[value]
def call(self, value: str, args: list[ContextExpression]) -> int | float:
if value in funcs_1 and len(args) == 1:
return funcs_1[value](args[0].value)
raise KeyError
if __name__ == '__main__':
try:
with open("example.graph", encoding='utf-8') as file: