3058 lines
104 KiB
Python
3058 lines
104 KiB
Python
# 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
|