small refactor and cleanup
This commit is contained in:
Binary file not shown.
295
main.py
295
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")
|
||||
|
||||
|
||||
|
||||
8
mem.sl
8
mem.sl
@@ -1,8 +0,0 @@
|
||||
import stdlib/stdlib.sl
|
||||
import stdlib/io.sl
|
||||
import stdlib/debug.sl
|
||||
|
||||
: main
|
||||
mem dup 5 swap !
|
||||
@ puti cr
|
||||
;
|
||||
15
stdlib/mem.sl
Normal file
15
stdlib/mem.sl
Normal 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
|
||||
;
|
||||
Binary file not shown.
2
tests/alloc.expected
Normal file
2
tests/alloc.expected
Normal file
@@ -0,0 +1,2 @@
|
||||
111
|
||||
222
|
||||
@@ -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
1
tests/alloc.test
Normal file
@@ -0,0 +1 @@
|
||||
python main.py tests/alloc.sl -o /tmp/alloc > /dev/null && /tmp/alloc
|
||||
2
tests/call_syntax_parens.expected
Normal file
2
tests/call_syntax_parens.expected
Normal file
@@ -0,0 +1,2 @@
|
||||
42
|
||||
3
|
||||
16
tests/call_syntax_parens.sl
Normal file
16
tests/call_syntax_parens.sl
Normal 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;
|
||||
}
|
||||
1
tests/call_syntax_parens.test
Normal file
1
tests/call_syntax_parens.test
Normal 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
27
tests/fib.expected
Normal 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
|
||||
@@ -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
1
tests/fib.test
Normal file
@@ -0,0 +1 @@
|
||||
python main.py tests/fib.sl -o /tmp/fib > /dev/null && /tmp/fib
|
||||
33
tests/integration_core.expected
Normal file
33
tests/integration_core.expected
Normal 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
|
||||
@@ -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]
|
||||
1
tests/integration_core.test
Normal file
1
tests/integration_core.test
Normal file
@@ -0,0 +1 @@
|
||||
python main.py tests/integration_core.sl -o /tmp/integration_core > /dev/null && /tmp/integration_core
|
||||
1
tests/io_read_file.expected
Normal file
1
tests/io_read_file.expected
Normal file
@@ -0,0 +1 @@
|
||||
read_file works
|
||||
@@ -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
1
tests/io_read_file.test
Normal file
@@ -0,0 +1 @@
|
||||
python main.py tests/io_read_file.sl -o /tmp/io_read_file > /dev/null && /tmp/io_read_file
|
||||
1
tests/io_read_stdin.expected
Normal file
1
tests/io_read_stdin.expected
Normal file
@@ -0,0 +1 @@
|
||||
stdin via test
|
||||
@@ -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
1
tests/io_read_stdin.test
Normal 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
|
||||
1
tests/io_write_buf.expected
Normal file
1
tests/io_write_buf.expected
Normal file
@@ -0,0 +1 @@
|
||||
hello from write_buf test
|
||||
@@ -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
1
tests/io_write_buf.test
Normal file
@@ -0,0 +1 @@
|
||||
python main.py tests/io_write_buf.sl -o /tmp/io_write_buf > /dev/null && /tmp/io_write_buf
|
||||
2
tests/io_write_file.expected
Normal file
2
tests/io_write_file.expected
Normal file
@@ -0,0 +1,2 @@
|
||||
wrote bytes:
|
||||
27
|
||||
@@ -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
1
tests/io_write_file.test
Normal 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
10
tests/loop_while.expected
Normal file
@@ -0,0 +1,10 @@
|
||||
10
|
||||
9
|
||||
8
|
||||
7
|
||||
6
|
||||
5
|
||||
4
|
||||
3
|
||||
2
|
||||
1
|
||||
@@ -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
1
tests/loop_while.test
Normal file
@@ -0,0 +1 @@
|
||||
python main.py tests/loop_while.sl -o /tmp/loop_while > /dev/null && /tmp/loop_while
|
||||
3
tests/loops_and_cmp.expected
Normal file
3
tests/loops_and_cmp.expected
Normal file
@@ -0,0 +1,3 @@
|
||||
5
|
||||
1
|
||||
0
|
||||
13
tests/loops_and_cmp.sl
Normal file
13
tests/loops_and_cmp.sl
Normal 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
1
tests/loops_and_cmp.test
Normal 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
2
tests/mem.expected
Normal file
@@ -0,0 +1,2 @@
|
||||
5
|
||||
6
|
||||
@@ -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
1
tests/mem.test
Normal file
@@ -0,0 +1 @@
|
||||
python main.py tests/mem.sl -o /tmp/mem > /dev/null && /tmp/mem
|
||||
1
tests/override_dup_compile_time.expected
Normal file
1
tests/override_dup_compile_time.expected
Normal file
@@ -0,0 +1 @@
|
||||
6
|
||||
28
tests/override_dup_compile_time.sl
Normal file
28
tests/override_dup_compile_time.sl
Normal 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
|
||||
;
|
||||
1
tests/override_dup_compile_time.test
Normal file
1
tests/override_dup_compile_time.test
Normal 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
|
||||
@@ -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()
|
||||
4
tests/string_puts.expected
Normal file
4
tests/string_puts.expected
Normal file
@@ -0,0 +1,4 @@
|
||||
hello world
|
||||
line1
|
||||
line2
|
||||
|
||||
9
tests/string_puts.sl
Normal file
9
tests/string_puts.sl
Normal 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
1
tests/string_puts.test
Normal 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
66
tests/test.py
Normal 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_buf_test
BIN
write_buf_test
Binary file not shown.
Reference in New Issue
Block a user