# Kyler Olsen # Feb 2024 # Feb 2025 from enum import Enum import math from typing import Callable, ClassVar, Iterable, Sequence from textwrap import indent class FileInfo: _filename: str _line: int _col: int _length: int _lines: int def __init__( self, filename: str, line: int, col: int, length: int, lines: int = 0, ): self._filename = filename self._line = line self._col = col self._length = length self._lines = lines def __repr__(self) -> str: return ( f"{type(self).__name__}" f"('{self._filename}',{self._line},{self._col},{self._length})" ) def __str__(self) -> str: return f"Ln {self.line}, Col {self.col} in file {self.filename}" def __add__(self, other: "FileInfo") -> "FileInfo": filename = self.filename line = self.line col = self.col if self.line != other.line: if other.lines == 0: length = other.col + other.length else: length = other.length lines = other.line - self.line else: length = (other.col + other.length) - col lines = 0 return FileInfo( filename, line, col, length, lines, ) @property def filename(self) -> str: return self._filename @property def line(self) -> int: return self._line @property def col(self) -> int: return self._col @property def length(self) -> int: return self._length @property def lines(self) -> int: return self._lines class CompilerError(Exception): _compiler_error_type = "Compiler" def __init__( self, message: str, file_info: FileInfo, file_info_context: FileInfo | None = None, ): new_message = message new_message += ( f"\nIn file {file_info.filename} at line {file_info.line} " ) if file_info_context is not None and file_info_context.lines: file_info_context = None if file_info.lines: new_message += f"to line {file_info.line + file_info.lines}" with open(file_info.filename, 'r', encoding='utf-8') as file: new_message += ''.join( file.readlines()[ file_info.line-1:file_info.line + file_info.lines]) else: new_message += f"col {file_info.col}\n\n" with open(file_info.filename, 'r', encoding='utf-8') as file: new_message += file.readlines()[file_info.line-1] if file_info_context is not None: context_line = [' '] * max( file_info.col + file_info.length, file_info_context.col +file_info_context.length, ) for i in range( file_info_context.col - 1, file_info_context.col + file_info_context.length ): context_line[i] = '~' for i in range( file_info.col - 1, file_info.col + file_info.length ): context_line[i] = '^' new_message += ''.join(context_line) else: new_message += ' ' * ( file_info.col - 1) + '^' * file_info.length super().__init__(new_message) def compiler_error(self) -> str: return ( f"[{self._compiler_error_type} Error] {type(self).__name__}:\n" f"{indent(str(self), ' |', lambda _: True)}" ) # -- Lexical -- class LexerError(CompilerError): _compiler_error_type = "Lexical" class _InterTokenType(Enum): Generic = 'Generic' Comment = 'Comment' Word = 'Word' NumberLiteral = 'NumberLiteral' Punctuation = 'Punctuation' class _NumberLiteralType(Enum): Number = 'Number' Real = 'Real' Exp = 'Exp' _OnlyNewLineTerminatedTokens = ( _InterTokenType.Comment, ) _NewLineTerminatedTokens = _OnlyNewLineTerminatedTokens + ( _InterTokenType.Word, _InterTokenType.NumberLiteral, _InterTokenType.Punctuation, ) _ID_Start = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" _ID_Continue = _ID_Start# + "0123456789" _Keywords = ( 'screen', 'graph', 'anim', 'const', ) _Num_Start = "0123456789" _Num_Start_Next = { _NumberLiteralType.Number: { '.': _NumberLiteralType.Real, }, } _Num_Continue = { _NumberLiteralType.Number: _Num_Start + ".eE_", _NumberLiteralType.Real: _Num_Start + "eE_", _NumberLiteralType.Exp: _Num_Start + "_", } _Num_Continue_Next = { _NumberLiteralType.Number: { '.': _NumberLiteralType.Real, 'e': _NumberLiteralType.Exp, 'E': _NumberLiteralType.Exp, }, _NumberLiteralType.Real: { 'e': _NumberLiteralType.Exp, 'E': _NumberLiteralType.Exp, }, } _Punctuation_Any = "_+-*/%^<>=!{[(}]),;:∑∏∞≠≤≥∫αβπ→" _Punctuation = ( "+", "-", "*", "/", "%", "^", "=", "!", "<", "<=", ">", ">=", "{", "}", "[", "]", "(", ")", "_", "->", ",", ";", ":", "∑", "∏", "∞", "≠", "≤", "≥", "∫", "α", "β", "θ", "π", "→", ) _Punctuation_Conversion = { "<=": "≤", ">=": "≥", "->": "→", } _Punctuation_Enclosing = { '(':')', ')':'(', '[':']', ']':'[', '{':'}', '}':'{', } _ID_Conversion = { "sum": "∑", "pi": "π", "alpha": "α", "beta": "β", "inf": "∞", "product": "∏", "integral": "∫", "theta": "θ", } class Token: _type: ClassVar[str] = 'Generic' _value: str _file_info: FileInfo def __init__(self, value: str, file_info: FileInfo): self._value = value self._file_info = file_info def __str__(self) -> str: return f"Type: {self._type}, Value: {self.value}" @property def value(self) -> str: return self._value @property def file_info(self) -> FileInfo: return self._file_info class Identifier(Token): _type = 'Identifier' class Keyword(Token): _type = 'Keyword' class NumberLiteral(Token): _type = 'NumberLiteral' class Punctuation(Token): _type = 'Punctuation' def lexer(file: str, filename: str) -> Sequence[Token]: tokens: list[Token] = [] current: str = "" current_line: int = 0 current_col: int = 0 number_type: _NumberLiteralType = _NumberLiteralType.Number token_type: _InterTokenType = _InterTokenType.Generic for line, line_str in enumerate(file.splitlines()): fi = FileInfo(filename, current_line, current_col, len(current)) if token_type in _NewLineTerminatedTokens: if token_type is _InterTokenType.Word: if len(current) > 15: raise LexerError("Identifier Too Long", fi) if current.lower() in _Keywords: tokens.append(Keyword(current, fi)) elif current.lower() in _ID_Conversion.keys(): tokens.append( Punctuation(_ID_Conversion[current.lower()], fi)) else: tokens.append(Identifier(current, fi)) elif token_type is _InterTokenType.NumberLiteral: tokens.append(NumberLiteral(current, fi)) number_type = _NumberLiteralType.Number elif token_type is _InterTokenType.Punctuation: if current not in _Punctuation: raise LexerError("Invalid Punctuation", fi) if current in _Punctuation_Conversion.keys(): current = _Punctuation_Conversion[current] tokens.append(Punctuation(current, fi)) token_type = _InterTokenType.Generic for col, char in enumerate(line_str): if token_type in _OnlyNewLineTerminatedTokens: current += char elif token_type is _InterTokenType.Word: if char in _ID_Continue: current += char else: fi = FileInfo( filename, current_line, current_col, len(current)) if len(current) > 15: raise LexerError("Identifier Too Long", fi) if current.lower() in _Keywords: tokens.append(Keyword(current, fi)) elif current.lower() in _ID_Conversion.keys(): tokens.append( Punctuation(_ID_Conversion[current.lower()], fi)) else: tokens.append(Identifier(current, fi)) token_type = _InterTokenType.Generic elif token_type is _InterTokenType.NumberLiteral: if ( number_type in _Num_Continue and char in _Num_Continue[number_type] ): current += char if ( number_type in _Num_Continue_Next and char in _Num_Continue_Next[number_type] ): number_type = _Num_Continue_Next[number_type][char] else: fi = FileInfo( filename, current_line, current_col, len(current)) tokens.append(NumberLiteral(current, fi)) number_type = _NumberLiteralType.Number token_type = _InterTokenType.Generic elif token_type is _InterTokenType.Punctuation: if char in _Punctuation_Any and current + char in _Punctuation: current += char else: fi = FileInfo( filename, current_line, current_col, len(current)) if current not in _Punctuation: raise LexerError("Invalid Punctuation", fi) if current in _Punctuation_Conversion.keys(): current = _Punctuation_Conversion[current] tokens.append(Punctuation(current, fi)) token_type = _InterTokenType.Generic if token_type is _InterTokenType.Generic: current = char current_line = line + 1 current_col = col + 1 if char == '#': token_type = _InterTokenType.Comment elif char in _ID_Start: token_type = _InterTokenType.Word elif ( char == '.' and line_str[col+1] in _Num_Continue[_NumberLiteralType.Real] ): token_type = _InterTokenType.NumberLiteral if char in _Num_Start_Next[number_type]: number_type = _Num_Start_Next[number_type][char] elif char in _Num_Start: token_type = _InterTokenType.NumberLiteral if char in _Num_Start_Next[number_type]: number_type = _Num_Start_Next[number_type][char] elif char in _Punctuation_Any: token_type = _InterTokenType.Punctuation fi = FileInfo(filename, current_line, current_col, len(current)) if token_type in _NewLineTerminatedTokens: if token_type is _InterTokenType.Word: if len(current) > 31: raise LexerError("Identifier Too Long", fi) if current.lower() in _Keywords: tokens.append(Keyword(current, fi)) elif current.lower() in _ID_Conversion.keys(): tokens.append(Punctuation(_ID_Conversion[current.lower()], fi)) else: tokens.append(Identifier(current, fi)) elif token_type is _InterTokenType.NumberLiteral: tokens.append(NumberLiteral(current, fi)) number_type = _NumberLiteralType.Number elif token_type is _InterTokenType.Punctuation: if current not in _Punctuation: raise LexerError("Invalid Punctuation", fi) if current in _Punctuation_Conversion.keys(): current = _Punctuation_Conversion[current] tokens.append(Punctuation(current, fi)) token_type = _InterTokenType.Generic return tokens # # -- Syntax -- class SyntaxError(CompilerError): _compiler_error_type = "Syntax" class UnexpectedEndOfTokenStream(SyntaxError): pass class _ExpectedTokenBase(SyntaxError): _token_type = Token def __init__( self, token: Token, expected: str | None = None, found: str | None = None, ): if expected is None: expected = self._token_type.__name__ found = found or type(token).__name__ else: found = found or token.value message = f"Expected '{expected}' but found '{found}'." super().__init__(message, token.file_info) class ExpectedIdentifier(_ExpectedTokenBase): _type_name = Identifier class ExpectedKeyword(_ExpectedTokenBase): _type_name = Keyword class ExpectedNumberLiteral(_ExpectedTokenBase): _type_name = NumberLiteral class ExpectedPunctuation(_ExpectedTokenBase): _type_name = Punctuation class ExpectedLiteral(_ExpectedTokenBase): _type_name = (NumberLiteral, Punctuation) class _UnexpectedTokenBase(_ExpectedTokenBase): def __init__( self, token: Token, expected: str | list[str] | None = None, found: str | None = None, ): if isinstance(expected, list): if len(expected) > 1: s = "" for i in expected[:-1]: s += i + "', '" s = s[:-1] + "or '" + expected[-1] expected = s else: expected = expected[0] super().__init__(token, expected, found) class UnexpectedToken(_UnexpectedTokenBase): def __init__( self, token: Token, expected: str | list[str], found: str | None = None, ): if isinstance(expected, list): if len(expected) > 1: s = "" for i in expected[:-1]: s += i + "', '" s = s[:-1] + "or '" + expected[-1] expected = s found = found or type(token).__name__ super().__init__(token, expected, found) class UnexpectedIdentifier(_UnexpectedTokenBase): _type_name = Identifier class UnexpectedKeyword(_UnexpectedTokenBase): _type_name = Keyword class UnexpectedNumberLiteral(_UnexpectedTokenBase): _type_name = NumberLiteral class UnexpectedPunctuation(_UnexpectedTokenBase): _type_name = Punctuation class ExpressionError(Exception): pass class ExpectedExpression(SyntaxError): def __init__( self, message: str, token: Token, ): super().__init__(message, token.file_info) _Id_Punctuation = [ '∑', 'π', 'α', 'β', '∞', '∏', '∫', 'θ', ] class Expression: _file_info: FileInfo @property def file_info(self) -> FileInfo: return self._file_info def has_pi(self) -> bool: return False def tree_str(self, pre: str = "", pre_cont: str = "") -> str: s: str = f"{pre} Expression\n" return s class LiteralExpression(Expression): _file_info: FileInfo _value: NumberLiteral | Punctuation | Identifier def __init__( self, file_info: FileInfo, 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: s: str = f"{pre} Literal Expression ({self._value.value})\n" return s class UnaryOperator(Enum): Negate = "-" Factorial = "!" class UnaryExpression(Expression): _file_info: FileInfo _expression: Expression _operator: UnaryOperator def __init__( self, file_info: FileInfo, expression: Expression, operator: UnaryOperator, ): self._file_info = file_info 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: s: str = f"{pre} Unary Expression ({self._operator})\n" s += self._expression.tree_str(f"{pre_cont}└─", f"{pre_cont} ") return s class BinaryOperator(Enum): Exponential = "^" Subscript = "_" Division = "/" Modulus = "%" Multiplication = "*" Subtraction = "-" Addition = "+" class BinaryExpression(Expression): _file_info: FileInfo _expression1: Expression _expression2: Expression _operator: BinaryOperator def __init__( self, file_info: FileInfo, expression1: Expression, expression2: Expression, operator: BinaryOperator, ): self._file_info = file_info self._expression1 = expression1 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() def tree_str(self, pre: str = "", pre_cont: str = "") -> str: s: str = f"{pre} 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 _Operator_Precedence: tuple[ UnaryOperator | BinaryOperator, ... ] = ( UnaryOperator.Negate, UnaryOperator.Factorial, BinaryOperator.Exponential, BinaryOperator.Subscript, BinaryOperator.Division, BinaryOperator.Modulus, BinaryOperator.Multiplication, BinaryOperator.Subtraction, BinaryOperator.Addition, ) class FunctionCall(Expression): _file_info: FileInfo _identifier: Identifier _arguments: list[Expression] def __init__( self, file_info: FileInfo, identifier: Identifier, arguments: list[Expression], ): self._file_info = file_info 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]: 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 Constant: _file_info: FileInfo _identifier: Identifier _expression: Expression def __init__( self, file_info: FileInfo, identifier: Identifier, expression: Expression, ): self._file_info = file_info self._identifier = identifier self._expression = expression @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} ") return s @staticmethod def _sa(tokens: list[Token], first_token: Token) -> "Constant": constant_tokens, last_token = _get_to_symbol(tokens, ';') name, _ = _get_to_symbol(constant_tokens, '=') if len(name) > 1: raise UnexpectedToken(name[1], '=') _assert_token(ExpectedIdentifier, name[0]) identifier: Identifier = name[0] # type: ignore fi = first_token.file_info + last_token.file_info try: return Constant( fi, identifier, _expression_sa(constant_tokens)) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) class AnimationDirection(Enum): Increase = "increase" Decrease = "decrease" Bounce = "bounce" class InlineAnimation: _file_info: FileInfo _range_start: Expression _range_start_inclusive: bool _range_end: Expression _range_end_inclusive: bool _step: Expression _direction: AnimationDirection def __init__( self, file_info: FileInfo, range_start: Expression, range_start_inclusive: bool, range_end: Expression, range_end_inclusive: bool, step: Expression, direction: AnimationDirection, ): self._file_info = file_info self._range_start = range_start self._range_start_inclusive = range_start_inclusive self._range_end = range_end self._range_end_inclusive = range_end_inclusive self._step = step self._direction = direction @property def file_info(self) -> FileInfo: return self._file_info @property def range_start(self) -> Expression: return self._range_start @property def range_start_inclusive(self) -> bool: return self._range_start_inclusive @property def range_end(self) -> Expression: return self._range_end @property def range_end_inclusive(self) -> bool: return self._range_end_inclusive @property def step(self) -> Expression: return self._step @property def direction(self) -> AnimationDirection: return self._direction def tree_str(self, pre: str = "", pre_cont: str = "") -> str: s: str = f"{pre} Inline Animation\n" s += f"{pre_cont}├─ Range Start \ ({'≤' if self._range_start_inclusive else '<'})\n" s += self._range_start.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") s += f"{pre_cont}├─ Range End \ ({'≤' if self._range_end_inclusive else '<'})\n" s += self._range_end.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") s += f"{pre_cont}├─ Step\n" s += self._step.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") s += f"{pre_cont}└─ Direction: {self._direction}\n" return s @staticmethod def _sa(tokens: list[Token], token: Token) -> "InlineAnimation": _, anim_tokens, last_token = _get_nested_group(tokens, ('{','}')) fi = token.file_info + last_token.file_info return InlineAnimation(fi, *_animation_sa(anim_tokens, last_token)) class Animation: _file_info: FileInfo _identifier: Identifier _range_start: Expression _range_start_inclusive: bool _range_end: Expression _range_end_inclusive: bool _step: Expression _direction: AnimationDirection def __init__( self, file_info: FileInfo, identifier: Identifier, range_start: Expression, range_start_inclusive: bool, range_end: Expression, range_end_inclusive: bool, step: Expression, direction: AnimationDirection, ): self._file_info = file_info self._identifier = identifier self._range_start = range_start self._range_start_inclusive = range_start_inclusive self._range_end = range_end self._range_end_inclusive = range_end_inclusive self._step = step self._direction = direction @property def file_info(self) -> FileInfo: return self._file_info @property def identifier(self) -> Identifier: return self._identifier @property def range_start(self) -> Expression: return self._range_start @property def range_start_inclusive(self) -> bool: return self._range_start_inclusive @property def range_end(self) -> Expression: return self._range_end @property def range_end_inclusive(self) -> bool: return self._range_end_inclusive @property def step(self) -> Expression: return self._step @property def direction(self) -> AnimationDirection: return self._direction def tree_str(self, pre: str = "", pre_cont: str = "") -> str: s: str = f"{pre} Animation ({self._identifier.value})\n" s += f"{pre_cont}├─ Range Start \ ({'≤' if self._range_start_inclusive else '<'})\n" s += self._range_start.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") s += f"{pre_cont}├─ Range End \ ({'≤' if self._range_end_inclusive else '<'})\n" s += self._range_end.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") s += f"{pre_cont}├─ Step\n" s += self._step.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") s += f"{pre_cont}└─ Direction: {self._direction}\n" return s @staticmethod def _sa(tokens: list[Token], token: Token) -> "Animation": _assert_token(ExpectedIdentifier, tokens[0]) identifier: Identifier = tokens.pop(0) # type: ignore _, anim_tokens, last_token = _get_nested_group(tokens, ('{','}')) fi = token.file_info + last_token.file_info return Animation( fi, identifier, *_animation_sa(anim_tokens, last_token)) class Graph: _parameter_conversions: ClassVar[dict] = { ("x",): "x", ("y",): "y", ("t",): "t", ("r",): "r", ("θ",): "theta", ("c","_","a",): "color_alpha", ("c","_","w",): "color_grey", ("c","_","r",): "color_red", ("c","_","g",): "color_green", ("c","_","b",): "color_blue", ("c","_","h",): "color_hue", ("c","_","s",): "color_saturation", ("c","_","l",): "color_luminosity", } _file_info: FileInfo _x: None | Expression | InlineAnimation _y: None | Expression | InlineAnimation _t: None | InlineAnimation _r: None | Expression _theta: None | InlineAnimation _color_alpha: None | Expression _color_grey: None | Expression _color_red: None | Expression _color_green: None | Expression _color_blue: None | Expression _color_hue: None | Expression _color_saturation: None | Expression _color_luminosity: None | Expression def __init__( self, file_info: FileInfo, x: None | Expression | InlineAnimation = None, y: None | Expression | InlineAnimation = None, t: None | InlineAnimation = None, r: None | Expression = None, theta: None | InlineAnimation = None, color_alpha: None | Expression = None, color_grey: None | Expression = None, color_red: None | Expression = None, color_green: None | Expression = None, color_blue: None | Expression = None, color_hue: None | Expression = None, color_saturation: None | Expression = None, color_luminosity: None | Expression = None, ): self._file_info = file_info self._x = x self._y = y self._t = t self._r = r self._theta = theta self._color_alpha = color_alpha self._color_grey = color_grey self._color_red = color_red self._color_green = color_green self._color_blue = color_blue self._color_hue = color_hue self._color_saturation = color_saturation self._color_luminosity = color_luminosity @property def file_info(self) -> FileInfo: return self._file_info @property def x(self) -> None | Expression | InlineAnimation: return self._x @property def y(self) -> None | Expression | InlineAnimation: return self._y @property def t(self) -> None | InlineAnimation: return self._t @property def r(self) -> None | Expression: return self._r @property def theta(self) -> None | InlineAnimation: return self._theta @property def color_alpha(self) -> None | Expression: return self._color_alpha @property def color_grey(self) -> None | Expression: return self._color_grey @property def color_red(self) -> None | Expression: return self._color_red @property def color_green(self) -> None | Expression: return self._color_green @property def color_blue(self) -> None | Expression: return self._color_blue @property def color_hue(self) -> None | Expression: return self._color_hue @property def color_saturation(self) -> None | Expression: return self._color_saturation @property def color_luminosity(self) -> None | Expression: return self._color_luminosity def tree_str(self, pre: str = "", pre_cont: str = "") -> str: s: str = f"{pre} Graph\n" if self._x is not None: s += pre_cont if ( self._y is not None or self._t is not None or self._r is not None or self._theta is not None or self._color_alpha is not None or self._color_grey is not None or self._color_red is not None or self._color_green is not None or self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ X\n' s += self._x.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ X\n' s += self._x.tree_str(f"{pre_cont} └─", f"{pre_cont} ") if self._y is not None: s += pre_cont if ( self._t is not None or self._r is not None or self._theta is not None or self._color_alpha is not None or self._color_grey is not None or self._color_red is not None or self._color_green is not None or self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ Y\n' s += self._y.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ Y\n' s += self._y.tree_str(f"{pre_cont} └─", f"{pre_cont} ") if self._t is not None: s += pre_cont if ( self._r is not None or self._theta is not None or self._color_alpha is not None or self._color_grey is not None or self._color_red is not None or self._color_green is not None or self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ T\n' s += self._t.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ T\n' s += self._t.tree_str(f"{pre_cont} └─", f"{pre_cont} ") if self._r is not None: s += pre_cont if ( self._theta is not None or self._color_alpha is not None or self._color_grey is not None or self._color_red is not None or self._color_green is not None or self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ R\n' s += self._r.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ R\n' s += self._r.tree_str(f"{pre_cont} └─", f"{pre_cont} ") if self._theta is not None: s += pre_cont if ( self._color_alpha is not None or self._color_grey is not None or self._color_red is not None or self._color_green is not None or self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ θ\n' s += self._theta.tree_str(f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ θ\n' s += self._theta.tree_str(f"{pre_cont} └─", f"{pre_cont} ") if self._color_alpha is not None: s += pre_cont if ( self._color_grey is not None or self._color_red is not None or self._color_green is not None or self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ C_a\n' s += self._color_alpha.tree_str( f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ C_a\n' s += self._color_alpha.tree_str( f"{pre_cont} └─", f"{pre_cont} ") if self._color_grey is not None: s += pre_cont if ( self._color_red is not None or self._color_green is not None or self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ C_w\n' s += self._color_grey.tree_str( f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ C_w\n' s += self._color_grey.tree_str( f"{pre_cont} └─", f"{pre_cont} ") if self._color_red is not None: s += pre_cont if ( self._color_green is not None or self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ C_r\n' s += self._color_red.tree_str( f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ C_r\n' s += self._color_red.tree_str( f"{pre_cont} └─", f"{pre_cont} ") if self._color_green is not None: s += pre_cont if ( self._color_blue is not None or self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ C_g\n' s += self._color_green.tree_str( f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ C_g\n' s += self._color_green.tree_str( f"{pre_cont} └─", f"{pre_cont} ") if self._color_blue is not None: s += pre_cont if ( self._color_hue is not None or self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ C_b\n' s += self._color_blue.tree_str( f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ C_b\n' s += self._color_blue.tree_str( f"{pre_cont} └─", f"{pre_cont} ") if self._color_hue is not None: s += pre_cont if ( self._color_saturation is not None or self._color_luminosity is not None ): s += '├─ C_h\n' s += self._color_hue.tree_str( f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ C_h\n' s += self._color_hue.tree_str( f"{pre_cont} └─", f"{pre_cont} ") if self._color_saturation is not None: s += pre_cont if self._color_luminosity is not None: s += '├─ C_s\n' s += self._color_saturation.tree_str( f"{pre_cont}│ └─", f"{pre_cont}│ ") else: s+= '└─ C_s\n' s += self._color_saturation.tree_str( f"{pre_cont} └─", f"{pre_cont} ") if self._color_luminosity is not None: s+= f'{pre_cont}└─ C_l\n' s += self._color_luminosity.tree_str( f"{pre_cont} └─", f"{pre_cont} ") return s @staticmethod def _sa(tokens: list[Token], first_token: Token) -> "Graph": values: dict = {} _, anim_tokens, last_token = _get_nested_group(tokens, ('{','}')) while anim_tokens: name, _ = _get_to_symbol(anim_tokens, ':') key = tuple(i.value.lower() for i in name) if key not in Graph._parameter_conversions: fi = name[0].file_info name = ''.join(i.value for i in name) fi._length = len(name) token = Identifier(name, fi) raise UnexpectedIdentifier(token, [ "x", "y", "t", "r", "θ", "C_a", "C_w", "C_r", "C_g", "C_b", "C_h", "C_s", "C_l", ]) try: value, _ = _get_to_symbol(anim_tokens, ',', '}') except UnexpectedEndOfTokenStream: value = anim_tokens[:] del anim_tokens[:] values[key] = value args: dict = {} if ('x',) in values: if isinstance(values[('x',)][0], Keyword): if values[('x',)][0].value.lower() != 'anim': raise ExpectedKeyword(values[('x',)][0], 'anim') args['x'] = InlineAnimation._sa( values[('x',)][1:], values[('x',)][0]) else: try: args['x'] = _expression_sa(values[('x',)]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('y',) in values: if isinstance(values[('y',)][0], Keyword): if values[('y',)][0].value.lower() != 'anim': raise ExpectedKeyword(values[('y',)][0], 'anim') args['y'] = InlineAnimation._sa( values[('y',)][1:], values[('y',)][0]) else: try: args['y'] = _expression_sa(values[('y',)]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('t',) in values: if values[('t',)][0].value.lower() != 'anim': raise ExpectedKeyword(values[('t',)][0], 'anim') args['t'] = InlineAnimation._sa( values[('t',)][1:], values[('t',)][0]) if ('r',) in values: try: args['r'] = _expression_sa(values[('r',)]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('θ',) in values: if values[('θ',)][0].value.lower() != 'anim': raise ExpectedKeyword(values[('θ',)][0], 'anim') args['theta'] = InlineAnimation._sa( values[('θ',)][1:], values[('θ',)][0]) if ('c','_','a') in values: try: args['color_alpha'] = _expression_sa(values[('c','_','a')]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('c','_','w') in values: try: args['color_grey'] = _expression_sa(values[('c','_','w')]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('c','_','r') in values: try: args['color_red'] = _expression_sa(values[('c','_','r')]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('c','_','g') in values: try: args['color_green'] = _expression_sa(values[('c','_','g')]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('c','_','b') in values: try: args['color_blue'] = _expression_sa(values[('c','_','b')]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('c','_','h') in values: try: args['color_hue'] = _expression_sa(values[('c','_','h')]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if ('c','_','s') in values: args['color_saturation'] = _expression_sa(values[('c','_','s')]) if ('c','_','l') in values: try: args['color_luminosity'] = _expression_sa(values[('c','_','l')]) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) fi = first_token.file_info + last_token.file_info return Graph(fi, **args) class Screen: _parameter_conversions: ClassVar[dict] = { ("top",): "top", ("bottom",): "bottom", ("right",): "right", ("left",): "left", ("width",): "width", ("height",): "height", ("width","scale",): "width_scale", ("height","scale",): "height_scale", ("fps",): "fps", } _file_info: FileInfo _top: None | int _bottom: None | int _right: None | int _left: None | int _width: None | int _height: None | int _width_scale: float _height_scale: float _fps: int def __init__( self, file_info: FileInfo, top: None | int = None, bottom: None | int = None, right: None | int = None, left: None | int = None, width: None | int = None, height: None | int = None, width_scale: float = 20, height_scale: float = 20, fps: int = 30, ): self._file_info = file_info self._top = top self._bottom = bottom self._right = right self._left = left self._width = width self._height = height self._width_scale = width_scale self._height_scale = height_scale self._fps = fps @property def file_info(self) -> FileInfo: return self._file_info @property def top(self) -> None | int: return self._top @property def bottom(self) -> None | int: return self._bottom @property def right(self) -> None | int: return self._right @property def left(self) -> None | int: return self._left @property def width(self) -> None | int: return self._width @property def height(self) -> None | int: return self._height @property def width_scale(self) -> float: return self._width_scale @property def height_scale(self) -> float: return self._height_scale @property def fps(self) -> int: return self._fps def tree_str(self, pre: str = "", pre_cont: str = "") -> str: s: str = f"{pre} Screen\n" if self._top is not None: s += pre_cont s += f'├─ Top: {self._top}\n' if ( self._bottom is not None and self._right is not None and self._left is not None and self._width is not None and self._height is not None and self._width_scale != 20 and self._height_scale != 20 and self._fps != 30 ) else f'└─ Top: {self._top}\n' if self._bottom is not None: s += pre_cont s += f'├─ Bottom: {self._bottom}\n' if ( self._right is not None and self._left is not None and self._width is not None and self._height is not None and self._width_scale != 20 and self._height_scale != 20 and self._fps != 30 ) else f'└─ Bottom: {self._bottom}\n' if self._right is not None: s += pre_cont s += f'├─ Right: {self._right}\n' if ( self._left is not None and self._width is not None and self._height is not None and self._width_scale != 20 and self._height_scale != 20 and self._fps != 30 ) else f'└─ Right: {self._right}\n' if self._left is not None: s += pre_cont s += f'├─ Left: {self._left}\n' if ( self._width is not None and self._height is not None and self._width_scale != 20 and self._height_scale != 20 and self._fps != 30 ) else f'└─ Left: {self._left}\n' if self._width is not None: s += pre_cont s += f'├─ Width: {self._width}\n' if ( self._height is not None and self._width_scale != 20 and self._height_scale != 20 and self._fps != 30 ) else f'└─ Width: {self._width}\n' if self._height is not None: s += pre_cont s += f'├─ Height: {self._height}\n' if ( self._width_scale != 20 and self._height_scale != 20 and self._fps != 30 ) else f'└─ Height: {self._height}\n' if self._width_scale != 20: s += pre_cont s += f'├─ Width Scale: {self._width_scale}\n' if ( self._height_scale != 20 and self._fps != 30 ) else f'└─ Width Scale: {self._width_scale}\n' if self._height_scale != 20: s += pre_cont s += f'├─ Height Scale: {self._height_scale}\n' if ( self._fps != 30 ) else f'└─ Height Scale: {self._height_scale}\n' if self._fps != 30: s += pre_cont s += f'└─ FPS: {self._fps}\n' return s @staticmethod def _sa(tokens: list[Token], first_token: Token) -> "Screen": values: dict = {} _, screen_tokens, last_token = _get_nested_group(tokens, ('{','}')) while screen_tokens: name, _ = _get_to_symbol(screen_tokens, ':') try: value, _ = _get_to_symbol(screen_tokens, ',') except UnexpectedEndOfTokenStream: value = screen_tokens[:] del screen_tokens[:] key = Screen._parameter_conversions[ tuple(i.value.lower() for i in name)] if len(value) > 1: raise UnexpectedToken(value[1], [",","}"]) values[key] = value[0].value fi = first_token.file_info + last_token.file_info return Screen(fi, **values) class File: _children: list[Screen | Graph | Animation | Constant] _file_info: FileInfo def __init__( self, children: list[Screen | Graph | Animation | Constant], file_info: FileInfo, ): self._children = children[:] self._file_info = file_info @property def children(self) -> list[ Screen | Graph | Animation | Constant ]: return self._children[:] @property def file_info(self) -> FileInfo: return self._file_info def tree_str(self) -> str: s: str = "File\n" if self._children: for child in self._children[:-1]: s += child.tree_str("├─", "│ ") s += self._children[-1].tree_str("└─", " ") return s @staticmethod def _sa(tokens: list[Token]) -> "File": children: list[Screen | Graph | Animation | Constant] = [] file_fi: FileInfo = tokens[0].file_info + tokens[-1].file_info while tokens: token = tokens.pop(0) if isinstance(token, Keyword): match token.value.lower(): case 'screen': children.append(Screen._sa(tokens, token)) case 'graph': children.append(Graph._sa(tokens, token)) case 'anim': children.append(Animation._sa(tokens, token)) case 'const': children.append(Constant._sa(tokens, token)) case _: raise ExpectedKeyword( token, "screen', 'graph', 'anim', or 'const", token.value.lower(), ) else: raise UnexpectedToken(token, "keyword") return File(children, file_fi) def _assert_token( exception: type[_ExpectedTokenBase], token: Token, value: str | None = None, token_type: type[Token] | None = None, ): if not isinstance(token, token_type or exception._token_type): raise exception(token) if value is not None and token.value != value: raise exception(token, value) def _get_nested_group( tokens: list[Token], encloses: tuple[str, str] = ('(',')'), ) -> tuple[Token, list[Token], Token]: first_token = tokens.pop(0) _assert_token(ExpectedPunctuation, first_token, encloses[0]) nested = 1 expr_len = -1 for i in range(len(tokens)): if tokens[i].value == encloses[0]: nested += 1 elif tokens[i].value == encloses[1]: nested -= 1 if nested == 0: expr_len = i break else: raise UnexpectedEndOfTokenStream( f"Expected '{encloses[1]}' but found '{tokens[-1].value}'.", tokens[-1].file_info, ) expr_tokens = tokens[:expr_len] last_token = tokens[expr_len] del tokens[:expr_len+1] return first_token, expr_tokens, last_token def _get_to_symbol( tokens: list[Token], symbols: str | Sequence[str] = ';', end: None | str = None ) -> tuple[list[Token], Token]: expr_len = -1 if end: start = _Punctuation_Enclosing[end] nested = 0 for i in range(len(tokens)): if tokens[i].value == start: nested += 1 elif tokens[i].value == end: if nested == 0 and end in symbols: expr_len = i break elif nested == 0: raise UnexpectedPunctuation( tokens[i], f"{start}' before '{end}", tokens[i].value, ) nested -= 1 elif nested == 0 and tokens[i].value in symbols: expr_len = i break else: raise UnexpectedEndOfTokenStream( "Unexpected End of Token Stream.", tokens[-1].file_info) else: for i in range(len(tokens)): if tokens[i].value in symbols: expr_len = i break else: raise UnexpectedEndOfTokenStream( "Unexpected End of Token Stream.", tokens[-1].file_info) expr_tokens = tokens[:expr_len] last_token = tokens[expr_len] del tokens[:expr_len+1] return expr_tokens, last_token def _animation_sa(tokens: list[Token], last_token: Token) -> tuple[ Expression, bool, Expression, bool, Expression, AnimationDirection, ]: _assert_token(ExpectedIdentifier, tokens[0], 'R') _assert_token(ExpectedPunctuation, tokens[1], ':') del tokens[:2] range_tokens, comparison = _get_to_symbol(tokens, ('<','≤')) try: range_start = _expression_sa(range_tokens) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) range_start_inclusive = comparison.value == '≤' value, comparison = _get_to_symbol(tokens, ('<','≤')) if len(value) != 1: raise ExpectedPunctuation(value[1], "<' or '≤") _assert_token(ExpectedIdentifier, value[0], 'x') try: range_tokens, _ = _get_to_symbol(tokens, ',') except UnexpectedEndOfTokenStream: range_tokens = tokens[:] del tokens[:] try: range_end = _expression_sa(range_tokens) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) range_end_inclusive = comparison.value == '≤' has_pi = range_start.has_pi() or range_end.has_pi() if tokens: _assert_token(ExpectedIdentifier, tokens[0], 'S') _assert_token(ExpectedPunctuation, tokens[1], ':') del tokens[:2] try: step_tokens, _ = _get_to_symbol(tokens, ',') except UnexpectedEndOfTokenStream: step_tokens = tokens[:] del tokens[:] try: step = _expression_sa(step_tokens) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if tokens: _assert_token(ExpectedIdentifier, tokens[0], 'D') _assert_token(ExpectedPunctuation, tokens[1], ':') del tokens[:2] token = tokens.pop(0) _assert_token(ExpectedIdentifier, token) if token.value.lower() in ["increase","decrease","bounce"]: direction = AnimationDirection(token.value.lower()) else: raise ExpectedIdentifier( token, "increase', 'decrease', or 'bounce", token.value.lower(), ) else: direction = AnimationDirection.Increase else: if has_pi: step = BinaryExpression( last_token.file_info, LiteralExpression( last_token.file_info, Punctuation("π", last_token.file_info) ), LiteralExpression( last_token.file_info, NumberLiteral("32", last_token.file_info) ), BinaryOperator.Division, ) else: step = BinaryExpression( last_token.file_info, LiteralExpression( last_token.file_info, NumberLiteral("1", last_token.file_info) ), LiteralExpression( last_token.file_info, NumberLiteral("10", last_token.file_info) ), BinaryOperator.Division, ) direction = AnimationDirection.Increase return ( range_start, range_start_inclusive, range_end, range_end_inclusive, step, direction, ) def _expression_sa(tokens: list[Token]) -> Expression: if not tokens: raise ExpressionError("Expected Expression.") elif len(tokens) == 1: token = tokens.pop(0) _assert_token(ExpectedLiteral,token) if isinstance(token, Punctuation): if token.value not in ['π']: raise ExpectedPunctuation( token, "', '".join(_Id_Punctuation)) return LiteralExpression(token.file_info, token) # type: ignore max_operator: int = -1 max_operator_precedence: int = -1 nested = 0 one_enclosed = True for i, token in enumerate(tokens): if token.value == '(': nested += 1 elif token.value == ')': if nested == 0: raise UnexpectedPunctuation(token, "(' before ')", token.value) nested -= 1 elif nested == 0 and isinstance(token, Punctuation): one_enclosed = False for j, operator in reversed(list(enumerate(_Operator_Precedence))): if j <= max_operator_precedence: break elif operator.value == token.value: max_operator = i max_operator_precedence = j break elif nested == 0: one_enclosed = False if one_enclosed and tokens[0].value == '(' and tokens[-1].value == ')': if not tokens[1:-1]: fi = tokens[0].file_info + tokens[-1].file_info raise UnexpectedEndOfTokenStream( "Expected expression between '(' and ')'.", fi) token = tokens.pop(0) last_token = tokens.pop(-1) try: return _expression_sa(tokens) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if max_operator == -1: function_identifier = tokens.pop(0) _assert_token(ExpectedIdentifier, function_identifier) token = tokens.pop(0) _assert_token(ExpectedPunctuation, token, '(') function_args: list[Expression] = [] while tokens: arg_tokens, last_token = _get_to_symbol(tokens, (',', ')'), ')') if arg_tokens: if len(arg_tokens) > 1 and arg_tokens[1].value == '=': _assert_token(ExpectedIdentifier, arg_tokens[0]) arg_identifier = Identifier( arg_tokens[0].value, arg_tokens[0].file_info, ) del arg_tokens[:2] else: arg_identifier = None if not arg_tokens: fi = last_token.file_info raise UnexpectedEndOfTokenStream("Expected Expression.", fi) try: expression = _expression_sa(arg_tokens) except ExpressionError as err: raise ExpectedExpression(str(err),last_token) if arg_identifier is not None: fi = arg_identifier.file_info + expression.file_info else: fi = expression.file_info function_args.append(expression) fi = function_identifier.file_info + last_token.file_info return FunctionCall( fi, Identifier( function_identifier.value, function_identifier.file_info, ), function_args, ) if ( tokens[max_operator].value in UnaryOperator and max_operator == 0 ): operator = UnaryOperator(tokens[max_operator].value) if not tokens[max_operator + 1:]: fi = tokens[max_operator].file_info raise UnexpectedEndOfTokenStream( f"Expected expression after '{tokens[max_operator].value}'.", fi, ) try: expression = _expression_sa(tokens[max_operator + 1:]) except ExpressionError as err: raise ExpectedExpression(str(err),tokens[max_operator]) fi = tokens[max_operator].file_info + expression.file_info return UnaryExpression(fi, expression, operator) elif tokens[max_operator].value in BinaryOperator: operator = BinaryOperator(tokens[max_operator].value) if not tokens[:max_operator]: fi = tokens[max_operator].file_info raise UnexpectedEndOfTokenStream( f"Expected expression before '{tokens[max_operator].value}'.", fi, ) try: expression1 = _expression_sa(tokens[:max_operator]) except ExpressionError as err: raise ExpectedExpression(str(err),tokens[max_operator]) if not tokens[max_operator + 1:]: fi = tokens[max_operator].file_info raise UnexpectedEndOfTokenStream( f"Expected expression after '{tokens[max_operator].value}'.", fi, ) try: expression2 = _expression_sa(tokens[max_operator + 1:]) except ExpressionError as err: raise ExpectedExpression(str(err),tokens[max_operator]) fi = expression1.file_info + expression2.file_info return BinaryExpression(fi, expression1, expression2, operator) else: raise SyntaxError("Expression Error", tokens[max_operator].file_info) def syntactical_analyzer(tokens: Sequence[Token]) -> File: return File._sa(list(tokens)) # -- Semantics -- class SemanticError(CompilerError): _compiler_error_type = "Semantics" class DuplicateScreen(SemanticError): def __init__(self, file_info: FileInfo): super().__init__( "Duplicate 'screen' declaration.", file_info, ) 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 InvalidGraphDefinition(SemanticError): pass class MultipleAnimationsInGraph(InvalidGraphDefinition): def __init__(self, file_info: FileInfo, file_info_context: FileInfo | None): super().__init__( "Only one animation is allowed in a graph.", 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 ContextNamespace: _parent: "Context | ContextNamespace" _values: "dict[str, int | float | ContextExpression | ContextAnimation]" def __init__( self, parent: "Context | ContextNamespace", values: "dict[str, int | float | ContextExpression | ContextAnimation] | None" = None, ): self._parent = parent self._values = values or {} def lookup_var(self, variable: "ContextVariable") -> int | float: if variable.name in self._values: value = self._values[variable.name] if isinstance(value, (int , float)): return value else: return value.value(self) else: return self._parent.lookup_var(variable) def lookup_func( self, function: "ContextFunctionCall") -> "ContextFunctionCallable": return self._parent.lookup_func(function) class ContextNamespaceNull(ContextNamespace): def __init__(self): pass class ContextExpression: _file_info: FileInfo @property def file_info(self) -> FileInfo: return self._file_info def value(self, context: ContextNamespace) -> 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: ContextNamespace) -> 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: ContextNamespace) -> 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: ContextNamespace) -> 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: ContextNamespace) -> int | float: a, b = self.expression1.value(context), self.expression2.value(context) match self.operator: case BinaryOperator.Exponential: return a ** b 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 BinaryOperator.Modulus: if b == 0: raise DivideByZeroErrorRuntime( self.expression2.file_info, self.file_info, ) return a % b case BinaryOperator.Multiplication: return a * b case BinaryOperator.Subtraction: return a - b 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: ContextNamespace) -> 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 ContextAnimation: _file_info: FileInfo _current: int | float _range_start: ContextExpression _range_start_inclusive: bool _range_end: ContextExpression _range_end_inclusive: bool _step: ContextExpression _direction: AnimationDirection _reversed: bool def __init__( self, file_info: FileInfo, start_value: int | float, range_start: ContextExpression, range_start_inclusive: bool, range_end: ContextExpression, range_end_inclusive: bool, step: ContextExpression, direction: AnimationDirection, ): self._file_info = file_info self._current = start_value self._range_start = range_start self._range_start_inclusive = range_start_inclusive self._range_end = range_end self._range_end_inclusive = range_end_inclusive self._step = step self._direction = direction self._reversed = direction == AnimationDirection.Decrease @property def file_info(self) -> FileInfo: return self._file_info @property def as_context_literal(self) -> ContextLiteral: return ContextLiteral(self.file_info, str(self._current)) def step(self, context: ContextNamespace) -> int | float: if self._direction == AnimationDirection.Increase or ( self._direction == AnimationDirection.Bounce and not self._reversed ): self._current += self._step.value(context) if self._range_end_inclusive: 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(context) else: 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(context) if self._direction == AnimationDirection.Decrease or ( self._direction == AnimationDirection.Bounce and self._reversed ): self._current -= self._step.value(context) if self._range_start_inclusive: 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(context) else: 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(context) return self.value(context) def value(self, context: ContextNamespace) -> 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: _file_info: FileInfo _x: None | ContextExpression | ContextAnimation _y: None | ContextExpression | ContextAnimation _t: None | ContextAnimation _r: None | ContextExpression _theta: None | ContextAnimation _color_alpha: None | ContextExpression _color_grey: None | ContextExpression _color_red: None | ContextExpression _color_green: None | ContextExpression _color_blue: None | ContextExpression _color_hue: None | ContextExpression _color_saturation: None | ContextExpression _color_luminosity: None | ContextExpression def __init__( self, file_info: FileInfo, x: None | ContextExpression | ContextAnimation, y: None | ContextExpression | ContextAnimation, t: None | ContextAnimation, r: None | ContextExpression, theta: None | ContextAnimation, color_alpha: None | ContextExpression, color_grey: None | ContextExpression, color_red: None | ContextExpression, color_green: None | ContextExpression, color_blue: None | ContextExpression, color_hue: None | ContextExpression, color_saturation: None | ContextExpression, color_luminosity: None | ContextExpression, ): self._file_info = file_info self._x = x self._y = y self._t = t self._r = r self._theta = theta self._color_alpha = color_alpha self._color_grey = color_grey self._color_red = color_red self._color_green = color_green self._color_blue = color_blue self._color_hue = color_hue self._color_saturation = color_saturation self._color_luminosity = color_luminosity @property def file_info(self) -> FileInfo: return self._file_info def _local_ns( self, context: ContextNamespace, skip: list[str] | None = None, ) -> ContextNamespace: skip = skip or [] namespace: dict[\ str, int | float | ContextExpression | ContextAnimation] = {} if self._x is not None and 'x' not in skip: namespace['x'] = self._x if self._y is not None and 'y' not in skip: namespace['y'] = self._y if self._t is not None and 't' not in skip: namespace['t'] = self._t if self._r is not None and 'r' not in skip: namespace['r'] = self._r if self._theta is not None and 'θ' not in skip: namespace['θ'] = self._theta if self._color_alpha is not None and 'c_a' not in skip: namespace['c_a'] = self._color_alpha if self._color_grey is not None and 'c_w' not in skip: namespace['c_w'] = self._color_grey if self._color_red is not None and 'c_r' not in skip: namespace['c_r'] = self._color_red if self._color_green is not None and 'c_g' not in skip: namespace['c_g'] = self._color_green if self._color_blue is not None and 'c_b' not in skip: namespace['c_b'] = self._color_blue if self._color_hue is not None and 'c_h' not in skip: namespace['c_h'] = self._color_hue if self._color_saturation is not None and 'c_s' not in skip: namespace['c_s'] = self._color_saturation if self._color_luminosity is not None and 'c_l' not in skip: namespace['c_l'] = self._color_luminosity return ContextNamespace(context, namespace) def step(self, context: ContextNamespace) -> 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(self._local_ns(context, ['x'])) # type: ignore y_last = self._y.value(self._local_ns(context, ['y'])) # type: ignore x = self._x.step(self._local_ns(context, ['x'])) # type: ignore y = self._y.value(self._local_ns(context, ['y'])) # type: ignore case GraphType.Y_Independent: y_last = self._y.value(self._local_ns(context, ['y'])) # type: ignore x_last = self._x.value(self._local_ns(context, ['x'])) # type: ignore y = self._y.step(self._local_ns(context, ['y'])) # type: ignore x = self._x.value(self._local_ns(context, ['x'])) # type: ignore case GraphType.Parametric: x_last = self._x.value(self._local_ns(context, ['x'])) # type: ignore y_last = self._y.value(self._local_ns(context, ['y'])) # type: ignore self._t.step(self._local_ns(context, ['t'])) # type: ignore x = self._x.value(self._local_ns(context, ['x'])) # type: ignore y = self._y.value(self._local_ns(context, ['y'])) # type: ignore case GraphType.Polar: theta_last = self._theta.value(self._local_ns(context, ['θ'])) # type: ignore r_last = self._r.value(self._local_ns(context, ['r'])) # type: ignore theta = self._theta.step(self._local_ns(context, ['θ'])) # type: ignore r = self._r.value(self._local_ns(context, ['r'])) # type: ignore x_last = r_last * math.cos(theta_last) y_last = r_last * math.sin(theta_last) x = r * math.cos(theta) y = r * math.sin(theta) return x_last, y_last, x, y, self.color(context) @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 GraphRuntimeError("Invalid Graph.", self.file_info) @property def color_space(self) -> ColorSpace: if isinstance(self._color_red, ContextExpression): return ColorSpace.RGB elif isinstance(self._color_green, ContextExpression): return ColorSpace.RGB elif isinstance(self._color_blue, ContextExpression): return ColorSpace.RGB elif isinstance(self._color_hue, ContextExpression): return ColorSpace.HSL elif isinstance(self._color_saturation, ContextExpression): return ColorSpace.HSL elif isinstance(self._color_luminosity, ContextExpression): return ColorSpace.HSL else: return ColorSpace.Grey def color( self, context: ContextNamespace) -> tuple[float,float,float,float,]: if self._color_alpha is not None: a = self._color_alpha.value(self._local_ns(context, ['c_a'])) else: a = 1 match self.color_space: case ColorSpace.Grey: if self._color_grey is not None: c = self._color_grey.value(self._local_ns(context, ['c_w'])) else: c = 1 r, g, b = c, c, c case ColorSpace.RGB: if self._color_red is not None: r = self._color_red.value(self._local_ns(context, ['c_r'])) else: r = 1 if self._color_green is not None: g = self._color_green.value(self._local_ns(context, ['c_g'])) else: g = 1 if self._color_blue is not None: b = self._color_blue.value(self._local_ns(context, ['c_b'])) else: b = 1 case ColorSpace.HSL: r,g,b = 1,1,1 return r, g, b, a def value(self, context: ContextNamespace) -> int | float: match self.graph_type: case GraphType.X_Independent: return self._x.value(self._local_ns(context, ['x'])) # type: ignore case GraphType.Y_Independent: return self._y.value(self._local_ns(context, ['y'])) # type: ignore case GraphType.Parametric: return self._t.value(self._local_ns(context, ['t'])) # type: ignore case GraphType.Polar: return self._theta.value(self._local_ns(context, ['θ'])) # type: ignore class Context: _file_info: FileInfo _screen: Screen _constants: dict[str, int | float] _functions: dict[str, ContextFunctionCallable] _animations: dict[str, ContextAnimation] _graphs: list[ContextGraph] def __init__( self, file_info: FileInfo, screen: Screen, constants: dict[str, int | float], functions: dict[str, ContextFunctionCallable], animations: dict[str, ContextAnimation], graphs: list[ContextGraph], ): self._file_info = file_info self._screen = screen self._constants = constants self._functions = functions self._animations = animations self._graphs = graphs @property def file_info(self) -> FileInfo: return self._file_info @property def screen(self) -> Screen: return self._screen def step(self): for anim in self._animations.values(): anim.step(ContextNamespace(self)) def lookup_var(self, variable: ContextVariable) -> int | float: if variable.name in self._animations: return self._animations[variable.name].value(ContextNamespace(self)) elif variable.name in self._constants: return self._constants[variable.name] raise UndefinedVariableIdentifierRuntime( variable.name, variable.file_info) 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 _constants_and_animations( constants: dict[str, ContextLiteral], animations: dict[str, ContextAnimation], ) -> dict[str, ContextLiteral]: combined: dict[str, ContextLiteral] = {} for key, value in constants.items(): combined[key] = value for key, value in animations.items(): combined[key] = value.as_context_literal return combined 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 = math.pi if value1._value == 'π' else float(value1._value) value2 = math.pi if value2._value == 'π' else 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 = [math.pi if a._value == 'π' else float(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 _simplify_animation( animation: Animation | InlineAnimation, constants: dict[str, ContextLiteral], functions: dict[str, ContextFunctionCallable], ) -> ContextAnimation: range_start = _simplify_expression( animation.range_start, constants, functions, True) if not isinstance(range_start, ContextLiteral): raise UnderDefinedConstantDefinition( animation.range_start.file_info, None) start_value = float(range_start._value) range_end = _simplify_expression( animation.range_end, constants, functions, True) if not isinstance(range_end, ContextLiteral): raise UnderDefinedConstantDefinition( animation.range_end.file_info, None) step = _simplify_expression(animation.step, constants, functions, True) if not isinstance(step, ContextLiteral): raise UnderDefinedConstantDefinition(animation.step.file_info, None) return ContextAnimation( animation.file_info, start_value, range_start, animation.range_start_inclusive, range_end, animation.range_end_inclusive, step, animation.direction, ) def _check_graph(graph, name, anim, yes_none, not_none): for i in anim: if isinstance(i, ContextAnimation): raise MultipleAnimationsInGraph( i.file_info, graph.file_info ) for i, j in yes_none: if i is not None: raise InvalidGraphDefinition( f"{j} cannot be defined in a {name} Graph", i.file_info, graph.file_info ) for i, j in not_none: if i is None: raise InvalidGraphDefinition( f"{j} must be defined in a {name} Graph", graph.file_info ) def _simplify_graph( graph: Graph, constants: dict[str, ContextLiteral], functions: dict[str, ContextFunctionCallable], ) -> ContextGraph: x = None y = None t = None r = None theta = None color_alpha = None color_grey = None color_red = None color_green = None color_blue = None color_hue = None color_saturation = None color_luminosity = None if graph.x is not None: if isinstance(graph.x, InlineAnimation): x = _simplify_animation( graph.x, constants, functions) else: x = _simplify_expression( graph.x, constants, functions) if graph.y is not None: if isinstance(graph.y, InlineAnimation): y = _simplify_animation( graph.y, constants, functions) else: y = _simplify_expression( graph.y, constants, functions) if graph.t is not None: t = _simplify_animation( graph.t, constants, functions) if graph.r is not None: r = _simplify_expression( graph.r, constants, functions) if graph.theta is not None: theta = _simplify_animation( graph.theta, constants, functions) if graph.color_alpha is not None: color_alpha = _simplify_expression( graph.color_alpha, constants, functions) if graph.color_grey is not None: color_grey = _simplify_expression( graph.color_grey, constants, functions) if graph.color_red is not None: color_red = _simplify_expression( graph.color_red, constants, functions) if graph.color_green is not None: color_green = _simplify_expression( graph.color_green, constants, functions) if graph.color_blue is not None: color_blue = _simplify_expression( graph.color_blue, constants, functions) if graph.color_hue is not None: color_hue = _simplify_expression( graph.color_hue, constants, functions) if graph.color_saturation is not None: color_saturation = _simplify_expression( graph.color_saturation, constants, functions) if graph.color_luminosity is not None: color_luminosity = _simplify_expression( graph.color_luminosity, constants, functions) if isinstance(x, ContextAnimation): _check_graph( graph, "X-Independent", [y, t, theta,], [(t, 'T'),(r, 'R'),(theta, 'θ'),], [(y, 'Y'),] ) elif isinstance(y, ContextAnimation): _check_graph( graph, "Y-Independent", [x, t, theta,], [(t, 'T'),(r, 'R'),(theta, 'θ'),], [(x, 'X'),] ) elif isinstance(t, ContextAnimation): _check_graph( graph, "Parametric", [x, y, theta,], [(r, 'R'),(theta, 'θ'),], [(x, 'X'),(y, 'Y'),] ) elif isinstance(theta, ContextAnimation): _check_graph( graph, "Polar", [x, y, t,], [(x, 'X'),(y, 'Y'),(t, 'T'),], [(r, 'R'),] ) else: raise InvalidGraphDefinition( "A graph must have at least one animation.", graph.file_info ) return ContextGraph( graph.file_info, x, y, t, r, theta, color_alpha, color_grey, color_red, color_green, color_blue, color_hue, color_saturation, color_luminosity, ) def semantics_analyzer(file: File) -> Context: screen: Screen | None = None 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: if isinstance(child, Screen): if screen is not None: raise DuplicateScreen(child.file_info) screen = child elif isinstance(child, Constant): value = _simplify_expression( child.expression, _constants_and_animations(constants,animations), functions, True, ) if not isinstance(value, ContextLiteral): raise UnderDefinedConstantDefinition(child.file_info, None) constants[child.identifier.value] = value elif isinstance(child, Animation): animations[child.identifier.value] = _simplify_animation( child, _constants_and_animations(constants,animations), functions, ) elif isinstance(child, Graph): graphs.append(_simplify_graph( child, _constants_and_animations(constants,animations), functions, )) if screen is None: screen = Screen(file.file_info) context_constants: dict[str, int | float] = {} for key, value in constants.items(): context_constants[key] = value.value(ContextNamespaceNull()) return Context( file.file_info, screen, context_constants, functions, animations, graphs, ) def compile(filename: str) -> Context | CompilerError: try: with open(filename, encoding='utf-8') as file: code = file.read() tokens = lexer(code, filename) syntax_tree = syntactical_analyzer(tokens) context = semantics_analyzer(syntax_tree) return context except CompilerError as err: print(err.compiler_error()) return err if __name__ == '__main__': try: with open("example.graph", encoding='utf-8') as file: code = file.read() tokens = lexer(code, "example.graph") with open("tokens.txt", 'w', encoding='utf-8') as file: file.write('\n'.join([str(t) for t in tokens])) syntax_tree = syntactical_analyzer(tokens) with open("syntax.txt", 'w', encoding='utf-8') as file: file.write(syntax_tree.tree_str()) context = semantics_analyzer(syntax_tree) except CompilerError as err: print(err.compiler_error()) # raise