trace: add execution route visibility + debug passthrough; phase2170 canaries; docs

- Add HAKO_TRACE_EXECUTION to trace executor route
  - Rust hv1_inline: stderr [trace] executor: hv1_inline (rust)
  - Hakovm dispatcher: stdout [trace] executor: hakovm (hako)
  - test_runner: trace lines for hv1_inline/core/hakovm routes
- Add HAKO_VERIFY_SHOW_LOGS and HAKO_DEBUG=1 (enables both)
  - verify_v1_inline_file() log passthrough with numeric rc extraction
  - test_runner exports via HAKO_DEBUG
- Canary expansion under phase2170 (state spec)
  - Array: push×5/10 → size, len/length alias, per‑recv/global, flow across blocks
  - Map: set dup-key non-increment, value_state get/has
  - run_all.sh: unify, remove SKIPs; all PASS
- Docs
  - ENV_VARS.md: add Debug/Tracing toggles and examples
  - PLAN.md/CURRENT_TASK.md: mark 21.7 green, add Quickstart lines

All changes gated by env vars; default behavior unchanged.
This commit is contained in:
nyash-codex
2025-11-08 23:45:29 +09:00
parent bf185ec2b2
commit fa3091061d
49 changed files with 1334 additions and 110 deletions

View File

@ -84,8 +84,17 @@ impl FileBox {
}
pub fn write_all(&self, _buf: &[u8]) -> Result<(), String> {
// CoreRo does not support write - Fail-Fast
Err("Write operation not supported in read-only mode".to_string())
// Fail-Fast by capability: consult provider caps
let caps = self.provider
.as_ref()
.map(|p| p.caps())
.or_else(|| provider_lock::get_filebox_caps())
.unwrap_or_else(|| provider::FileCaps::read_only());
if !caps.write {
return Err("Write unsupported by current FileBox provider (read-only)".to_string());
}
// Write-capable provider not wired yet
Err("Write supported by provider but not implemented in this build".to_string())
}
/// ファイルの内容を読み取る
@ -98,8 +107,15 @@ impl FileBox {
/// ファイルに内容を書き込む
pub fn write(&self, _content: Box<dyn NyashBox>) -> Box<dyn NyashBox> {
// Fail-Fast: CoreRo does not support write
Box::new(StringBox::new("Error: Write operation not supported in read-only mode"))
let caps = self.provider
.as_ref()
.map(|p| p.caps())
.or_else(|| provider_lock::get_filebox_caps())
.unwrap_or_else(|| provider::FileCaps::read_only());
if !caps.write {
return Box::new(StringBox::new("Error: write unsupported by provider (read-only)"));
}
Box::new(StringBox::new("Error: write supported but not implemented in this build"))
}
/// ファイルが存在するかチェック
@ -110,14 +126,28 @@ impl FileBox {
/// ファイルを削除
pub fn delete(&self) -> Box<dyn NyashBox> {
// Fail-Fast: CoreRo does not support delete
Box::new(StringBox::new("Error: Delete operation not supported in read-only mode"))
let caps = self.provider
.as_ref()
.map(|p| p.caps())
.or_else(|| provider_lock::get_filebox_caps())
.unwrap_or_else(|| provider::FileCaps::read_only());
if !caps.write {
return Box::new(StringBox::new("Error: delete unsupported by provider (read-only)"));
}
Box::new(StringBox::new("Error: delete supported but not implemented in this build"))
}
/// ファイルをコピー
pub fn copy(&self, _dest: &str) -> Box<dyn NyashBox> {
// Fail-Fast: CoreRo does not support copy
Box::new(StringBox::new("Error: Copy operation not supported in read-only mode"))
let caps = self.provider
.as_ref()
.map(|p| p.caps())
.or_else(|| provider_lock::get_filebox_caps())
.unwrap_or_else(|| provider::FileCaps::read_only());
if !caps.write {
return Box::new(StringBox::new("Error: copy unsupported by provider (read-only)"));
}
Box::new(StringBox::new("Error: copy supported but not implemented in this build"))
}
}

View File

@ -8,6 +8,10 @@ use std::collections::HashMap;
/// - mir_call(Method ArrayBox.push/size/len/length) with optional per-receiver state
/// - ret (by register id)
pub fn run_json_v1_inline(json: &str) -> i32 {
// Optional execution trace (stderr) for debugging
if crate::config::env::env_bool("HAKO_TRACE_EXECUTION") {
eprintln!("[trace] executor: hv1_inline (rust)");
}
// Parse JSON
let v: Value = match serde_json::from_str(json) {
Ok(v) => v,
@ -31,13 +35,21 @@ pub fn run_json_v1_inline(json: &str) -> i32 {
// Registers and simple method state
let mut regs: HashMap<i64, i64> = HashMap::new();
let mut str_regs: HashMap<i64, String> = HashMap::new(); // Track string values loaded via const
let sizestate = crate::config::env::env_bool("HAKO_VM_MIRCALL_SIZESTATE");
let per_recv = crate::config::env::env_bool("HAKO_VM_MIRCALL_SIZESTATE_PER_RECV");
let value_state = crate::config::env::env_bool("HAKO_VM_MIRCALL_VALUESTATE");
// Keep Array and Map length states separate to avoid crossbox interference
let mut len_global_arr: i64 = 0;
let mut len_by_recv_arr: HashMap<i64, i64> = HashMap::new();
let mut len_global_map: i64 = 0;
let mut len_by_recv_map: HashMap<i64, i64> = HashMap::new();
// Map dup-key detection: track which keys have been set (per-receiver or global)
let mut map_keys_global: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut map_keys_by_recv: HashMap<i64, std::collections::HashSet<String>> = HashMap::new();
// Map value storage (when value_state=1)
let mut map_vals_global: HashMap<String, i64> = HashMap::new();
let mut map_vals_by_recv: HashMap<i64, HashMap<String, i64>> = HashMap::new();
fn is_size_alias(name: &str) -> bool {
matches!(name, "size" | "len" | "length")
@ -140,6 +152,10 @@ pub fn run_json_v1_inline(json: &str) -> i32 {
// Prefer i64 numeric constants; otherwise stub non-numeric to 0 for inline path
if let Some(n) = inst.get("value").and_then(|vv| vv.get("value")).and_then(Value::as_i64) {
regs.insert(dst, n);
} else if let Some(s) = inst.get("value").and_then(|vv| vv.get("value")).and_then(Value::as_str) {
// Store string value for Map key tracking
str_regs.insert(dst, s.to_string());
regs.insert(dst, 0); // Also set numeric register to 0 for compatibility
} else {
regs.insert(dst, 0);
}
@ -205,17 +221,93 @@ pub fn run_json_v1_inline(json: &str) -> i32 {
let method = callee.get("method").and_then(Value::as_str).unwrap_or("");
let recv = callee.get("receiver").and_then(Value::as_i64).unwrap_or(-1);
if method == "set" {
if sizestate {
if per_recv {
let e = len_by_recv_map.entry(recv).or_insert(0);
*e += 1;
// Extract key from first arg (register containing string value)
let args = if let Some(mc) = mc { mc.get("args") } else { inst.get("args") };
let key_str = if let Some(args_arr) = args.and_then(Value::as_array) {
if let Some(key_reg) = args_arr.get(0).and_then(Value::as_i64) {
// Look up string value from str_regs
str_regs.get(&key_reg).cloned().unwrap_or_default()
} else { String::new() }
} else { String::new() };
if sizestate && !key_str.is_empty() {
let is_new_key = if per_recv {
let keys = map_keys_by_recv.entry(recv).or_insert_with(std::collections::HashSet::new);
keys.insert(key_str.clone())
} else {
len_global_map += 1;
map_keys_global.insert(key_str.clone())
};
// Only increment size if this is a new key
if is_new_key {
if per_recv {
let e = len_by_recv_map.entry(recv).or_insert(0);
*e += 1;
} else {
len_global_map += 1;
}
}
}
// value_state: store the value
if value_state && !key_str.is_empty() {
if let Some(args_arr) = args.and_then(Value::as_array) {
if let Some(val_reg) = args_arr.get(1).and_then(Value::as_i64) {
let val = *regs.get(&val_reg).unwrap_or(&0);
if per_recv {
let vals = map_vals_by_recv.entry(recv).or_insert_with(HashMap::new);
vals.insert(key_str.clone(), val);
} else {
map_vals_global.insert(key_str.clone(), val);
}
}
}
}
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); }
continue;
}
if method == "get" && value_state {
// Extract key and retrieve value
let args = if let Some(mc) = mc { mc.get("args") } else { inst.get("args") };
let key_str = if let Some(args_arr) = args.and_then(Value::as_array) {
if let Some(key_reg) = args_arr.get(0).and_then(Value::as_i64) {
str_regs.get(&key_reg).cloned().unwrap_or_default()
} else { String::new() }
} else { String::new() };
if !key_str.is_empty() {
let val = if per_recv {
map_vals_by_recv.get(&recv).and_then(|m| m.get(&key_str)).cloned().unwrap_or(0)
} else {
*map_vals_global.get(&key_str).unwrap_or(&0)
};
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, val); }
}
continue;
}
if method == "has" && value_state {
// Check if key exists
let args = if let Some(mc) = mc { mc.get("args") } else { inst.get("args") };
let key_str = if let Some(args_arr) = args.and_then(Value::as_array) {
if let Some(key_reg) = args_arr.get(0).and_then(Value::as_i64) {
str_regs.get(&key_reg).cloned().unwrap_or_default()
} else { String::new() }
} else { String::new() };
let has = if !key_str.is_empty() {
if per_recv {
map_vals_by_recv.get(&recv).map(|m| m.contains_key(&key_str)).unwrap_or(false)
} else {
map_vals_global.contains_key(&key_str)
}
} else { false };
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) {
regs.insert(dst, if has { 1 } else { 0 });
}
continue;
}
if is_size_alias(method) {
let value = if sizestate {
if per_recv { *len_by_recv_map.get(&recv).unwrap_or(&0) } else { len_global_map }

View File

@ -7,7 +7,7 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use crate::boxes::file::provider::FileIo;
use crate::boxes::file::provider::{FileCaps, FileIo};
static LOCKED: AtomicBool = AtomicBool::new(false);
static WARN_ONCE: OnceLock<()> = OnceLock::new();
@ -49,3 +49,8 @@ pub fn get_filebox_provider() -> Option<&'static Arc<dyn FileIo>> {
FILEBOX_PROVIDER.get()
}
/// Convenience: fetch current FileBox provider capabilities (if initialized).
/// Returns None when no provider is registered yet.
pub fn get_filebox_caps() -> Option<FileCaps> {
get_filebox_provider().map(|p| p.caps())
}