Worked on semantics

This commit is contained in:
Kyler Olsen 2025-02-11 00:44:21 -07:00
parent 38ee57547d
commit 7e33f6294c
1 changed files with 672 additions and 82 deletions

View File

@ -3,7 +3,7 @@
from enum import Enum
import math
from typing import ClassVar, Sequence
from typing import Any, Callable, ClassVar, Iterable, Sequence
from textwrap import indent
@ -508,16 +508,20 @@ class Expression:
class LiteralExpression(Expression):
_file_info: FileInfo
_value: NumberLiteral | Punctuation
_value: NumberLiteral | Punctuation | Identifier
def __init__(
self,
file_info: FileInfo,
value: NumberLiteral | Punctuation,
value: NumberLiteral | Punctuation | Identifier,
):
self._file_info = file_info
self._value = value
@property
def value(self) -> NumberLiteral | Punctuation | Identifier:
return self._value
def has_pi(self) -> bool: return self._value.value == 'π'
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
@ -546,6 +550,12 @@ class UnaryExpression(Expression):
self._expression = expression
self._operator = operator
@property
def expression(self) -> Expression: return self._expression
@property
def operator(self) -> UnaryOperator: return self._operator
def has_pi(self) -> bool: return self._expression.has_pi()
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
@ -583,6 +593,15 @@ class BinaryExpression(Expression):
self._expression2 = expression2
self._operator = operator
@property
def expression1(self) -> Expression: return self._expression1
@property
def expression2(self) -> Expression: return self._expression2
@property
def operator(self) -> BinaryOperator: return self._operator
def has_pi(self) -> bool:
return self._expression1.has_pi() or self._expression2.has_pi()
@ -626,6 +645,12 @@ class FunctionCall(Expression):
self._identifier = identifier
self._arguments = arguments
@property
def identifier(self) -> Identifier: return self._identifier
@property
def arguments(self) -> list[Expression]: return self._arguments[:]
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
s: str = f"{pre} Function Call ({self._identifier.value})\n"
for arg in self._arguments[:-1]:
@ -653,6 +678,12 @@ class Constant:
@property
def file_info(self) -> FileInfo: return self._file_info
@property
def identifier(self) -> Identifier: return self._identifier
@property
def expression(self) -> Expression: return self._expression
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
s: str = f"{pre} Constant ({self._identifier.value})\n"
s += self._expression.tree_str(f"{pre_cont}└─", f"{pre_cont} ")
@ -1687,65 +1718,440 @@ class DuplicateScreen(SemanticError):
)
class ContextOperator(Enum):
NoOp = "0"
Identifier = "i"
FunctionCall = "f"
Negate = "n"
Factorial = "!"
Exponential = "^"
Division = "/"
Modulus = "%"
Multiplication = "*"
Subtraction = "-"
Addition = "+"
class UndefinedVariableIdentifier(SemanticError):
def __init__(self, name: str, file_info: FileInfo):
super().__init__(
f"Undefined Variable {name}.",
file_info,
)
class UndefinedFunctionIdentifier(SemanticError):
def __init__(self, name: str, file_info: FileInfo):
super().__init__(
f"Undefined Function {name}.",
file_info,
)
class DivideByZeroError(SemanticError):
def __init__(self, file_info: FileInfo, file_info_context: FileInfo):
super().__init__(
"Cannot divide by zero.",
file_info,
file_info_context,
)
class ParameterCountError(SemanticError):
def __init__(
self,
name: str,
expected: int,
found: int,
file_info: FileInfo,
file_info_context: FileInfo | None,
):
super().__init__(
f"Function {name} expects {expected} parameters but was given "
f"{found} parameters.",
file_info,
file_info_context,
)
class FunctionDomainError(SemanticError):
def __init__(
self,
arg: int | float,
domain: str,
file_info: FileInfo,
file_info_context: FileInfo,
):
super().__init__(
f"Argument ({arg}) outside of function domain: {domain}.",
file_info,
file_info_context,
)
class UnderDefinedConstantDefinition(SemanticError):
def __init__(self, file_info: FileInfo, file_info_context: FileInfo | None):
super().__init__(
"Under-Defined Constant Definition.",
file_info,
file_info_context,
)
class GraphRuntimeError(CompilerError):
_compiler_error_type = "Runtime"
class UndefinedVariableIdentifierRuntime(GraphRuntimeError):
def __init__(self, name: str, file_info: FileInfo):
super().__init__(
f"Undefined Variable {name}.",
file_info,
)
class UndefinedFunctionIdentifierRuntime(GraphRuntimeError):
def __init__(self, name: str, file_info: FileInfo):
super().__init__(
f"Undefined Function {name}.",
file_info,
)
class DivideByZeroErrorRuntime(GraphRuntimeError):
def __init__(self, file_info: FileInfo, file_info_context: FileInfo):
super().__init__(
"Cannot divide by zero.",
file_info,
file_info_context,
)
class ParameterCountErrorRuntime(GraphRuntimeError):
def __init__(
self,
name: str,
expected: int,
found: int,
file_info: FileInfo,
file_info_context: FileInfo | None,
):
super().__init__(
f"Function {name} expects {expected} parameters but was given "
f"{found} parameters.",
file_info,
file_info_context,
)
class FunctionDomainErrorRuntime(GraphRuntimeError):
def __init__(
self,
arg: int | float,
domain: str,
file_info: FileInfo,
file_info_context: FileInfo,
):
super().__init__(
f"Argument ({arg}) outside of function domain: {domain}.",
file_info,
file_info_context,
)
class ContextExpression:
_context: "Context | ContextFunction"
_value: None | int | float
_name: None | str
_operator: ContextOperator
_expressions: "list[ContextExpression]"
_file_info: FileInfo
@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:
def file_info(self) -> FileInfo: return self._file_info
def value(self, context: "Context") -> int | float:
raise RuntimeError
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
s: str = f"{pre} Context Expression\n"
return s
class ContextLiteral(ContextExpression):
_file_info: FileInfo
_value: str
def __init__(
self,
file_info: FileInfo,
value: str,
):
self._file_info = file_info
self._value = value
def value(self, context: "Context") -> int | float:
if self._value == 'π': return math.pi
elif '.' in self._value: return float(self._value)
else: return int(self._value)
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
s: str = f"{pre} Context Literal ({self._value})\n"
return s
class ContextVariable(ContextExpression):
_file_info: FileInfo
_name: str
def __init__(
self,
file_info: FileInfo,
name: str,
):
self._file_info = file_info
self._name = name
@property
def name(self) -> str: return self._name
def value(self, context: "Context") -> int | float:
return context.lookup_var(self)
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
s: str = f"{pre} Context Variable ({self.name})\n"
return s
class ContextUnaryExpression(ContextExpression):
_file_info: FileInfo
_expression: ContextExpression
_operator: UnaryOperator
def __init__(
self,
file_info: FileInfo,
expression: ContextExpression,
operator: UnaryOperator,
):
self._file_info = file_info
self._expression = expression
self._operator = operator
@property
def expression(self) -> ContextExpression: return self._expression
@property
def operator(self) -> UnaryOperator: return self._operator
def value(self, context: "Context") -> int | float:
match self.operator:
case UnaryOperator.Negate:
return - self.expression.value(context)
case UnaryOperator.Factorial:
return math.factorial(
math.floor(self.expression.value(context)))
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
s: str = f"{pre} Context Unary Expression ({self._operator})\n"
s += self._expression.tree_str(f"{pre_cont}└─", f"{pre_cont} ")
return s
class ContextBinaryExpression(ContextExpression):
_file_info: FileInfo
_expression1: ContextExpression
_expression2: ContextExpression
_operator: BinaryOperator
def __init__(
self,
file_info: FileInfo,
expression1: ContextExpression,
expression2: ContextExpression,
operator: BinaryOperator,
):
self._file_info = file_info
self._expression1 = expression1
self._expression2 = expression2
self._operator = operator
@property
def expression1(self) -> ContextExpression: return self._expression1
@property
def expression2(self) -> ContextExpression: return self._expression2
@property
def operator(self) -> BinaryOperator: return self._operator
def value(self, context: "Context") -> int | float:
a, b = self.expression1.value(context), self.expression2.value(context)
match self.operator:
case BinaryOperator.Exponential:
return a ** b
case ContextOperator.Division:
case BinaryOperator.Subscript:
raise GraphRuntimeError("ERROR", self.file_info)
case BinaryOperator.Division:
if b == 0:
raise DivideByZeroErrorRuntime(
self.expression2.file_info,
self.file_info,
)
return a / b
case ContextOperator.Modulus:
case BinaryOperator.Modulus:
if b == 0:
raise DivideByZeroErrorRuntime(
self.expression2.file_info,
self.file_info,
)
return a % b
case ContextOperator.Multiplication:
case BinaryOperator.Multiplication:
return a * b
case ContextOperator.Subtraction:
case BinaryOperator.Subtraction:
return a - b
case ContextOperator.Addition:
case BinaryOperator.Addition:
return a + b
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
s: str = f"{pre} Context Binary Expression ({self._operator})\n"
s += self._expression1.tree_str(f"{pre_cont}├─", f"{pre_cont}")
s += self._expression2.tree_str(f"{pre_cont}└─", f"{pre_cont} ")
return s
class ContextFunctionDomain:
_start: int | float
_start_inclusive: bool
_end: int | float
_end_inclusive: bool
def __init__(
self,
start: int | float,
start_inclusive: bool,
end: int | float,
end_inclusive: bool,
):
self._start = start
self._start_inclusive = start_inclusive
self._end = end
self._end_inclusive = end_inclusive
def __str__(self) -> str:
return (
('[' if self._start_inclusive else '(') +
f"{self._start}, {self._end}" +
(']' if self._start_inclusive else ')')
)
def __contains__(self, other: int | float):
return (
(self._start_inclusive and self._start <= other) or
((not self._start_inclusive) and self._start < other) or
(self._end_inclusive and self._end >= other) or
((not self._end_inclusive) and self._end > other)
)
class ContextFunctionCallable:
_parameters: int
_function: Callable
_domain: list[ContextFunctionDomain | None]
def __init__(
self,
parameters: int,
function: Callable,
domain: list[ContextFunctionDomain | None] | None = None,
):
self._parameters = parameters
self._function = function
self._domain = domain or []
@property
def parameters(self) -> int: return self._parameters
def check_domain(self, args: Iterable[int | float]) -> tuple[int, str]:
for i, (d, a) in enumerate(zip(self._domain, args)):
if d is not None and not (a in d): return i, str(d)
return -1, ''
def __call__(self, *args) -> int | float:
return self._function(*args)
class ContextFunctionCall(ContextExpression):
_file_info: FileInfo
_identifier: str
_identifier_file_info: FileInfo
_arguments: list[ContextExpression]
def __init__(
self,
file_info: FileInfo,
identifier: str,
identifier_file_info: FileInfo,
arguments: list[ContextExpression],
):
self._file_info = file_info
self._identifier = identifier
self._identifier_file_info = identifier_file_info
self._arguments = arguments
@property
def identifier(self) -> str: return self._identifier
@property
def identifier_file_info(self) -> FileInfo:
return self._identifier_file_info
@property
def arguments(self) -> list[ContextExpression]: return self._arguments[:]
def value(self, context: "Context") -> int | float:
args = [a.value(context) for a in self._arguments]
func = context.lookup_func(self)
if len(args) > func.parameters:
raise ParameterCountErrorRuntime(
self.identifier,
func.parameters,
len(args),
self._arguments[func.parameters].file_info,
self.file_info,
)
elif len(args) < func.parameters:
raise ParameterCountErrorRuntime(
self.identifier,
func.parameters,
len(args),
self.identifier_file_info,
self.file_info,
)
else:
bad_arg, domain = func.check_domain(args)
if bad_arg > 0:
raise FunctionDomainErrorRuntime(
args[bad_arg],
domain,
self._arguments[bad_arg].file_info,
self.file_info,
)
return func(*args)
def tree_str(self, pre: str = "", pre_cont: str = "") -> str:
s: str = f"{pre} Context Function Call ({self.identifier})\n"
for arg in self._arguments[:-1]:
s += arg.tree_str(f"{pre_cont}├─", f"{pre_cont}")
s += self._arguments[-1].tree_str(f"{pre_cont}└─", f"{pre_cont} ")
return s
class ContextFunction:
@ -1757,8 +2163,8 @@ class ContextFunction:
def lookup(self, value: str) -> int | float:
if value in self._values:
return self._values[value].value
else: return self._context.lookup(value)
return self._values[value].value(context)
else: return self._context.lookup_var(value)
def call(self, value: str, args: list[ContextExpression]) -> int | float:
return self._context.call(value, args)
@ -1775,45 +2181,43 @@ class ContextAnimation:
_direction: AnimationDirection
_reversed: bool
def __iter__(self): return self
def __next__(self):
def step(self, context: "Context"):
if self._direction == AnimationDirection.Increase or (
self._direction == AnimationDirection.Bounce and not self._reversed
):
self._current += self._step.value
self._current += self._step.value(context)
if self._range_end_inclusive:
if self._current > self._range_end.value:
self._current = self._range_start.value
if self._current > self._range_end.value(context):
self._current = self._range_start.value(context)
if self._direction == AnimationDirection.Bounce:
self._reversed = True
if not self._range_start_inclusive:
self._current += self._step.value
self._current += self._step.value(context)
else:
if self._current >= self._range_end.value:
self._current = self._range_start.value
if self._current >= self._range_end.value(context):
self._current = self._range_start.value(context)
if self._direction == AnimationDirection.Bounce:
self._reversed = True
if not self._range_start_inclusive:
self._current += self._step.value
self._current += self._step.value(context)
if self._direction == AnimationDirection.Decrease or (
self._direction == AnimationDirection.Bounce and self._reversed
):
self._current -= self._step.value
self._current -= self._step.value(context)
if self._range_start_inclusive:
if self._current < self._range_start.value:
self._current = self._range_end.value
if self._current < self._range_start.value(context):
self._current = self._range_end.value(context)
if self._direction == AnimationDirection.Bounce:
self._reversed = False
if not self._range_end_inclusive:
self._current -= self._step.value
self._current -= self._step.value(context)
else:
if self._current <= self._range_start.value:
self._current = self._range_end.value
if self._current <= self._range_start.value(context):
self._current = self._range_end.value(context)
if self._direction == AnimationDirection.Bounce:
self._reversed = False
if not self._range_end_inclusive:
self._current -= self._step.value
self._current -= self._step.value(context)
return self.value
@property
@ -1851,7 +2255,7 @@ class ContextGraph:
def __iter__(self): return self
def __next__(self) -> tuple[
def step(self, context: "Context") -> tuple[
int | float,
int | float,
int | float,
@ -1903,15 +2307,15 @@ class ContextGraph:
def color_space(self) -> ColorSpace:
if isinstance(self._color_red, ContextFunction):
return ColorSpace.RGB
elif isinstance(self._color_green, ContextFunction):
elif isinstance(self._color_green, ContextFunctionCallable):
return ColorSpace.RGB
elif isinstance(self._color_blue, ContextFunction):
elif isinstance(self._color_blue, ContextFunctionCallable):
return ColorSpace.RGB
elif isinstance(self._color_hue, ContextFunction):
elif isinstance(self._color_hue, ContextFunctionCallable):
return ColorSpace.HSL
elif isinstance(self._color_saturation, ContextFunction):
elif isinstance(self._color_saturation, ContextFunctionCallable):
return ColorSpace.HSL
elif isinstance(self._color_luminosity, ContextFunction):
elif isinstance(self._color_luminosity, ContextFunctionCallable):
return ColorSpace.HSL
else:
return ColorSpace.Grey
@ -1969,6 +2373,7 @@ funcs_1 = {
class Context:
_constants: dict[str, int | float]
_functions: dict[str, ContextFunctionCallable]
_animations: dict[str, ContextAnimation]
_graphs: list[ContextGraph]
@ -1976,18 +2381,195 @@ class Context:
for anim in self._animations.values():
next(anim)
def lookup(self, value: str) -> int | float:
return self._constants[value]
def lookup_var(self, variable: ContextVariable) -> int | float:
if variable.name not in self._constants:
raise UndefinedVariableIdentifierRuntime(
variable.name, variable.file_info)
return self._constants[variable.name]
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
def lookup_func(self, function: ContextFunctionCall) -> ContextFunctionCallable:
if function.identifier not in self._functions:
raise UndefinedFunctionIdentifierRuntime(
function.identifier, function.file_info)
return self._functions[function.identifier]
def _simplify_expression(
expression: Expression,
constants: dict[str, ContextLiteral],
functions: dict[str, ContextFunctionCallable],
constant: bool = False,
) -> ContextExpression:
if isinstance(expression, LiteralExpression):
if isinstance(expression.value, (Punctuation, Identifier)):
if expression.value.value in ['π',]:
return ContextLiteral(
expression.file_info, expression.value.value)
elif expression.value.value in constants:
return constants[expression.value.value]
elif constant:
raise UndefinedVariableIdentifier(
expression.value.value,
expression.file_info
)
else:
return ContextVariable(
expression.file_info, expression.value.value)
else:
return ContextLiteral(
expression.file_info, expression.value.value)
elif isinstance(expression, UnaryExpression):
value = _simplify_expression(
expression.expression, constants, functions, constant)
if isinstance(value, ContextLiteral):
value = float(value._value)
match expression.operator:
case UnaryOperator.Negate:
return ContextLiteral(
expression.file_info, str(-value))
case UnaryOperator.Factorial:
return ContextLiteral(
expression.file_info,
str(math.factorial(math.floor(value))),
)
else:
if constant:
raise UnderDefinedConstantDefinition(
value.file_info,
expression.file_info,
)
return ContextUnaryExpression(
expression.file_info,
value,
expression.operator,
)
elif isinstance(expression, BinaryExpression):
value1 = _simplify_expression(
expression.expression1, constants, functions, constant)
value2 = _simplify_expression(
expression.expression2, constants, functions, constant)
if (
isinstance(value1, ContextLiteral) and
isinstance(value2, ContextLiteral)
):
value1 = float(value1._value)
value2 = float(value2._value)
match expression.operator:
case BinaryOperator.Exponential:
return ContextLiteral(
expression.file_info, str(value1 ** value2))
case BinaryOperator.Subscript:
raise SemanticError("ERROR", expression.file_info)
case BinaryOperator.Division:
if value2 == 0:
raise DivideByZeroError(
expression.expression2.file_info,
expression.file_info,
)
return ContextLiteral(
expression.file_info, str(value1 / value2))
case BinaryOperator.Modulus:
if value2 == 0:
raise DivideByZeroError(
expression.expression2.file_info,
expression.file_info,
)
return ContextLiteral(
expression.file_info, str(value1 % value2))
case BinaryOperator.Multiplication:
return ContextLiteral(
expression.file_info, str(value1 * value2))
case BinaryOperator.Subtraction:
return ContextLiteral(
expression.file_info, str(value1 - value2))
case BinaryOperator.Addition:
return ContextLiteral(
expression.file_info, str(value1 + value2))
elif constant and not isinstance(value1, ContextLiteral):
raise UnderDefinedConstantDefinition(
value1.file_info,
expression.file_info,
)
elif constant and not isinstance(value2, ContextLiteral):
raise UnderDefinedConstantDefinition(
value2.file_info,
expression.file_info,
)
else:
return ContextBinaryExpression(
expression.file_info,
value1,
value2,
expression.operator,
)
elif isinstance(expression, FunctionCall):
args = [_simplify_expression(a, constants, functions, constant)
for a in expression.arguments]
if expression.identifier.value not in functions:
raise UndefinedFunctionIdentifier(
expression.identifier.value,
expression.identifier.file_info,
)
func = functions[expression.identifier.value]
if len(args) > func.parameters:
raise ParameterCountError(
expression.identifier.value,
func.parameters,
len(args),
expression.arguments[func.parameters].file_info,
expression.file_info,
)
elif len(args) < func.parameters:
raise ParameterCountError(
expression.identifier.value,
func.parameters,
len(args),
expression.identifier.file_info,
expression.file_info,
)
else:
if all(isinstance(a, ContextLiteral) for a in args):
args = [a._value for a in args] # type: ignore
bad_arg, domain = func.check_domain(args)
if bad_arg > 0:
raise FunctionDomainError(
args[bad_arg],
domain,
expression.arguments[bad_arg].file_info,
expression.file_info,
)
return ContextLiteral(
expression.file_info, str(func(*args)))
elif constant:
bad_arg = [isinstance(a, ContextLiteral) for a in args]\
.index(False)
raise UnderDefinedConstantDefinition(
expression.arguments[bad_arg].file_info,
expression.file_info,
)
else:
return ContextFunctionCall(
expression.file_info,
expression.identifier.value,
expression.identifier.file_info,
args,
)
else: raise SemanticError("Expression Error", expression.file_info)
def semantics_analyzer(file: File) -> Context:
screen: Screen | None = None
constraints: dict[str, int | float] = {}
constants: dict[str, ContextLiteral] = {}
functions: dict[str, ContextFunctionCallable] = {
"sin": ContextFunctionCallable(1, math.sin),
"asin": ContextFunctionCallable(1, math.asin),
"cos": ContextFunctionCallable(1, math.cos),
"acos": ContextFunctionCallable(1, math.acos),
"tan": ContextFunctionCallable(1, math.tan),
"atan": ContextFunctionCallable(1, math.atan),
"ln": ContextFunctionCallable(1, math.log),
"log": ContextFunctionCallable(1, math.log10),
}
animations: dict[str, ContextAnimation] = {}
graphs: list[ContextGraph] = []
for child in file.children:
@ -1996,7 +2578,15 @@ def semantics_analyzer(file: File) -> Context:
raise DuplicateScreen(child.file_info)
screen = child
elif isinstance(child, Constant):
pass
value = _simplify_expression(
child.expression,
constants,
functions,
True,
)
if not isinstance(value, ContextLiteral):
raise UnderDefinedConstantDefinition(child.file_info, None)
constants[child.identifier.value] = value
elif isinstance(child, Animation):
pass
elif isinstance(child, Graph):