fix(bridge): implement env.box_introspect.kind lowering + Stage0 build fixes

Phase 25.1b type system groundwork - env.* namespace support in Bridge layer

Changes:
- Bridge layer (JSON v0 → MIR):
  - Add 'env' as well-known variable in MapVars::resolve()
  - Implement env.box_introspect.kind(value) → ExternCall lowering
  - Pattern: Method { recv: Method { recv: Var("env"), method: "box_introspect" }, method: "kind" }

- VM/extern fixes:
  - Add Arc::from() conversion for env.box_introspect.kind result
  - Fix MapBox API usage in extern_functions.rs logging

- Build fixes:
  - Comment out missing llvm_legacy/llvm modules in src/backend/mod.rs
  - Comment out missing gui_visual_node_prototype in Cargo.toml

- New files:
  - lang/src/shared/common/box_type_inspector_box.hako (type introspection API)

Context:
- Enables BoxTypeInspectorBox to query runtime Box types via env.box_introspect.kind
- Required for selfhost MirBuilder type-aware lowering (multi-carrier loops, etc.)
- Part of Phase 25.1b "no fallback" selfhosting strategy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-16 17:19:56 +09:00
parent 5f06d82ee5
commit fbf4687ea1
6 changed files with 391 additions and 34 deletions

View File

@ -124,10 +124,10 @@ name = "gui_test_icon_extraction"
path = "examples/test_icon_extraction.rs"
required-features = ["gui-examples"]
[[example]]
name = "gui_visual_node_prototype"
path = "development/egui_research/experiments/visual_node_prototype.rs"
required-features = ["gui-examples"]
# [[example]]
# name = "gui_visual_node_prototype"
# path = "development/egui_research/experiments/visual_node_prototype.rs"
# required-features = ["gui-examples"]

View File

@ -0,0 +1,59 @@
// box_type_inspector_box.hako — Runtime box type metadata helper
// Responsibility:
// - Wrap env.box_introspect.* extern calls for Stage1 code.
// - Provide describe/kind/is_map/is_array helpers without depending on `"" + MapBox`.
static box BoxTypeInspectorBox {
method _describe(value) {
local args = new ArrayBox()
args.push(value)
local info = hostbridge.extern_invoke("env.box_introspect", "kind", args)
if env.get("HAKO_BOX_INTROSPECT_TRACE") == "1" {
if info != null {
print("[box_introspect:hako] kind=" + ("" + info.get("kind")) +
" is_map=" + ("" + info.get("is_map")) +
" is_array=" + ("" + info.get("is_array")))
} else {
print("[box_introspect:hako] info=null")
}
}
return info
}
method kind(value) {
local info = me._describe(value)
if info == null { return "Unknown" }
local k = info.get("kind")
if k == null { return "Unknown" }
return "" + k
}
method is_map(value) {
local info = me._describe(value)
if info != null {
local flag = info.get("is_map")
if flag != null { return flag }
}
// Fallback: detect MapBox from repr when extern is unavailable
local repr = "" + value
if repr.indexOf("MapBox(") == 0 { return true }
return false
}
method is_array(value) {
local info = me._describe(value)
if info != null {
local flag = info.get("is_array")
if flag != null { return flag }
}
// Fallback: detect ArrayBox from repr when extern is unavailable
local repr = "" + value
if repr.indexOf("ArrayBox(") == 0 { return true }
return false
}
method describe(value) {
// Expose full Map so callers can inspect additional fields (type_name/type_id/etc.)
return me._describe(value)
}
}

View File

@ -1,6 +1,7 @@
use super::*;
use crate::backend::mir_interpreter::utils::error_helpers::ErrorBuilder;
use serde_json::Value as JsonValue;
use std::sync::Arc;
impl MirInterpreter {
#[inline]
@ -212,7 +213,44 @@ impl MirInterpreter {
let val = std::env::var(&key).ok();
Some(Ok(match val { Some(s) => VMValue::String(s), None => VMValue::Void }))
}
// Legacy global-call form: hostbridge.extern_invoke(name, method, args?)
// Direct env.box_introspect.kind extern (ExternCall form)
"env.box_introspect.kind" => {
use crate::box_trait::{NyashBox, StringBox};
use crate::runtime::plugin_loader_v2;
let mut collected: Vec<Box<dyn NyashBox>> = Vec::new();
if let Some(arg_reg) = args.get(0) {
let v = match self.reg_load(*arg_reg) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
match v {
VMValue::BoxRef(b) => collected.push(b.clone_box()),
other => {
collected.push(Box::new(StringBox::new(&other.to_string())));
}
}
} else {
return Some(Err(self.err_invalid(
"env.box_introspect.kind expects 1 arg",
)));
}
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
let result = plugin_loader_v2::handle_box_introspect("kind", &collected);
#[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))]
let result: crate::bid::BidResult<Option<Box<dyn crate::box_trait::NyashBox>>> =
Err(crate::bid::BidError::PluginError);
match result {
Ok(Some(b)) => Some(Ok(VMValue::BoxRef(Arc::from(b)))),
Ok(None) => Some(Ok(VMValue::Void)),
Err(e) => Some(Err(self.err_with_context(
"env.box_introspect.kind",
&format!("{:?}", e),
))),
}
}
"hostbridge.extern_invoke" => {
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
eprintln!("[hb:entry:provider] hostbridge.extern_invoke");
@ -386,12 +424,93 @@ impl MirInterpreter {
Err(e) => Err(ErrorBuilder::with_context("env.codegen.link_object", &e.to_string()))
}
}
("env.box_introspect", "kind") => {
// hostbridge.extern_invoke("env.box_introspect","kind",[value])
// args layout (regs): [name, method, array_box_or_value, ...]
// For BoxTypeInspectorBox we only care about the first element of the ArrayBox.
use crate::box_trait::{NyashBox, StringBox};
use crate::runtime::plugin_loader_v2;
let mut collected: Vec<Box<dyn NyashBox>> = Vec::new();
if let Some(arg_reg) = args.get(2) {
let v = match self.reg_load(*arg_reg) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
match v {
VMValue::BoxRef(b) => {
if let Some(ab) =
b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>()
{
let idx0: Box<dyn NyashBox> =
Box::new(crate::box_trait::IntegerBox::new(0));
let elem0 = ab.get(idx0);
if std::env::var("NYASH_BOX_INTROSPECT_TRACE")
.ok()
.as_deref()
== Some("1")
{
eprintln!(
"[box_introspect:extern] using ArrayBox[0] value_type={}",
elem0.type_name()
);
}
collected.push(elem0);
} else {
if std::env::var("NYASH_BOX_INTROSPECT_TRACE")
.ok()
.as_deref()
== Some("1")
{
eprintln!(
"[box_introspect:extern] using BoxRef({}) directly",
b.type_name()
);
}
collected.push(b.clone_box());
}
}
other => {
if std::env::var("NYASH_BOX_INTROSPECT_TRACE")
.ok()
.as_deref()
== Some("1")
{
eprintln!(
"[box_introspect:extern] non-box arg kind={:?}",
other
);
}
collected.push(Box::new(StringBox::new(&other.to_string())));
}
}
} else {
return Some(Err(self.err_invalid(
"extern_invoke env.box_introspect.kind expects args array",
)));
}
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
let result = plugin_loader_v2::handle_box_introspect("kind", &collected);
#[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))]
let result: crate::bid::BidResult<Option<Box<dyn NyashBox>>> =
Err(crate::bid::BidError::PluginError);
match result {
Ok(Some(b)) => Ok(VMValue::BoxRef(Arc::from(b))),
Ok(None) => Ok(VMValue::Void),
Err(e) => Err(self.err_with_context(
"env.box_introspect.kind",
&format!("{:?}", e),
)),
}
}
_ => {
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
eprintln!("[hb:unsupported:provider] {}.{}", name, method);
}
Err(self.err_invalid(format!(
"hostbridge.extern_invoke unsupported for {}.{} [provider]",
"hostbridge.extern_invoke unsupported for {}.{} [provider] (check extern_provider_dispatch / env.* handlers)",
name, method
)))
},

View File

@ -23,10 +23,10 @@ pub mod wasm;
#[cfg(feature = "wasm-backend")]
pub mod wasm_v2;
#[cfg(feature = "llvm-inkwell-legacy")]
pub mod llvm_legacy;
#[cfg(feature = "llvm-inkwell-legacy")]
pub mod llvm;
// #[cfg(feature = "llvm-inkwell-legacy")]
// pub mod llvm_legacy;
// #[cfg(feature = "llvm-inkwell-legacy")]
// pub mod llvm;
// Public aliases to make the role of the VM clear in runner/tests
pub use mir_interpreter::MirInterpreter;

View File

@ -80,6 +80,19 @@ impl<'a> VarScope for MapVars<'a> {
self.vars.insert(name.to_string(), dst);
return Ok(Some(dst));
}
// Phase 25.1b: Treat `env` as a well-known global for env.box_introspect.* etc.
// Similar to hostbridge, we need a placeholder value for the nested method pattern.
if name == "env" {
let dst = f.next_value_id();
if let Some(bb) = f.get_block_mut(cur_bb) {
bb.add_instruction(MirInstruction::Const {
dst,
value: ConstValue::String("env".into()),
});
}
self.vars.insert(name.to_string(), dst);
return Ok(Some(dst));
}
if name == "me" {
if env.allow_me_dummy {
let dst = f.next_value_id();
@ -363,6 +376,29 @@ pub(super) fn lower_expr_with_scope<S: VarScope>(
}
return Ok((dst, cur2));
}
// Phase 25.1b: Handle env.box_introspect.kind(value) pattern
// Pattern: Method { recv: Method { recv: Var("env"), method: "box_introspect" }, method: "kind", args }
if let ExprV0::Method { recv: inner_recv, method: inner_method, args: inner_args } = &**recv {
if matches!(&**inner_recv, ExprV0::Var { name } if name == "env")
&& inner_method == "box_introspect"
&& inner_args.is_empty() {
// Lower args for the final method call
let (arg_ids, cur2) = lower_args_with_scope(env, f, cur_bb, args, vars)?;
let dst = f.next_value_id();
if let Some(bb) = f.get_block_mut(cur2) {
bb.add_instruction(MirInstruction::ExternCall {
dst: Some(dst),
iface_name: "env.box_introspect".into(),
method_name: method.clone(),
args: arg_ids,
effects: EffectMask::READ,
});
}
return Ok((dst, cur2));
}
}
let (recv_v, cur) = lower_expr_with_scope(env, f, cur_bb, recv, vars)?;
let (arg_ids, cur2) = lower_args_with_scope(env, f, cur, args, vars)?;
let dst = f.next_value_id();

View File

@ -4,12 +4,18 @@
//! that were previously in a large switch statement in loader.rs
use crate::bid::{BidError, BidResult};
use crate::box_trait::{NyashBox, StringBox, VoidBox};
use crate::boxes::result::NyashResultBox;
use crate::box_trait::IntegerBox;
use crate::box_trait::{BoolBox, NyashBox, StringBox, VoidBox};
use crate::boxes::array::ArrayBox;
use crate::boxes::future::FutureBox;
use crate::boxes::map_box::MapBox;
use crate::boxes::null_box::NullBox;
use crate::boxes::result::NyashResultBox;
use crate::boxes::token_box::TokenBox;
use crate::runtime::modules_registry;
use crate::runtime::global_hooks;
use crate::runtime::modules_registry;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
/// Handle external function calls from the runtime
pub fn extern_call(
@ -32,6 +38,7 @@ pub fn extern_call(
"env.future" => handle_future(method_name, args),
"env.mirbuilder" => handle_mirbuilder(method_name, args),
"env.codegen" => handle_codegen(method_name, args),
"env.box_introspect" => handle_box_introspect(method_name, args),
_ => Err(BidError::PluginError),
}
}
@ -41,8 +48,12 @@ fn should_trace_call_extern(target: &str, method: &str) -> bool {
let key = format!("{}.{}", target, method);
for pat in flt.split(',') {
let p = pat.trim();
if p.is_empty() { continue; }
if p == method || p == key { return true; }
if p.is_empty() {
continue;
}
if p == method || p == key {
return true;
}
}
return false;
}
@ -50,7 +61,10 @@ fn should_trace_call_extern(target: &str, method: &str) -> bool {
}
/// Handle env.console.* methods
fn handle_console(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_console(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"log" => {
let trace = std::env::var("NYASH_CONSOLE_TRACE").ok().as_deref() == Some("1");
@ -68,7 +82,10 @@ fn handle_console(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Op
}
/// Handle env.result.* methods
fn handle_result(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_result(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"ok" => {
// Wrap the first argument as Result.Ok; if missing, use Void
@ -91,7 +108,10 @@ fn handle_result(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Opt
}
/// Handle env.modules.* methods
fn handle_modules(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_modules(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"set" => {
if args.len() >= 2 {
@ -115,7 +135,10 @@ fn handle_modules(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Op
}
/// Handle env.task.* methods
fn handle_task(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_task(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"cancelCurrent" => {
let tok = global_hooks::current_group_token();
@ -153,7 +176,10 @@ fn handle_task_wait(_args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn Nya
}
/// Handle env.debug.* methods
fn handle_debug(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_debug(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"trace" => {
if std::env::var("NYASH_DEBUG_TRACE").ok().as_deref() == Some("1") {
@ -168,7 +194,10 @@ fn handle_debug(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Opti
}
/// Handle env.runtime.* methods
fn handle_runtime(method_name: &str, _args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_runtime(
method_name: &str,
_args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"checkpoint" => {
if crate::config::env::runtime_checkpoint_trace() {
@ -182,7 +211,10 @@ fn handle_runtime(method_name: &str, _args: &[Box<dyn NyashBox>]) -> BidResult<O
}
/// Handle env.future.* methods
fn handle_future(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_future(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"new" | "birth" => {
let fut = FutureBox::new();
@ -193,10 +225,7 @@ fn handle_future(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Opt
}
"set" => {
if args.len() >= 2 {
if let Some(fut) = args[0]
.as_any()
.downcast_ref::<FutureBox>()
{
if let Some(fut) = args[0].as_any().downcast_ref::<FutureBox>() {
fut.set_result(args[1].clone_box());
}
}
@ -207,11 +236,110 @@ fn handle_future(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Opt
}
}
/// Handle env.box_introspect.* methods
pub fn handle_box_introspect(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"kind" => {
let value = args.get(0).ok_or(BidError::PluginError)?;
let info = build_box_info(value.as_ref());
if std::env::var("NYASH_BOX_INTROSPECT_TRACE")
.ok()
.as_deref() == Some("1")
{
eprintln!(
"[box_introspect:plugin] kind={} type_name={} is_map={} is_array={}",
info.get(Box::new(StringBox::new("kind"))).to_string_box().value,
info.get(Box::new(StringBox::new("type_name"))).to_string_box().value,
info.get(Box::new(StringBox::new("is_map"))).to_string_box().value,
info.get(Box::new(StringBox::new("is_array"))).to_string_box().value,
);
}
Ok(Some(Box::new(info)))
}
_ => Err(BidError::PluginError),
}
}
fn build_box_info(value: &dyn NyashBox) -> MapBox {
let info = MapBox::new();
insert_string(&info, "kind", &classify_kind(value));
insert_string(&info, "type_name", value.type_name());
insert_string(&info, "type_id", &format!("{:016x}", type_id_hash(value)));
insert_bool(
&info,
"is_map",
value.as_any().downcast_ref::<MapBox>().is_some(),
);
insert_bool(
&info,
"is_array",
value.as_any().downcast_ref::<ArrayBox>().is_some(),
);
insert_bool(
&info,
"is_null",
value.as_any().downcast_ref::<NullBox>().is_some(),
);
info
}
fn insert_string(target: &MapBox, key: &str, value: &str) {
let _ = target.set(
Box::new(StringBox::new(key)),
Box::new(StringBox::new(value)),
);
}
fn insert_bool(target: &MapBox, key: &str, value: bool) {
let _ = target.set(Box::new(StringBox::new(key)), Box::new(BoolBox::new(value)));
}
fn classify_kind(value: &dyn NyashBox) -> String {
if value.as_any().downcast_ref::<MapBox>().is_some() {
return "MapBox".to_string();
}
if value.as_any().downcast_ref::<ArrayBox>().is_some() {
return "ArrayBox".to_string();
}
if value.as_any().downcast_ref::<StringBox>().is_some() {
return "StringBox".to_string();
}
if value.as_any().downcast_ref::<IntegerBox>().is_some() {
return "IntegerBox".to_string();
}
if value.as_any().downcast_ref::<BoolBox>().is_some() {
return "BoolBox".to_string();
}
if value.as_any().downcast_ref::<NullBox>().is_some() {
return "NullBox".to_string();
}
simplify_type_name(value.type_name())
}
fn simplify_type_name(full: &str) -> String {
full.rsplit("::").next().unwrap_or(full).to_string()
}
fn type_id_hash(value: &dyn NyashBox) -> u64 {
let mut hasher = DefaultHasher::new();
value.as_any().type_id().hash(&mut hasher);
hasher.finish()
}
/// Handle env.mirbuilder.* methods (Program(JSON v0) → MIR(JSON v0))
fn handle_mirbuilder(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_mirbuilder(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"emit" => {
let program_json = args.get(0).map(|b| b.to_string_box().value).unwrap_or_default();
let program_json = args
.get(0)
.map(|b| b.to_string_box().value)
.unwrap_or_default();
match crate::host_providers::mir_builder::program_json_to_mir_json(&program_json) {
Ok(s) => Ok(Some(Box::new(StringBox::new(&s)) as Box<dyn NyashBox>)),
Err(_e) => Ok(None),
@ -222,21 +350,36 @@ fn handle_mirbuilder(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult
}
/// Handle env.codegen.* methods (MIR(JSON v0) → object via ny-llvmc)
fn handle_codegen(method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
fn handle_codegen(
method_name: &str,
args: &[Box<dyn NyashBox>],
) -> BidResult<Option<Box<dyn NyashBox>>> {
match method_name {
"emit_object" => {
let mir_json = args.get(0).map(|b| b.to_string_box().value).unwrap_or_default();
let mir_json = args
.get(0)
.map(|b| b.to_string_box().value)
.unwrap_or_default();
// Collect minimal options from env (optional)
let opt_level = std::env::var("HAKO_LLVM_OPT_LEVEL").ok().or_else(|| std::env::var("NYASH_LLVM_OPT_LEVEL").ok());
let opt_level = std::env::var("HAKO_LLVM_OPT_LEVEL")
.ok()
.or_else(|| std::env::var("NYASH_LLVM_OPT_LEVEL").ok());
let out = None;
let nyrt = std::env::var("NYASH_EMIT_EXE_NYRT").ok().map(std::path::PathBuf::from);
let opts = crate::host_providers::llvm_codegen::Opts { out, nyrt, opt_level, timeout_ms: None };
let nyrt = std::env::var("NYASH_EMIT_EXE_NYRT")
.ok()
.map(std::path::PathBuf::from);
let opts = crate::host_providers::llvm_codegen::Opts {
out,
nyrt,
opt_level,
timeout_ms: None,
};
match crate::host_providers::llvm_codegen::mir_json_to_object(&mir_json, opts) {
Ok(p) => {
// Convert PathBuf → String via lossy conversion (owned)
let s = p.to_string_lossy().into_owned();
Ok(Some(Box::new(StringBox::new(s)) as Box<dyn NyashBox>))
},
}
Err(_e) => Ok(None),
}
}