""" 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": {}} 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 ("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 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)