docs: update CURRENT_TASK with Box Theory PHI plan (defer/finalize) and MIR v0.5 type meta; add parity tooling and PyVM scaffolding
impl(pyvm/llvmlite): - add tools/parity.sh; tools/pyvm_runner.py; src/llvm_py/pyvm/* - emit string const as handle type in MIR JSON; add dst_type hints - unify '+' to concat_hh with from_i64/from_i8_string bridges; console print via to_i8p_h - add runtime bridges: nyash.box.from_i64, nyash.string.to_i8p_h tests: - add apps/tests/min_str_cat_loop (minimal repro for string cat loop)
This commit is contained in:
7
src/llvm_py/__init__.py
Normal file
7
src/llvm_py/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""Top-level package for Nyash Python backends.
|
||||
|
||||
Subpackages:
|
||||
- pyvm: Python MIR interpreter (PyVM)
|
||||
- instructions/*: llvmlite lowering helpers (AOT harness)
|
||||
"""
|
||||
|
||||
@ -69,6 +69,7 @@ def lower_binop(
|
||||
i8p = ir.IntType(8).as_pointer()
|
||||
lhs_raw = vmap.get(lhs)
|
||||
rhs_raw = vmap.get(rhs)
|
||||
# Prefer handle pipeline to keep handles consistent across blocks/ret
|
||||
# pointer present?
|
||||
is_ptr_side = (hasattr(lhs_raw, 'type') and isinstance(lhs_raw.type, ir.PointerType)) or \
|
||||
(hasattr(rhs_raw, 'type') and isinstance(rhs_raw.type, ir.PointerType))
|
||||
@ -86,7 +87,7 @@ def lower_binop(
|
||||
is_str = is_ptr_side or any_tagged
|
||||
if is_str:
|
||||
# Helper: convert raw or resolved value to string handle
|
||||
def to_handle(raw, val, tag: str):
|
||||
def to_handle(raw, val, tag: str, vid: int):
|
||||
if raw is not None and hasattr(raw, 'type') and isinstance(raw.type, ir.PointerType):
|
||||
# pointer-to-array -> GEP
|
||||
try:
|
||||
@ -104,11 +105,29 @@ def lower_binop(
|
||||
return builder.call(cal, [raw], name=f"str_ptr2h_{tag}_{dst}")
|
||||
# if already i64
|
||||
if val is not None and hasattr(val, 'type') and isinstance(val.type, ir.IntType) and val.type.width == 64:
|
||||
return val
|
||||
# Distinguish handle vs numeric: if vid is tagged string-ish, treat as handle; otherwise box numeric to handle
|
||||
is_tag = False
|
||||
try:
|
||||
if resolver is not None and hasattr(resolver, 'is_stringish'):
|
||||
is_tag = resolver.is_stringish(vid)
|
||||
except Exception:
|
||||
is_tag = False
|
||||
if is_tag:
|
||||
return val
|
||||
# Box numeric i64 to IntegerBox handle
|
||||
cal = None
|
||||
for f in builder.module.functions:
|
||||
if f.name == 'nyash.box.from_i64':
|
||||
cal = f; break
|
||||
if cal is None:
|
||||
cal = ir.Function(builder.module, ir.FunctionType(i64, [i64]), name='nyash.box.from_i64')
|
||||
# Ensure value is i64
|
||||
v64 = val if val.type.width == 64 else builder.zext(val, i64)
|
||||
return builder.call(cal, [v64], name=f"int_i2h_{tag}_{dst}")
|
||||
return ir.Constant(i64, 0)
|
||||
|
||||
hl = to_handle(lhs_raw, lhs_val, 'l')
|
||||
hr = to_handle(rhs_raw, rhs_val, 'r')
|
||||
hl = to_handle(lhs_raw, lhs_val, 'l', lhs)
|
||||
hr = to_handle(rhs_raw, rhs_val, 'r', rhs)
|
||||
# concat_hh(handle, handle) -> handle
|
||||
hh_fnty = ir.FunctionType(i64, [i64, i64])
|
||||
callee = None
|
||||
|
||||
@ -131,6 +131,8 @@ def lower_boxcall(
|
||||
try:
|
||||
if resolver is not None and hasattr(resolver, 'mark_string'):
|
||||
resolver.mark_string(dst_vid)
|
||||
if resolver is not None and hasattr(resolver, 'string_ptrs'):
|
||||
resolver.string_ptrs[int(dst_vid)] = p
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
@ -196,27 +198,42 @@ def lower_boxcall(
|
||||
return
|
||||
|
||||
if method_name in ("print", "println", "log"):
|
||||
# Console mapping
|
||||
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
arg0 = resolver.resolve_i64(args[0], builder.block, preds, block_end_values, vmap, bb_map) if args else None
|
||||
else:
|
||||
arg0 = vmap.get(args[0]) if args else None
|
||||
if arg0 is None:
|
||||
arg0 = ir.Constant(i8p, None)
|
||||
# Prefer handle API if arg is i64, else pointer API
|
||||
if hasattr(arg0, 'type') and isinstance(arg0.type, ir.IntType) and arg0.type.width == 64:
|
||||
# Optional runtime trace of the handle
|
||||
import os as _os
|
||||
if _os.environ.get('NYASH_LLVM_TRACE_FINAL') == '1':
|
||||
trace = _declare(module, "nyash.debug.trace_handle", i64, [i64])
|
||||
_ = builder.call(trace, [arg0], name="trace_handle")
|
||||
callee = _declare(module, "nyash.console.log_handle", i64, [i64])
|
||||
_ = builder.call(callee, [arg0], name="console_log_h")
|
||||
else:
|
||||
if hasattr(arg0, 'type') and isinstance(arg0.type, ir.IntType):
|
||||
arg0 = builder.inttoptr(arg0, i8p)
|
||||
# Console mapping (prefer pointer-API when possible to avoid handle registry mismatch)
|
||||
use_ptr = False
|
||||
arg0_vid = args[0] if args else None
|
||||
arg0_ptr = None
|
||||
if resolver is not None and hasattr(resolver, 'string_ptrs') and arg0_vid is not None:
|
||||
try:
|
||||
arg0_ptr = resolver.string_ptrs.get(int(arg0_vid))
|
||||
if arg0_ptr is not None:
|
||||
use_ptr = True
|
||||
except Exception:
|
||||
pass
|
||||
if use_ptr and arg0_ptr is not None:
|
||||
callee = _declare(module, "nyash.console.log", i64, [i8p])
|
||||
_ = builder.call(callee, [arg0], name="console_log")
|
||||
_ = builder.call(callee, [arg0_ptr], name="console_log_ptr")
|
||||
else:
|
||||
# Fallback: resolve i64 and prefer pointer API via to_i8p_h bridge
|
||||
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
arg0 = resolver.resolve_i64(args[0], builder.block, preds, block_end_values, vmap, bb_map) if args else None
|
||||
else:
|
||||
arg0 = vmap.get(args[0]) if args else None
|
||||
if arg0 is None:
|
||||
arg0 = ir.Constant(i64, 0)
|
||||
# If we have a handle (i64), convert to i8* via bridge and log via pointer API
|
||||
if hasattr(arg0, 'type') and isinstance(arg0.type, ir.IntType):
|
||||
if arg0.type.width != 64:
|
||||
arg0 = builder.zext(arg0, i64)
|
||||
bridge = _declare(module, "nyash.string.to_i8p_h", i8p, [i64])
|
||||
p = builder.call(bridge, [arg0], name="str_h2p_for_log")
|
||||
callee = _declare(module, "nyash.console.log", i64, [i8p])
|
||||
_ = builder.call(callee, [p], name="console_log_p")
|
||||
else:
|
||||
# Non-integer value: coerce to i8* and log
|
||||
if hasattr(arg0, 'type') and isinstance(arg0.type, ir.IntType):
|
||||
arg0 = builder.inttoptr(arg0, i8p)
|
||||
callee = _declare(module, "nyash.console.log", i64, [i8p])
|
||||
_ = builder.call(callee, [arg0], name="console_log")
|
||||
if dst_vid is not None:
|
||||
vmap[dst_vid] = ir.Constant(i64, 0)
|
||||
return
|
||||
|
||||
@ -107,5 +107,18 @@ def lower_call(
|
||||
'esc_json', 'node_json', 'dirname', 'join', 'read_all', 'toJson'
|
||||
]):
|
||||
resolver.mark_string(dst_vid)
|
||||
# Additionally, create a pointer view via bridge for println pointer-API
|
||||
if resolver is not None and hasattr(resolver, 'string_ptrs'):
|
||||
i64 = ir.IntType(64)
|
||||
i8p = ir.IntType(8).as_pointer()
|
||||
if hasattr(result, 'type') and isinstance(result.type, ir.IntType) and result.type.width == 64:
|
||||
bridge = None
|
||||
for f in module.functions:
|
||||
if f.name == 'nyash.string.to_i8p_h':
|
||||
bridge = f; break
|
||||
if bridge is None:
|
||||
bridge = ir.Function(module, ir.FunctionType(i8p, [i64]), name='nyash.string.to_i8p_h')
|
||||
pv = builder.call(bridge, [result], name=f"ret_h2p_{dst_vid}")
|
||||
resolver.string_ptrs[int(dst_vid)] = pv
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -39,7 +39,7 @@ def lower_const(
|
||||
llvm_val = ir.Constant(f64, float(const_val))
|
||||
vmap[dst] = llvm_val
|
||||
|
||||
elif const_type == 'string':
|
||||
elif const_type == 'string' or (isinstance(const_type, dict) and const_type.get('kind') in ('handle','ptr') and const_type.get('box_type') == 'StringBox'):
|
||||
# String constant - create global and immediately box to i64 handle
|
||||
i8 = ir.IntType(8)
|
||||
str_val = str(const_val)
|
||||
@ -82,6 +82,11 @@ def lower_const(
|
||||
# Mark this value-id as string-ish to guide '+' and '==' lowering
|
||||
if hasattr(resolver, 'mark_string'):
|
||||
resolver.mark_string(dst)
|
||||
# Keep raw pointer for potential pointer-API sites (e.g., console.log)
|
||||
try:
|
||||
resolver.string_ptrs[dst] = gep
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elif const_type == 'void':
|
||||
# Void/null constant - use i64 zero
|
||||
|
||||
@ -58,6 +58,7 @@ def lower_phi(
|
||||
|
||||
# Collect incoming values
|
||||
incoming_pairs: List[Tuple[ir.Block, ir.Value]] = []
|
||||
used_default_zero = False
|
||||
for block_id in actual_preds:
|
||||
block = bb_map.get(block_id)
|
||||
vid = incoming_map.get(block_id)
|
||||
@ -76,6 +77,7 @@ def lower_phi(
|
||||
if val is None:
|
||||
# Missing incoming for this predecessor → default 0
|
||||
val = ir.Constant(phi_type, 0)
|
||||
used_default_zero = True
|
||||
else:
|
||||
# Snapshot fallback
|
||||
if block_end_values is not None:
|
||||
@ -86,6 +88,7 @@ def lower_phi(
|
||||
if not val:
|
||||
# Missing incoming for this predecessor → default 0
|
||||
val = ir.Constant(phi_type, 0)
|
||||
used_default_zero = True
|
||||
# Coerce pointer to i64 at predecessor end
|
||||
if hasattr(val, 'type') and val.type != phi_type:
|
||||
pb = ir.IRBuilder(block)
|
||||
@ -127,6 +130,16 @@ def lower_phi(
|
||||
|
||||
# Store PHI result
|
||||
vmap[dst_vid] = phi
|
||||
# Strict mode: fail fast on synthesized zeros (indicates incomplete incoming or dominance issue)
|
||||
import os
|
||||
if used_default_zero and os.environ.get('NYASH_LLVM_PHI_STRICT') == '1':
|
||||
raise RuntimeError(f"[LLVM_PY] PHI dst={dst_vid} used synthesized zero; check preds/incoming")
|
||||
if os.environ.get('NYASH_LLVM_TRACE_PHI') == '1':
|
||||
try:
|
||||
blkname = str(current_block.name)
|
||||
except Exception:
|
||||
blkname = '<blk>'
|
||||
print(f"[PHI] {blkname} v{dst_vid} incoming={len(incoming_pairs)} zero={1 if used_default_zero else 0}")
|
||||
# Propagate string-ness: if any incoming value-id is tagged string-ish, mark dst as string-ish.
|
||||
try:
|
||||
if resolver is not None and hasattr(resolver, 'is_stringish') and hasattr(resolver, 'mark_string'):
|
||||
|
||||
@ -30,12 +30,36 @@ def lower_return(
|
||||
builder.ret_void()
|
||||
else:
|
||||
# Get return value (prefer resolver)
|
||||
ret_val = None
|
||||
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
if isinstance(return_type, ir.PointerType):
|
||||
ret_val = resolver.resolve_ptr(value_id, builder.block, preds, block_end_values, vmap)
|
||||
else:
|
||||
ret_val = resolver.resolve_i64(value_id, builder.block, preds, block_end_values, vmap, bb_map)
|
||||
else:
|
||||
try:
|
||||
if isinstance(return_type, ir.PointerType):
|
||||
ret_val = resolver.resolve_ptr(value_id, builder.block, preds, block_end_values, vmap)
|
||||
else:
|
||||
# Prefer pointer→handle reboxing for string-ish returns even if function return type is i64
|
||||
is_stringish = False
|
||||
if hasattr(resolver, 'is_stringish'):
|
||||
try:
|
||||
is_stringish = resolver.is_stringish(int(value_id))
|
||||
except Exception:
|
||||
is_stringish = False
|
||||
if is_stringish and hasattr(resolver, 'string_ptrs') and int(value_id) in getattr(resolver, 'string_ptrs'):
|
||||
# Re-box known string pointer to handle
|
||||
p = resolver.string_ptrs[int(value_id)]
|
||||
i8p = ir.IntType(8).as_pointer()
|
||||
i64 = ir.IntType(64)
|
||||
boxer = None
|
||||
for f in builder.module.functions:
|
||||
if f.name == 'nyash.box.from_i8_string':
|
||||
boxer = f; break
|
||||
if boxer is None:
|
||||
boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string')
|
||||
ret_val = builder.call(boxer, [p], name='ret_ptr2h')
|
||||
else:
|
||||
ret_val = resolver.resolve_i64(value_id, builder.block, preds, block_end_values, vmap, bb_map)
|
||||
except Exception:
|
||||
ret_val = None
|
||||
if ret_val is None:
|
||||
ret_val = vmap.get(value_id)
|
||||
if not ret_val:
|
||||
# Default based on return type
|
||||
|
||||
@ -140,7 +140,22 @@ class NyashLLVMBuilder:
|
||||
else:
|
||||
b.ret(ir.Constant(self.i32, 0))
|
||||
|
||||
return str(self.module)
|
||||
ir_text = str(self.module)
|
||||
# Optional IR dump to file for debugging
|
||||
try:
|
||||
dump_path = os.environ.get('NYASH_LLVM_DUMP_IR')
|
||||
if dump_path:
|
||||
os.makedirs(os.path.dirname(dump_path), exist_ok=True)
|
||||
with open(dump_path, 'w') as f:
|
||||
f.write(ir_text)
|
||||
elif os.environ.get('NYASH_CLI_VERBOSE') == '1':
|
||||
# Default dump location when verbose and not explicitly set
|
||||
os.makedirs('tmp', exist_ok=True)
|
||||
with open('tmp/nyash_harness.ll', 'w') as f:
|
||||
f.write(ir_text)
|
||||
except Exception:
|
||||
pass
|
||||
return ir_text
|
||||
|
||||
def _create_dummy_main(self) -> str:
|
||||
"""Create dummy ny_main that returns 0"""
|
||||
@ -185,6 +200,8 @@ class NyashLLVMBuilder:
|
||||
self.resolver.string_ids.clear()
|
||||
if hasattr(self.resolver, 'string_literals'):
|
||||
self.resolver.string_literals.clear()
|
||||
if hasattr(self.resolver, 'string_ptrs'):
|
||||
self.resolver.string_ptrs.clear()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -403,6 +420,15 @@ class NyashLLVMBuilder:
|
||||
dst = inst.get("dst")
|
||||
lower_boxcall(builder, self.module, box_vid, method, args, dst,
|
||||
self.vmap, self.resolver, self.preds, self.block_end_values, self.bb_map)
|
||||
# Optional: honor explicit dst_type for tagging (string handle)
|
||||
try:
|
||||
dst_type = inst.get("dst_type")
|
||||
if dst is not None and isinstance(dst_type, dict):
|
||||
if dst_type.get("kind") == "handle" and dst_type.get("box_type") == "StringBox":
|
||||
if hasattr(self.resolver, 'mark_string'):
|
||||
self.resolver.mark_string(int(dst))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elif op == "externcall":
|
||||
func_name = inst.get("func")
|
||||
@ -661,7 +687,7 @@ def main():
|
||||
|
||||
llvm_ir = builder.build_from_mir(mir_json)
|
||||
if os.environ.get('NYASH_CLI_VERBOSE') == '1':
|
||||
print(f"[Python LLVM] Generated LLVM IR:\n{llvm_ir}")
|
||||
print(f"[Python LLVM] Generated LLVM IR (see NYASH_LLVM_DUMP_IR or tmp/nyash_harness.ll)")
|
||||
|
||||
builder.compile_to_object(output_file)
|
||||
print(f"Compiled to {output_file}")
|
||||
|
||||
6
src/llvm_py/pyvm/__init__.py
Normal file
6
src/llvm_py/pyvm/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""PyVM package scaffold for Nyash MIR interpreter (Python).
|
||||
|
||||
Modules:
|
||||
- vm: Tiny interpreter for MIR(JSON) produced by runner's mir_json_emit
|
||||
"""
|
||||
|
||||
390
src/llvm_py/pyvm/vm.py
Normal file
390
src/llvm_py/pyvm/vm.py
Normal file
@ -0,0 +1,390 @@
|
||||
"""
|
||||
Minimal Python VM for Nyash MIR(JSON) parity with llvmlite.
|
||||
|
||||
Supported ops (MVP):
|
||||
- const/binop/compare/branch/jump/ret
|
||||
- phi (select by predecessor block)
|
||||
- newbox: ConsoleBox, StringBox (minimal semantics)
|
||||
- boxcall: String.length/substring/lastIndexOf, Console.print/println/log
|
||||
- externcall: nyash.console.println
|
||||
|
||||
Value model:
|
||||
- i64 -> Python int
|
||||
- f64 -> Python float
|
||||
- string -> Python str
|
||||
- void/null -> None
|
||||
- ConsoleBox -> {"__box__":"ConsoleBox"}
|
||||
- StringBox receiver -> Python str
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
import os
|
||||
|
||||
|
||||
@dataclass
|
||||
class Block:
|
||||
id: int
|
||||
instructions: List[Dict[str, Any]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Function:
|
||||
name: str
|
||||
params: List[int]
|
||||
blocks: Dict[int, Block]
|
||||
|
||||
|
||||
class PyVM:
|
||||
def __init__(self, program: Dict[str, Any]):
|
||||
self.functions: Dict[str, Function] = {}
|
||||
for f in program.get("functions", []):
|
||||
name = f.get("name")
|
||||
params = [int(p) for p in f.get("params", [])]
|
||||
bmap: Dict[int, Block] = {}
|
||||
for bb in f.get("blocks", []):
|
||||
bmap[int(bb.get("id"))] = Block(id=int(bb.get("id")), instructions=list(bb.get("instructions", [])))
|
||||
self.functions[name] = Function(name=name, params=params, blocks=bmap)
|
||||
|
||||
def _read(self, regs: Dict[int, Any], v: Optional[int]) -> Any:
|
||||
if v is None:
|
||||
return None
|
||||
return regs.get(int(v))
|
||||
|
||||
def _set(self, regs: Dict[int, Any], dst: Optional[int], val: Any) -> None:
|
||||
if dst is None:
|
||||
return
|
||||
regs[int(dst)] = val
|
||||
|
||||
def _truthy(self, v: Any) -> bool:
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
if isinstance(v, (int, float)):
|
||||
return v != 0
|
||||
if isinstance(v, str):
|
||||
return len(v) != 0
|
||||
return v is not None
|
||||
|
||||
def _is_console(self, v: Any) -> bool:
|
||||
return isinstance(v, dict) and v.get("__box__") == "ConsoleBox"
|
||||
|
||||
def run(self, entry: str) -> Any:
|
||||
fn = self.functions.get(entry)
|
||||
if fn is None:
|
||||
raise RuntimeError(f"entry function not found: {entry}")
|
||||
return self._exec_function(fn, [])
|
||||
|
||||
def _exec_function(self, fn: Function, args: List[Any]) -> Any:
|
||||
# Intrinsic fast path for small helpers used in smokes
|
||||
ok, ret = self._try_intrinsic(fn.name, args)
|
||||
if ok:
|
||||
return ret
|
||||
# Initialize registers and bind params
|
||||
regs: Dict[int, Any] = {}
|
||||
if fn.params:
|
||||
for i, pid in enumerate(fn.params):
|
||||
regs[int(pid)] = args[i] if i < len(args) else None
|
||||
else:
|
||||
# Heuristic: derive param count from name suffix '/N' and bind to vids 0..N-1
|
||||
n = 0
|
||||
if "/" in fn.name:
|
||||
try:
|
||||
n = int(fn.name.split("/")[-1])
|
||||
except Exception:
|
||||
n = 0
|
||||
for i in range(n):
|
||||
regs[i] = args[i] if i < len(args) else None
|
||||
# Choose a deterministic first block (lowest id)
|
||||
if not fn.blocks:
|
||||
return 0
|
||||
cur = min(fn.blocks.keys())
|
||||
prev: Optional[int] = None
|
||||
|
||||
# Simple block execution loop
|
||||
while True:
|
||||
block = fn.blocks.get(cur)
|
||||
if block is None:
|
||||
raise RuntimeError(f"block not found: {cur}")
|
||||
# Evaluate instructions sequentially
|
||||
i = 0
|
||||
while i < len(block.instructions):
|
||||
inst = block.instructions[i]
|
||||
op = inst.get("op")
|
||||
|
||||
if op == "phi":
|
||||
# incoming: [[vid, pred_bid], ...]
|
||||
incoming = inst.get("incoming", [])
|
||||
chosen: Any = None
|
||||
# Prefer predecessor match; otherwise fallback to first
|
||||
for vid, pb in incoming:
|
||||
if prev is not None and int(pb) == int(prev):
|
||||
chosen = regs.get(int(vid))
|
||||
break
|
||||
if chosen is None and incoming:
|
||||
vid, _ = incoming[0]
|
||||
chosen = regs.get(int(vid))
|
||||
self._set(regs, inst.get("dst"), chosen)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if op == "const":
|
||||
val = inst.get("value", {})
|
||||
ty = val.get("type")
|
||||
vv = val.get("value")
|
||||
if ty == "i64":
|
||||
out = int(vv)
|
||||
elif ty == "f64":
|
||||
out = float(vv)
|
||||
elif ty == "string":
|
||||
out = str(vv)
|
||||
else:
|
||||
out = None
|
||||
self._set(regs, inst.get("dst"), out)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if op == "binop":
|
||||
operation = inst.get("operation")
|
||||
a = self._read(regs, inst.get("lhs"))
|
||||
b = self._read(regs, inst.get("rhs"))
|
||||
res: Any = None
|
||||
if operation == "+":
|
||||
if isinstance(a, str) or isinstance(b, str):
|
||||
res = (str(a) if a is not None else "") + (str(b) if b is not None else "")
|
||||
else:
|
||||
av = 0 if a is None else int(a)
|
||||
bv = 0 if b is None else int(b)
|
||||
res = av + bv
|
||||
elif operation == "-":
|
||||
av = 0 if a is None else int(a)
|
||||
bv = 0 if b is None else int(b)
|
||||
res = av - bv
|
||||
elif operation == "*":
|
||||
av = 0 if a is None else int(a)
|
||||
bv = 0 if b is None else int(b)
|
||||
res = av * bv
|
||||
elif operation == "/":
|
||||
# integer division semantics for now
|
||||
av = 0 if a is None else int(a)
|
||||
bv = 1 if b in (None, 0) else int(b)
|
||||
res = av // bv
|
||||
elif operation == "%":
|
||||
av = 0 if a is None else int(a)
|
||||
bv = 1 if b in (None, 0) else int(b)
|
||||
res = av % bv
|
||||
elif operation in ("&", "|", "^"):
|
||||
# treat as bitwise on ints
|
||||
ai, bi = (0 if a is None else int(a)), (0 if b is None else int(b))
|
||||
if operation == "&":
|
||||
res = ai & bi
|
||||
elif operation == "|":
|
||||
res = ai | bi
|
||||
else:
|
||||
res = ai ^ bi
|
||||
elif operation in ("<<", ">>"):
|
||||
ai, bi = (0 if a is None else int(a)), (0 if b is None else int(b))
|
||||
res = (ai << bi) if operation == "<<" else (ai >> bi)
|
||||
else:
|
||||
raise RuntimeError(f"unsupported binop: {operation}")
|
||||
self._set(regs, inst.get("dst"), res)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if op == "compare":
|
||||
operation = inst.get("operation")
|
||||
a = self._read(regs, inst.get("lhs"))
|
||||
b = self._read(regs, inst.get("rhs"))
|
||||
res: bool
|
||||
if operation == "==":
|
||||
res = (a == b)
|
||||
elif operation == "!=":
|
||||
res = (a != b)
|
||||
elif operation == "<":
|
||||
res = (a < b)
|
||||
elif operation == "<=":
|
||||
res = (a <= b)
|
||||
elif operation == ">":
|
||||
res = (a > b)
|
||||
elif operation == ">=":
|
||||
res = (a >= b)
|
||||
else:
|
||||
raise RuntimeError(f"unsupported compare: {operation}")
|
||||
# VM convention: booleans are i64 0/1
|
||||
self._set(regs, inst.get("dst"), 1 if res else 0)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if op == "newbox":
|
||||
btype = inst.get("type")
|
||||
if btype == "ConsoleBox":
|
||||
val = {"__box__": "ConsoleBox"}
|
||||
elif btype == "StringBox":
|
||||
# empty string instance
|
||||
val = ""
|
||||
else:
|
||||
# Unknown box -> opaque
|
||||
val = {"__box__": btype}
|
||||
self._set(regs, inst.get("dst"), val)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if op == "boxcall":
|
||||
recv = self._read(regs, inst.get("box"))
|
||||
method = inst.get("method")
|
||||
args = [self._read(regs, a) for a in inst.get("args", [])]
|
||||
out: Any = None
|
||||
# ConsoleBox methods
|
||||
if method in ("print", "println", "log") and self._is_console(recv):
|
||||
s = args[0] if args else ""
|
||||
if s is None:
|
||||
s = ""
|
||||
if method == "println":
|
||||
print(str(s))
|
||||
else:
|
||||
# println is the primary one used by smokes; keep print/log equivalent
|
||||
print(str(s))
|
||||
out = 0
|
||||
# FileBox methods (minimal read-only)
|
||||
elif isinstance(recv, dict) and recv.get("__box__") == "FileBox":
|
||||
if method == "open":
|
||||
path = str(args[0]) if len(args) > 0 else ""
|
||||
mode = str(args[1]) if len(args) > 1 else "r"
|
||||
ok = 0
|
||||
content = None
|
||||
if mode == "r":
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
ok = 1
|
||||
except Exception:
|
||||
ok = 0
|
||||
content = None
|
||||
recv["__open"] = (ok == 1)
|
||||
recv["__path"] = path
|
||||
recv["__content"] = content
|
||||
out = ok
|
||||
elif method == "read":
|
||||
if isinstance(recv.get("__content"), str):
|
||||
out = recv.get("__content")
|
||||
else:
|
||||
out = None
|
||||
elif method == "close":
|
||||
recv["__open"] = False
|
||||
out = 0
|
||||
else:
|
||||
out = None
|
||||
# PathBox methods (posix-like)
|
||||
elif isinstance(recv, dict) and recv.get("__box__") == "PathBox":
|
||||
if method == "dirname":
|
||||
p = str(args[0]) if args else ""
|
||||
# Normalize to POSIX-style
|
||||
out = os.path.dirname(p)
|
||||
if out == "":
|
||||
out = "."
|
||||
elif method == "join":
|
||||
base = str(args[0]) if len(args) > 0 else ""
|
||||
rel = str(args[1]) if len(args) > 1 else ""
|
||||
out = os.path.join(base, rel)
|
||||
else:
|
||||
out = None
|
||||
elif method == "length":
|
||||
out = len(str(recv))
|
||||
elif method == "substring":
|
||||
s = str(recv)
|
||||
start = int(args[0]) if len(args) > 0 else 0
|
||||
end = int(args[1]) if len(args) > 1 else len(s)
|
||||
out = s[start:end]
|
||||
elif method == "lastIndexOf":
|
||||
s = str(recv)
|
||||
needle = str(args[0]) if args else ""
|
||||
out = s.rfind(needle)
|
||||
else:
|
||||
# Unimplemented method -> no-op
|
||||
out = None
|
||||
self._set(regs, inst.get("dst"), out)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if op == "externcall":
|
||||
func = inst.get("func")
|
||||
args = [self._read(regs, a) for a in inst.get("args", [])]
|
||||
out: Any = None
|
||||
if func == "nyash.console.println":
|
||||
s = args[0] if args else ""
|
||||
if s is None:
|
||||
s = ""
|
||||
print(str(s))
|
||||
out = 0
|
||||
else:
|
||||
# Unknown extern
|
||||
out = None
|
||||
self._set(regs, inst.get("dst"), out)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if op == "branch":
|
||||
cond = self._read(regs, inst.get("cond"))
|
||||
tid = int(inst.get("then"))
|
||||
eid = int(inst.get("else"))
|
||||
prev = cur
|
||||
cur = tid if self._truthy(cond) else eid
|
||||
# Restart execution at next block
|
||||
break
|
||||
|
||||
if op == "jump":
|
||||
tgt = int(inst.get("target"))
|
||||
prev = cur
|
||||
cur = tgt
|
||||
break
|
||||
|
||||
if op == "ret":
|
||||
v = self._read(regs, inst.get("value"))
|
||||
return v
|
||||
|
||||
if op == "call":
|
||||
# Resolve function name from value or take as literal
|
||||
fval = inst.get("func")
|
||||
fname = self._read(regs, fval)
|
||||
if not isinstance(fname, str):
|
||||
# Fallback: if JSON encoded a literal name
|
||||
fname = fval if isinstance(fval, str) else None
|
||||
call_args = [self._read(regs, a) for a in inst.get("args", [])]
|
||||
result = None
|
||||
if isinstance(fname, str) and fname in self.functions:
|
||||
callee = self.functions[fname]
|
||||
result = self._exec_function(callee, call_args)
|
||||
# Store result if needed
|
||||
self._set(regs, inst.get("dst"), result)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Unhandled op -> skip
|
||||
i += 1
|
||||
|
||||
else:
|
||||
# No explicit terminator; finish
|
||||
return 0
|
||||
|
||||
def _try_intrinsic(self, name: str, args: List[Any]) -> Tuple[bool, Any]:
|
||||
try:
|
||||
if name == "Main.esc_json/1":
|
||||
s = "" if not args else ("" if args[0] is None else str(args[0]))
|
||||
out = []
|
||||
for ch in s:
|
||||
if ch == "\\":
|
||||
out.append("\\\\")
|
||||
elif ch == '"':
|
||||
out.append('\\"')
|
||||
else:
|
||||
out.append(ch)
|
||||
return True, "".join(out)
|
||||
if name == "Main.dirname/1":
|
||||
p = "" if not args else ("" if args[0] is None else str(args[0]))
|
||||
d = os.path.dirname(p)
|
||||
if d == "":
|
||||
d = "."
|
||||
return True, d
|
||||
except Exception:
|
||||
pass
|
||||
return (False, None)
|
||||
@ -33,6 +33,8 @@ class Resolver:
|
||||
self.f64_cache: Dict[Tuple[str, int], ir.Value] = {}
|
||||
# String literal map: value_id -> Python string (for by-name calls)
|
||||
self.string_literals: Dict[int, str] = {}
|
||||
# Optional: value_id -> i8* pointer for string constants (lower_const can populate)
|
||||
self.string_ptrs: Dict[int, ir.Value] = {}
|
||||
# Track value-ids that are known to represent string handles (i64)
|
||||
# This is a best-effort tag used to decide '+' as string concat when both sides are i64.
|
||||
self.string_ids: set[int] = set()
|
||||
|
||||
Reference in New Issue
Block a user