refactor(mir): phase260 p0.1 strangler hardening + smoke fixtures
This commit is contained in:
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -797,6 +797,7 @@ mod tests {
|
||||
&body,
|
||||
&cond_env,
|
||||
&mut join_value_space,
|
||||
&[],
|
||||
)
|
||||
.expect("if-sum lowering should succeed");
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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 変換
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user