diff --git a/__pycache__/main.cpython-314.pyc b/__pycache__/main.cpython-314.pyc deleted file mode 100644 index a3aa38d..0000000 Binary files a/__pycache__/main.cpython-314.pyc and /dev/null differ diff --git a/main b/main deleted file mode 100755 index 922fbf7..0000000 Binary files a/main and /dev/null differ diff --git a/main.bin b/main.bin deleted file mode 100755 index 9a54c1f..0000000 Binary files a/main.bin and /dev/null differ diff --git a/main.py b/main.py index bf46120..ac11318 100644 --- a/main.py +++ b/main.py @@ -187,30 +187,24 @@ class Reader: # --------------------------------------------------------------------------- -class ASTNode: - """Base class for all AST nodes.""" +@dataclass +class Op: + """Flat operation used for both compile-time execution and emission.""" + + op: str + data: Any = None @dataclass -class WordRef(ASTNode): +class Definition: name: str - - -@dataclass -class Literal(ASTNode): - value: Any - - -@dataclass -class Definition(ASTNode): - name: str - body: List[ASTNode] + body: List[Op] immediate: bool = False compile_only: bool = False @dataclass -class AsmDefinition(ASTNode): +class AsmDefinition: name: str body: str immediate: bool = False @@ -218,8 +212,8 @@ class AsmDefinition(ASTNode): @dataclass -class Module(ASTNode): - forms: List[ASTNode] +class Module: + forms: List[Any] @dataclass @@ -236,33 +230,6 @@ class StructField: size: int -@dataclass -class BranchZero(ASTNode): - target: str - - -@dataclass -class Jump(ASTNode): - target: str - - -@dataclass -class Label(ASTNode): - name: str - - -@dataclass -class ForBegin(ASTNode): - loop_label: str - end_label: str - - -@dataclass -class ForEnd(ASTNode): - loop_label: str - end_label: str - - class MacroContext: """Small facade exposed to Python-defined macros.""" @@ -280,12 +247,12 @@ class MacroContext: return self._parser.peek_token() def emit_literal(self, value: int) -> None: - self._parser.emit_node(Literal(value=value)) + self._parser.emit_node(Op(op="literal", data=value)) def emit_word(self, name: str) -> None: - self._parser.emit_node(WordRef(name=name)) + self._parser.emit_node(Op(op="word", data=name)) - def emit_node(self, node: ASTNode) -> None: + def emit_node(self, node: Op) -> None: self._parser.emit_node(node) def inject_tokens(self, tokens: Sequence[str], template: Optional[Token] = None) -> None: @@ -316,7 +283,7 @@ class MacroContext: return self._parser.most_recent_definition() -MacroHandler = Callable[[MacroContext], Optional[List[ASTNode]]] +MacroHandler = Callable[[MacroContext], Optional[List[Op]]] IntrinsicEmitter = Callable[["FunctionEmitter"], None] @@ -390,8 +357,8 @@ class Parser: self._ensure_tokens(self.pos) return None if self._eof() else self.tokens[self.pos] - def emit_node(self, node: ASTNode) -> None: - self._append_node(node) + def emit_node(self, node: Op) -> None: + self._append_op(node) def most_recent_definition(self) -> Optional[Word]: return self.last_defined @@ -406,18 +373,18 @@ class Parser: if entry["type"] == "if": # For if without else if "false" in entry: - self._append_node(Label(name=entry["false"])) + self._append_op(Op(op="label", data=entry["false"])) elif entry["type"] == "else": - self._append_node(Label(name=entry["end"])) + self._append_op(Op(op="label", data=entry["end"])) elif entry["type"] == "while": - self._append_node(Jump(target=entry["begin"])) - self._append_node(Label(name=entry["end"])) + self._append_op(Op(op="jump", data=entry["begin"])) + self._append_op(Op(op="label", data=entry["end"])) elif entry["type"] == "for": # Emit ForEnd node for loop decrement - self._append_node(ForEnd(loop_label=entry["loop"], end_label=entry["end"])) + self._append_op(Op(op="for_end", data={"loop": entry["loop"], "end": entry["end"]})) elif entry["type"] == "begin": - self._append_node(Jump(target=entry["begin"])) - self._append_node(Label(name=entry["end"])) + self._append_op(Op(op="jump", data=entry["begin"])) + self._append_op(Op(op="label", data=entry["end"])) # Parsing ------------------------------------------------------------------ def parse(self, tokens: Iterable[Token], source: str) -> Module: @@ -608,12 +575,12 @@ class Parser: produced = word.macro(MacroContext(self)) if produced: for node in produced: - self._append_node(node) + self._append_op(node) else: self._execute_immediate_word(word) return - self._append_node(WordRef(name=token.lexeme)) + self._append_op(Op(op="word", data=token.lexeme)) def _execute_immediate_word(self, word: Word) -> None: try: @@ -721,31 +688,31 @@ class Parser: def _handle_if_control(self) -> None: false_label = self._new_label("if_false") - self._append_node(BranchZero(target=false_label)) + self._append_op(Op(op="branch_zero", data=false_label)) self._push_control({"type": "if", "false": false_label}) def _handle_else_control(self) -> None: entry = self._pop_control(("if",)) end_label = self._new_label("if_end") - self._append_node(Jump(target=end_label)) - self._append_node(Label(name=entry["false"])) + self._append_op(Op(op="jump", data=end_label)) + self._append_op(Op(op="label", data=entry["false"])) self._push_control({"type": "else", "end": end_label}) def _handle_for_control(self) -> None: loop_label = self._new_label("for_loop") end_label = self._new_label("for_end") - self._append_node(ForBegin(loop_label=loop_label, end_label=end_label)) + self._append_op(Op(op="for_begin", data={"loop": loop_label, "end": end_label})) self._push_control({"type": "for", "loop": loop_label, "end": end_label}) def _handle_while_control(self) -> None: begin_label = self._new_label("begin") end_label = self._new_label("end") - self._append_node(Label(name=begin_label)) + self._append_op(Op(op="label", data=begin_label)) self._push_control({"type": "begin", "begin": begin_label, "end": end_label}) def _handle_do_control(self) -> None: entry = self._pop_control(("begin",)) - self._append_node(BranchZero(target=entry["end"])) + self._append_op(Op(op="branch_zero", data=entry["end"])) self._push_control(entry) def _begin_definition(self, token: Token) -> None: @@ -859,7 +826,7 @@ class Parser: def _py_exec_namespace(self) -> Dict[str, Any]: return dict(PY_EXEC_GLOBALS) - def _append_node(self, node: ASTNode) -> None: + def _append_op(self, node: Op) -> None: target = self.context_stack[-1] if isinstance(target, Module): target.forms.append(node) @@ -868,10 +835,10 @@ class Parser: else: # pragma: no cover - defensive raise ParseError("unknown parse context") - def _try_literal(self, token: Token) -> None: + def _try_literal(self, token: Token) -> bool: try: value = int(token.lexeme, 0) - self._append_node(Literal(value=value)) + self._append_op(Op(op="literal", data=value)) return True except ValueError: pass @@ -880,14 +847,14 @@ class Parser: try: if "." in token.lexeme or "e" in token.lexeme.lower(): value = float(token.lexeme) - self._append_node(Literal(value=value)) + self._append_op(Op(op="literal", data=value)) return True except ValueError: pass string_value = _parse_string_literal(token) if string_value is not None: - self._append_node(Literal(value=string_value)) + self._append_op(Op(op="literal", data=string_value)) return True return False @@ -1229,7 +1196,7 @@ class CompileTimeVM: raise ParseError(f"unknown word '{name}' during compile-time execution") self._call_word(word) - def _execute_nodes(self, nodes: Sequence[ASTNode]) -> None: + def _execute_nodes(self, nodes: Sequence[Op]) -> None: label_positions = self._label_positions(nodes) loop_pairs = self._for_pairs(nodes) begin_pairs = self._begin_pairs(nodes) @@ -1238,12 +1205,16 @@ class CompileTimeVM: ip = 0 while ip < len(nodes): node = nodes[ip] - if isinstance(node, Literal): - self.push(node.value) + kind = node.op + data = node.data + + if kind == "literal": + self.push(data) ip += 1 continue - if isinstance(node, WordRef): - name = node.name + + if kind == "word": + name = str(data) if name == "begin": end_idx = begin_pairs.get(ip) if end_idx is None: @@ -1270,9 +1241,9 @@ class CompileTimeVM: self._call_word_by_name(name) ip += 1 continue - if isinstance(node, BranchZero): + + if kind == "branch_zero": condition = self.pop() - flag: bool if isinstance(condition, bool): flag = condition elif isinstance(condition, int): @@ -1280,17 +1251,20 @@ class CompileTimeVM: else: raise ParseError("branch expects integer or boolean condition") if not flag: - ip = self._jump_to_label(label_positions, node.target) + ip = self._jump_to_label(label_positions, str(data)) else: ip += 1 continue - if isinstance(node, Jump): - ip = self._jump_to_label(label_positions, node.target) + + if kind == "jump": + ip = self._jump_to_label(label_positions, str(data)) continue - if isinstance(node, Label): + + if kind == "label": ip += 1 continue - if isinstance(node, ForBegin): + + if kind == "for_begin": count = self.pop_int() if count <= 0: match = loop_pairs.get(ip) @@ -1301,7 +1275,8 @@ class CompileTimeVM: self.loop_stack.append({"remaining": count, "begin": ip, "initial": count}) ip += 1 continue - if isinstance(node, ForEnd): + + if kind == "for_end": if not self.loop_stack: raise ParseError("'next' without matching 'for'") frame = self.loop_stack[-1] @@ -1312,22 +1287,23 @@ class CompileTimeVM: self.loop_stack.pop() ip += 1 continue - raise ParseError(f"unsupported compile-time AST node {node!r}") - def _label_positions(self, nodes: Sequence[ASTNode]) -> Dict[str, int]: + raise ParseError(f"unsupported compile-time op {node!r}") + + def _label_positions(self, nodes: Sequence[Op]) -> Dict[str, int]: positions: Dict[str, int] = {} for idx, node in enumerate(nodes): - if isinstance(node, Label): - positions[node.name] = idx + if node.op == "label": + positions[str(node.data)] = idx return positions - def _for_pairs(self, nodes: Sequence[ASTNode]) -> Dict[int, int]: + def _for_pairs(self, nodes: Sequence[Op]) -> Dict[int, int]: stack: List[int] = [] pairs: Dict[int, int] = {} for idx, node in enumerate(nodes): - if isinstance(node, ForBegin): + if node.op == "for_begin": stack.append(idx) - elif isinstance(node, ForEnd): + elif node.op == "for_end": if not stack: raise ParseError("'next' without matching 'for'") begin_idx = stack.pop() @@ -1337,13 +1313,13 @@ class CompileTimeVM: raise ParseError("'for' without matching 'next'") return pairs - def _begin_pairs(self, nodes: Sequence[ASTNode]) -> Dict[int, int]: + def _begin_pairs(self, nodes: Sequence[Op]) -> Dict[int, int]: stack: List[int] = [] pairs: Dict[int, int] = {} for idx, node in enumerate(nodes): - if isinstance(node, WordRef) and node.name == "begin": + if node.op == "word" and node.data == "begin": stack.append(idx) - elif isinstance(node, WordRef) and node.name == "again": + elif node.op == "word" and node.data == "again": if not stack: raise ParseError("'again' without matching 'begin'") begin_idx = stack.pop() @@ -1611,48 +1587,57 @@ class Assembler: else: builder.emit("") - def _emit_node(self, node: ASTNode, builder: FunctionEmitter) -> None: - if isinstance(node, Literal): - if isinstance(node.value, int): - builder.push_literal(node.value) + def _emit_node(self, node: Op, builder: FunctionEmitter) -> None: + kind = node.op + data = node.data + + if kind == "literal": + if isinstance(data, int): + builder.push_literal(data) return - if isinstance(node.value, float): - label = self._intern_float_literal(node.value) + if isinstance(data, float): + label = self._intern_float_literal(data) builder.push_float(label) return - if isinstance(node.value, str): - label, length = self._intern_string_literal(node.value) + if isinstance(data, str): + label, length = self._intern_string_literal(data) builder.push_label(label) builder.push_literal(length) return - raise CompileError(f"unsupported literal type {type(node.value)!r}") - return - if isinstance(node, WordRef): - self._emit_wordref(node, builder) - return - if isinstance(node, BranchZero): - self._emit_branch_zero(node, builder) - return - if isinstance(node, Jump): - builder.emit(f" jmp {node.target}") - return - if isinstance(node, Label): - builder.emit(f"{node.name}:") - return - if isinstance(node, ForBegin): - self._emit_for_begin(node, builder) - return - if isinstance(node, ForEnd): - self._emit_for_next(node, builder) - return - raise CompileError(f"unsupported AST node {node!r}") + raise CompileError(f"unsupported literal type {type(data)!r}") - def _emit_wordref(self, ref: WordRef, builder: FunctionEmitter) -> None: - word = self.dictionary.lookup(ref.name) + if kind == "word": + self._emit_wordref(str(data), builder) + return + + if kind == "branch_zero": + self._emit_branch_zero(str(data), builder) + return + + if kind == "jump": + builder.emit(f" jmp {data}") + return + + if kind == "label": + builder.emit(f"{data}:") + return + + if kind == "for_begin": + self._emit_for_begin(data, builder) + return + + if kind == "for_end": + self._emit_for_next(data, builder) + return + + raise CompileError(f"unsupported op {node!r}") + + def _emit_wordref(self, name: str, builder: FunctionEmitter) -> None: + word = self.dictionary.lookup(name) if word is None: - raise CompileError(f"unknown word '{ref.name}'") + raise CompileError(f"unknown word '{name}'") if word.compile_only: - raise CompileError(f"word '{ref.name}' is compile-time only") + raise CompileError(f"word '{name}' is compile-time only") if word.intrinsic: word.intrinsic(builder) return @@ -1669,7 +1654,7 @@ class Assembler: ret_type = signature[1] if signature else None if len(arg_types) != inputs and signature: - raise CompileError(f"extern '{ref.name}' mismatch: {inputs} inputs vs {len(arg_types)} types") + raise CompileError(f"extern '{name}' mismatch: {inputs} inputs vs {len(arg_types)} types") int_idx = 0 xmm_idx = 0 @@ -1679,19 +1664,19 @@ class Assembler: if not arg_types: # Legacy/Raw mode: assume all ints if inputs > 6: - raise CompileError(f"extern '{ref.name}' has too many inputs ({inputs} > 6)") + raise CompileError(f"extern '{name}' has too many inputs ({inputs} > 6)") for i in range(inputs): mapping.append(("int", regs[i])) else: for type_name in arg_types: if type_name in ("float", "double"): if xmm_idx >= 8: - raise CompileError(f"extern '{ref.name}' has too many float inputs") + raise CompileError(f"extern '{name}' has too many float inputs") mapping.append(("float", xmm_regs[xmm_idx])) xmm_idx += 1 else: if int_idx >= 6: - raise CompileError(f"extern '{ref.name}' has too many int inputs") + raise CompileError(f"extern '{name}' has too many int inputs") mapping.append(("int", regs[int_idx])) int_idx += 1 @@ -1706,7 +1691,7 @@ class Assembler: builder.emit(" mov rbp, rsp") builder.emit(" and rsp, -16") builder.emit(f" mov al, {xmm_idx}") - builder.emit(f" call {ref.name}") + builder.emit(f" call {name}") builder.emit(" leave") # Handle Return Value @@ -1721,30 +1706,34 @@ class Assembler: raise CompileError("extern only supports 0 or 1 output") else: # Emit call to unresolved symbol (let linker resolve it) - builder.emit(f" call {ref.name}") + builder.emit(f" call {name}") else: - builder.emit(f" call {sanitize_label(ref.name)}") + builder.emit(f" call {sanitize_label(name)}") - def _emit_branch_zero(self, node: BranchZero, builder: FunctionEmitter) -> None: + def _emit_branch_zero(self, target: str, builder: FunctionEmitter) -> None: builder.pop_to("rax") builder.emit(" test rax, rax") - builder.emit(f" jz {node.target}") + builder.emit(f" jz {target}") - def _emit_for_begin(self, node: ForBegin, builder: FunctionEmitter) -> None: + def _emit_for_begin(self, data: Dict[str, str], builder: FunctionEmitter) -> None: + loop_label = data["loop"] + end_label = data["end"] builder.pop_to("rax") builder.emit(" cmp rax, 0") - builder.emit(f" jle {node.end_label}") + builder.emit(f" jle {end_label}") builder.emit(" sub r13, 8") builder.emit(" mov [r13], rax") - builder.emit(f"{node.loop_label}:") + builder.emit(f"{loop_label}:") - def _emit_for_next(self, node: ForEnd, builder: FunctionEmitter) -> None: + def _emit_for_next(self, data: Dict[str, str], builder: FunctionEmitter) -> None: + loop_label = data["loop"] + end_label = data["end"] builder.emit(" mov rax, [r13]") builder.emit(" dec rax") builder.emit(" mov [r13], rax") - builder.emit(f" jg {node.loop_label}") + builder.emit(f" jg {loop_label}") builder.emit(" add r13, 8") - builder.emit(f"{node.end_label}:") + builder.emit(f"{end_label}:") def _runtime_prelude(self) -> List[str]: return [ @@ -1804,7 +1793,7 @@ class Assembler: # --------------------------------------------------------------------------- -def macro_immediate(ctx: MacroContext) -> Optional[List[ASTNode]]: +def macro_immediate(ctx: MacroContext) -> Optional[List[Op]]: parser = ctx.parser word = parser.most_recent_definition() if word is None: @@ -1815,7 +1804,7 @@ def macro_immediate(ctx: MacroContext) -> Optional[List[ASTNode]]: return None -def macro_compile_only(ctx: MacroContext) -> Optional[List[ASTNode]]: +def macro_compile_only(ctx: MacroContext) -> Optional[List[Op]]: parser = ctx.parser word = parser.most_recent_definition() if word is None: @@ -1826,7 +1815,7 @@ def macro_compile_only(ctx: MacroContext) -> Optional[List[ASTNode]]: return None -def macro_compile_time(ctx: MacroContext) -> Optional[List[ASTNode]]: +def macro_compile_time(ctx: MacroContext) -> Optional[List[Op]]: """Run the next word at compile time and still emit it for runtime.""" parser = ctx.parser if parser._eof(): @@ -1840,11 +1829,11 @@ def macro_compile_time(ctx: MacroContext) -> Optional[List[ASTNode]]: raise ParseError(f"word '{name}' is compile-time only") parser.compile_time_vm.invoke(word) if isinstance(parser.context_stack[-1], Definition): - parser.emit_node(WordRef(name=name)) + parser.emit_node(Op(op="word", data=name)) return None -def macro_begin_text_macro(ctx: MacroContext) -> Optional[List[ASTNode]]: +def macro_begin_text_macro(ctx: MacroContext) -> Optional[List[Op]]: parser = ctx.parser if parser._eof(): raise ParseError("macro name missing after 'macro:'") @@ -1861,7 +1850,7 @@ def macro_begin_text_macro(ctx: MacroContext) -> Optional[List[ASTNode]]: return None -def macro_end_text_macro(ctx: MacroContext) -> Optional[List[ASTNode]]: +def macro_end_text_macro(ctx: MacroContext) -> Optional[List[Op]]: parser = ctx.parser if parser.macro_recording is None: raise ParseError("';macro' without matching 'macro:'") @@ -2458,13 +2447,7 @@ def _register_compile_time_primitives(dictionary: Dictionary) -> None: PY_EXEC_GLOBALS: Dict[str, Any] = { "MacroContext": MacroContext, "Token": Token, - "Literal": Literal, - "WordRef": WordRef, - "BranchZero": BranchZero, - "Jump": Jump, - "Label": Label, - "ForBegin": ForBegin, - "ForEnd": ForEnd, + "Op": Op, "StructField": StructField, "Definition": Definition, "Module": Module, @@ -2474,7 +2457,7 @@ PY_EXEC_GLOBALS: Dict[str, Any] = { } -def macro_struct_begin(ctx: MacroContext) -> Optional[List[ASTNode]]: +def macro_struct_begin(ctx: MacroContext) -> Optional[List[Op]]: parser = ctx.parser if parser._eof(): raise ParseError("struct name missing after 'struct:'") @@ -2529,7 +2512,7 @@ def macro_struct_begin(ctx: MacroContext) -> Optional[List[ASTNode]]: return None -def macro_struct_end(ctx: MacroContext) -> Optional[List[ASTNode]]: +def macro_struct_end(ctx: MacroContext) -> Optional[List[Op]]: raise ParseError("';struct' must follow a 'struct:' block") diff --git a/mem.sl b/mem.sl deleted file mode 100644 index 7f58d6f..0000000 --- a/mem.sl +++ /dev/null @@ -1,8 +0,0 @@ -import stdlib/stdlib.sl -import stdlib/io.sl -import stdlib/debug.sl - -: main - mem dup 5 swap ! - @ puti cr -; diff --git a/readstdin b/readstdin deleted file mode 100755 index 5f6fb07..0000000 Binary files a/readstdin and /dev/null differ diff --git a/stdlib/mem.sl b/stdlib/mem.sl new file mode 100644 index 0000000..b161ed8 --- /dev/null +++ b/stdlib/mem.sl @@ -0,0 +1,15 @@ +import stdlib.sl + +: alloc + 0 # addr hint (NULL) + swap # size + 3 # prot (PROT_READ | PROT_WRITE) + 34 # flags (MAP_PRIVATE | MAP_ANON) + -1 # fd + 0 # offset + mmap +; + +: free + munmap drop +; \ No newline at end of file diff --git a/test.bin b/test.bin deleted file mode 100755 index 792e00c..0000000 Binary files a/test.bin and /dev/null differ diff --git a/test_read_file.out b/test_read_file.out deleted file mode 100755 index dbf7c09..0000000 Binary files a/test_read_file.out and /dev/null differ diff --git a/tests/alloc.expected b/tests/alloc.expected new file mode 100644 index 0000000..7787faa --- /dev/null +++ b/tests/alloc.expected @@ -0,0 +1,2 @@ +111 +222 \ No newline at end of file diff --git a/alloc.sl b/tests/alloc.sl similarity index 63% rename from alloc.sl rename to tests/alloc.sl index 69e7128..4649f0b 100644 --- a/alloc.sl +++ b/tests/alloc.sl @@ -1,19 +1,6 @@ -import stdlib/stdlib.sl -import stdlib/io.sl - -: alloc - 0 # addr hint (NULL) - swap # size - 3 # prot (PROT_READ | PROT_WRITE) - 34 # flags (MAP_PRIVATE | MAP_ANON) - -1 # fd - 0 # offset - mmap -; - -: free - munmap drop -; +import ../stdlib/stdlib.sl +import ../stdlib/io.sl +import ../stdlib/mem.sl : test-mem-alloc 4096 alloc dup 1337 swap ! # allocate 4096 bytes, store 1337 at start @@ -26,7 +13,7 @@ struct: Point field y 8 ;struct -: main2 +: main 32 alloc # allocate 32 bytes (enough for a Point struct) dup 111 swap Point.x! dup 222 swap Point.y! diff --git a/tests/alloc.test b/tests/alloc.test new file mode 100644 index 0000000..c345ca5 --- /dev/null +++ b/tests/alloc.test @@ -0,0 +1 @@ +python main.py tests/alloc.sl -o /tmp/alloc > /dev/null && /tmp/alloc \ No newline at end of file diff --git a/tests/call_syntax_parens.expected b/tests/call_syntax_parens.expected new file mode 100644 index 0000000..72350c3 --- /dev/null +++ b/tests/call_syntax_parens.expected @@ -0,0 +1,2 @@ +42 +3 diff --git a/tests/call_syntax_parens.sl b/tests/call_syntax_parens.sl new file mode 100644 index 0000000..229b874 --- /dev/null +++ b/tests/call_syntax_parens.sl @@ -0,0 +1,16 @@ +import ../stdlib/stdlib.sl +import ../stdlib/io.sl +import ../fn.sl + +: main + 2 40 + + puti cr + extend-syntax + foo(1, 2) + puti cr + 0 +; + +fn foo(int a, int b){ + return a + b; +} diff --git a/tests/call_syntax_parens.test b/tests/call_syntax_parens.test new file mode 100644 index 0000000..d5cb6e7 --- /dev/null +++ b/tests/call_syntax_parens.test @@ -0,0 +1 @@ +python main.py tests/call_syntax_parens.sl -o /tmp/call_syntax_parens > /dev/null && /tmp/call_syntax_parens diff --git a/tests/fib.expected b/tests/fib.expected new file mode 100644 index 0000000..7afbcfe --- /dev/null +++ b/tests/fib.expected @@ -0,0 +1,27 @@ +1 +1 +2 +3 +5 +8 +13 +21 +34 +55 +89 +144 +233 +377 +610 +987 +1597 +2584 +4181 +6765 +10946 +17711 +28657 +46368 +75025 +------- +25 numbers printed from the fibonaci sequence diff --git a/fib.sl b/tests/fib.sl similarity index 79% rename from fib.sl rename to tests/fib.sl index 8b174ed..237cea1 100644 --- a/fib.sl +++ b/tests/fib.sl @@ -1,5 +1,6 @@ -import stdlib/stdlib.sl -import stdlib/io.sl +import ../stdlib/stdlib.sl +import ../stdlib/io.sl +import ../stdlib/debug.sl : main 1 1 2dup 2dup puti cr puti cr @@ -13,6 +14,7 @@ import stdlib/io.sl "-------" puts r> 3 + puti " numbers printed from the fibonaci sequence" puts + 0 ; : main2 diff --git a/tests/fib.test b/tests/fib.test new file mode 100644 index 0000000..3be9ca5 --- /dev/null +++ b/tests/fib.test @@ -0,0 +1 @@ +python main.py tests/fib.sl -o /tmp/fib > /dev/null && /tmp/fib \ No newline at end of file diff --git a/tests/integration_core.expected b/tests/integration_core.expected new file mode 100644 index 0000000..430631c --- /dev/null +++ b/tests/integration_core.expected @@ -0,0 +1,33 @@ +12 +7 +42 +12 +1 +10 +22 +3 +123 +1337 +81 +99 +13 +111 +60 +5 +123 +1 +0 +1 +0 +1 +0 +1 +0 +1 +0 +1 +0 +111 +222 +16 +70 diff --git a/test.sl b/tests/integration_core.sl similarity index 97% rename from test.sl rename to tests/integration_core.sl index 3ce255f..37c3cc2 100644 --- a/test.sl +++ b/tests/integration_core.sl @@ -1,6 +1,6 @@ -import stdlib/stdlib.sl -import stdlib/io.sl -import fn.sl +import ../stdlib/stdlib.sl +import ../stdlib/io.sl +import ../fn.sl :asm mem-slot { lea rax, [rel print_buf] diff --git a/tests/integration_core.test b/tests/integration_core.test new file mode 100644 index 0000000..acda8b4 --- /dev/null +++ b/tests/integration_core.test @@ -0,0 +1 @@ +python main.py tests/integration_core.sl -o /tmp/integration_core > /dev/null && /tmp/integration_core diff --git a/tests/io_read_file.expected b/tests/io_read_file.expected new file mode 100644 index 0000000..f4b3548 --- /dev/null +++ b/tests/io_read_file.expected @@ -0,0 +1 @@ +read_file works diff --git a/test_read_file.sl b/tests/io_read_file.sl similarity index 82% rename from test_read_file.sl rename to tests/io_read_file.sl index bac2e2e..e058829 100644 --- a/test_read_file.sl +++ b/tests/io_read_file.sl @@ -1,8 +1,12 @@ -import stdlib/stdlib.sl -import stdlib/io.sl +import ../stdlib/stdlib.sl +import ../stdlib/io.sl : main - "/etc/hostname" # (addr len) + "/tmp/l2_read_file_test.txt" + "read_file works\n" + write_file drop + + "/tmp/l2_read_file_test.txt" # (addr len) read_file # (file_addr file_len) dup 0 > if # if file_len > 0, success write_buf # print file contents (file_len file_addr) diff --git a/tests/io_read_file.test b/tests/io_read_file.test new file mode 100644 index 0000000..958face --- /dev/null +++ b/tests/io_read_file.test @@ -0,0 +1 @@ +python main.py tests/io_read_file.sl -o /tmp/io_read_file > /dev/null && /tmp/io_read_file diff --git a/tests/io_read_stdin.expected b/tests/io_read_stdin.expected new file mode 100644 index 0000000..4d19008 --- /dev/null +++ b/tests/io_read_stdin.expected @@ -0,0 +1 @@ +stdin via test diff --git a/test_read_stdin.sl b/tests/io_read_stdin.sl similarity index 75% rename from test_read_stdin.sl rename to tests/io_read_stdin.sl index 7c07b2d..7d68b70 100644 --- a/test_read_stdin.sl +++ b/tests/io_read_stdin.sl @@ -1,5 +1,5 @@ -import stdlib/stdlib.sl -import stdlib/io.sl +import ../stdlib/stdlib.sl +import ../stdlib/io.sl : main 1024 diff --git a/tests/io_read_stdin.test b/tests/io_read_stdin.test new file mode 100644 index 0000000..97244eb --- /dev/null +++ b/tests/io_read_stdin.test @@ -0,0 +1 @@ +python main.py tests/io_read_stdin.sl -o /tmp/io_read_stdin > /dev/null && printf 'stdin via test\n' | /tmp/io_read_stdin diff --git a/tests/io_write_buf.expected b/tests/io_write_buf.expected new file mode 100644 index 0000000..3d6db8b --- /dev/null +++ b/tests/io_write_buf.expected @@ -0,0 +1 @@ +hello from write_buf test diff --git a/test_write_buf.sl b/tests/io_write_buf.sl similarity index 56% rename from test_write_buf.sl rename to tests/io_write_buf.sl index 72720b1..bcb91e0 100644 --- a/test_write_buf.sl +++ b/tests/io_write_buf.sl @@ -1,5 +1,5 @@ -import stdlib/stdlib.sl -import stdlib/io.sl +import ../stdlib/stdlib.sl +import ../stdlib/io.sl : main "hello from write_buf test\n" diff --git a/tests/io_write_buf.test b/tests/io_write_buf.test new file mode 100644 index 0000000..f656e52 --- /dev/null +++ b/tests/io_write_buf.test @@ -0,0 +1 @@ +python main.py tests/io_write_buf.sl -o /tmp/io_write_buf > /dev/null && /tmp/io_write_buf diff --git a/tests/io_write_file.expected b/tests/io_write_file.expected new file mode 100644 index 0000000..30fcc89 --- /dev/null +++ b/tests/io_write_file.expected @@ -0,0 +1,2 @@ +wrote bytes: +27 diff --git a/test_write_file.sl b/tests/io_write_file.sl similarity index 52% rename from test_write_file.sl rename to tests/io_write_file.sl index de74b64..2b2f8b8 100644 --- a/test_write_file.sl +++ b/tests/io_write_file.sl @@ -1,9 +1,9 @@ -import stdlib/stdlib.sl -import stdlib/io.sl +import ../stdlib/stdlib.sl +import ../stdlib/io.sl : main - "/tmp/l2_test_write.txt" # push path (addr len) - "hello from write_file test\n" # push buf (addr len) + "/tmp/l2_write_file_test.txt" # path + "hello from write_file test\n" # buffer write_file dup 0 > if "wrote bytes: " puts diff --git a/tests/io_write_file.test b/tests/io_write_file.test new file mode 100644 index 0000000..9ed47bf --- /dev/null +++ b/tests/io_write_file.test @@ -0,0 +1 @@ +python main.py tests/io_write_file.sl -o /tmp/io_write_file > /dev/null && /tmp/io_write_file diff --git a/tests/loop_while.expected b/tests/loop_while.expected new file mode 100644 index 0000000..f4c0c86 --- /dev/null +++ b/tests/loop_while.expected @@ -0,0 +1,10 @@ +10 +9 +8 +7 +6 +5 +4 +3 +2 +1 diff --git a/while_test.sl b/tests/loop_while.sl similarity index 66% rename from while_test.sl rename to tests/loop_while.sl index bdf4051..f0000bb 100644 --- a/while_test.sl +++ b/tests/loop_while.sl @@ -1,5 +1,5 @@ -import stdlib/stdlib.sl -import stdlib/io.sl +import ../stdlib/stdlib.sl +import ../stdlib/io.sl : main 10 diff --git a/tests/loop_while.test b/tests/loop_while.test new file mode 100644 index 0000000..885a048 --- /dev/null +++ b/tests/loop_while.test @@ -0,0 +1 @@ +python main.py tests/loop_while.sl -o /tmp/loop_while > /dev/null && /tmp/loop_while diff --git a/tests/loops_and_cmp.expected b/tests/loops_and_cmp.expected new file mode 100644 index 0000000..54d9c92 --- /dev/null +++ b/tests/loops_and_cmp.expected @@ -0,0 +1,3 @@ +5 +1 +0 diff --git a/tests/loops_and_cmp.sl b/tests/loops_and_cmp.sl new file mode 100644 index 0000000..6397221 --- /dev/null +++ b/tests/loops_and_cmp.sl @@ -0,0 +1,13 @@ +import ../stdlib/stdlib.sl +import ../stdlib/io.sl + +: main + 0 + 5 for + 1 + + end + puti cr + 5 5 == puti cr + 5 4 == puti cr + 0 +; diff --git a/tests/loops_and_cmp.test b/tests/loops_and_cmp.test new file mode 100644 index 0000000..6b1583f --- /dev/null +++ b/tests/loops_and_cmp.test @@ -0,0 +1 @@ +python main.py tests/loops_and_cmp.sl -o /tmp/loops_and_cmp > /dev/null && /tmp/loops_and_cmp diff --git a/tests/mem.expected b/tests/mem.expected new file mode 100644 index 0000000..1f7a723 --- /dev/null +++ b/tests/mem.expected @@ -0,0 +1,2 @@ +5 +6 \ No newline at end of file diff --git a/aa.sl b/tests/mem.sl similarity index 63% rename from aa.sl rename to tests/mem.sl index a6e7c1e..4e6de88 100644 --- a/aa.sl +++ b/tests/mem.sl @@ -1,5 +1,5 @@ -import stdlib/stdlib.sl -import stdlib/io.sl +import ../stdlib/stdlib.sl +import ../stdlib/io.sl : main mem 5 swap ! diff --git a/tests/mem.test b/tests/mem.test new file mode 100644 index 0000000..fc410e8 --- /dev/null +++ b/tests/mem.test @@ -0,0 +1 @@ +python main.py tests/mem.sl -o /tmp/mem > /dev/null && /tmp/mem diff --git a/tests/override_dup_compile_time.expected b/tests/override_dup_compile_time.expected new file mode 100644 index 0000000..1e8b314 --- /dev/null +++ b/tests/override_dup_compile_time.expected @@ -0,0 +1 @@ +6 diff --git a/tests/override_dup_compile_time.sl b/tests/override_dup_compile_time.sl new file mode 100644 index 0000000..29b3534 --- /dev/null +++ b/tests/override_dup_compile_time.sl @@ -0,0 +1,28 @@ +import ../stdlib/stdlib.sl +import ../stdlib/io.sl + +: dup + 6 +; +compile-only + +: emit-overridden + "dup" use-l2-ct + 42 + dup + int>string + nil + token-from-lexeme + list-new + swap + list-append + inject-tokens +; +immediate +compile-only + +: main + emit-overridden + puti cr + 0 +; diff --git a/tests/override_dup_compile_time.test b/tests/override_dup_compile_time.test new file mode 100644 index 0000000..13bd19b --- /dev/null +++ b/tests/override_dup_compile_time.test @@ -0,0 +1 @@ +python main.py tests/override_dup_compile_time.sl -o /tmp/override_dup_compile_time > /dev/null && /tmp/override_dup_compile_time diff --git a/tests/run_tests.py b/tests/run_tests.py deleted file mode 100644 index d70befe..0000000 --- a/tests/run_tests.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 -"""Simple end-to-end test runner for L2. - -Each test case provides an L2 program source and an expected stdout. The runner -invokes the bootstrap compiler on the fly and executes the produced binary. -""" - -from __future__ import annotations - -import subprocess -import sys -import tempfile -from dataclasses import dataclass -from pathlib import Path -from typing import List - -ROOT = Path(__file__).resolve().parents[1] -COMPILER = ROOT / "main.py" -PYTHON = Path(sys.executable) - - -@dataclass -class TestCase: - name: str - source: str - expected_stdout: str - - -CASES: List[TestCase] = [ - TestCase( - name="call_syntax_parens", - source=f""" -import {ROOT / 'stdlib/stdlib.sl'} -import {ROOT / 'stdlib/io.sl'} -import {ROOT / 'fn.sl'} - -: main - 2 40 + - puti cr - extend-syntax - foo(1, 2) - puti cr - 0 -; - -fn foo(int a, int b){{ - return a + b; -}} -""", - expected_stdout="42\n3\n", - ), - TestCase( - name="loops_and_cmp", - source=f""" -import {ROOT / 'stdlib/stdlib.sl'} -import {ROOT / 'stdlib/io.sl'} - -: main - 0 - 5 for - 1 + - end - puti cr - 5 5 == puti cr - 5 4 == puti cr - 0 -; -""", - expected_stdout="5\n1\n0\n", - ), - TestCase( - name="override_dup_compile_time", - source=f""" -import {ROOT / 'stdlib/stdlib.sl'} -import {ROOT / 'stdlib/io.sl'} - -: dup - 6 -; -compile-only - -: emit-overridden - "dup" use-l2-ct - 42 - dup - int>string - nil - token-from-lexeme - list-new - swap - list-append - inject-tokens -; -immediate -compile-only - -: main - emit-overridden - puti cr - 0 -; -""", - expected_stdout="6\n", - ), - TestCase( - name="string_puts", - source=f""" -import {ROOT / 'stdlib/stdlib.sl'} -import {ROOT / 'stdlib/io.sl'} - -: main - \"hello world\" puts - \"line1\\nline2\" puts - \"\" puts - 0 -; -""", - expected_stdout="hello world\nline1\nline2\n\n", - ), -] - - -def run_case(case: TestCase) -> None: - print(f"[run] {case.name}") - with tempfile.TemporaryDirectory() as tmp: - src_path = Path(tmp) / f"{case.name}.sl" - exe_path = Path(tmp) / f"{case.name}.out" - src_path.write_text(case.source.strip() + "\n", encoding="utf-8") - - compile_cmd = [str(PYTHON), str(COMPILER), str(src_path), "-o", str(exe_path)] - compile_result = subprocess.run( - compile_cmd, - capture_output=True, - text=True, - cwd=ROOT, - ) - if compile_result.returncode != 0: - sys.stderr.write("[fail] compile error\n") - sys.stderr.write(compile_result.stdout) - sys.stderr.write(compile_result.stderr) - raise SystemExit(compile_result.returncode) - - run_result = subprocess.run( - [str(exe_path)], - capture_output=True, - text=True, - cwd=ROOT, - ) - if run_result.returncode != 0: - sys.stderr.write("[fail] execution error\n") - sys.stderr.write(run_result.stdout) - sys.stderr.write(run_result.stderr) - raise SystemExit(run_result.returncode) - - if run_result.stdout != case.expected_stdout: - sys.stderr.write(f"[fail] output mismatch for {case.name}\n") - sys.stderr.write("expected:\n" + case.expected_stdout) - sys.stderr.write("got:\n" + run_result.stdout) - raise SystemExit(1) - - print(f"[ok] {case.name}") - - -def main() -> None: - for case in CASES: - run_case(case) - print("[all tests passed]") - - -if __name__ == "__main__": - main() diff --git a/tests/string_puts.expected b/tests/string_puts.expected new file mode 100644 index 0000000..78e9c9f --- /dev/null +++ b/tests/string_puts.expected @@ -0,0 +1,4 @@ +hello world +line1 +line2 + diff --git a/tests/string_puts.sl b/tests/string_puts.sl new file mode 100644 index 0000000..1c87ef7 --- /dev/null +++ b/tests/string_puts.sl @@ -0,0 +1,9 @@ +import ../stdlib/stdlib.sl +import ../stdlib/io.sl + +: main + "hello world" puts + "line1\nline2" puts + "" puts + 0 +; diff --git a/tests/string_puts.test b/tests/string_puts.test new file mode 100644 index 0000000..493f022 --- /dev/null +++ b/tests/string_puts.test @@ -0,0 +1 @@ +python main.py tests/string_puts.sl -o /tmp/string_puts > /dev/null && /tmp/string_puts diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 0000000..f41fc4f --- /dev/null +++ b/tests/test.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +import sys +import os +import subprocess + +COLORS = { + "red": "\033[91m", + "green": "\033[92m", + "yellow": "\033[93m", + "blue": "\033[94m", + "reset": "\033[0m" +} + +def print_colored(text, color): + print(COLORS.get(color, "") + text + COLORS["reset"], end="") + +def run_tests(): + test_dir = "tests" + any_failed = False + + if not os.path.isdir(test_dir): + print("No 'tests' directory found.") + return 1 + + for file in sorted(os.listdir(test_dir)): + if file.endswith(".test"): + test_path = os.path.join(test_dir, file) + expected_path = test_path.replace(".test", ".expected") + + if not os.path.isfile(expected_path): + print(f"Missing expected output file for {file}") + any_failed = True + continue + + with open(test_path, "r") as test_file: + command = test_file.read().strip() + + with open(expected_path, "r") as expected_file: + expected_output = expected_file.read().strip() + + try: + result = subprocess.run(command, shell=True, text=True, capture_output=True) + actual_output = result.stdout.strip() + stderr_output = result.stderr.strip() + + if result.returncode == 0 and actual_output == expected_output: + print_colored("[OK] ", "green") + print(f"{file} passed") + else: + print_colored("[ERR] ", "red") + print(f"{file} failed (exit {result.returncode})") + print(f"Expected:\n{expected_output}") + print(f"Got:\n{actual_output}") + if stderr_output: + print(f"Stderr:\n{stderr_output}") + any_failed = True + + except Exception as e: + print_colored(f"Error running {file}: {e}", "red") + any_failed = True + + return 1 if any_failed else 0 + +if __name__ == "__main__": + sys.exit(run_tests()) + diff --git a/write b/write deleted file mode 100755 index ec3c604..0000000 Binary files a/write and /dev/null differ diff --git a/write_buf_test b/write_buf_test deleted file mode 100755 index 3f474ae..0000000 Binary files a/write_buf_test and /dev/null differ