🚀 Break/Continue/Try-Catch構文のサポート追加とMIRループ制御強化
## 主な変更点 ### 🎯 MIRループ制御の実装(根治対応) - src/mir/loop_builder.rs: Break/Continue対応のループコンテキスト管理 - ループのbreak/continueターゲットブロック追跡 - ネストループの適切な処理 - src/mir/builder.rs: Break/Continue文のMIR生成実装 - src/tokenizer.rs: Break/Continue/Tryトークン認識追加 ### 📝 セルフホストパーサーの拡張 - apps/selfhost-compiler/boxes/parser_box.nyash: - Stage-3: break/continue構文受理(no-op実装) - Stage-3: try-catch-finally構文受理(構文解析のみ) - エラー処理構文の将来対応準備 ### 📚 ドキュメント更新 - 論文K(爆速事件簿): 45事例に更新(4件追加) - PyVM迂回路の混乱事件 - Break/Continue無限ループ事件 - EXE-first戦略の再発見 - 論文I(開発秘話): Day 38の重要決定追加 ### 🧪 テストケース追加 - apps/tests/: ループ制御とPHIのテストケース - nested_loop_inner_break_isolated.nyash - nested_loop_inner_continue_isolated.nyash - loop_phi_one_sided.nyash - shortcircuit関連テスト ## 技術的詳細 - Break/ContinueをMIRレベルで適切に処理 - 無限ループ問題(CPU 99.9%暴走)の根本解決 - 将来の例外処理機能への準備 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -80,6 +80,11 @@ pub struct MirBuilder {
|
||||
include_loading: HashSet<String>,
|
||||
/// Include visited cache: canonical path -> box name
|
||||
include_box_map: HashMap<String, String>,
|
||||
|
||||
/// Loop context stacks for lowering break/continue inside nested control flow
|
||||
/// Top of stack corresponds to the innermost active loop
|
||||
pub(super) loop_header_stack: Vec<BasicBlockId>,
|
||||
pub(super) loop_exit_stack: Vec<BasicBlockId>,
|
||||
}
|
||||
|
||||
impl MirBuilder {
|
||||
@ -159,6 +164,8 @@ impl MirBuilder {
|
||||
current_static_box: None,
|
||||
include_loading: HashSet::new(),
|
||||
include_box_map: HashMap::new(),
|
||||
loop_header_stack: Vec::new(),
|
||||
loop_exit_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +77,8 @@ impl super::MirBuilder {
|
||||
|
||||
ASTNode::Return { value, .. } => self.build_return_statement(value.clone()),
|
||||
|
||||
// Control flow: break/continue are handled inside LoopBuilder context
|
||||
|
||||
ASTNode::Local { variables, initial_values, .. } =>
|
||||
self.build_local_statement(variables.clone(), initial_values.clone()),
|
||||
|
||||
|
||||
@ -67,6 +67,9 @@ impl<'a> LoopBuilder<'a> {
|
||||
let after_loop_id = self.new_block();
|
||||
self.loop_header = Some(header_id);
|
||||
self.continue_snapshots.clear();
|
||||
self.parent_builder.loop_exit_stack.push(after_loop_id);
|
||||
// Push loop context to parent builder (for nested break/continue lowering)
|
||||
self.parent_builder.loop_header_stack.push(header_id);
|
||||
|
||||
// 2. Preheader -> Header へのジャンプ
|
||||
self.emit_jump(header_id)?;
|
||||
@ -115,11 +118,15 @@ impl<'a> LoopBuilder<'a> {
|
||||
|
||||
// 10. ループ後の処理
|
||||
self.set_current_block(after_loop_id)?;
|
||||
|
||||
// Pop loop context
|
||||
let _ = self.parent_builder.loop_header_stack.pop();
|
||||
// loop exit stack mirrors header stack; maintain symmetry
|
||||
let _ = self.parent_builder.loop_exit_stack.pop();
|
||||
|
||||
// void値を返す
|
||||
let void_dst = self.new_value();
|
||||
self.emit_const(void_dst, ConstValue::Void)?;
|
||||
|
||||
|
||||
Ok(void_dst)
|
||||
}
|
||||
|
||||
@ -313,6 +320,88 @@ impl<'a> LoopBuilder<'a> {
|
||||
|
||||
fn build_statement(&mut self, stmt: ASTNode) -> Result<ValueId, String> {
|
||||
match stmt {
|
||||
ASTNode::If { condition, then_body, else_body, .. } => {
|
||||
// Lower a simple if inside loop, ensuring continue/break inside branches are handled
|
||||
let cond_val = self.parent_builder.build_expression(*condition.clone())?;
|
||||
let then_bb = self.new_block();
|
||||
let else_bb = self.new_block();
|
||||
let merge_bb = self.new_block();
|
||||
self.emit_branch(cond_val, then_bb, else_bb)?;
|
||||
|
||||
// then
|
||||
self.set_current_block(then_bb)?;
|
||||
for s in then_body.iter().cloned() {
|
||||
let _ = self.build_statement(s)?;
|
||||
// Stop if block terminated
|
||||
let cur_id = self.current_block()?;
|
||||
let terminated = {
|
||||
if let Some(ref fun_ro) = self.parent_builder.current_function {
|
||||
if let Some(bb) = fun_ro.get_block(cur_id) { bb.is_terminated() } else { false }
|
||||
} else { false }
|
||||
};
|
||||
if terminated { break; }
|
||||
}
|
||||
// Only jump to merge if not already terminated (e.g., continue/break)
|
||||
{
|
||||
let cur_id = self.current_block()?;
|
||||
let need_jump = {
|
||||
if let Some(ref fun_ro) = self.parent_builder.current_function {
|
||||
if let Some(bb) = fun_ro.get_block(cur_id) { !bb.is_terminated() } else { false }
|
||||
} else { false }
|
||||
};
|
||||
if need_jump { self.emit_jump(merge_bb)?; }
|
||||
}
|
||||
|
||||
// else
|
||||
self.set_current_block(else_bb)?;
|
||||
if let Some(es) = else_body {
|
||||
for s in es.into_iter() {
|
||||
let _ = self.build_statement(s)?;
|
||||
let cur_id = self.current_block()?;
|
||||
let terminated = {
|
||||
if let Some(ref fun_ro) = self.parent_builder.current_function {
|
||||
if let Some(bb) = fun_ro.get_block(cur_id) { bb.is_terminated() } else { false }
|
||||
} else { false }
|
||||
};
|
||||
if terminated { break; }
|
||||
}
|
||||
}
|
||||
{
|
||||
let cur_id = self.current_block()?;
|
||||
let need_jump = {
|
||||
if let Some(ref fun_ro) = self.parent_builder.current_function {
|
||||
if let Some(bb) = fun_ro.get_block(cur_id) { !bb.is_terminated() } else { false }
|
||||
} else { false }
|
||||
};
|
||||
if need_jump { self.emit_jump(merge_bb)?; }
|
||||
}
|
||||
|
||||
// Continue at merge
|
||||
self.set_current_block(merge_bb)?;
|
||||
let void_id = self.new_value();
|
||||
self.emit_const(void_id, ConstValue::Void)?;
|
||||
Ok(void_id)
|
||||
}
|
||||
ASTNode::Break { .. } => {
|
||||
// Jump to loop exit (after_loop_id) if available
|
||||
let cur_block = self.current_block()?;
|
||||
// Ensure parent has recorded current loop exit; if not, record now
|
||||
if self.parent_builder.loop_exit_stack.last().copied().is_none() {
|
||||
// Determine after_loop by peeking the next id used earlier:
|
||||
// In this builder, after_loop_id was created above; record it for nested lowering
|
||||
// We approximate by using the next block id minus 1 (after_loop) which we set below before branch
|
||||
}
|
||||
if let Some(exit_bb) = self.parent_builder.loop_exit_stack.last().copied() {
|
||||
self.emit_jump(exit_bb)?;
|
||||
let _ = self.add_predecessor(exit_bb, cur_block);
|
||||
}
|
||||
// Keep building in a fresh (unreachable) block to satisfy callers
|
||||
let next_block = self.new_block();
|
||||
self.set_current_block(next_block)?;
|
||||
let void_id = self.new_value();
|
||||
self.emit_const(void_id, ConstValue::Void)?;
|
||||
Ok(void_id)
|
||||
}
|
||||
ASTNode::Continue { .. } => {
|
||||
let snapshot = self.get_current_variable_map();
|
||||
let cur_block = self.current_block()?;
|
||||
|
||||
@ -298,6 +298,57 @@ impl NyashRunner {
|
||||
if emit_only {
|
||||
return false;
|
||||
} else {
|
||||
// Prefer PyVM when requested AND the module contains BoxCalls (Stage-2 semantics)
|
||||
let needs_pyvm = module.functions.values().any(|f| {
|
||||
f.blocks.values().any(|bb| bb.instructions.iter().any(|inst| matches!(inst, crate::mir::MirInstruction::BoxCall { .. })))
|
||||
});
|
||||
if needs_pyvm && std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1") {
|
||||
if let Ok(py3) = which::which("python3") {
|
||||
let runner = std::path::Path::new("tools/pyvm_runner.py");
|
||||
if runner.exists() {
|
||||
let tmp_dir = std::path::Path::new("tmp");
|
||||
let _ = std::fs::create_dir_all(tmp_dir);
|
||||
let mir_json_path = tmp_dir.join("nyash_pyvm_mir.json");
|
||||
if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(&module, &mir_json_path) {
|
||||
eprintln!("❌ PyVM MIR JSON emit error: {}", e);
|
||||
return true; // prevent double-run fallback
|
||||
}
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[ny-compiler] using PyVM (exe) → {}", mir_json_path.display());
|
||||
}
|
||||
// Determine entry function hint (prefer Main.main if present)
|
||||
let entry = if module.functions.contains_key("Main.main") { "Main.main" }
|
||||
else if module.functions.contains_key("main") { "main" } else { "Main.main" };
|
||||
let status = std::process::Command::new(py3)
|
||||
.args([
|
||||
runner.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
])
|
||||
.status()
|
||||
.map_err(|e| format!("spawn pyvm: {}", e))
|
||||
.unwrap();
|
||||
let code = status.code().unwrap_or(1);
|
||||
if !status.success() {
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("❌ PyVM (exe) failed (status={})", code);
|
||||
}
|
||||
}
|
||||
// Harmonize CLI output with interpreter path for smokes
|
||||
println!("Result: {}", code);
|
||||
std::process::exit(code);
|
||||
} else {
|
||||
eprintln!("❌ PyVM runner not found: {}", runner.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
eprintln!("❌ python3 not found in PATH. Install Python 3 to use PyVM.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
// Default: execute via built-in MIR interpreter
|
||||
self.execute_mir_module(&module);
|
||||
return true;
|
||||
}
|
||||
@ -449,6 +500,57 @@ impl NyashRunner {
|
||||
// Do not execute; fall back to default path to keep final Result unaffected (Stage‑1 policy)
|
||||
false
|
||||
} else {
|
||||
// Prefer PyVM when requested AND the module contains BoxCalls
|
||||
let needs_pyvm = module.functions.values().any(|f| {
|
||||
f.blocks.values().any(|bb| bb.instructions.iter().any(|inst| matches!(inst, crate::mir::MirInstruction::BoxCall { .. })))
|
||||
});
|
||||
if needs_pyvm && std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1") {
|
||||
if let Ok(py3) = which::which("python3") {
|
||||
let runner = std::path::Path::new("tools/pyvm_runner.py");
|
||||
if runner.exists() {
|
||||
let tmp_dir = std::path::Path::new("tmp");
|
||||
let _ = std::fs::create_dir_all(tmp_dir);
|
||||
let mir_json_path = tmp_dir.join("nyash_pyvm_mir.json");
|
||||
if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(&module, &mir_json_path) {
|
||||
eprintln!("❌ PyVM MIR JSON emit error: {}", e);
|
||||
return true; // prevent double-run fallback
|
||||
}
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[ny-compiler] using PyVM (mvp) → {}", mir_json_path.display());
|
||||
}
|
||||
// Determine entry function hint (prefer Main.main if present)
|
||||
let entry = if module.functions.contains_key("Main.main") { "Main.main" }
|
||||
else if module.functions.contains_key("main") { "main" } else { "Main.main" };
|
||||
let status = std::process::Command::new(py3)
|
||||
.args([
|
||||
runner.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
])
|
||||
.status()
|
||||
.map_err(|e| format!("spawn pyvm: {}", e))
|
||||
.unwrap();
|
||||
let code = status.code().unwrap_or(1);
|
||||
if !status.success() {
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("❌ PyVM (mvp) failed (status={})", code);
|
||||
}
|
||||
}
|
||||
// Harmonize CLI output with interpreter path for smokes
|
||||
println!("Result: {}", code);
|
||||
std::process::exit(code);
|
||||
} else {
|
||||
eprintln!("❌ PyVM runner not found: {}", runner.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
eprintln!("❌ python3 not found in PATH. Install Python 3 to use PyVM.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
// Default: execute via MIR interpreter
|
||||
self.execute_mir_module(&module);
|
||||
true
|
||||
}
|
||||
|
||||
78
src/tests/mir_controlflow_extras.rs
Normal file
78
src/tests/mir_controlflow_extras.rs
Normal file
@ -0,0 +1,78 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::parser::NyashParser;
|
||||
use crate::backend::VM;
|
||||
|
||||
fn run(code: &str) -> String {
|
||||
let ast = NyashParser::parse_from_string(code).expect("parse");
|
||||
let mut compiler = crate::mir::MirCompiler::new();
|
||||
let result = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new();
|
||||
let out = vm.execute_module(&result.module).expect("vm exec");
|
||||
out.to_string_box().value
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phi_merge_then_only_assignment() {
|
||||
let code = r#"
|
||||
local x = 5
|
||||
if 1 < 2 { x = 7 } else { }
|
||||
return x
|
||||
"#;
|
||||
assert_eq!(run(code), "7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phi_merge_else_only_assignment() {
|
||||
let code = r#"
|
||||
local y = 5
|
||||
if 2 < 1 { y = 7 } else { }
|
||||
return y
|
||||
"#;
|
||||
assert_eq!(run(code), "5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortcircuit_and_skips_rhs_side_effect() {
|
||||
let code = r#"
|
||||
local x = 0
|
||||
((x = x + 1) < 0) && ((x = x + 1) < 0)
|
||||
return x
|
||||
"#;
|
||||
// LHS false ⇒ RHS not evaluated ⇒ x == 1
|
||||
assert_eq!(run(code), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortcircuit_or_skips_rhs_side_effect() {
|
||||
let code = r#"
|
||||
local x = 0
|
||||
((x = x + 1) >= 0) || ((x = x + 1) < 0)
|
||||
return x
|
||||
"#;
|
||||
// LHS true ⇒ RHS not evaluated ⇒ x == 1
|
||||
assert_eq!(run(code), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_loops_break_continue_mixed() {
|
||||
let code = r#"
|
||||
local i = 0
|
||||
local s = 0
|
||||
loop(i < 3) {
|
||||
local j = 0
|
||||
loop(j < 4) {
|
||||
j = j + 1
|
||||
if j == 1 { continue }
|
||||
if j == 3 { break }
|
||||
s = s + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return s
|
||||
"#;
|
||||
// For each i: j=1 continue (skip s), j=2 => s++, then j=3 break ⇒ s increments once per outer iter ⇒ 3
|
||||
assert_eq!(run(code), "3");
|
||||
}
|
||||
}
|
||||
|
||||
60
src/tests/mir_ctrlflow_break_continue.rs
Normal file
60
src/tests/mir_ctrlflow_break_continue.rs
Normal file
@ -0,0 +1,60 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::parser::NyashParser;
|
||||
use crate::backend::VM;
|
||||
|
||||
#[test]
|
||||
fn vm_exec_simple_break() {
|
||||
let code = r#"
|
||||
loop(1) {
|
||||
break
|
||||
}
|
||||
return 1
|
||||
"#;
|
||||
let ast = NyashParser::parse_from_string(code).expect("parse");
|
||||
let mut compiler = crate::mir::MirCompiler::new();
|
||||
let result = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new();
|
||||
let out = vm.execute_module(&result.module).expect("vm exec");
|
||||
assert_eq!(out.to_string_box().value, "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vm_exec_continue_skips_body() {
|
||||
let code = r#"
|
||||
local i = 0
|
||||
local s = 0
|
||||
loop(i < 5) {
|
||||
i = i + 1
|
||||
if i == 3 { continue }
|
||||
s = s + 1
|
||||
}
|
||||
return s
|
||||
"#;
|
||||
let ast = NyashParser::parse_from_string(code).expect("parse");
|
||||
let mut compiler = crate::mir::MirCompiler::new();
|
||||
let result = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new();
|
||||
let out = vm.execute_module(&result.module).expect("vm exec");
|
||||
assert_eq!(out.to_string_box().value, "4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vm_exec_break_inside_if() {
|
||||
let code = r#"
|
||||
local i = 0
|
||||
loop(i < 10) {
|
||||
if i == 3 { break }
|
||||
i = i + 1
|
||||
}
|
||||
return i
|
||||
"#;
|
||||
let ast = NyashParser::parse_from_string(code).expect("parse");
|
||||
let mut compiler = crate::mir::MirCompiler::new();
|
||||
let result = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new();
|
||||
let out = vm.execute_module(&result.module).expect("vm exec");
|
||||
assert_eq!(out.to_string_box().value, "3");
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,8 +166,24 @@ impl NyashTokenizer {
|
||||
let mut tokens = Vec::new();
|
||||
|
||||
while !self.is_at_end() {
|
||||
// 空白をスキップ
|
||||
// 空白・コメントをスキップ
|
||||
self.skip_whitespace();
|
||||
// 連続するブロックコメントや行コメントもまとめてスキップ
|
||||
loop {
|
||||
// block comment: /* ... */
|
||||
if self.current_char() == Some('/') && self.peek_char() == Some('*') {
|
||||
self.skip_block_comment()?;
|
||||
self.skip_whitespace();
|
||||
continue;
|
||||
}
|
||||
// line comments: // ... or # ...
|
||||
if (self.current_char() == Some('/') && self.peek_char() == Some('/')) || self.current_char() == Some('#') {
|
||||
self.skip_line_comment();
|
||||
self.skip_whitespace();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if self.is_at_end() {
|
||||
break;
|
||||
@ -190,6 +206,12 @@ impl NyashTokenizer {
|
||||
let start_column = self.column;
|
||||
|
||||
match self.current_char() {
|
||||
// Block comment should have been skipped by tokenize() pre-loop, but be defensive here
|
||||
Some('/') if self.peek_char() == Some('*') => {
|
||||
self.skip_block_comment()?;
|
||||
// After skipping, restart tokenization for next token
|
||||
return self.tokenize_next();
|
||||
}
|
||||
// 2文字(またはそれ以上)の演算子は最長一致で先に判定
|
||||
Some('|') if self.peek_char() == Some('>') => {
|
||||
self.advance();
|
||||
@ -588,6 +610,24 @@ impl NyashTokenizer {
|
||||
}
|
||||
}
|
||||
|
||||
/// ブロックコメントをスキップ: /* ... */(ネスト非対応)
|
||||
fn skip_block_comment(&mut self) -> Result<(), TokenizeError> {
|
||||
// Assume current position is at '/' and next is '*'
|
||||
self.advance(); // '/'
|
||||
self.advance(); // '*'
|
||||
while let Some(c) = self.current_char() {
|
||||
// detect end '*/'
|
||||
if c == '*' && self.peek_char() == Some('/') {
|
||||
self.advance(); // '*'
|
||||
self.advance(); // '/'
|
||||
return Ok(());
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
// EOF reached without closing */
|
||||
Err(TokenizeError::UnterminatedComment { line: self.line })
|
||||
}
|
||||
|
||||
/// 空白文字をスキップ(改行は除く:改行はNEWLINEトークンとして扱う)
|
||||
fn skip_whitespace(&mut self) {
|
||||
while let Some(c) = self.current_char() {
|
||||
|
||||
Reference in New Issue
Block a user