json: add v2 JsonDoc/JsonNode plugin with runtime provider switch; vendored yyjson + FFI; loader resolve(name)->method_id; PyVM JSON shims; smokes + CI gate; disable MiniVmPrints fallbacks by default
- plugin_loader_v2: store per-Box resolve() from TypeBox FFI; add resolve_method_id() and use in invoke_instance_method
- plugin_loader_unified: resolve_method() falls back to loader’s resolve when TOML lacks method entries
- nyash.toml: register JsonDocBox/JsonNodeBox methods (birth/parse/root/error; kind/get/size/at/str/int/bool)
- plugins/nyash-json-plugin:
* serde/yyjson provider switch via env NYASH_JSON_PROVIDER (default serde)
* vendored yyjson (c/yyjson) + shim; parse/root/get/size/at/str/int/bool implemented for yyjson
* TLV void returns aligned to tag=9
- PyVM: add minimal JsonDocBox/JsonNodeBox shims in ops_box.py (for dev path)
- tests/smokes: add jsonbox_{parse_ok,parse_err,nested,collect_prints}; wire two into min-gate CI
- tools: collect_prints_mixed now uses JSON-based app
- MiniVmPrints: move BinaryOp and fallback heuristics behind a dev toggle (default OFF)
- CURRENT_TASK.md: updated with provider policy and fallback stance
This commit is contained in:
@ -22,6 +22,12 @@ def op_newbox(owner, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
|
||||
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}
|
||||
@ -135,7 +141,10 @@ def op_boxcall(owner, fn, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
|
||||
# ArrayBox minimal methods
|
||||
elif isinstance(recv, dict) and recv.get("__box__") == "ArrayBox":
|
||||
arr = recv.get("__arr", [])
|
||||
if method in ("len", "size"):
|
||||
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
|
||||
@ -185,6 +194,87 @@ def op_boxcall(owner, fn, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
|
||||
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)
|
||||
@ -236,4 +326,3 @@ def op_boxcall(owner, fn, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
|
||||
out = None
|
||||
|
||||
owner._set(regs, inst.get("dst"), out)
|
||||
|
||||
|
||||
@ -94,16 +94,22 @@ impl PluginHost {
|
||||
let box_conf = cfg
|
||||
.get_box_config(lib_name, box_type, &toml_value)
|
||||
.ok_or(BidError::InvalidType)?;
|
||||
let m = box_conf
|
||||
.methods
|
||||
.get(method_name)
|
||||
.ok_or(BidError::InvalidMethod)?;
|
||||
// Prefer config mapping; fallback to loader's TypeBox resolve(name)
|
||||
let (method_id, returns_result) = if let Some(m) = box_conf.methods.get(method_name) {
|
||||
(m.method_id, m.returns_result)
|
||||
} else {
|
||||
let l = self.loader.read().unwrap();
|
||||
let mid = l
|
||||
.resolve_method_id(box_type, method_name)
|
||||
.map_err(|_| BidError::InvalidMethod)?;
|
||||
(mid, false)
|
||||
};
|
||||
Ok(MethodHandle {
|
||||
lib: lib_name.to_string(),
|
||||
box_type: box_type.to_string(),
|
||||
type_id: box_conf.type_id,
|
||||
method_id: m.method_id,
|
||||
returns_result: m.returns_result,
|
||||
method_id,
|
||||
returns_result,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,8 @@ struct LoadedBoxSpec {
|
||||
fini_method_id: Option<u32>,
|
||||
// Optional Nyash ABI v2 per-box invoke entry (not yet used for calls)
|
||||
invoke_id: Option<BoxInvokeFn>,
|
||||
// Optional resolve(name)->method_id provided by NyashTypeBoxFfi
|
||||
resolve_fn: Option<extern "C" fn(*const std::os::raw::c_char) -> u32>,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct MethodSpec {
|
||||
@ -172,7 +174,7 @@ impl PluginLoaderV2 {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Remember invoke_id in box_specs for (lib_name, box_type)
|
||||
// Remember invoke_id/resolve in box_specs for (lib_name, box_type)
|
||||
if let Some(invoke_id) = st.invoke_id {
|
||||
let key = (lib_name.to_string(), box_type.to_string());
|
||||
let mut map = self.box_specs.write().map_err(|_| BidError::PluginError)?;
|
||||
@ -181,8 +183,10 @@ impl PluginLoaderV2 {
|
||||
methods: HashMap::new(),
|
||||
fini_method_id: None,
|
||||
invoke_id: None,
|
||||
resolve_fn: None,
|
||||
});
|
||||
entry.invoke_id = Some(invoke_id);
|
||||
entry.resolve_fn = st.resolve;
|
||||
} else if dbg_on() {
|
||||
eprintln!(
|
||||
"[PluginLoaderV2] WARN: TypeBox present but no invoke_id for {}.{} — plugin should export per-Box invoke",
|
||||
@ -311,6 +315,56 @@ impl PluginLoaderV2 {
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve method_id for (box_type, method_name), consulting config first, then TypeBox resolve() if available.
|
||||
pub(crate) fn resolve_method_id(&self, box_type: &str, method_name: &str) -> BidResult<u32> {
|
||||
use std::ffi::CString;
|
||||
let cfg = self.config.as_ref().ok_or(BidError::PluginError)?;
|
||||
let cfg_path = self.config_path.as_deref().unwrap_or("nyash.toml");
|
||||
let toml_value: toml::Value = super::errors::from_toml(toml::from_str(
|
||||
&std::fs::read_to_string(cfg_path).map_err(|_| BidError::PluginError)?,
|
||||
))?;
|
||||
// 1) config mapping
|
||||
if let Some((lib_name, _)) = cfg.find_library_for_box(box_type) {
|
||||
if let Some(bc) = cfg.get_box_config(&lib_name, box_type, &toml_value) {
|
||||
if let Some(m) = bc.methods.get(method_name) {
|
||||
return Ok(m.method_id);
|
||||
}
|
||||
}
|
||||
// 2) v2 TypeBox resolve (and cache)
|
||||
let key = (lib_name.to_string(), box_type.to_string());
|
||||
if let Ok(mut map) = self.box_specs.write() {
|
||||
if let Some(spec) = map.get_mut(&key) {
|
||||
if let Some(ms) = spec.methods.get(method_name) {
|
||||
return Ok(ms.method_id);
|
||||
}
|
||||
if let Some(res_fn) = spec.resolve_fn {
|
||||
if let Ok(cstr) = CString::new(method_name) {
|
||||
let mid = res_fn(cstr.as_ptr());
|
||||
if mid != 0 {
|
||||
// Cache minimal MethodSpec (returns_result unknown → false)
|
||||
spec.methods.insert(
|
||||
method_name.to_string(),
|
||||
MethodSpec {
|
||||
method_id: mid,
|
||||
returns_result: false,
|
||||
},
|
||||
);
|
||||
if dbg_on() {
|
||||
eprintln!(
|
||||
"[PluginLoaderV2] resolve(name) {}.{} -> id {}",
|
||||
box_type, method_name, mid
|
||||
);
|
||||
}
|
||||
return Ok(mid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(BidError::InvalidMethod)
|
||||
}
|
||||
|
||||
pub fn construct_existing_instance(
|
||||
&self,
|
||||
type_id: u32,
|
||||
@ -634,10 +688,19 @@ impl PluginLoaderV2 {
|
||||
.get_box_config(lib_name, box_type, &toml_value)
|
||||
.ok_or(BidError::InvalidType)?;
|
||||
let type_id = box_conf.type_id;
|
||||
let method = box_conf
|
||||
.methods
|
||||
.get(method_name)
|
||||
.ok_or(BidError::InvalidMethod)?;
|
||||
// Resolve method id via config or TypeBox resolve()
|
||||
let method_id = match self.resolve_method_id(box_type, method_name) {
|
||||
Ok(mid) => mid,
|
||||
Err(e) => {
|
||||
if dbg_on() {
|
||||
eprintln!(
|
||||
"[PluginLoaderV2] ERR: method resolve failed for {}.{}: {:?}",
|
||||
box_type, method_name, e
|
||||
);
|
||||
}
|
||||
return Err(BidError::InvalidMethod);
|
||||
}
|
||||
};
|
||||
// Get plugin handle
|
||||
let plugins = self.plugins.read().map_err(|_| BidError::PluginError)?;
|
||||
let _plugin = plugins.get(lib_name).ok_or(BidError::PluginError)?;
|
||||
@ -646,7 +709,7 @@ impl PluginLoaderV2 {
|
||||
let (_code, out_len, out) = super::host_bridge::invoke_alloc(
|
||||
super::super::nyash_plugin_invoke_v2_shim,
|
||||
type_id,
|
||||
method.method_id,
|
||||
method_id,
|
||||
instance_id,
|
||||
&tlv,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user