fix(mir): PHI検証panic修正 - update_cfg()を検証前に呼び出し

A案実装: debug_verify_phi_inputs呼び出し前にCFG predecessorを更新

修正箇所(7箇所):
- src/mir/builder/phi.rs:50, 73, 132, 143
- src/mir/builder/ops.rs:273, 328, 351

根本原因:
- Branch/Jump命令でsuccessorは即座に更新
- predecessorはupdate_cfg()で遅延再構築
- PHI検証が先に実行されてpredecessor未更新でpanic

解決策:
- 各debug_verify_phi_inputs呼び出し前に
  if let Some(func) = self.current_function.as_mut() {
      func.update_cfg();
  }
  を挿入してCFGを同期

影響: if/else文、論理演算子(&&/||)のPHI生成が正常動作
This commit is contained in:
nyash-codex
2025-11-01 13:28:56 +09:00
parent 149ec61d4d
commit 6a452b2dca
174 changed files with 2432 additions and 1014 deletions

View File

@ -19,10 +19,18 @@ impl MirInterpreter {
let saved_fn = self.cur_fn.clone();
self.cur_fn = Some(func.signature.name.clone());
if let Some(args) = arg_vals {
for (i, pid) in func.params.iter().enumerate() {
let v = args.get(i).cloned().unwrap_or(VMValue::Void);
self.regs.insert(*pid, v);
match arg_vals {
Some(args) => {
for (i, pid) in func.params.iter().enumerate() {
let v = args.get(i).cloned().unwrap_or(VMValue::Void);
self.regs.insert(*pid, v);
}
}
None => {
// Seed all parameters with Void so SSA consumers observe defined values.
for pid in &func.params {
self.regs.insert(*pid, VMValue::Void);
}
}
}

View File

@ -23,6 +23,34 @@ pub(super) fn try_handle_map_box(
if let Some(d) = dst { this.regs.insert(d, VMValue::Void); }
return Ok(true);
}
// Field bridge: treat getField/setField as get/set with string key
"getField" => {
if args.len() != 1 {
return Err(VMError::InvalidInstruction(
"MapBox.getField expects 1 arg".into(),
));
}
let k = this.reg_load(args[0])?.to_nyash_box();
let ret = mb.get(k);
if let Some(d) = dst {
this.regs.insert(d, VMValue::from_nyash_box(ret));
}
return Ok(true);
}
"setField" => {
if args.len() != 2 {
return Err(VMError::InvalidInstruction(
"MapBox.setField expects 2 args".into(),
));
}
let k = this.reg_load(args[0])?.to_nyash_box();
let v = this.reg_load(args[1])?.to_nyash_box();
let ret = mb.set(k, v);
if let Some(d) = dst {
this.regs.insert(d, VMValue::from_nyash_box(ret));
}
return Ok(true);
}
"set" => {
if args.len() != 2 { return Err(VMError::InvalidInstruction("MapBox.set expects 2 args".into())); }
let k = this.reg_load(args[0])?.to_nyash_box();

View File

@ -839,8 +839,8 @@ impl super::MirBuilder {
self.variable_map.insert(p.clone(), pid);
}
}
let program_ast = function_lowering::wrap_in_program(body);
let _last = self.build_expression(program_ast)?;
// Lower statements in sequence to preserve def→use order
let _last = self.cf_block(body)?;
if !returns_value && !self.is_current_block_terminated() {
let void_val = crate::mir::builder::emission::constant::emit_void(self);
self.emit_instruction(MirInstruction::Return {

View File

@ -54,6 +54,15 @@ impl super::MirBuilder {
self
.value_types
.insert(pid, super::MirType::Box("ArrayBox".to_string()));
// Explicitly call birth() to initialize internal state
self.emit_instruction(MirInstruction::BoxCall {
dst: None,
box_val: pid,
method: "birth".to_string(),
method_id: None,
args: vec![],
effects: super::EffectMask::MUT,
})?;
if let Some(args) = script_args.as_ref() {
for arg in args {
let val = crate::mir::builder::emission::constant::emit_string(self, arg.clone());
@ -75,7 +84,8 @@ impl super::MirBuilder {
}
self.variable_map.insert(p.clone(), pid);
}
let lowered = self.build_expression(program_ast);
// Lower statements in order to preserve def→use
let lowered = self.cf_block(body.clone());
self.variable_map = saved_var_map;
lowered
} else {

View File

@ -219,6 +219,15 @@ impl super::MirBuilder {
box_type: "ArrayBox".to_string(),
args: vec![],
})?;
// Explicit birth() to satisfy runtime invariant (NewBox→birth)
self.emit_instruction(MirInstruction::BoxCall {
dst: None,
box_val: arr_id,
method: "birth".to_string(),
method_id: None,
args: vec![],
effects: super::EffectMask::MUT,
})?;
self.value_origin_newbox
.insert(arr_id, "ArrayBox".to_string());
self
@ -244,6 +253,15 @@ impl super::MirBuilder {
box_type: "MapBox".to_string(),
args: vec![],
})?;
// Explicit birth() to satisfy runtime invariant (NewBox→birth)
self.emit_instruction(MirInstruction::BoxCall {
dst: None,
box_val: map_id,
method: "birth".to_string(),
method_id: None,
args: vec![],
effects: super::EffectMask::MUT,
})?;
self
.value_origin_newbox
.insert(map_id, "MapBox".to_string());

View File

@ -269,6 +269,9 @@ impl super::MirBuilder {
self.start_new_block(rhs_join)?;
let rhs_bool = self.value_gen.next();
let inputs = vec![(rhs_true_exit, t_id), (rhs_false_exit, f_id)];
if let Some(func) = self.current_function.as_mut() {
func.update_cfg();
}
if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) {
crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs);
}
@ -324,6 +327,9 @@ impl super::MirBuilder {
self.start_new_block(rhs_join)?;
let rhs_bool = self.value_gen.next();
let inputs = vec![(rhs_true_exit, t_id), (rhs_false_exit, f_id)];
if let Some(func) = self.current_function.as_mut() {
func.update_cfg();
}
if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) {
crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs);
}
@ -347,6 +353,9 @@ impl super::MirBuilder {
// Result PHI (bool)
let result_val = self.value_gen.next();
let inputs = vec![(then_exit_block, then_value_raw), (else_exit_block, else_value_raw)];
if let Some(func) = self.current_function.as_mut() {
func.update_cfg();
}
if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) {
crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs);
}

View File

@ -46,6 +46,9 @@ impl MirBuilder {
let else_pred = else_exit_block_opt.unwrap_or(else_block);
let merged = self.value_gen.next();
let inputs = vec![(then_pred, then_v), (else_pred, else_v)];
if let Some(func) = self.current_function.as_mut() {
func.update_cfg();
}
if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) {
crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs);
}
@ -69,6 +72,9 @@ impl MirBuilder {
let else_pred = else_exit_block_opt.unwrap_or(else_block);
let merged = self.value_gen.next();
let inputs = vec![(then_pred, then_v), (else_pred, else_v)];
if let Some(func) = self.current_function.as_mut() {
func.update_cfg();
}
if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) {
crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs);
}
@ -128,6 +134,9 @@ impl MirBuilder {
let else_pred = else_exit_block_opt.unwrap_or(else_block);
// Emit Phi for the assigned variable and bind it
let inputs = vec![(then_pred, then_value_for_var), (else_pred, else_value_for_var)];
if let Some(func) = self.current_function.as_mut() {
func.update_cfg();
}
if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) {
crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs);
}
@ -139,6 +148,9 @@ impl MirBuilder {
let then_pred = then_exit_block_opt.unwrap_or(then_block);
let else_pred = else_exit_block_opt.unwrap_or(else_block);
let inputs = vec![(then_pred, then_value_raw), (else_pred, else_value_raw)];
if let Some(func) = self.current_function.as_mut() {
func.update_cfg();
}
if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) {
crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs);
}

View File

@ -8,6 +8,11 @@ pub fn check_ssa_form(function: &MirFunction) -> Result<(), Vec<VerificationErro
let mut errors = Vec::new();
let mut definitions: HashMap<ValueId, (crate::mir::BasicBlockId, usize)> = HashMap::new();
// Treat parameters as defined at the entry block.
for pid in &function.params {
definitions.insert(*pid, (function.entry_block, 0));
}
for (block_id, block) in &function.blocks {
for (inst_idx, instruction) in block.all_instructions().enumerate() {
if let Some(dst) = instruction.dst_value() {
@ -38,4 +43,3 @@ pub fn check_ssa_form(function: &MirFunction) -> Result<(), Vec<VerificationErro
if errors.is_empty() { Ok(()) } else { Err(errors) }
}

View File

@ -13,6 +13,9 @@ pub fn compute_predecessors(function: &MirFunction) -> HashMap<BasicBlockId, Vec
pub fn compute_def_blocks(function: &MirFunction) -> HashMap<ValueId, BasicBlockId> {
let mut def_block: HashMap<ValueId, BasicBlockId> = HashMap::new();
for pid in &function.params {
def_block.insert(*pid, function.entry_block);
}
for (bid, block) in &function.blocks {
for inst in block.all_instructions() {
if let Some(dst) = inst.dst_value() {

View File

@ -60,8 +60,8 @@ pub trait ParserUtils {
if !cursor_on {
let allow_sc = std::env::var("NYASH_PARSER_ALLOW_SEMICOLON").ok().map(|v| {
let lv = v.to_ascii_lowercase();
lv == "1" || lv == "true" || lv == "on"
}).unwrap_or(false);
!(lv == "0" || lv == "false" || lv == "off")
}).unwrap_or(true);
loop {
let is_nl = matches!(self.current_token().token_type, TokenType::NEWLINE);
let is_sc = allow_sc && matches!(self.current_token().token_type, TokenType::SEMICOLON);

View File

@ -232,8 +232,8 @@ impl NyashParser {
let allow_sc = std::env::var("NYASH_PARSER_ALLOW_SEMICOLON").ok().map(|v| {
let lv = v.to_ascii_lowercase();
lv == "1" || lv == "true" || lv == "on"
}).unwrap_or(false);
!(lv == "0" || lv == "false" || lv == "off")
}).unwrap_or(true);
while !self.is_at_end() {
// EOF tokenはスキップ

View File

@ -64,7 +64,8 @@ pub fn collect_using_and_strip(
let is_path = target.starts_with('"')
|| target.starts_with("./")
|| target.starts_with('/')
|| target.ends_with(".nyash");
|| target.ends_with(".nyash")
|| target.ends_with(".hako");
if is_path {
// SSOT: Disallow file-using at top-level; allow only for sources located
// under a declared package root (internal package wiring), so that packages
@ -169,12 +170,12 @@ pub fn collect_using_and_strip(
PackageKind::Package => {
let base = std::path::Path::new(&pkg.path);
let out = if let Some(m) = &pkg.main {
if base.extension().and_then(|s| s.to_str()) == Some("nyash") {
if matches!(base.extension().and_then(|s| s.to_str()), Some("nyash") | Some("hako")) {
pkg.path.clone()
} else {
base.join(m).to_string_lossy().to_string()
}
} else if base.extension().and_then(|s| s.to_str()) == Some("nyash") {
} else if matches!(base.extension().and_then(|s| s.to_str()), Some("nyash") | Some("hako")) {
pkg.path.clone()
} else {
let leaf = base

View File

@ -101,33 +101,66 @@ impl NyashRunner {
}
};
// Using handling: AST-prelude collection (legacy inlining removed)
let code = if crate::config::env::enable_using() {
match crate::runner::modes::common_util::resolve::resolve_prelude_paths_profiled(self, &code, filename) {
// Using handling: collect/merge preludes when enabled
let using_ast = crate::config::env::using_ast_enabled();
let mut code_ref: &str = &code;
let cleaned_owned;
let mut prelude_asts: Vec<nyash_rust::ast::ASTNode> = Vec::new();
if crate::config::env::enable_using() {
match crate::runner::modes::common_util::resolve::resolve_prelude_paths_profiled(
self,
&code,
filename,
) {
Ok((clean, paths)) => {
if !paths.is_empty() && !crate::config::env::using_ast_enabled() {
cleaned_owned = clean;
code_ref = &cleaned_owned;
if !paths.is_empty() && !using_ast {
eprintln!("❌ using: AST prelude merge is disabled in this profile. Enable NYASH_USING_AST=1 or remove 'using' lines.");
process::exit(1);
}
// VM path currently does not merge prelude ASTs; rely on compile pipeline path for that.
clean
if using_ast && !paths.is_empty() {
match crate::runner::modes::common_util::resolve::parse_preludes_to_asts(
self, &paths,
) {
Ok(v) => prelude_asts = v,
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
}
}
}
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
Err(e) => { eprintln!("{}", e); process::exit(1); }
}
} else { code };
}
// Pre-expand '@name[:T] = expr' sugar at line-head (same as common/llvm/pyvm paths)
let code = crate::runner::modes::common_util::resolve::preexpand_at_local(&code);
let preexpanded_owned =
crate::runner::modes::common_util::resolve::preexpand_at_local(code_ref);
code_ref = &preexpanded_owned;
// Parse to AST
let ast = match NyashParser::parse_from_string(&code) {
let main_ast = match NyashParser::parse_from_string(code_ref) {
Ok(ast) => ast,
Err(e) => {
eprintln!("❌ Parse error: {}", e);
process::exit(1);
}
};
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
// Merge prelude ASTs (opt-in)
let merged_ast = if using_ast && !prelude_asts.is_empty() {
crate::runner::modes::common_util::resolve::merge_prelude_asts_with_main(
prelude_asts,
&main_ast,
)
} else {
main_ast
};
let ast = crate::r#macro::maybe_expand_and_dump(&merged_ast, false);
// Prepare runtime and collect Box declarations for VM user-defined types
let runtime = {
@ -480,6 +513,13 @@ impl NyashRunner {
type_parameters: type_parameters.clone(),
};
if let Ok(mut map) = runtime.box_declarations.write() {
if std::env::var("NYASH_BOX_DECL_TRACE")
.ok()
.as_deref()
== Some("1")
{
eprintln!("[box-decl] register {}", name);
}
map.insert(name.clone(), decl);
}
}

View File

@ -206,18 +206,21 @@ pub(super) fn resolve_using_target(
// Compute entry: main or <dir_last>.nyash
let base = std::path::Path::new(&pkg.path);
let out = if let Some(m) = &pkg.main {
if base.extension().and_then(|s| s.to_str()) == Some("nyash") {
if matches!(base.extension().and_then(|s| s.to_str()), Some("nyash") | Some("hako")) {
// path is a file; ignore main and use as-is
pkg.path.clone()
} else {
base.join(m).to_string_lossy().to_string()
}
} else {
if base.extension().and_then(|s| s.to_str()) == Some("nyash") {
if matches!(base.extension().and_then(|s| s.to_str()), Some("nyash") | Some("hako")) {
pkg.path.clone()
} else {
let leaf = base.file_name().and_then(|s| s.to_str()).unwrap_or(tgt);
base.join(format!("{}.nyash", leaf)).to_string_lossy().to_string()
// prefer .hako when package path points to a directory without explicit main
let hako = base.join(format!("{}.hako", leaf));
if hako.exists() { hako.to_string_lossy().to_string() }
else { base.join(format!("{}.nyash", leaf)).to_string_lossy().to_string() }
}
};
if trace {

View File

@ -0,0 +1,26 @@
use crate::parser::NyashParser;
#[test]
fn parse_top_level_semicolons_optional() {
let src = r#"
local a = 1; local b = 2
return a + b;
"#;
let ast = NyashParser::parse_from_string(src).expect("parser should accept semicolons by default");
// Smoke: just ensure it parses into a Program
match ast { crate::ast::ASTNode::Program { .. } => {}, _ => panic!("expected Program") }
}
#[test]
fn parse_block_with_semicolons() {
let src = r#"
static box Main {
static method main() {
local out = ""; local digits = "0123456789"; return 0
}
}
"#;
let ast = NyashParser::parse_from_string(src).expect("parser should accept semicolons inside blocks");
match ast { crate::ast::ASTNode::Program { .. } => {}, _ => panic!("expected Program") }
}

View File

@ -3,12 +3,15 @@ use super::{NyashTokenizer, Token, TokenType, TokenizeError};
impl NyashTokenizer {
#[inline]
pub(crate) fn allow_semicolon() -> bool {
// Default: ON (semicolon is an optional statement separator)
// Allow opt-out via NYASH_PARSER_ALLOW_SEMICOLON=0|false|off
match std::env::var("NYASH_PARSER_ALLOW_SEMICOLON").ok() {
Some(v) => {
let lv = v.to_ascii_lowercase();
lv == "1" || lv == "true" || lv == "on"
if lv == "0" || lv == "false" || lv == "off" { return false; }
true
}
None => false,
None => true,
}
}