small refactor and cleanup

This commit is contained in:
IgorCielniak
2026-01-08 13:15:27 +01:00
parent 963e024108
commit d4dc6ceef5
52 changed files with 418 additions and 372 deletions

Binary file not shown.

BIN
main

Binary file not shown.

BIN
main.bin

Binary file not shown.

295
main.py
View File

@@ -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")

8
mem.sl
View File

@@ -1,8 +0,0 @@
import stdlib/stdlib.sl
import stdlib/io.sl
import stdlib/debug.sl
: main
mem dup 5 swap !
@ puti cr
;

BIN
readstdin

Binary file not shown.

15
stdlib/mem.sl Normal file
View File

@@ -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
;

BIN
test.bin

Binary file not shown.

Binary file not shown.

2
tests/alloc.expected Normal file
View File

@@ -0,0 +1,2 @@
111
222

View File

@@ -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!

1
tests/alloc.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/alloc.sl -o /tmp/alloc > /dev/null && /tmp/alloc

View File

@@ -0,0 +1,2 @@
42
3

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
python main.py tests/call_syntax_parens.sl -o /tmp/call_syntax_parens > /dev/null && /tmp/call_syntax_parens

27
tests/fib.expected Normal file
View File

@@ -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

View File

@@ -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

1
tests/fib.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/fib.sl -o /tmp/fib > /dev/null && /tmp/fib

View File

@@ -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

View File

@@ -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]

View File

@@ -0,0 +1 @@
python main.py tests/integration_core.sl -o /tmp/integration_core > /dev/null && /tmp/integration_core

View File

@@ -0,0 +1 @@
read_file works

View File

@@ -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)

1
tests/io_read_file.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/io_read_file.sl -o /tmp/io_read_file > /dev/null && /tmp/io_read_file

View File

@@ -0,0 +1 @@
stdin via test

View File

@@ -1,5 +1,5 @@
import stdlib/stdlib.sl
import stdlib/io.sl
import ../stdlib/stdlib.sl
import ../stdlib/io.sl
: main
1024

1
tests/io_read_stdin.test Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
hello from write_buf test

View File

@@ -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"

1
tests/io_write_buf.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/io_write_buf.sl -o /tmp/io_write_buf > /dev/null && /tmp/io_write_buf

View File

@@ -0,0 +1,2 @@
wrote bytes:
27

View File

@@ -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

1
tests/io_write_file.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/io_write_file.sl -o /tmp/io_write_file > /dev/null && /tmp/io_write_file

10
tests/loop_while.expected Normal file
View File

@@ -0,0 +1,10 @@
10
9
8
7
6
5
4
3
2
1

View File

@@ -1,5 +1,5 @@
import stdlib/stdlib.sl
import stdlib/io.sl
import ../stdlib/stdlib.sl
import ../stdlib/io.sl
: main
10

1
tests/loop_while.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/loop_while.sl -o /tmp/loop_while > /dev/null && /tmp/loop_while

View File

@@ -0,0 +1,3 @@
5
1
0

13
tests/loops_and_cmp.sl Normal file
View File

@@ -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
;

1
tests/loops_and_cmp.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/loops_and_cmp.sl -o /tmp/loops_and_cmp > /dev/null && /tmp/loops_and_cmp

2
tests/mem.expected Normal file
View File

@@ -0,0 +1,2 @@
5
6

View File

@@ -1,5 +1,5 @@
import stdlib/stdlib.sl
import stdlib/io.sl
import ../stdlib/stdlib.sl
import ../stdlib/io.sl
: main
mem 5 swap !

1
tests/mem.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/mem.sl -o /tmp/mem > /dev/null && /tmp/mem

View File

@@ -0,0 +1 @@
6

View File

@@ -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
;

View File

@@ -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

View File

@@ -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()

View File

@@ -0,0 +1,4 @@
hello world
line1
line2

9
tests/string_puts.sl Normal file
View File

@@ -0,0 +1,9 @@
import ../stdlib/stdlib.sl
import ../stdlib/io.sl
: main
"hello world" puts
"line1\nline2" puts
"" puts
0
;

1
tests/string_puts.test Normal file
View File

@@ -0,0 +1 @@
python main.py tests/string_puts.sl -o /tmp/string_puts > /dev/null && /tmp/string_puts

66
tests/test.py Normal file
View File

@@ -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())

BIN
write

Binary file not shown.

Binary file not shown.