refactor(mir): phase260 p0.1 strangler hardening + smoke fixtures

This commit is contained in:
2025-12-21 05:47:37 +09:00
parent 4dfe3349bf
commit 1fe5be347d
28 changed files with 442 additions and 504 deletions

View File

@ -136,6 +136,26 @@ impl MirInterpreter {
}
}
}
// Primitive stringify/toString helpers (compat with std/operators/stringify.hako probes).
// Keep this narrow and total: it prevents "unknown method stringify on IntegerBox" when
// code checks `value.stringify != null` and then calls it.
if args.is_empty() && (method == "stringify" || method == "toString") {
match self.reg_load(box_val)? {
VMValue::Integer(i) => {
self.write_string(dst, i.to_string());
return Ok(());
}
VMValue::Bool(b) => {
self.write_string(dst, if b { "true" } else { "false" }.to_string());
return Ok(());
}
VMValue::Float(f) => {
self.write_string(dst, f.to_string());
return Ok(());
}
_ => {}
}
}
// Trace: method call (class inferred from receiver)
if Self::box_trace_enabled() {
let cls = match self.reg_load(box_val).unwrap_or(VMValue::Void) {

View File

@ -190,52 +190,43 @@ impl MirInterpreter {
// This handles using-imported static boxes that aren't in AST
self.detect_static_boxes_from_functions();
// Determine entry function with sensible fallbacks
// Priority:
// 1) NYASH_ENTRY env (exact), then basename before '/' if provided (e.g., "Main.main/0" → "Main.main")
// 2) "Main.main" if present
// 3) "main" (legacy/simple scripts)
// Determine entry function with sensible fallbacks (arity-aware, Strangler-safe).
//
// Priority (SSOT-ish for VM backend):
// 1) NYASH_ENTRY env (exact; if arity-less, try auto-match)
// 2) Main.main/0
// 3) Main.main
// 4) main (top-level; only if NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1)
let mut candidates: Vec<String> = Vec::new();
if let Ok(e) = std::env::var("NYASH_ENTRY") {
if !e.trim().is_empty() {
candidates.push(e.trim().to_string());
let entry = e.trim();
if !entry.is_empty() {
candidates.push(entry.to_string());
if !entry.contains('/') {
candidates.push(format!("{}/0", entry));
candidates.push(format!("{}/1", entry));
}
}
}
candidates.push(crate::mir::naming::encode_static_method("Main", "main", 0));
candidates.push("Main.main".to_string());
candidates.push("main".to_string());
if crate::config::env::entry_allow_toplevel_main() {
candidates.push("main".to_string());
}
// Try candidates in order
let mut chosen: Option<&nyash_rust::mir::MirFunction> = None;
let mut chosen_name: Option<String> = None;
for c in &candidates {
// exact
if let Some(f) = module.functions.get(c) {
chosen = Some(f);
chosen_name = Some(c.clone());
break;
}
// if contains '/': try name before '/'
if let Some((head, _)) = c.split_once('/') {
if let Some(f) = module.functions.get(head) {
chosen = Some(f);
chosen_name = Some(head.to_string());
break;
}
}
// if looks like "Box.method": try plain "main" as last resort only when c endswith .main
if c.ends_with(".main") {
if let Some(f) = module.functions.get("main") {
chosen = Some(f);
chosen_name = Some("main".to_string());
break;
}
}
}
let func = match chosen {
Some(f) => f,
None => {
// Build helpful error message
let mut names: Vec<&String> = module.functions.keys().collect();
names.sort();
let avail = names

View File

@ -10,6 +10,8 @@
//! silently fall back to an unrelated route.
use crate::ast::ASTNode;
use crate::ast::UnaryOperator;
use crate::ast::LiteralValue;
use crate::mir::loop_pattern_detection::break_condition_analyzer::BreakConditionAnalyzer;
use super::policies::{loop_true_read_digits_policy, PolicyDecision};
@ -24,6 +26,21 @@ pub(crate) struct Pattern2BreakConditionRouting {
pub(crate) struct Pattern2BreakConditionPolicyRouterBox;
impl Pattern2BreakConditionPolicyRouterBox {
fn negate_condition(condition: &ASTNode) -> ASTNode {
match condition {
ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand,
..
} => operand.as_ref().clone(),
other => ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(other.clone()),
span: other.span(),
},
}
}
pub(crate) fn route(condition: &ASTNode, body: &[ASTNode]) -> Result<Pattern2BreakConditionRouting, String> {
// loop(true) read-digits family:
// - multiple breaks exist; normalize as:
@ -36,7 +53,24 @@ impl Pattern2BreakConditionPolicyRouterBox {
}),
PolicyDecision::Reject(reason) => Err(format!("[cf_loop/pattern2] {}", reason)),
PolicyDecision::None => Ok(Pattern2BreakConditionRouting {
// Phase 260 P0.1: If the loop has an explicit header condition and the body
// does not contain a top-level break-guard pattern, the exit condition is
// structurally derived as `!(loop_condition)`.
break_condition_node: BreakConditionAnalyzer::extract_break_condition_node(body)
.or_else(|_| {
if matches!(
condition,
ASTNode::Literal {
value: LiteralValue::Bool(true),
..
}
) {
Err("[cf_loop/pattern2] loop(true) requires a break guard pattern"
.to_string())
} else {
Ok(Self::negate_condition(condition))
}
})
.map_err(|_| {
"[cf_loop/pattern2] Failed to extract break condition from loop body".to_string()
})?,
@ -46,4 +80,3 @@ impl Pattern2BreakConditionPolicyRouterBox {
}
}
}

View File

@ -853,15 +853,15 @@ mod tests {
#[test]
fn test_is_const_step_pattern_negative() {
// i - 1
// i - j (non-const step)
let value = ASTNode::BinaryOp {
operator: BinaryOperator::Subtract,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
right: Box::new(ASTNode::Variable {
name: "j".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),

View File

@ -69,14 +69,28 @@ impl JoinLoopTrace {
/// Create a new tracer, reading environment variables.
pub fn new() -> Self {
use crate::config::env::is_joinir_debug;
let varmap_enabled = std::env::var("NYASH_TRACE_VARMAP").is_ok();
let joinir_enabled = is_joinir_debug();
let phi_enabled = std::env::var("NYASH_OPTION_C_DEBUG").is_ok();
let mainline_enabled = std::env::var("NYASH_JOINIR_MAINLINE_DEBUG").is_ok();
let loopform_enabled = std::env::var("NYASH_LOOPFORM_DEBUG").is_ok();
let capture_enabled = std::env::var("NYASH_CAPTURE_DEBUG").is_ok();
// IMPORTANT:
// `NYASH_JOINIR_DEV=1` is a semantic/feature toggle used by the smoke SSOT.
// It must not implicitly enable noisy stderr traces.
//
// Dev traces are enabled only when JoinIR debug is explicitly requested.
let dev_enabled = crate::config::env::joinir_dev_enabled() && joinir_enabled;
Self {
varmap_enabled: std::env::var("NYASH_TRACE_VARMAP").is_ok(),
joinir_enabled: is_joinir_debug(),
phi_enabled: std::env::var("NYASH_OPTION_C_DEBUG").is_ok(),
mainline_enabled: std::env::var("NYASH_JOINIR_MAINLINE_DEBUG").is_ok(),
loopform_enabled: std::env::var("NYASH_LOOPFORM_DEBUG").is_ok(),
dev_enabled: crate::config::env::joinir_dev_enabled(),
capture_enabled: std::env::var("NYASH_CAPTURE_DEBUG").is_ok(),
varmap_enabled,
joinir_enabled,
phi_enabled,
mainline_enabled,
loopform_enabled,
dev_enabled,
capture_enabled,
}
}
@ -87,7 +101,6 @@ impl JoinLoopTrace {
|| self.phi_enabled
|| self.mainline_enabled
|| self.loopform_enabled
|| self.dev_enabled
|| self.capture_enabled
}

View File

@ -797,6 +797,7 @@ mod tests {
&body,
&cond_env,
&mut join_value_space,
&[],
)
.expect("if-sum lowering should succeed");

View File

@ -361,7 +361,7 @@ mod tests {
fn test_lower_scan_with_init_minimal() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_minimal(&mut join_value_space);
let join_module = lower_scan_with_init_minimal(&mut join_value_space, false);
// main + loop_step + k_exit の3関数
assert_eq!(join_module.functions.len(), 3);
@ -381,7 +381,7 @@ mod tests {
fn test_loop_step_has_substring_box_call() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_minimal(&mut join_value_space);
let join_module = lower_scan_with_init_minimal(&mut join_value_space, false);
// loop_step 関数を取得
let loop_step = join_module
@ -408,7 +408,7 @@ mod tests {
fn test_loop_step_has_exit_jumps() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_minimal(&mut join_value_space);
let join_module = lower_scan_with_init_minimal(&mut join_value_space, false);
// loop_step 関数を取得
let loop_step = join_module

View File

@ -7,6 +7,7 @@
use crate::ast::Span;
use crate::mir::join_ir::{JoinFuncId, JoinInst, MirLikeInst};
use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout;
use crate::mir::{BasicBlockId, EffectMask, MirFunction, MirInstruction, MirType, ValueId};
use crate::mir::types::ConstValue;
use std::collections::BTreeMap;
@ -433,7 +434,7 @@ impl JoinIrBlockConverter {
// produce undefined ValueIds in DirectValue mode.
if let Some(block) = mir_func.blocks.get_mut(&self.current_block_id) {
if !block.has_legacy_jump_args() {
block.set_legacy_jump_args(args.to_vec(), None);
block.set_legacy_jump_args(args.to_vec(), Some(JumpArgsLayout::CarriersOnly));
}
}
@ -503,7 +504,7 @@ impl JoinIrBlockConverter {
let mut exit_block = crate::mir::BasicBlock::new(exit_block_id);
// Phase 246-EX: Store Jump args in metadata for exit PHI construction
exit_block.set_legacy_jump_args(args.to_vec(), None);
exit_block.set_legacy_jump_args(args.to_vec(), Some(JumpArgsLayout::CarriersOnly));
// Phase 256 P1.9: Generate tail call to continuation
exit_block.instructions.push(MirInstruction::Const {
@ -546,7 +547,7 @@ impl JoinIrBlockConverter {
// Preserve jump args as metadata (SSOT for ExitLine/jump_args wiring).
if let Some(block) = mir_func.blocks.get_mut(&self.current_block_id) {
if !block.has_legacy_jump_args() {
block.set_legacy_jump_args(args.to_vec(), None);
block.set_legacy_jump_args(args.to_vec(), Some(JumpArgsLayout::CarriersOnly));
}
}

View File

@ -4,6 +4,7 @@ use super::super::convert_mir_like_inst;
use super::super::join_func_name;
use super::super::JoinIrVmBridgeError;
use crate::ast::Span;
use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout;
use crate::mir::join_ir::normalized::{JpFuncId, JpFunction, JpInst, JpOp, NormalizedModule};
use crate::mir::join_ir::{JoinFuncId, JoinIrPhase, MirLikeInst};
use crate::mir::{
@ -421,7 +422,7 @@ fn build_exit_or_tail_branch(
block.instructions.clear();
block.instruction_spans.clear();
block.set_terminator(MirInstruction::Return { value: ret_val });
block.set_legacy_jump_args(env.to_vec(), None);
block.set_legacy_jump_args(env.to_vec(), Some(JumpArgsLayout::CarriersOnly));
Ok(())
}
@ -467,7 +468,7 @@ fn finalize_block(
block.instruction_spans = vec![Span::unknown(); block.instructions.len()];
block.set_terminator(terminator);
if let Some(args) = jump_args {
block.set_legacy_jump_args(args, None);
block.set_legacy_jump_args(args, Some(JumpArgsLayout::CarriersOnly));
} else {
block.clear_legacy_jump_args();
}

View File

@ -52,7 +52,22 @@ pub fn run_joinir_via_vm(
// Convert JoinValue → VMValue (BoxRef 含む)
let vm_args: Vec<VMValue> = args.iter().cloned().map(|v| v.into_vm_value()).collect();
let entry_name = join_func_name(entry_func);
// Phase 256 P1.7+: Prefer the actual JoinFunction name as the MIR function key.
// Some bridge paths use `join_func_name()` ("join_func_N"), others use JoinFunction.name.
let entry_name_actual = join_module
.functions
.get(&entry_func)
.map(|f| f.name.clone());
let entry_name_fallback = join_func_name(entry_func);
let entry_name = if let Some(name) = entry_name_actual {
if mir_module.functions.contains_key(&name) {
name
} else {
entry_name_fallback
}
} else {
entry_name_fallback
};
let result = vm.execute_function_with_args(&mir_module, &entry_name, &vm_args)?;
// Step 3: VMValue → JoinValue 変換

View File

@ -399,8 +399,7 @@ pub fn normalize_legacy_instructions(
}
other => other,
};
block.terminator = Some(rewritten);
block.terminator_span = Some(span);
block.set_terminator_with_span(rewritten, span);
}
let (insts, spans): (Vec<_>, Vec<_>) =
@ -508,7 +507,7 @@ pub fn normalize_ref_field_access(
if let Some(term) = block.terminator.take() {
let term_span = block.terminator_span.take().unwrap_or_else(Span::unknown);
block.terminator = Some(match term {
let rewritten = match term {
I::RefGet {
dst,
reference,
@ -557,10 +556,8 @@ pub fn normalize_ref_field_access(
}
}
other => other,
});
if block.terminator_span.is_none() {
block.terminator_span = Some(term_span);
}
};
block.set_terminator_with_span(rewritten, term_span);
}
block.effects = block

View File

@ -151,7 +151,7 @@ pub fn normalize_pure_core13(_opt: &mut MirOptimizer, module: &mut MirModule) ->
if let Some(term) = block.terminator.take() {
let term_span = block.terminator_span.take().unwrap_or_else(Span::unknown);
block.terminator = Some(match term {
let rewritten = match term {
I::Load { dst, ptr } => I::ExternCall {
dst: Some(dst),
iface_name: "env.local".to_string(),
@ -237,8 +237,8 @@ pub fn normalize_pure_core13(_opt: &mut MirOptimizer, module: &mut MirModule) ->
}
},
other => other,
});
block.terminator_span = Some(term_span);
};
block.set_terminator_with_span(rewritten, term_span);
}
}
}

View File

@ -28,6 +28,12 @@ pub fn check_control_flow(function: &MirFunction) -> Result<(), Vec<Verification
}
// Phase 260 P0: Fail-fast if terminator edge-args and legacy jump_args diverge.
if block.has_legacy_jump_args() && block.legacy_jump_args_layout().is_none() {
errors.push(VerificationError::ControlFlowError {
block: *block_id,
reason: "Legacy jump_args layout missing".to_string(),
});
}
if let Some(term) = &block.terminator {
match term {
MirInstruction::Jump {

View File

@ -488,6 +488,41 @@ impl NyashRunner {
}
}
// CLI emit: MIR JSON / EXE
// NOTE: These flags are CLI-level and should work regardless of selected backend.
// The VM runner is a common default backend, so we honor them here and exit early.
{
let groups = self.config.as_groups();
if let Some(path) = groups.emit.emit_mir_json.as_ref() {
let p = std::path::Path::new(path);
if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(
&module_vm, p,
) {
eprintln!("❌ MIR JSON emit error: {}", e);
process::exit(1);
}
if !quiet_pipe {
println!("MIR JSON written: {}", p.display());
}
process::exit(0);
}
if let Some(exe_out) = groups.emit.emit_exe.as_ref() {
if let Err(e) = crate::runner::modes::common_util::exec::ny_llvmc_emit_exe_bin(
&module_vm,
exe_out,
groups.emit.emit_exe_nyrt.as_deref(),
groups.emit.emit_exe_libs.as_deref(),
) {
eprintln!("{}", e);
process::exit(1);
}
if !quiet_pipe {
println!("EXE written: {}", exe_out);
}
process::exit(0);
}
}
// Optional: dump MIR for diagnostics
// Phase 25.1: File dump for offline analysis (ParserBox等)
if let Ok(path) = std::env::var("RUST_MIR_DUMP_PATH") {