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: @dataclass
"""Base class for all AST nodes.""" class Op:
"""Flat operation used for both compile-time execution and emission."""
op: str
data: Any = None
@dataclass @dataclass
class WordRef(ASTNode): class Definition:
name: str name: str
body: List[Op]
@dataclass
class Literal(ASTNode):
value: Any
@dataclass
class Definition(ASTNode):
name: str
body: List[ASTNode]
immediate: bool = False immediate: bool = False
compile_only: bool = False compile_only: bool = False
@dataclass @dataclass
class AsmDefinition(ASTNode): class AsmDefinition:
name: str name: str
body: str body: str
immediate: bool = False immediate: bool = False
@@ -218,8 +212,8 @@ class AsmDefinition(ASTNode):
@dataclass @dataclass
class Module(ASTNode): class Module:
forms: List[ASTNode] forms: List[Any]
@dataclass @dataclass
@@ -236,33 +230,6 @@ class StructField:
size: int 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: class MacroContext:
"""Small facade exposed to Python-defined macros.""" """Small facade exposed to Python-defined macros."""
@@ -280,12 +247,12 @@ class MacroContext:
return self._parser.peek_token() return self._parser.peek_token()
def emit_literal(self, value: int) -> None: 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: 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) self._parser.emit_node(node)
def inject_tokens(self, tokens: Sequence[str], template: Optional[Token] = None) -> None: def inject_tokens(self, tokens: Sequence[str], template: Optional[Token] = None) -> None:
@@ -316,7 +283,7 @@ class MacroContext:
return self._parser.most_recent_definition() return self._parser.most_recent_definition()
MacroHandler = Callable[[MacroContext], Optional[List[ASTNode]]] MacroHandler = Callable[[MacroContext], Optional[List[Op]]]
IntrinsicEmitter = Callable[["FunctionEmitter"], None] IntrinsicEmitter = Callable[["FunctionEmitter"], None]
@@ -390,8 +357,8 @@ class Parser:
self._ensure_tokens(self.pos) self._ensure_tokens(self.pos)
return None if self._eof() else self.tokens[self.pos] return None if self._eof() else self.tokens[self.pos]
def emit_node(self, node: ASTNode) -> None: def emit_node(self, node: Op) -> None:
self._append_node(node) self._append_op(node)
def most_recent_definition(self) -> Optional[Word]: def most_recent_definition(self) -> Optional[Word]:
return self.last_defined return self.last_defined
@@ -406,18 +373,18 @@ class Parser:
if entry["type"] == "if": if entry["type"] == "if":
# For if without else # For if without else
if "false" in entry: if "false" in entry:
self._append_node(Label(name=entry["false"])) self._append_op(Op(op="label", data=entry["false"]))
elif entry["type"] == "else": elif entry["type"] == "else":
self._append_node(Label(name=entry["end"])) self._append_op(Op(op="label", data=entry["end"]))
elif entry["type"] == "while": elif entry["type"] == "while":
self._append_node(Jump(target=entry["begin"])) self._append_op(Op(op="jump", data=entry["begin"]))
self._append_node(Label(name=entry["end"])) self._append_op(Op(op="label", data=entry["end"]))
elif entry["type"] == "for": elif entry["type"] == "for":
# Emit ForEnd node for loop decrement # 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": elif entry["type"] == "begin":
self._append_node(Jump(target=entry["begin"])) self._append_op(Op(op="jump", data=entry["begin"]))
self._append_node(Label(name=entry["end"])) self._append_op(Op(op="label", data=entry["end"]))
# Parsing ------------------------------------------------------------------ # Parsing ------------------------------------------------------------------
def parse(self, tokens: Iterable[Token], source: str) -> Module: def parse(self, tokens: Iterable[Token], source: str) -> Module:
@@ -608,12 +575,12 @@ class Parser:
produced = word.macro(MacroContext(self)) produced = word.macro(MacroContext(self))
if produced: if produced:
for node in produced: for node in produced:
self._append_node(node) self._append_op(node)
else: else:
self._execute_immediate_word(word) self._execute_immediate_word(word)
return 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: def _execute_immediate_word(self, word: Word) -> None:
try: try:
@@ -721,31 +688,31 @@ class Parser:
def _handle_if_control(self) -> None: def _handle_if_control(self) -> None:
false_label = self._new_label("if_false") 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}) self._push_control({"type": "if", "false": false_label})
def _handle_else_control(self) -> None: def _handle_else_control(self) -> None:
entry = self._pop_control(("if",)) entry = self._pop_control(("if",))
end_label = self._new_label("if_end") end_label = self._new_label("if_end")
self._append_node(Jump(target=end_label)) self._append_op(Op(op="jump", data=end_label))
self._append_node(Label(name=entry["false"])) self._append_op(Op(op="label", data=entry["false"]))
self._push_control({"type": "else", "end": end_label}) self._push_control({"type": "else", "end": end_label})
def _handle_for_control(self) -> None: def _handle_for_control(self) -> None:
loop_label = self._new_label("for_loop") loop_label = self._new_label("for_loop")
end_label = self._new_label("for_end") 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}) self._push_control({"type": "for", "loop": loop_label, "end": end_label})
def _handle_while_control(self) -> None: def _handle_while_control(self) -> None:
begin_label = self._new_label("begin") begin_label = self._new_label("begin")
end_label = self._new_label("end") 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}) self._push_control({"type": "begin", "begin": begin_label, "end": end_label})
def _handle_do_control(self) -> None: def _handle_do_control(self) -> None:
entry = self._pop_control(("begin",)) 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) self._push_control(entry)
def _begin_definition(self, token: Token) -> None: def _begin_definition(self, token: Token) -> None:
@@ -859,7 +826,7 @@ class Parser:
def _py_exec_namespace(self) -> Dict[str, Any]: def _py_exec_namespace(self) -> Dict[str, Any]:
return dict(PY_EXEC_GLOBALS) return dict(PY_EXEC_GLOBALS)
def _append_node(self, node: ASTNode) -> None: def _append_op(self, node: Op) -> None:
target = self.context_stack[-1] target = self.context_stack[-1]
if isinstance(target, Module): if isinstance(target, Module):
target.forms.append(node) target.forms.append(node)
@@ -868,10 +835,10 @@ class Parser:
else: # pragma: no cover - defensive else: # pragma: no cover - defensive
raise ParseError("unknown parse context") raise ParseError("unknown parse context")
def _try_literal(self, token: Token) -> None: def _try_literal(self, token: Token) -> bool:
try: try:
value = int(token.lexeme, 0) value = int(token.lexeme, 0)
self._append_node(Literal(value=value)) self._append_op(Op(op="literal", data=value))
return True return True
except ValueError: except ValueError:
pass pass
@@ -880,14 +847,14 @@ class Parser:
try: try:
if "." in token.lexeme or "e" in token.lexeme.lower(): if "." in token.lexeme or "e" in token.lexeme.lower():
value = float(token.lexeme) value = float(token.lexeme)
self._append_node(Literal(value=value)) self._append_op(Op(op="literal", data=value))
return True return True
except ValueError: except ValueError:
pass pass
string_value = _parse_string_literal(token) string_value = _parse_string_literal(token)
if string_value is not None: 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 True
return False return False
@@ -1229,7 +1196,7 @@ class CompileTimeVM:
raise ParseError(f"unknown word '{name}' during compile-time execution") raise ParseError(f"unknown word '{name}' during compile-time execution")
self._call_word(word) 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) label_positions = self._label_positions(nodes)
loop_pairs = self._for_pairs(nodes) loop_pairs = self._for_pairs(nodes)
begin_pairs = self._begin_pairs(nodes) begin_pairs = self._begin_pairs(nodes)
@@ -1238,12 +1205,16 @@ class CompileTimeVM:
ip = 0 ip = 0
while ip < len(nodes): while ip < len(nodes):
node = nodes[ip] node = nodes[ip]
if isinstance(node, Literal): kind = node.op
self.push(node.value) data = node.data
if kind == "literal":
self.push(data)
ip += 1 ip += 1
continue continue
if isinstance(node, WordRef):
name = node.name if kind == "word":
name = str(data)
if name == "begin": if name == "begin":
end_idx = begin_pairs.get(ip) end_idx = begin_pairs.get(ip)
if end_idx is None: if end_idx is None:
@@ -1270,9 +1241,9 @@ class CompileTimeVM:
self._call_word_by_name(name) self._call_word_by_name(name)
ip += 1 ip += 1
continue continue
if isinstance(node, BranchZero):
if kind == "branch_zero":
condition = self.pop() condition = self.pop()
flag: bool
if isinstance(condition, bool): if isinstance(condition, bool):
flag = condition flag = condition
elif isinstance(condition, int): elif isinstance(condition, int):
@@ -1280,17 +1251,20 @@ class CompileTimeVM:
else: else:
raise ParseError("branch expects integer or boolean condition") raise ParseError("branch expects integer or boolean condition")
if not flag: if not flag:
ip = self._jump_to_label(label_positions, node.target) ip = self._jump_to_label(label_positions, str(data))
else: else:
ip += 1 ip += 1
continue 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 continue
if isinstance(node, Label):
if kind == "label":
ip += 1 ip += 1
continue continue
if isinstance(node, ForBegin):
if kind == "for_begin":
count = self.pop_int() count = self.pop_int()
if count <= 0: if count <= 0:
match = loop_pairs.get(ip) match = loop_pairs.get(ip)
@@ -1301,7 +1275,8 @@ class CompileTimeVM:
self.loop_stack.append({"remaining": count, "begin": ip, "initial": count}) self.loop_stack.append({"remaining": count, "begin": ip, "initial": count})
ip += 1 ip += 1
continue continue
if isinstance(node, ForEnd):
if kind == "for_end":
if not self.loop_stack: if not self.loop_stack:
raise ParseError("'next' without matching 'for'") raise ParseError("'next' without matching 'for'")
frame = self.loop_stack[-1] frame = self.loop_stack[-1]
@@ -1312,22 +1287,23 @@ class CompileTimeVM:
self.loop_stack.pop() self.loop_stack.pop()
ip += 1 ip += 1
continue 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] = {} positions: Dict[str, int] = {}
for idx, node in enumerate(nodes): for idx, node in enumerate(nodes):
if isinstance(node, Label): if node.op == "label":
positions[node.name] = idx positions[str(node.data)] = idx
return positions 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] = [] stack: List[int] = []
pairs: Dict[int, int] = {} pairs: Dict[int, int] = {}
for idx, node in enumerate(nodes): for idx, node in enumerate(nodes):
if isinstance(node, ForBegin): if node.op == "for_begin":
stack.append(idx) stack.append(idx)
elif isinstance(node, ForEnd): elif node.op == "for_end":
if not stack: if not stack:
raise ParseError("'next' without matching 'for'") raise ParseError("'next' without matching 'for'")
begin_idx = stack.pop() begin_idx = stack.pop()
@@ -1337,13 +1313,13 @@ class CompileTimeVM:
raise ParseError("'for' without matching 'next'") raise ParseError("'for' without matching 'next'")
return pairs 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] = [] stack: List[int] = []
pairs: Dict[int, int] = {} pairs: Dict[int, int] = {}
for idx, node in enumerate(nodes): 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) stack.append(idx)
elif isinstance(node, WordRef) and node.name == "again": elif node.op == "word" and node.data == "again":
if not stack: if not stack:
raise ParseError("'again' without matching 'begin'") raise ParseError("'again' without matching 'begin'")
begin_idx = stack.pop() begin_idx = stack.pop()
@@ -1611,48 +1587,57 @@ class Assembler:
else: else:
builder.emit("") builder.emit("")
def _emit_node(self, node: ASTNode, builder: FunctionEmitter) -> None: def _emit_node(self, node: Op, builder: FunctionEmitter) -> None:
if isinstance(node, Literal): kind = node.op
if isinstance(node.value, int): data = node.data
builder.push_literal(node.value)
if kind == "literal":
if isinstance(data, int):
builder.push_literal(data)
return return
if isinstance(node.value, float): if isinstance(data, float):
label = self._intern_float_literal(node.value) label = self._intern_float_literal(data)
builder.push_float(label) builder.push_float(label)
return return
if isinstance(node.value, str): if isinstance(data, str):
label, length = self._intern_string_literal(node.value) label, length = self._intern_string_literal(data)
builder.push_label(label) builder.push_label(label)
builder.push_literal(length) builder.push_literal(length)
return return
raise CompileError(f"unsupported literal type {type(node.value)!r}") raise CompileError(f"unsupported literal type {type(data)!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}")
def _emit_wordref(self, ref: WordRef, builder: FunctionEmitter) -> None: if kind == "word":
word = self.dictionary.lookup(ref.name) 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: if word is None:
raise CompileError(f"unknown word '{ref.name}'") raise CompileError(f"unknown word '{name}'")
if word.compile_only: 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: if word.intrinsic:
word.intrinsic(builder) word.intrinsic(builder)
return return
@@ -1669,7 +1654,7 @@ class Assembler:
ret_type = signature[1] if signature else None ret_type = signature[1] if signature else None
if len(arg_types) != inputs and signature: 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 int_idx = 0
xmm_idx = 0 xmm_idx = 0
@@ -1679,19 +1664,19 @@ class Assembler:
if not arg_types: if not arg_types:
# Legacy/Raw mode: assume all ints # Legacy/Raw mode: assume all ints
if inputs > 6: 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): for i in range(inputs):
mapping.append(("int", regs[i])) mapping.append(("int", regs[i]))
else: else:
for type_name in arg_types: for type_name in arg_types:
if type_name in ("float", "double"): if type_name in ("float", "double"):
if xmm_idx >= 8: 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])) mapping.append(("float", xmm_regs[xmm_idx]))
xmm_idx += 1 xmm_idx += 1
else: else:
if int_idx >= 6: 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])) mapping.append(("int", regs[int_idx]))
int_idx += 1 int_idx += 1
@@ -1706,7 +1691,7 @@ class Assembler:
builder.emit(" mov rbp, rsp") builder.emit(" mov rbp, rsp")
builder.emit(" and rsp, -16") builder.emit(" and rsp, -16")
builder.emit(f" mov al, {xmm_idx}") builder.emit(f" mov al, {xmm_idx}")
builder.emit(f" call {ref.name}") builder.emit(f" call {name}")
builder.emit(" leave") builder.emit(" leave")
# Handle Return Value # Handle Return Value
@@ -1721,30 +1706,34 @@ class Assembler:
raise CompileError("extern only supports 0 or 1 output") raise CompileError("extern only supports 0 or 1 output")
else: else:
# Emit call to unresolved symbol (let linker resolve it) # Emit call to unresolved symbol (let linker resolve it)
builder.emit(f" call {ref.name}") builder.emit(f" call {name}")
else: 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.pop_to("rax")
builder.emit(" test rax, 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.pop_to("rax")
builder.emit(" cmp rax, 0") 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(" sub r13, 8")
builder.emit(" mov [r13], rax") 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(" mov rax, [r13]")
builder.emit(" dec rax") builder.emit(" dec rax")
builder.emit(" mov [r13], 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(" add r13, 8")
builder.emit(f"{node.end_label}:") builder.emit(f"{end_label}:")
def _runtime_prelude(self) -> List[str]: def _runtime_prelude(self) -> List[str]:
return [ 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 parser = ctx.parser
word = parser.most_recent_definition() word = parser.most_recent_definition()
if word is None: if word is None:
@@ -1815,7 +1804,7 @@ def macro_immediate(ctx: MacroContext) -> Optional[List[ASTNode]]:
return None return None
def macro_compile_only(ctx: MacroContext) -> Optional[List[ASTNode]]: def macro_compile_only(ctx: MacroContext) -> Optional[List[Op]]:
parser = ctx.parser parser = ctx.parser
word = parser.most_recent_definition() word = parser.most_recent_definition()
if word is None: if word is None:
@@ -1826,7 +1815,7 @@ def macro_compile_only(ctx: MacroContext) -> Optional[List[ASTNode]]:
return None 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.""" """Run the next word at compile time and still emit it for runtime."""
parser = ctx.parser parser = ctx.parser
if parser._eof(): 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") raise ParseError(f"word '{name}' is compile-time only")
parser.compile_time_vm.invoke(word) parser.compile_time_vm.invoke(word)
if isinstance(parser.context_stack[-1], Definition): if isinstance(parser.context_stack[-1], Definition):
parser.emit_node(WordRef(name=name)) parser.emit_node(Op(op="word", data=name))
return None 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 parser = ctx.parser
if parser._eof(): if parser._eof():
raise ParseError("macro name missing after 'macro:'") raise ParseError("macro name missing after 'macro:'")
@@ -1861,7 +1850,7 @@ def macro_begin_text_macro(ctx: MacroContext) -> Optional[List[ASTNode]]:
return None 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 parser = ctx.parser
if parser.macro_recording is None: if parser.macro_recording is None:
raise ParseError("';macro' without matching 'macro:'") raise ParseError("';macro' without matching 'macro:'")
@@ -2458,13 +2447,7 @@ def _register_compile_time_primitives(dictionary: Dictionary) -> None:
PY_EXEC_GLOBALS: Dict[str, Any] = { PY_EXEC_GLOBALS: Dict[str, Any] = {
"MacroContext": MacroContext, "MacroContext": MacroContext,
"Token": Token, "Token": Token,
"Literal": Literal, "Op": Op,
"WordRef": WordRef,
"BranchZero": BranchZero,
"Jump": Jump,
"Label": Label,
"ForBegin": ForBegin,
"ForEnd": ForEnd,
"StructField": StructField, "StructField": StructField,
"Definition": Definition, "Definition": Definition,
"Module": Module, "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 parser = ctx.parser
if parser._eof(): if parser._eof():
raise ParseError("struct name missing after 'struct:'") raise ParseError("struct name missing after 'struct:'")
@@ -2529,7 +2512,7 @@ def macro_struct_begin(ctx: MacroContext) -> Optional[List[ASTNode]]:
return None 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") 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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
import ../stdlib/mem.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
;
: test-mem-alloc : test-mem-alloc
4096 alloc dup 1337 swap ! # allocate 4096 bytes, store 1337 at start 4096 alloc dup 1337 swap ! # allocate 4096 bytes, store 1337 at start
@@ -26,7 +13,7 @@ struct: Point
field y 8 field y 8
;struct ;struct
: main2 : main
32 alloc # allocate 32 bytes (enough for a Point struct) 32 alloc # allocate 32 bytes (enough for a Point struct)
dup 111 swap Point.x! dup 111 swap Point.x!
dup 222 swap Point.y! 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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
import ../stdlib/debug.sl
: main : main
1 1 2dup 2dup puti cr puti cr 1 1 2dup 2dup puti cr puti cr
@@ -13,6 +14,7 @@ import stdlib/io.sl
"-------" puts "-------" puts
r> 3 + puti r> 3 + puti
" numbers printed from the fibonaci sequence" puts " numbers printed from the fibonaci sequence" puts
0
; ;
: main2 : 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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
import fn.sl import ../fn.sl
:asm mem-slot { :asm mem-slot {
lea rax, [rel print_buf] 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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
: main : 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) read_file # (file_addr file_len)
dup 0 > if # if file_len > 0, success dup 0 > if # if file_len > 0, success
write_buf # print file contents (file_len file_addr) 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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
: main : main
1024 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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
: main : main
"hello from write_buf test\n" "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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
: main : main
"/tmp/l2_test_write.txt" # push path (addr len) "/tmp/l2_write_file_test.txt" # path
"hello from write_file test\n" # push buf (addr len) "hello from write_file test\n" # buffer
write_file write_file
dup 0 > if dup 0 > if
"wrote bytes: " puts "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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
: main : main
10 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/stdlib.sl
import stdlib/io.sl import ../stdlib/io.sl
: main : main
mem 5 swap ! 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.