From 695e5859ca97652e163ba7de9f370cf9966944ff Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Thu, 25 Apr 2024 16:11:43 +0100 Subject: [PATCH] Array & ArrayField --- tealish/expression_nodes.py | 164 ++++++++++++++++++++++++++++++++- tealish/nodes.py | 136 +++++++++++++++++++-------- tealish/stdlib.py | 3 +- tealish/tealish_expressions.tx | 7 +- tealish/types.py | 35 ++++++- 5 files changed, 299 insertions(+), 46 deletions(-) diff --git a/tealish/expression_nodes.py b/tealish/expression_nodes.py index 0e55aee..bda1c66 100644 --- a/tealish/expression_nodes.py +++ b/tealish/expression_nodes.py @@ -5,12 +5,14 @@ from .types import ( AVMType, AddrType, + ArrayType, BigIntType, BoxType, StructType, IntType, BytesType, UIntType, + get_struct, ) from .langspec import Op, type_lookup @@ -273,9 +275,9 @@ def __init__( def process(self) -> None: try: op = self.lookup_op(self.name) - return self.process_op_call(op) except KeyError: raise CompileError(f'Unknown function or opcode "{self.name}"', node=self) + return self.process_op_call(op) def process_op_call(self, op: Op) -> None: self.func_call_type = "op" @@ -336,9 +338,9 @@ def __init__( def process(self) -> None: try: func = self.lookup_func(self.name) - self.process_user_defined_func_call(func) except KeyError: raise CompileError(f'Unknown function or opcode "{self.name}"', node=self) + self.process_user_defined_func_call(func) def process_user_defined_func_call(self, func: "Func") -> None: self.func_call_type = "user_defined" @@ -584,7 +586,9 @@ def write_teal(self, writer: "TealWriter") -> None: if isinstance(self.object_type, StructType): teal = [ f"load {self.var.scratch_slot}", - f"extract {self.offset} {self.size}", + f"pushint {self.offset}", + f"pushint {self.size}", + "extract", ] elif isinstance(self.object_type, BoxType): teal = [ @@ -605,6 +609,156 @@ def _tealish(self) -> str: return f"{self.name}.{self.field}" +class StructOrBoxArrayField(BaseNode): + def __init__(self, name, field, arrayIndex, parent=None) -> None: + self.name = name + self.field = field + self.type = AVMType.none + self.parent = parent + self.arrayIndex = arrayIndex + + def process(self) -> None: + self.var = self.lookup_var(self.name) + self.object_type = self.var.tealish_type + struct = self.var.tealish_type + array_field = struct.fields[self.field] + array = array_field.tealish_type + self.offset = array_field.offset + self.size = array.type.size + self.type = array.type + if not isinstance(self.arrayIndex, Integer): + # index is an expression that needs to be evaluated + self.arrayIndex.process() + + def write_teal(self, writer: "TealWriter") -> None: + if isinstance(self.object_type, StructType): + writer.write(self, f"load {self.var.scratch_slot}") + writer.write(self, f"pushint {self.offset}") + writer.write(self, f"pushint {self.size}") + writer.write(self, self.arrayIndex) + writer.write(self, "*") + writer.write(self, "+") + writer.write(self, f"pushint {self.size}") + writer.write(self, "extract") + + elif isinstance(self.object_type, BoxType): + writer.write(self, f"load {self.var.scratch_slot}") + writer.write(self, f"pushint {self.offset}") + writer.write(self, f"pushint {self.size}") + writer.write(self, self.arrayIndex) + writer.write(self, "*") + writer.write(self, "+") + writer.write(self, f"pushint {self.size}") + writer.write(self, "box_extract") + else: + raise Exception() + # If the field is a Int or Uint convert it from bytes to int + if isinstance(self.type, IntType): + writer.write(self, "btoi") + writer.write(self, f"// {self.name}.{self.field}") + + def _tealish(self) -> str: + return f"{self.name}.{self.field}" + + +class KeyValue(BaseNode): + def __init__( + self, key: str, value: "Node", parent: Optional[BaseNode] = None + ) -> None: + + self.parent = parent + self.key = key + self.expression = value + + def process(self) -> None: + self.expression.process() + + def write_teal(self, writer: "TealWriter") -> None: + writer.write(self, self.expression) + + def _tealish(self) -> str: + return f"{self.key}: {self.expression}" + + +class StructLiteral(BaseNode): + def __init__( + self, name: str, args: List["Node"], parent: Optional[BaseNode] = None + ) -> None: + + self.args = args + self.parent = parent + self.nodes = args + self.func_call = None + self.struct = get_struct(name) + self.type = self.struct + + def process(self) -> None: + for arg in self.args: + arg.process() + struct_field = self.struct.fields[arg.key] + if not struct_field.tealish_type.can_hold(arg.expression.type): + message = f'Incorrect type for struct field "{arg.key}". Expected {struct_field.tealish_type}, got {arg.expression.type} at line {self.line_no}.' + raise CompileError(message) + + def write_teal(self, writer: "TealWriter") -> None: + writer.write( + self, f"pushint {self.struct.size}; bzero // make {self.struct.name} struct" + ) + for arg in self.args: + field = self.struct.fields[arg.key] + writer.write(self, arg.expression) + teal = [] + if isinstance(field.tealish_type, IntType): + teal.append("itob") + if isinstance(field.tealish_type, UIntType): + teal.append( + f"extract {8 - field.tealish_type.size} {field.tealish_type.size}" + ) + teal += [ + f"replace {field.offset} // field: {arg.key}", + ] + writer.write(self, teal) + + def _tealish(self) -> str: + return f"{self.struct.name}{{}}" + + +class ArrayElement(BaseNode): + def __init__( + self, name: str, arrayIndex, parent: Optional[BaseNode] = None + ) -> None: + self.name = name + self.parent = parent + self.array_index = arrayIndex + + def process(self) -> None: + try: + self.var = self.lookup_var(self.name) + except KeyError as e: + raise CompileError(e.args[0], node=self) + + assert isinstance(self.var.tealish_type, ArrayType), ( + self.name, + self.var.tealish_type, + ) + self.type = self.var.tealish_type.type + if not isinstance(self.array_index, Integer): + self.array_index.process() + + def write_teal(self, writer: "TealWriter") -> None: + writer.write(self, f"load {self.var.scratch_slot} // {self.name}") + writer.write(self, f"pushint {self.type.size}") + writer.write(self, self.array_index) + writer.write(self, "*") + writer.write(self, f"pushint {self.type.size}") + writer.write(self, "extract") + if isinstance(self.type, IntType): + writer.write(self, "btoi") + + def _tealish(self) -> str: + return f"{self.name}[{self.array_index.tealish()}]" + + def class_provider(name: str) -> Optional[type]: classes = { "Variable": Variable, @@ -626,5 +780,9 @@ def class_provider(name: str) -> Optional[type]: "GlobalField": GlobalField, "InnerTxnField": InnerTxnField, "StructOrBoxField": StructOrBoxField, + "StructOrBoxArrayField": StructOrBoxArrayField, + "KeyValue": KeyValue, + "StructLiteral": StructLiteral, + "ArrayElement": ArrayElement, } return classes.get(name) diff --git a/tealish/nodes.py b/tealish/nodes.py index 994b63e..0226c42 100644 --- a/tealish/nodes.py +++ b/tealish/nodes.py @@ -389,8 +389,10 @@ def consume(cls, compiler: "TealishCompiler", parent: Node) -> "LineStatement": return VarDeclaration(line, parent, compiler=compiler) elif line.startswith("box<"): return BoxDeclaration(line, parent, compiler=compiler) - elif re.match(r"[a-z][a-zA-Z_0-9]*\.[a-z][a-zA-Z_0-9]* = .*", line): + elif re.match(r"[a-z][a-zA-Z_0-9]*\.[a-z][a-zA-Z_0-9]*(\[(.*)\])? = .*", line): return StructOrBoxAssignment(line, parent, compiler=compiler) + elif re.match(r"[a-z][a-zA-Z_0-9]*\[.*\] = .*", line): + return ElementAssignment(line, parent, compiler=compiler) elif " = " in line: return Assignment(line, parent, compiler=compiler) # Statement functions @@ -644,6 +646,59 @@ def _tealish(self) -> str: ) +class ElementAssignment(LineStatement): + pattern = r"(?P[a-z_][a-zA-Z0-9_]*)\[(?P.*)\] = (?P.*)$" + name: str + name_nodes: List[Name] + expression: GenericExpression + index: GenericExpression + + def process(self) -> None: + self.expression.process() + t = self.expression.type + self.incoming_types = t if type(t) == list else [t] + self.name = Name(self.name) + if len(self.incoming_types) != 1: + raise CompileError( + "Incorrect number of names (1) for " + + f"values ({len(self.incoming_types)}) in assignment", + node=self, + ) + + self.var = self.get_var(self.name.value) + if self.var is None: + raise CompileError( + f'Var "{self.name.value}" not declared in current scope', node=self + ) + + if not self.var.tealish_type.type.can_hold(self.incoming_types[0]): + raise CompileError( + "Incorrect type for assignment. " + + f"Expected {self.var.tealish_type}, got {self.incoming_types[0]}", + node=self, + ) + self.name.slot = self.var.scratch_slot + self.name._type = self.var.avm_type + self.element_size = self.var.tealish_type.type.size + self.index.process() + + def write_teal(self, writer: "TealWriter") -> None: + writer.write(self, f"// tl:{self.line_no}: {self.line}") + writer.write(self, f"load {self.name.slot} // {self.name.value}") + writer.write(self, self.index) + writer.write(self, f"pushint {self.element_size}") + writer.write(self, "*") + writer.write(self, self.expression) + writer.write(self, "replace") + writer.write(self, f"store {self.name.slot} // {self.name.value}") + + def _tealish(self) -> str: + return ( + f"{', '.join(n.tealish() for n in self.name_nodes)}" + + f" = {self.expression.tealish()}\n" + ) + + class Block(Statement): possible_child_nodes = [Statement] pattern = r"block (?P[a-zA-Z_0-9]+):$" @@ -1873,12 +1928,13 @@ def _tealish(self) -> str: class StructOrBoxAssignment(LineStatement): pattern = ( - r"(?P[a-z][a-zA-Z0-9_]*).(?P[a-z][a-zA-Z0-9_]*)" + r"(?P[a-z][a-zA-Z0-9_]*).(?P[a-z][a-zA-Z0-9_]*)(\[(?P.*)\])?" r"( = (?P.*))?$" ) name: Name field_name: str expression: GenericExpression + index: GenericExpression def process(self) -> None: self.var = self.get_var(self.name.value) @@ -1893,22 +1949,30 @@ def process(self) -> None: struct = self.var.tealish_type struct_field = struct.fields[self.field_name] - self.offset = struct_field.offset - self.size = struct_field.size - self.data_type = struct_field.tealish_type + + if self.index is None: + self.offset = struct_field.offset + self.size = struct_field.size + self.data_type = struct_field.tealish_type + else: + array = struct_field.tealish_type + self.data_type = array.type + self.size = self.data_type.size + self.offset = struct_field.offset + self.index.process() self.expression.process() - if not struct_field.tealish_type.can_hold(self.expression.type): + if not self.data_type.can_hold(self.expression.type): # raise CompileError( # "Incorrect type for struct field assignment. " - # + f"Expected {struct_field.tealish_type}, got {self.expression.type}", + # + f"Expected {self.data_type}, got {self.expression.type}", # node=self, # ) message = f"Incorrect type for struct field assignment. Expected {self.data_type}, got {self.expression.type} at line {self.line_no}." - if struct_field.tealish_type.can_hold_with_cast(self.expression.type): + if self.data_type.can_hold_with_cast(self.expression.type): message += "\nPerhaps Cast or padding is required? " message += f"\n- {self.line}" message += f"\n+ {self.name.value}.{self.field_name} = Cast({self.expression.tealish()}, {self.data_type})" - if not isinstance(struct_field.tealish_type, (StructType, IntType)): + if not isinstance(self.data_type, (StructType, IntType)): message += f"\n+ {self.name.value}.{self.field_name} = Rpad({self.expression.tealish()}, {self.data_type.size})" raise CompileError(message) @@ -1918,43 +1982,43 @@ def write_teal(self, writer: "TealWriter") -> None: self, f"// tl:{self.line_no}: {self.line} [slot {self.var.scratch_slot}]", ) + writer.write(self, f"load {self.var.scratch_slot}") + writer.write(self, f"pushint {self.offset}") + if self.index: + writer.write(self, f"pushint {self.data_type.size}") + writer.write(self, self.index) + writer.write(self, "*") + writer.write(self, "+") writer.write(self, self.expression) - teal = [] if isinstance(self.data_type, IntType): - teal.append("itob") + writer.write(self, "itob") if isinstance(self.data_type, UIntType): - teal.append( - f"extract {8 - self.data_type.size} {self.data_type.size}" + writer.write( + self, f"extract {8 - self.data_type.size} {self.data_type.size}" ) - # struct setter one liner - teal += [ - f"load {self.var.scratch_slot}", - "swap", - f"replace {self.offset}", - f"store {self.var.scratch_slot}", - f"// set {self.name.value}.{self.field_name}", - ] - writer.write(self, teal) + writer.write(self, "replace") + writer.write(self, f"store {self.var.scratch_slot}") + writer.write(self, f"// set {self.name.value}.{self.field_name}") elif isinstance(self.object_type, BoxType): writer.write(self, f"// tl:{self.line_no}: {self.line}") writer.write(self, self.expression) - teal = [] if isinstance(self.data_type, IntType): - teal.append("itob") + writer.write(self, "itob") if isinstance(self.data_type, UIntType): - teal.append( - f"extract {8 - self.data_type.size} {self.data_type.size}" + writer.write( + self, f"extract {8 - self.data_type.size} {self.data_type.size}" ) - # box setter one liner # Use uncover to bring the value to the top of the stack above the box name and offset - teal += [ - f"load {self.var.scratch_slot}", - f"pushint {self.offset}", - "uncover 2", - "box_replace", - f"// boxset {self.name.value}.{self.field_name}", - ] - writer.write(self, teal) + writer.write(self, f"load {self.var.scratch_slot}") + writer.write(self, f"pushint {self.offset}") + if self.index: + writer.write(self, f"pushint {self.data_type.size}") + writer.write(self, self.index) + writer.write(self, "*") + writer.write(self, "+") + writer.write(self, "uncover 2") + writer.write(self, "box_replace") + writer.write(self, f"// boxset {self.name.value}.{self.field_name}") def _tealish(self) -> str: s = f"{self.name.tealish()}.{self.field_name}" @@ -1972,7 +2036,7 @@ class BoxDeclaration(LineStatement): # box item1 = Box("a") pattern = ( r"box<(?P[A-Z][a-zA-Z0-9_]*)> (?P[a-z][a-zA-Z0-9_]*)" - r" = (?POpen|Create)?Box\((?P.*)\)$" + r" = (?POpenOrCreate|Open|Create)?Box\((?P.*)\)$" ) # Name to struct type struct_name: str diff --git a/tealish/stdlib.py b/tealish/stdlib.py index 674012c..01da316 100644 --- a/tealish/stdlib.py +++ b/tealish/stdlib.py @@ -131,7 +131,8 @@ class Cast(FunctionCall): name = "Cast" def process(self) -> None: - self.type = get_type_instance(self.args[1].name) + type_name = self.args[1].tealish() + self.type = get_type_instance(type_name) self.args[0].process() self.expression = self.args[0] if str(self.expression.type) == str(self.type): diff --git a/tealish/tealish_expressions.tx b/tealish/tealish_expressions.tx index 2a21431..d846f41 100644 --- a/tealish/tealish_expressions.tx +++ b/tealish/tealish_expressions.tx @@ -33,11 +33,14 @@ InnerTxnField: 'Itxn.' field=FieldName; InnerTxnArrayField: 'Itxn.' field=FieldName '[' arrayIndex=Expression ']'; GlobalField: 'Global.' field=FieldName; StructOrBoxField: name=Name '.' field=Name; -Value: StdLibFunctionCall | FunctionCall | Field | StructOrBoxField | UnaryOp | Group | Integer | Bytes | Constant | Enum | Variable; +StructOrBoxArrayField: name=Name '.' field=StructFieldName '[' arrayIndex=Expression ']'; +ArrayElement: name=StructFieldName '[' arrayIndex=Expression ']'; +Value: StructLiteral | StdLibFunctionCall | FunctionCall | Field | StructOrBoxArrayField| StructOrBoxField | ArrayElement | UnaryOp | Group | Integer | Bytes | Constant | Enum | Variable; Variable: name=Name; Constant: name=/([A-Z][A-Z_0-9]+)/; Enum: name=/([A-Z][A-Za-z_0-9]+)/; Name: (/([a-z][A-Za-z_0-9]*)(\[[0-9]+\])?/ | /_/); +StructFieldName: /([A-Za-z_0-9]*)/; StdLibFunctionName: /([A-Z][A-Za-z_0-9]+)/; GroupIndex: NegativeGroupIndex | PositiveGroupIndex | Expression; NegativeGroupIndex: '-' index=INT; @@ -45,3 +48,5 @@ PositiveGroupIndex: '+' index=INT; HexBytes: value=/0x([a-fA-F0-9]+)/; Integer: value=/[0-9_]+/; Bytes: value=STRING | HexBytes; +KeyValue: key=Name ':' value=Expression; +StructLiteral: name=FieldName ('{' args*=KeyValue[','] '}'); diff --git a/tealish/types.py b/tealish/types.py index 8981eb2..06400ad 100644 --- a/tealish/types.py +++ b/tealish/types.py @@ -188,6 +188,18 @@ def __init__(self, type, length): self.length = length super().__init__(length * type.size) + def can_hold(self, other): + if isinstance(other, BytesType): + if not other.size: + return False + if self.size == other.size: + return True + return False + + def __str__(self) -> str: + s = f"{self.type.name}[{self.length}]" + return s + def define_struct(struct: StructType) -> None: _structs[struct.name] = struct @@ -210,17 +222,30 @@ def get_type_instance(type_name): return UInt8Type() elif type_name == "uint64": return IntType() - elif m := re.match(r"bytes\[([0-9]+)\]", type_name): + elif m := re.match(r"uint([0-9]+)$", type_name): + size = int(m.groups()[0]) + return UIntType(size // 8) + # elif m := re.match(r"bigint([0-9]+)", type_name): + # size = int(m.groups()[0]) + # return BigIntType(size // 8) + elif m := re.match(r"bytes\[([0-9]+)\]$", type_name): size = int(m.groups()[0]) return BytesType(size) - elif m := re.match(r"box<([A-Z][a-zA-Z0-9_]+)>", type_name): + elif m := re.match(r"box<([A-Z][a-zA-Z0-9_]+)>$", type_name): struct_name = m.groups()[0] return BoxType(struct_name=struct_name) - elif m := re.match(r"([A-Z][a-zA-Z0-9_]+)", type_name): + elif m := re.match(r"([A-Z][a-zA-Z0-9_]+)\[([0-9]+)\]$", type_name): + struct_name = m.groups()[0] + size = int(m.groups()[1]) + return ArrayType(get_struct(struct_name), size) + elif m := re.match(r"([A-Z][a-zA-Z0-9_]+)$", type_name): struct_name = m.groups()[0] return get_struct(struct_name) - elif m := re.match(r"uint8\[([0-9]+)\]", type_name): + elif m := re.match(r"uint8\[([0-9]+)\]$", type_name): + size = int(m.groups()[0]) + return ArrayType(UInt8Type(), size) + elif m := re.match(r"int\[([0-9]+)\]$", type_name): size = int(m.groups()[0]) - return ArrayType(UInt8Type, size) + return ArrayType(IntType(), size) else: raise KeyError(f"Unknown type {type_name}")