Files
hakorune/src/llvm_py/pyvm/ops_box.py

329 lines
12 KiB
Python
Raw Normal View History

"""
Box-related operations for the Nyash PyVM: newbox and boxcall.
Kept behaviorally identical to the original vm.py code.
"""
from __future__ import annotations
from typing import Any, Dict, List
import os
def op_newbox(owner, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
btype = inst.get("type")
# Sandbox gate: only allow minimal boxes when sandbox is active
if not owner._sandbox_allow_newbox(str(btype)):
val = {"__box__": str(btype), "__denied__": True}
elif btype == "ConsoleBox":
val = {"__box__": "ConsoleBox"}
elif btype == "StringBox":
# empty string instance
val = ""
elif btype == "ArrayBox":
val = {"__box__": "ArrayBox", "__arr": []}
elif btype == "MapBox":
val = {"__box__": "MapBox", "__map": {}}
elif btype == "JsonDocBox":
# Minimal JsonDocBox (PyVM-only): stores parsed JSON and last error
val = {"__box__": "JsonDocBox", "__doc": None, "__err": None}
elif btype == "JsonNodeBox":
# Minimal JsonNodeBox with empty Null node
val = {"__box__": "JsonNodeBox", "__node": None}
else:
# Unknown box -> opaque
val = {"__box__": btype}
owner._set(regs, inst.get("dst"), val)
def op_boxcall(owner, fn, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
recv = owner._read(regs, inst.get("box"))
method = inst.get("method")
args: List[Any] = [owner._read(regs, a) for a in inst.get("args", [])]
out: Any = None
owner._dbg(f"[pyvm] boxcall recv={recv} method={method} args={args}")
# Sandbox gate: disallow unsafe/unknown boxcalls
if not owner._sandbox_allow_boxcall(recv, method):
owner._set(regs, inst.get("dst"), out)
return
# Special-case: inside a method body, 'me.method(...)' lowers to a
# boxcall with a synthetic receiver marker '__me__'. Resolve it by
# dispatching to the current box's lowered function if available.
if isinstance(recv, str) and recv == "__me__" and isinstance(method, str):
box_name = ""
try:
if "." in fn.name:
box_name = fn.name.split(".")[0]
except Exception:
box_name = ""
if box_name:
cand = f"{box_name}.{method}/{len(args)}"
callee = owner.functions.get(cand)
if callee is not None:
owner._dbg(f"[pyvm] boxcall(__me__) -> {cand} args={args}")
out = owner._exec_function(callee, args)
owner._set(regs, inst.get("dst"), out)
return
# User-defined box: dispatch to lowered function if available (Box.method/N)
if isinstance(recv, dict) and isinstance(method, str) and "__box__" in recv:
box_name = recv.get("__box__")
cand = f"{box_name}.{method}/{len(args)}"
callee = owner.functions.get(cand)
if callee is not None:
owner._dbg(f"[pyvm] boxcall dispatch -> {cand} args={args}")
out = owner._exec_function(callee, args)
owner._set(regs, inst.get("dst"), out)
return
else:
if owner._debug:
prefix = f"{box_name}.{method}/"
cands = sorted([k for k in owner.functions.keys() if k.startswith(prefix)])
if cands:
owner._dbg(f"[pyvm] boxcall unresolved: '{cand}' — available: {cands}")
else:
any_for_box = sorted([k for k in owner.functions.keys() if k.startswith(f"{box_name}.")])
owner._dbg(f"[pyvm] boxcall unresolved: '{cand}' — no candidates; methods for {box_name}: {any_for_box}")
# ConsoleBox methods
if method in ("print", "println", "log") and owner._is_console(recv):
s = args[0] if args else ""
if s is None:
s = ""
# 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 ""
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
# ArrayBox minimal methods
elif isinstance(recv, dict) and recv.get("__box__") == "ArrayBox":
arr = recv.get("__arr", [])
if method in ("birth",):
# No-op initializer for parity with Nyash VM
out = 0
elif method in ("len", "size"):
out = len(arr)
elif method == "get":
idx = int(args[0]) if args else 0
out = arr[idx] if 0 <= idx < len(arr) else None
elif method == "set":
idx = int(args[0]) if len(args) > 0 else 0
val = args[1] if len(args) > 1 else None
if 0 <= idx < len(arr):
arr[idx] = val
elif idx == len(arr):
arr.append(val)
else:
while len(arr) < idx:
arr.append(None)
arr.append(val)
out = 0
elif method == "push":
val = args[0] if args else None
arr.append(val)
out = len(arr)
elif method == "toString":
out = "[" + ",".join(str(x) for x in arr) + "]"
else:
out = None
recv["__arr"] = arr
# MapBox minimal methods
elif isinstance(recv, dict) and recv.get("__box__") == "MapBox":
m = recv.get("__map", {})
if method == "size":
out = len(m)
elif method == "has":
key = str(args[0]) if args else ""
out = 1 if key in m else 0
elif method == "get":
key = str(args[0]) if args else ""
out = m.get(key)
elif method == "set":
key = str(args[0]) if len(args) > 0 else ""
val = args[1] if len(args) > 1 else None
m[key] = val
out = 0
elif method == "toString":
items = ",".join(f"{k}:{m[k]}" for k in m)
out = "{" + items + "}"
else:
out = None
recv["__map"] = m
# JsonDocBox (PyVM-native shim)
elif isinstance(recv, dict) and recv.get("__box__") == "JsonDocBox":
import json
if method == "parse":
s = args[0] if args else ""
try:
recv["__doc"] = json.loads(str(s))
recv["__err"] = None
except Exception as e:
recv["__doc"] = None
recv["__err"] = str(e)
out = 0
elif method == "root":
out = {"__box__": "JsonNodeBox", "__node": recv.get("__doc", None)}
elif method == "error":
out = recv.get("__err") or ""
else:
out = None
# JsonNodeBox (PyVM-native shim)
elif isinstance(recv, dict) and recv.get("__box__") == "JsonNodeBox":
node = recv.get("__node", None)
if method == "kind":
if node is None:
out = "null"
elif isinstance(node, bool):
out = "bool"
elif isinstance(node, int):
out = "int"
elif isinstance(node, float):
out = "real"
elif isinstance(node, str):
out = "string"
elif isinstance(node, list):
out = "array"
elif isinstance(node, dict):
out = "object"
else:
out = "null"
elif method == "get":
key = str(args[0]) if args else ""
if isinstance(node, dict) and key in node:
out = {"__box__": "JsonNodeBox", "__node": node.get(key)}
else:
out = {"__box__": "JsonNodeBox", "__node": None}
elif method == "size":
if isinstance(node, list):
out = len(node)
elif isinstance(node, dict):
out = len(node)
else:
out = 0
elif method == "at":
try:
idx = int(args[0]) if args else 0
except Exception:
idx = 0
if isinstance(node, list) and 0 <= idx < len(node):
out = {"__box__": "JsonNodeBox", "__node": node[idx]}
else:
out = {"__box__": "JsonNodeBox", "__node": None}
elif method == "str":
if isinstance(node, str):
out = node
elif isinstance(node, dict) and isinstance(node.get("value"), str):
out = node.get("value")
else:
out = ""
elif method == "int":
if isinstance(node, int):
out = node
elif isinstance(node, dict):
v = node.get("value")
out = int(v) if isinstance(v, int) else 0
else:
out = 0
elif method == "bool":
out = bool(node) if isinstance(node, bool) else False
else:
out = None
elif method == "esc_json":
s = args[0] if args else ""
s = "" if s is None else str(s)
out_chars: List[str] = []
for ch in s:
if ch == "\\":
out_chars.append("\\\\")
elif ch == '"':
out_chars.append('\\"')
else:
out_chars.append(ch)
out = "".join(out_chars)
elif method == "length":
out = len(str(recv))
elif method == "substring":
s = str(recv)
start = int(args[0]) if (len(args) > 0 and args[0] is not None) else 0
end = int(args[1]) if (len(args) > 1 and args[1] is not None) else len(s)
out = s[start:end]
elif method == "lastIndexOf":
s = str(recv)
needle = str(args[0]) if args else ""
if len(args) > 1 and args[1] is not None:
try:
start = int(args[1])
except Exception:
start = 0
out = s.rfind(needle, start)
else:
out = s.rfind(needle)
elif method == "indexOf":
s = str(recv)
needle = str(args[0]) if args else ""
if len(args) > 1 and args[1] is not None:
try:
start = int(args[1])
except Exception:
start = 0
out = s.find(needle, start)
else:
out = s.find(needle)
else:
# Unimplemented method -> no-op
out = None
owner._set(regs, inst.get("dst"), out)