Gate‑C(Core) OOB strict fail‑fast; String VM handler normalization; JSON lint Stage‑B root fixes via scanner field boxing and BinOp operand slotify; docs + smokes update
This commit is contained in:
@ -134,6 +134,9 @@ impl MirInterpreter {
|
||||
};
|
||||
self.box_trace_emit_call(&cls, method, args.len());
|
||||
}
|
||||
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && method == "trim" {
|
||||
eprintln!("[vm-trace] handle_box_call: method=trim (pre-dispatch)");
|
||||
}
|
||||
// Debug: trace length dispatch receiver type before any handler resolution
|
||||
if method == "length" && std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
|
||||
let recv = self.reg_load(box_val).unwrap_or(VMValue::Void);
|
||||
@ -202,8 +205,14 @@ impl MirInterpreter {
|
||||
trace_dispatch!(method, "instance_box");
|
||||
return Ok(());
|
||||
}
|
||||
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && method == "trim" {
|
||||
eprintln!("[vm-trace] dispatch trying boxes_string");
|
||||
}
|
||||
if super::boxes_string::try_handle_string_box(self, dst, box_val, method, args)? {
|
||||
trace_dispatch!(method, "string_box");
|
||||
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && method == "trim" {
|
||||
eprintln!("[vm-trace] dispatch handled by boxes_string");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if super::boxes_array::try_handle_array_box(self, dst, box_val, method, args)? {
|
||||
|
||||
@ -8,94 +8,101 @@ pub(super) fn try_handle_string_box(
|
||||
method: &str,
|
||||
args: &[ValueId],
|
||||
) -> Result<bool, VMError> {
|
||||
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[vm-trace] try_handle_string_box(method={})", method);
|
||||
}
|
||||
let recv = this.reg_load(box_val)?;
|
||||
let recv_box_any: Box<dyn NyashBox> = match recv.clone() {
|
||||
VMValue::BoxRef(b) => b.share_box(),
|
||||
other => other.to_nyash_box(),
|
||||
// Normalize receiver to trait-level StringBox to bridge old/new StringBox implementations
|
||||
let sb_norm: crate::box_trait::StringBox = match recv.clone() {
|
||||
VMValue::String(s) => crate::box_trait::StringBox::new(s),
|
||||
VMValue::BoxRef(b) => b.to_string_box(),
|
||||
other => other.to_nyash_box().to_string_box(),
|
||||
};
|
||||
if let Some(sb) = recv_box_any
|
||||
.as_any()
|
||||
.downcast_ref::<crate::box_trait::StringBox>()
|
||||
{
|
||||
match method {
|
||||
"length" => {
|
||||
let ret = sb.length();
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); }
|
||||
return Ok(true);
|
||||
// Only handle known string methods here
|
||||
match method {
|
||||
"length" => {
|
||||
let ret = sb_norm.length();
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); }
|
||||
return Ok(true);
|
||||
}
|
||||
"trim" => {
|
||||
let ret = sb_norm.trim();
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); }
|
||||
return Ok(true);
|
||||
}
|
||||
"indexOf" => {
|
||||
// indexOf(substr) -> first index or -1
|
||||
if args.len() != 1 {
|
||||
return Err(VMError::InvalidInstruction("indexOf expects 1 arg".into()));
|
||||
}
|
||||
"indexOf" => {
|
||||
// indexOf(substr) -> first index or -1
|
||||
if args.len() != 1 {
|
||||
return Err(VMError::InvalidInstruction("indexOf expects 1 arg".into()));
|
||||
let needle = this.reg_load(args[0])?.to_string();
|
||||
let idx = sb_norm.value.find(&needle).map(|i| i as i64).unwrap_or(-1);
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::Integer(idx)); }
|
||||
return Ok(true);
|
||||
}
|
||||
"stringify" => {
|
||||
// JSON-style stringify for strings: quote and escape common characters
|
||||
let mut quoted = String::with_capacity(sb_norm.value.len() + 2);
|
||||
quoted.push('"');
|
||||
for ch in sb_norm.value.chars() {
|
||||
match ch {
|
||||
'"' => quoted.push_str("\\\""),
|
||||
'\\' => quoted.push_str("\\\\"),
|
||||
'\n' => quoted.push_str("\\n"),
|
||||
'\r' => quoted.push_str("\\r"),
|
||||
'\t' => quoted.push_str("\\t"),
|
||||
c if c.is_control() => quoted.push(' '),
|
||||
c => quoted.push(c),
|
||||
}
|
||||
let needle = this.reg_load(args[0])?.to_string();
|
||||
let idx = sb.value.find(&needle).map(|i| i as i64).unwrap_or(-1);
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::Integer(idx)); }
|
||||
return Ok(true);
|
||||
}
|
||||
"stringify" => {
|
||||
// JSON-style stringify for strings: quote and escape common characters
|
||||
let mut quoted = String::with_capacity(sb.value.len() + 2);
|
||||
quoted.push('"');
|
||||
for ch in sb.value.chars() {
|
||||
match ch {
|
||||
'"' => quoted.push_str("\\\""),
|
||||
'\\' => quoted.push_str("\\\\"),
|
||||
'\n' => quoted.push_str("\\n"),
|
||||
'\r' => quoted.push_str("\\r"),
|
||||
'\t' => quoted.push_str("\\t"),
|
||||
c if c.is_control() => quoted.push(' '),
|
||||
c => quoted.push(c),
|
||||
}
|
||||
}
|
||||
quoted.push('"');
|
||||
if let Some(d) = dst {
|
||||
this.regs.insert(d, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(quoted))));
|
||||
}
|
||||
return Ok(true);
|
||||
quoted.push('"');
|
||||
if let Some(d) = dst {
|
||||
this.regs.insert(d, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(quoted))));
|
||||
}
|
||||
"substring" => {
|
||||
if args.len() != 2 {
|
||||
return Err(VMError::InvalidInstruction(
|
||||
"substring expects 2 args (start, end)".into(),
|
||||
));
|
||||
}
|
||||
let s_idx = this.reg_load(args[0])?.as_integer().unwrap_or(0);
|
||||
let e_idx = this.reg_load(args[1])?.as_integer().unwrap_or(0);
|
||||
let len = sb.value.chars().count() as i64;
|
||||
let start = s_idx.max(0).min(len) as usize;
|
||||
let end = e_idx.max(start as i64).min(len) as usize;
|
||||
let chars: Vec<char> = sb.value.chars().collect();
|
||||
let sub: String = chars[start..end].iter().collect();
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(sub)))) ; }
|
||||
return Ok(true);
|
||||
return Ok(true);
|
||||
}
|
||||
"substring" => {
|
||||
if args.len() != 2 {
|
||||
return Err(VMError::InvalidInstruction(
|
||||
"substring expects 2 args (start, end)".into(),
|
||||
));
|
||||
}
|
||||
"concat" => {
|
||||
if args.len() != 1 {
|
||||
return Err(VMError::InvalidInstruction("concat expects 1 arg".into()));
|
||||
}
|
||||
let rhs = this.reg_load(args[0])?;
|
||||
let new_s = format!("{}{}", sb.value, rhs.to_string());
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(new_s)))) ; }
|
||||
return Ok(true);
|
||||
let s_idx = this.reg_load(args[0])?.as_integer().unwrap_or(0);
|
||||
let e_idx = this.reg_load(args[1])?.as_integer().unwrap_or(0);
|
||||
let len = sb_norm.value.chars().count() as i64;
|
||||
let start = s_idx.max(0).min(len) as usize;
|
||||
let end = e_idx.max(start as i64).min(len) as usize;
|
||||
let chars: Vec<char> = sb_norm.value.chars().collect();
|
||||
let sub: String = chars[start..end].iter().collect();
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(sub)))) ; }
|
||||
return Ok(true);
|
||||
}
|
||||
"concat" => {
|
||||
if args.len() != 1 {
|
||||
return Err(VMError::InvalidInstruction("concat expects 1 arg".into()));
|
||||
}
|
||||
"is_digit_char" => {
|
||||
// Accept either 0-arg (use first char of receiver) or 1-arg (string/char to test)
|
||||
let ch_opt = if args.is_empty() {
|
||||
sb.value.chars().next()
|
||||
} else if args.len() == 1 {
|
||||
let s = this.reg_load(args[0])?.to_string();
|
||||
s.chars().next()
|
||||
} else {
|
||||
return Err(VMError::InvalidInstruction("is_digit_char expects 0 or 1 arg".into()));
|
||||
};
|
||||
let is_digit = ch_opt.map(|c| c.is_ascii_digit()).unwrap_or(false);
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::Bool(is_digit)); }
|
||||
return Ok(true);
|
||||
}
|
||||
"is_hex_digit_char" => {
|
||||
let ch_opt = if args.is_empty() {
|
||||
sb.value.chars().next()
|
||||
let rhs = this.reg_load(args[0])?;
|
||||
let new_s = format!("{}{}", sb_norm.value, rhs.to_string());
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(new_s)))) ; }
|
||||
return Ok(true);
|
||||
}
|
||||
"is_digit_char" => {
|
||||
// Accept either 0-arg (use first char of receiver) or 1-arg (string/char to test)
|
||||
let ch_opt = if args.is_empty() {
|
||||
sb_norm.value.chars().next()
|
||||
} else if args.len() == 1 {
|
||||
let s = this.reg_load(args[0])?.to_string();
|
||||
s.chars().next()
|
||||
} else {
|
||||
return Err(VMError::InvalidInstruction("is_digit_char expects 0 or 1 arg".into()));
|
||||
};
|
||||
let is_digit = ch_opt.map(|c| c.is_ascii_digit()).unwrap_or(false);
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::Bool(is_digit)); }
|
||||
return Ok(true);
|
||||
}
|
||||
"is_hex_digit_char" => {
|
||||
let ch_opt = if args.is_empty() {
|
||||
sb_norm.value.chars().next()
|
||||
} else if args.len() == 1 {
|
||||
let s = this.reg_load(args[0])?.to_string();
|
||||
s.chars().next()
|
||||
@ -105,9 +112,8 @@ pub(super) fn try_handle_string_box(
|
||||
let is_hex = ch_opt.map(|c| c.is_ascii_hexdigit()).unwrap_or(false);
|
||||
if let Some(d) = dst { this.regs.insert(d, VMValue::Bool(is_hex)); }
|
||||
return Ok(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@ -95,6 +95,9 @@ impl MirInterpreter {
|
||||
(Add, VMValue::Void, Integer(y)) | (Add, Integer(y), VMValue::Void) if tolerate => Integer(y),
|
||||
(Add, VMValue::Void, Float(y)) | (Add, Float(y), VMValue::Void) if tolerate => Float(y),
|
||||
(Add, String(s), VMValue::Void) | (Add, VMValue::Void, String(s)) if tolerate => String(s),
|
||||
// Dev-only safety valve for Sub (guarded): treat Void as 0
|
||||
(Sub, Integer(x), VMValue::Void) if tolerate => Integer(x),
|
||||
(Sub, VMValue::Void, Integer(y)) if tolerate => Integer(0 - y),
|
||||
(Add, Integer(x), Integer(y)) => Integer(x + y),
|
||||
(Add, String(s), Integer(y)) => String(format!("{}{}", s, y)),
|
||||
(Add, String(s), Float(y)) => String(format!("{}{}", s, y)),
|
||||
@ -123,6 +126,12 @@ impl MirInterpreter {
|
||||
(Shl, Integer(x), Integer(y)) => Integer(x.wrapping_shl(y as u32)),
|
||||
(Shr, Integer(x), Integer(y)) => Integer(x.wrapping_shr(y as u32)),
|
||||
(opk, va, vb) => {
|
||||
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!(
|
||||
"[vm-trace] binop error fn={:?} op={:?} a={:?} b={:?} last_block={:?} last_inst={:?}",
|
||||
self.cur_fn, opk, va, vb, self.last_block, self.last_inst
|
||||
);
|
||||
}
|
||||
return Err(VMError::TypeError(format!(
|
||||
"unsupported binop {:?} on {:?} and {:?}",
|
||||
opk, va, vb
|
||||
|
||||
@ -80,6 +80,8 @@ impl ArrayBox {
|
||||
.map(|v| matches!(v.as_str(), "1"|"true"|"on"))
|
||||
.unwrap_or(false);
|
||||
if strict {
|
||||
// Mark OOB occurrence for runner policies (Gate‑C strict fail, etc.)
|
||||
crate::runtime::observe::mark_oob();
|
||||
Box::new(StringBox::new("[oob/array/get] index out of bounds"))
|
||||
} else {
|
||||
Box::new(crate::boxes::null_box::NullBox::new())
|
||||
@ -113,6 +115,7 @@ impl ArrayBox {
|
||||
.map(|v| matches!(v.as_str(), "1"|"true"|"on"))
|
||||
.unwrap_or(false);
|
||||
if strict {
|
||||
crate::runtime::observe::mark_oob();
|
||||
Box::new(StringBox::new("[oob/array/set] index out of bounds"))
|
||||
} else {
|
||||
Box::new(StringBox::new("Error: index out of bounds"))
|
||||
|
||||
@ -554,3 +554,12 @@ pub fn nyvm_v1_downconvert() -> bool {
|
||||
.or_else(|| env_flag("NYASH_NYVM_V1_DOWNCONVERT"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Gate‑C(Core) strict OOB handling: when enabled, any observed OOB tag
|
||||
/// (emitted by runtime during ArrayBox get/set with HAKO_OOB_STRICT=1) should
|
||||
/// cause non‑zero exit at the end of JSON→VM execution.
|
||||
pub fn oob_strict_fail() -> bool {
|
||||
env_flag("HAKO_OOB_STRICT_FAIL")
|
||||
.or_else(|| env_flag("NYASH_OOB_STRICT_FAIL"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@ -23,8 +23,17 @@ impl super::MirBuilder {
|
||||
return self.build_logical_shortcircuit(left, operator, right);
|
||||
}
|
||||
|
||||
let lhs = self.build_expression(left)?;
|
||||
let rhs = self.build_expression(right)?;
|
||||
let lhs_raw = self.build_expression(left)?;
|
||||
let rhs_raw = self.build_expression(right)?;
|
||||
// Correctness-first: ensure both operands have block-local definitions
|
||||
// so they participate in PHI/materialization and avoid use-before-def across
|
||||
// complex control-flow (e.g., loop headers and nested branches).
|
||||
let lhs = self
|
||||
.ensure_slotified_for_use(lhs_raw, "@binop_lhs")
|
||||
.unwrap_or(lhs_raw);
|
||||
let rhs = self
|
||||
.ensure_slotified_for_use(rhs_raw, "@binop_rhs")
|
||||
.unwrap_or(rhs_raw);
|
||||
let dst = self.value_gen.next();
|
||||
|
||||
let mir_op = self.convert_binary_operator(operator)?;
|
||||
|
||||
@ -46,7 +46,13 @@ impl NyashRunner {
|
||||
match crate::runner::json_v1_bridge::try_parse_v1_to_module(&json) {
|
||||
Ok(Some(module)) => {
|
||||
super::json_v0_bridge::maybe_dump_mir(&module);
|
||||
// Gate‑C(Core) strict OOB fail‑fast: reset observe flag before run
|
||||
if crate::config::env::oob_strict_fail() { crate::runtime::observe::reset(); }
|
||||
self.execute_mir_module(&module);
|
||||
if crate::config::env::oob_strict_fail() && crate::runtime::observe::oob_seen() {
|
||||
eprintln!("[gate-c][oob-strict] Out-of-bounds observed → exit(1)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Ok(None) => {}
|
||||
@ -114,7 +120,12 @@ impl NyashRunner {
|
||||
}
|
||||
}
|
||||
// Default: Execute via MIR interpreter
|
||||
if crate::config::env::oob_strict_fail() { crate::runtime::observe::reset(); }
|
||||
self.execute_mir_module(&module);
|
||||
if crate::config::env::oob_strict_fail() && crate::runtime::observe::oob_seen() {
|
||||
eprintln!("[gate-c][oob-strict] Out-of-bounds observed → exit(1)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@ -19,6 +19,7 @@ pub mod semantics;
|
||||
pub mod unified_registry;
|
||||
pub mod provider_lock;
|
||||
pub mod provider_verify;
|
||||
pub mod observe; // Lightweight observability flags (OOB etc.)
|
||||
// pub mod plugin_box; // legacy - 古いPluginBox
|
||||
// pub mod plugin_loader; // legacy - Host VTable使用
|
||||
pub mod extern_registry; // ExternCall (env.*) 登録・診断用レジストリ
|
||||
|
||||
22
src/runtime/observe.rs
Normal file
22
src/runtime/observe.rs
Normal file
@ -0,0 +1,22 @@
|
||||
//! Lightweight execution observability flags used by runner policies
|
||||
//! (e.g., Gate‑C(Core) OOB strict fail‑fast).
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static OOB_SEEN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Reset all transient observation flags before a run.
|
||||
pub fn reset() {
|
||||
OOB_SEEN.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Mark that an out‑of‑bounds access was observed in the runtime.
|
||||
pub fn mark_oob() {
|
||||
OOB_SEEN.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Returns true if an out‑of‑bounds access was observed during the run.
|
||||
pub fn oob_seen() -> bool {
|
||||
OOB_SEEN.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user