Screen-Savers/compiler.py

3058 lines
104 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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