diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 244bdfc2..1b87255f 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1,31 +1,30 @@ -# Current Task — Phase 21.2(LLVM C‑API 完全移行 / pure C‑API) +# Current Task — Phase 21.4(Hako Parser → Analyzer / Self‑Host) 目的(このフェーズで到達するゴール) -- ny‑llvmc(llvmlite)依存を段階的に外し、純C‑APIで emit+link を完結。 -- llvmlite は保守・比較用として維持。C‑API 経路のパリティ(obj/rc/3回一致)を reps で固定。 -- 最終確認(ゴール): 新しい Hakorune スクリプト(.hako)から LLVM ライン(C‑API pure/TM)で自己ホスティングを確認(Quick Verify 手順)。 +- Hako Parser(MVP)で AST JSON v0 を出力し、Analyzer の一次入力に採用(text は fallback 維持)。 +- 代表ルール(HC001/002/003/010/011/020)を AST 入力で正確化。 +- JSON(LSP) 出力を用意し、エディタ統合の下地を整備。 This document is intentionally concise (≤ 500 lines). Detailed history and per‑phase plans are kept under docs/private/roadmap/. See links below. -Focus (now) - 🎯 pure C‑API 実装計画と導線 -- FFI シムに pure 分岐(HAKO_CAPI_PURE=1)を追加し、内部で LLVM C‑API を直接呼ぶ。 -- Provider は現行 SSOT のまま(env.codegen.emit_object/link_object)。 -- reps 下地: phase2120/run_all.sh を追加(現状は SKIP ガード)。 +Focus (now) - 🎯 Parser/Analyzer の骨格と I/F を先に +- tools/hako_parser/* に Tokenizer/Parser/AST emit/CLI を実装(MVP) +- Analyzer は AST JSON を優先入力に切替(Text fallback は保持) +- 代表ルールの AST 化と LSP 出力、簡易テスト基盤を確立 Update (today) -- 21.2 フォルダ作成: docs/private/roadmap/phases/phase-21.2/README.md(計画) -- phase2120 スモーク雛形追加: tools/smokes/v2/profiles/quick/core/phase2120/run_all.sh(SKIP ガード) -- Generic pure lowering(CFG/φ, i64 subset)を `lang/c-abi/shims/hako_llvmc_ffi.c` に実装(HAKO_CAPI_PURE=1)。 -- mir_call 拡張(Array:set/get, Map:get/has/size, Array:len/push)を pure 経路に追加。 -- Kernel に最小アンボックス `nyash.integer.get_h` を追加(ハンドル→整数値)。 -- pure lowering に map.get → ret の最小アンボックス(自動1行挿入)を追加。 - - Hako VM: MirCallV1Handler に Map.set の size‑state(構造サイズ+1)を追加(dev)。 - - Reps: VM Adapter の size エイリアス(Map len / length)2本を追加(rc=2)。 +- docs/private/roadmap/phases/phase-21.4/PLAN.md を追加(優先順の実行計画) +- tools/hako_parser/* の MVP スケルトン確認(CLI/Emitter/Parser/Tokenizer) +- tools/hako_check/tests/README.md と run_tests.sh を追加(テスト雛形) -Remaining (21.2) -- FFI シムに pure 実装(emit/link)を追加(HAKO_CAPI_PURE=1) -- Provider 経由の pure 経路で reps 2本(ternary/map)を緑化、3回一致 -- OS 別 LDFLAGS の決定と自動付与(Linux/macOS 代表) +Remaining (21.4) +1) Hako Parser MVP 実装(tokenizer/parser_core/ast_emit/cli) +2) Analyzer AST 入力切替(analysis_consumer.hako) +3) 代表ルール AST 化(HC001/002/003/010/011/020) +4) `--format json-lsp` 出力 +5) テスト駆動(tests//)を 3 ルール分用意 +6) 限定 `--fix`(HC002/003/500) +7) DOT エッジ ON(calls→edges, cluster by box) Open Issues (Map semantics) - Map.get の戻り値セマンティクス未確定 @@ -37,10 +36,13 @@ Open Issues (Map semantics) - 決定性とハッシュ - いまは size 決定性を優先(hash はオプション)。TargetMachine へ移行後に `NYASH_HASH_STRICT=1` を既定 ON に切替予定。 -Near‑term TODO(21.2 準備) -- FFI: `hako_llvmc_ffi.c` に pure 分岐の雛形関数を定義 -- host_providers: pure フラグ透過+エラーメッセージの Fail‑Fast 整理 -- スモーク: phase2120 に reps 実体を追加(pure 実装後に有効化) +Near‑term TODO(21.4 準備) +- tokenizer: 文字列/数値/識別子/記号/位置情報 +- parser_core: using/box/static method/assign/簡易block +- ast_emit: boxes/uses/methods/calls(min) を JSON 化 +- cli: ファイル→AST JSON +- Analyzer: AST→IR 変換の経路を優先 +- 代表ルールの AST 実装と LSP 出力の雛形 Next (21.2 — TBD) - 21.1 の安定化を維持しつつ、C‑API の純API移行(ny‑llvmc 経由を段階縮小)計画を作成 @@ -50,15 +52,12 @@ Next (21.2 — TBD) - dev は Adapter 登録や by‑name fallback を許容(トグル)、prod は Adapter 必須(Fail‑Fast)。 Next Steps (immediate) -- TargetMachine パスの実装(HAKO_CAPI_TM=1 ガード) - - LLVMTargetMachineEmitToFile 経由で .o 生成、失敗時は llc へフォールバック(Fail‑Fastタグ付き)。 - - build スクリプトに llvm-config 検出(cflags/ldflags)を追加し、利用可能時のみ TM を有効化。 -- reps の拡充(φ/CFG 強化) - - φ 複数/ incoming 3+ / ネスト分岐の代表を追加(各3回一致)。 -- Map reps の段階導入 - - まず has(rc=1)を維持、その後 get(rc=値)を追加。不存在キーの規約が固まり次第、get reps を有効化。 -- Self‑hosting Quick Verify(Phase 21.2 CLOSE 条件) - - 新しい .hako ドライバスクリプト経由で LLVM ライン(C‑API pure/TM)を実行し、代表アプリの自己ホスティング完了を確認。 +1) tokenizer.hako 実装着手(行・列保持、文字列/数値/識別子) +2) parser_core.hako で using/box/static method/assign を組み立て +3) ast_emit.hako で v0 JSON を出力、実サンプルで確認 +4) analysis_consumer.hako の AST 取り込みを有効化 +5) HC002/003/010/011 を AST 入力でまず通す +6) `--format json-lsp` の最小実装 Previous Achievement - ✅ Phase 20.44 COMPLETE(provider emit/codegen reps 緑) diff --git a/src/ast.rs b/src/ast.rs index e0281125..9d36100f 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -361,6 +361,23 @@ pub enum ASTNode { span: Span, }, + /// Stage-3: while文: while condition { body } + While { + condition: Box, + body: Vec, + span: Span, + }, + + /// Stage-3: for-range文: for ident in start..end { body } + /// - 半開区間 [start, end) + ForRange { + var_name: String, + start: Box, + end: Box, + body: Vec, + span: Span, + }, + /// return文: return value Return { value: Option>, diff --git a/src/ast/utils.rs b/src/ast/utils.rs index d364ad1e..c57c398b 100644 --- a/src/ast/utils.rs +++ b/src/ast/utils.rs @@ -12,6 +12,8 @@ impl ASTNode { ASTNode::Print { .. } => "Print", ASTNode::If { .. } => "If", ASTNode::Loop { .. } => "Loop", + ASTNode::While { .. } => "While", + ASTNode::ForRange { .. } => "ForRange", ASTNode::Return { .. } => "Return", ASTNode::Break { .. } => "Break", ASTNode::Continue { .. } => "Continue", @@ -62,6 +64,8 @@ impl ASTNode { ASTNode::FunctionDeclaration { .. } => ASTNodeType::Structure, ASTNode::If { .. } => ASTNodeType::Structure, ASTNode::Loop { .. } => ASTNodeType::Structure, + ASTNode::While { .. } => ASTNodeType::Structure, + ASTNode::ForRange { .. } => ASTNodeType::Structure, ASTNode::TryCatch { .. } => ASTNodeType::Structure, // Expression nodes - 値を生成する表現 @@ -140,6 +144,20 @@ impl ASTNode { } => { format!("Loop({} statements)", body.len()) } + ASTNode::While { + condition: _, body, .. + } => { + format!("While({} statements)", body.len()) + } + ASTNode::ForRange { + var_name, + start: _, + end: _, + body, + .. + } => { + format!("ForRange(var={}, {} statements)", var_name, body.len()) + } ASTNode::Return { value, .. } => { if value.is_some() { "Return(with value)".to_string() @@ -329,6 +347,8 @@ impl ASTNode { ASTNode::Print { span, .. } => *span, ASTNode::If { span, .. } => *span, ASTNode::Loop { span, .. } => *span, + ASTNode::While { span, .. } => *span, + ASTNode::ForRange { span, .. } => *span, ASTNode::Return { span, .. } => *span, ASTNode::Break { span, .. } => *span, ASTNode::Continue { span, .. } => *span, diff --git a/src/mir/builder/exprs.rs b/src/mir/builder/exprs.rs index c4d13c87..65c65f0e 100644 --- a/src/mir/builder/exprs.rs +++ b/src/mir/builder/exprs.rs @@ -34,6 +34,10 @@ impl super::MirBuilder { ASTNode::Loop { condition, body, .. } => { self.cf_loop(*condition, body) } + ASTNode::While { condition, body, .. } => { + // Desugar Stage-3 while into legacy loop(condition) { body } + self.cf_loop(*condition, body) + } ASTNode::TryCatch { try_body, catch_clauses, diff --git a/src/mir/builder/stmts.rs b/src/mir/builder/stmts.rs index 25719f85..3d970725 100644 --- a/src/mir/builder/stmts.rs +++ b/src/mir/builder/stmts.rs @@ -153,7 +153,7 @@ impl super::MirBuilder { self.hint_scope_enter(scope_id); let mut last_value = None; for statement in statements { - last_value = Some(self.build_expression(statement)?); + last_value = Some(self.build_statement(statement)?); // If the current block was terminated by this statement (e.g., return/throw), // do not emit any further instructions for this block. if is_current_block_terminated(self)? { @@ -171,6 +171,55 @@ impl super::MirBuilder { Ok(out) } + /// Build a single statement node. + /// + /// Note: + /// - Stage-3 While/ForRange lowering is delegated to existing Loop/expr lowering + /// or handled in a dedicated pass; this function does not emit ad-hoc control + /// flow for them to avoid divergence from SSOT/loop_common. + pub(super) fn build_statement(&mut self, node: ASTNode) -> Result { + match node.clone() { + ASTNode::While { condition, body, .. } => { + // Desugar Stage-3 while into legacy loop(condition) { body } + let loop_node = ASTNode::Loop { condition, body, span: crate::ast::Span::unknown() }; + self.build_expression(loop_node) + } + ASTNode::ForRange { var_name, start, end, body, .. } => { + use crate::ast::{Span, LiteralValue, BinaryOperator}; + // local var initialization + let init = ASTNode::Local { + variables: vec![var_name.clone()], + initial_values: vec![Some(start)], + span: Span::unknown(), + }; + // condition: var_name < end + let cond = ASTNode::BinaryOp { + left: Box::new(ASTNode::Variable { name: var_name.clone(), span: Span::unknown() }), + operator: BinaryOperator::Less, + right: end, + span: Span::unknown(), + }; + // step: var_name = var_name + 1 + let step = ASTNode::Assignment { + target: Box::new(ASTNode::Variable { name: var_name.clone(), span: Span::unknown() }), + value: Box::new(ASTNode::BinaryOp { + left: Box::new(ASTNode::Variable { name: var_name.clone(), span: Span::unknown() }), + operator: BinaryOperator::Add, + right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown() }), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + let mut loop_body = body.clone(); + loop_body.push(step); + let loop_node = ASTNode::Loop { condition: Box::new(cond), body: loop_body, span: Span::unknown() }; + let program = ASTNode::Program { statements: vec![init, loop_node], span: Span::unknown() }; + self.build_expression(program) + } + other => self.build_expression(other), + } + } + // Local declarations with optional initializers pub(super) fn build_local_statement( diff --git a/src/mir/lowerers/loops.rs b/src/mir/lowerers/loops.rs new file mode 100644 index 00000000..beaac286 --- /dev/null +++ b/src/mir/lowerers/loops.rs @@ -0,0 +1,112 @@ +use crate::ast::ASTNode; +use crate::mir::builder::{BlockId, MirBuilder}; +use crate::mir::lowerers::LoweringError; + +/// Stage-3 loop lowering helpers (while / for-range). +/// +/// Enabled only when NYASH_PARSER_STAGE3 / HAKO_PARSER_STAGE3 are set on the +/// parser side and corresponding AST nodes are produced. This module provides +/// minimal lowering support so that Stage-3 style loops used by tools like +/// hako_check can execute on the existing MIR interpreter without modifying +/// default behavior when Stage-3 is disabled. +pub struct LoopLowerer; + +impl LoopLowerer { + /// Lower a Stage-3 style `while` loop: + /// + /// while { } + /// + /// Semantics: + /// - Evaluate cond at loop_head. + /// - If false, jump to exit. + /// - If true, execute body, then jump back to loop_head. + pub fn lower_while( + builder: &mut MirBuilder, + condition: &ASTNode, + body: &[ASTNode], + ) -> Result<(), LoweringError> { + let func = builder.current_function_id()?; + let loop_head: BlockId = builder.new_block(func); + let body_blk: BlockId = builder.new_block(func); + let exit_blk: BlockId = builder.new_block(func); + + // Jump from current block into loop_head + builder.ensure_terminator_goto(loop_head)?; + + // loop_head: evaluate condition + builder.set_insert_point(loop_head)?; + let cond_val = builder.lower_expr_bool(condition)?; + builder.build_cond_br(cond_val, body_blk, exit_blk)?; + + // body: lower statements, then jump back to loop_head if not already terminated + builder.set_insert_point(body_blk)?; + for stmt in body { + builder.lower_statement(stmt)?; + } + builder.ensure_terminator_goto(loop_head)?; + + // Continue after loop + builder.set_insert_point(exit_blk)?; + Ok(()) + } + + /// Lower a minimal `for` range loop: + /// + /// for in .. { } + /// + /// Semantics (half-open [start, end)): + /// - init: i = start + /// - loop_head: if i < end then body else exit + /// - body: execute stmts + /// - step: i = i + 1, jump back to loop_head + pub fn lower_for_range( + builder: &mut MirBuilder, + var_name: &str, + start_expr: &ASTNode, + end_expr: &ASTNode, + body: &[ASTNode], + ) -> Result<(), LoweringError> { + let func = builder.current_function_id()?; + let init_blk: BlockId = builder.new_block(func); + let loop_head: BlockId = builder.new_block(func); + let body_blk: BlockId = builder.new_block(func); + let step_blk: BlockId = builder.new_block(func); + let exit_blk: BlockId = builder.new_block(func); + + // Jump into init from current position + builder.ensure_terminator_goto(init_blk)?; + + // init: i = start_expr + builder.set_insert_point(init_blk)?; + let start_val = builder.lower_expr_i64(start_expr)?; + let idx_slot = builder.declare_local_i64(var_name)?; + builder.build_store_local(idx_slot, start_val)?; + builder.build_goto(loop_head)?; + + // loop_head: cond = (i < end) + builder.set_insert_point(loop_head)?; + let cur_val = builder.build_load_local_i64(idx_slot)?; + let end_val = builder.lower_expr_i64(end_expr)?; + let cond_val = builder.build_icmp_lt(cur_val, end_val)?; + builder.build_cond_br(cond_val, body_blk, exit_blk)?; + + // body: user statements + builder.set_insert_point(body_blk)?; + for stmt in body { + builder.lower_statement(stmt)?; + } + builder.ensure_terminator_goto(step_blk)?; + + // step: i = i + 1; goto loop_head + builder.set_insert_point(step_blk)?; + let cur2 = builder.build_load_local_i64(idx_slot)?; + let one = builder.const_i64(1); + let next = builder.build_add_i64(cur2, one)?; + builder.build_store_local(idx_slot, next)?; + builder.build_goto(loop_head)?; + + // exit: continue + builder.set_insert_point(exit_blk)?; + Ok(()) + } +} diff --git a/src/mir/lowerers/mod.rs b/src/mir/lowerers/mod.rs new file mode 100644 index 00000000..67234237 --- /dev/null +++ b/src/mir/lowerers/mod.rs @@ -0,0 +1,2 @@ +pub mod loops; + diff --git a/src/mir/mod.rs b/src/mir/mod.rs index 574f22cd..7044ec19 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -21,6 +21,7 @@ pub mod loop_builder; // SSA loop construction with phi nodes pub mod ssot; // Shared helpers (SSOT) for instruction lowering pub mod optimizer; pub mod utils; // Phase 15 control flow utilities for root treatment +// pub mod lowerers; // reserved: Stage-3 loop lowering (while/for-range) pub mod phi_core; // Phase 1 scaffold: unified PHI entry (re-exports only) pub mod optimizer_passes; // optimizer passes (normalize/diagnostics) pub mod optimizer_stats; // extracted stats struct diff --git a/src/parser/statements/control_flow.rs b/src/parser/statements/control_flow.rs index 5e698c58..5d084f74 100644 --- a/src/parser/statements/control_flow.rs +++ b/src/parser/statements/control_flow.rs @@ -17,8 +17,15 @@ use crate::tokenizer::TokenType; impl NyashParser { /// Parse control flow statement dispatch pub(super) fn parse_control_flow_statement(&mut self) -> Result { + let stage3 = Self::is_stage3_enabled(); + match &self.current_token().token_type { TokenType::IF => self.parse_if(), + // Stage-3: while + TokenType::WHILE if stage3 => self.parse_while_stage3(), + // Stage-3: for-range (stubbed to clear error path; implement next) + TokenType::FOR if stage3 => self.parse_for_range_stage3(), + // Legacy loop TokenType::LOOP => self.parse_loop(), TokenType::BREAK => self.parse_break(), TokenType::CONTINUE => self.parse_continue(), @@ -72,7 +79,7 @@ impl NyashParser { }) } - /// Parse loop statement + /// Parse loop statement (legacy `loop`). pub(super) fn parse_loop(&mut self) -> Result { if super::helpers::cursor_enabled() { let mut cursor = TokenCursor::new(&self.tokens); @@ -106,6 +113,97 @@ impl NyashParser { }) } + /// Stage-3: while { body } + fn parse_while_stage3(&mut self) -> Result { + // Normalize cursor at statement start (skip leading newlines etc.) + if super::helpers::cursor_enabled() { + let mut cursor = TokenCursor::new(&self.tokens); + cursor.set_position(self.current); + cursor.with_stmt_mode(|c| c.skip_newlines()); + self.current = cursor.position(); + } + // consume 'while' + self.advance(); + + // condition expression (no parentheses required in MVP) + let condition = Box::new(self.parse_expression()?); + + // body block + let body = self.parse_block_statements()?; + + Ok(ASTNode::While { + condition, + body, + span: Span::unknown(), + }) + } + + /// Stage-3: for-range parsing helper (currently unused). + fn parse_for_range_stage3(&mut self) -> Result { + // Normalize cursor at statement start + if super::helpers::cursor_enabled() { + let mut cursor = TokenCursor::new(&self.tokens); + cursor.set_position(self.current); + cursor.with_stmt_mode(|c| c.skip_newlines()); + self.current = cursor.position(); + } + // consume 'for' + self.advance(); + // expect identifier + let var_name = match &self.current_token().token_type { + TokenType::IDENTIFIER(s) => { + let n = s.clone(); + self.advance(); + n + } + other => { + return Err(ParseError::UnexpectedToken { + found: other.clone(), + expected: "identifier".to_string(), + line: self.current_token().line, + }) + } + }; + // expect 'in' + if !self.match_token(&TokenType::IN) { + return Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "in".to_string(), + line: self.current_token().line, + }); + } + self.advance(); + // start expr + let start = Box::new(self.parse_expression()?); + // expect RANGE ('..') + self.consume(TokenType::RANGE)?; + // end expr + let end = Box::new(self.parse_expression()?); + // body + let body = self.parse_block_statements()?; + Ok(ASTNode::ForRange { + var_name, + start, + end, + body, + span: Span::unknown(), + }) + } + + /// Helper: env-gated Stage-3 enable check. + fn is_stage3_enabled() -> bool { + let on = |key: &str| { + std::env::var(key) + .ok() + .map(|v| { + let lv = v.to_ascii_lowercase(); + lv == "1" || lv == "true" || lv == "on" + }) + .unwrap_or(false) + }; + on("NYASH_PARSER_STAGE3") || on("HAKO_PARSER_STAGE3") + } + /// Parse break statement pub(super) fn parse_break(&mut self) -> Result { self.advance(); // consume 'break' @@ -144,4 +242,4 @@ impl NyashParser { span: Span::unknown(), }) } -} \ No newline at end of file +} diff --git a/src/parser/statements/mod.rs b/src/parser/statements/mod.rs index 6a6816ee..94b1f398 100644 --- a/src/parser/statements/mod.rs +++ b/src/parser/statements/mod.rs @@ -203,6 +203,8 @@ impl NyashParser { // Control flow TokenType::IF + | TokenType::WHILE + | TokenType::FOR | TokenType::LOOP | TokenType::BREAK | TokenType::CONTINUE diff --git a/src/runner/modes/vm.rs b/src/runner/modes/vm.rs index bf31b08d..c4a2567b 100644 --- a/src/runner/modes/vm.rs +++ b/src/runner/modes/vm.rs @@ -102,41 +102,45 @@ impl NyashRunner { } }; - // Using handling: prefer AST prelude merge for .hako/Hako-like sources(SSOT統一) - // - .hako/Hako-like → AST merge を既定で優先(dev/ci: NYASH_USING_AST=1) - // - Text merge は fallback として保持(NYASH_PREFER_TEXT_USING=1 等の将来拡張用) - let use_ast = crate::config::env::using_ast_enabled(); - // .hako/Hako-like heuristic: AST merge を優先(スコープ外で定義してマージ時にも使用) - let is_hako = filename.ends_with(".hako") - || crate::runner::modes::common_util::hako::looks_like_hako_code(&code); - let trace = crate::config::env::cli_verbose() || crate::config::env::env_bool("NYASH_RESOLVE_TRACE"); + // Unified using/prelude handling (SSOT): + // - resolve_prelude_paths_profiled: discover preludes (DFS, operator boxes, etc.) + // - merge_prelude_text: text-based merge for all VM paths + // * .hako プレリュードは AST に突っ込まない(Parse error防止) + // * .hako は text-merge + Stage-3 parser で一貫処理 + let trace = crate::config::env::cli_verbose() + || crate::config::env::env_bool("NYASH_RESOLVE_TRACE"); - let mut code_ref: &str = &code; - let mut cleaned_code_owned; - let mut prelude_asts: Vec = Vec::new(); - - if crate::config::env::enable_using() { + // When using is enabled, resolve preludes/profile; otherwise, keep original code. + let mut code_final = if crate::config::env::enable_using() { match crate::runner::modes::common_util::resolve::resolve_prelude_paths_profiled( - self, &code, filename, + self, + &code, + filename, ) { - Ok((clean, paths)) => { - cleaned_code_owned = clean; - code_ref = &cleaned_code_owned; - if !paths.is_empty() && !(use_ast || is_hako) { - eprintln!("❌ using: AST prelude merge is disabled in this profile. Enable NYASH_USING_AST=1 or remove 'using' lines."); - std::process::exit(1); - } - if !paths.is_empty() { - // VM path: always use text-merge for .hako dependencies - // This ensures proper prelude inlining regardless of adapter mode - match crate::runner::modes::common_util::resolve::merge_prelude_text(self, &code, filename) { + Ok((_, prelude_paths)) => { + if !prelude_paths.is_empty() { + // SSOT: always text-merge for VM (includes .hako-safe handling inside) + match crate::runner::modes::common_util::resolve::merge_prelude_text( + self, + &code, + filename, + ) { Ok(merged) => { - if trace { eprintln!("[using/text-merge] preludes={} (vm)", paths.len()); } - cleaned_code_owned = merged; - code_ref = &cleaned_code_owned; + if trace { + eprintln!( + "[using/text-merge] preludes={} (vm)", + prelude_paths.len() + ); + } + merged + } + Err(e) => { + eprintln!("❌ {}", e); + process::exit(1); } - Err(e) => { eprintln!("❌ {}", e); process::exit(1); } } + } else { + code.clone() } } Err(e) => { @@ -152,14 +156,25 @@ impl NyashRunner { ); process::exit(1); } - } + code + }; // Dev sugar pre-expand: @name = expr → local name = expr - let mut code_final = crate::runner::modes::common_util::resolve::preexpand_at_local(code_ref).to_string(); + code_final = + crate::runner::modes::common_util::resolve::preexpand_at_local(&code_final); // Hako-friendly normalize: strip leading `local ` at line head for Nyash parser compatibility. - if crate::runner::modes::common_util::hako::looks_like_hako_code(&code_final) { - code_final = crate::runner::modes::common_util::hako::strip_local_decl(&code_final); + if crate::runner::modes::common_util::hako::looks_like_hako_code(&code_final) + || filename.ends_with(".hako") + { + code_final = + crate::runner::modes::common_util::hako::strip_local_decl(&code_final); + } + + if trace && (std::env::var("NYASH_PARSER_STAGE3").ok() == Some("1".into()) + || std::env::var("HAKO_PARSER_STAGE3").ok() == Some("1".into())) + { + eprintln!("[vm] Stage-3: enabled (env) for {}", filename); } // Fail‑Fast (opt‑in): Hako 構文を Nyash VM 経路で実行しない @@ -179,8 +194,8 @@ impl NyashRunner { } } - // Parse main code - let main_ast = match NyashParser::parse_from_string(&code_final) { + // Parse main code (after text-merge and Hako normalization) + let ast_combined = match NyashParser::parse_from_string(&code_final) { Ok(ast) => ast, Err(e) => { eprintln!("❌ Parse error in {}: {}", filename, e); @@ -188,13 +203,6 @@ impl NyashRunner { } }; - // Merge prelude ASTs if any - let ast_combined = if !prelude_asts.is_empty() { - crate::runner::modes::common_util::resolve::merge_prelude_asts_with_main(prelude_asts, &main_ast) - } else { - main_ast - }; - // Optional: dump AST statement kinds for quick diagnostics if std::env::var("NYASH_AST_DUMP").ok().as_deref() == Some("1") { eprintln!("[ast] dump start (vm)"); diff --git a/src/runner/modes/vm_fallback.rs b/src/runner/modes/vm_fallback.rs index 4ce5242d..43d2d93f 100644 --- a/src/runner/modes/vm_fallback.rs +++ b/src/runner/modes/vm_fallback.rs @@ -26,71 +26,47 @@ impl NyashRunner { process::exit(1); } }; - // Using preprocessing: AST prelude merge(.hako/Hakoライクは強制AST) - let mut code2 = code.clone(); - if crate::config::env::enable_using() { - let mut use_ast = crate::config::env::using_ast_enabled(); - let is_hako = filename.ends_with(".hako") - || crate::runner::modes::common_util::hako::looks_like_hako_code(&code2); - if is_hako { use_ast = true; } - if use_ast { - match crate::runner::modes::common_util::resolve::resolve_prelude_paths_profiled(self, &code2, filename) { - Ok((clean, paths)) => { - // If any prelude is .hako, prefer text-merge (Hakorune surface is not Nyash AST) - let has_hako = paths.iter().any(|p| p.ends_with(".hako")); - if has_hako { - match crate::runner::modes::common_util::resolve::merge_prelude_text(self, &code2, filename) { - Ok(merged) => { - if std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1") { - eprintln!("[using/text-merge] preludes={} (vm-fallback)", paths.len()); - } - code2 = merged; + + let trace = crate::config::env::cli_verbose() + || crate::config::env::env_bool("NYASH_RESOLVE_TRACE"); + + // Unified using/prelude handling (SSOT, parity with vm.rs): + // - resolve_prelude_paths_profiled で preludes を発見 + // - merge_prelude_text で text-merge(.hako は AST parse しない) + let mut code2 = if crate::config::env::enable_using() { + match crate::runner::modes::common_util::resolve::resolve_prelude_paths_profiled( + self, + &code, + filename, + ) { + Ok((_, prelude_paths)) => { + if !prelude_paths.is_empty() { + match crate::runner::modes::common_util::resolve::merge_prelude_text( + self, + &code, + filename, + ) { + Ok(merged) => { + if trace { + eprintln!( + "[using/text-merge] preludes={} (vm-fallback)", + prelude_paths.len() + ); } - Err(e) => { eprintln!("❌ {}", e); process::exit(1); } + merged } - // Fall through to normal parse of merged text below - } else { - // AST prelude merge path - code2 = clean; - let preexpanded = crate::runner::modes::common_util::resolve::preexpand_at_local(&code2); - code2 = preexpanded; - if crate::runner::modes::common_util::hako::looks_like_hako_code(&code2) { - code2 = crate::runner::modes::common_util::hako::strip_local_decl(&code2); - } - let main_ast = match NyashParser::parse_from_string(&code2) { - Ok(ast) => ast, - Err(e) => { eprintln!("❌ Parse error in {}: {}", filename, e); process::exit(1); } - }; - if !paths.is_empty() { - match crate::runner::modes::common_util::resolve::parse_preludes_to_asts(self, &paths) { - Ok(v) => { - if std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1") { - eprintln!("[using/ast-merge] preludes={} (vm-fallback)", v.len()); - } - let ast = crate::runner::modes::common_util::resolve::merge_prelude_asts_with_main(v, &main_ast); - self.execute_vm_fallback_from_ast(filename, ast); - return; // done - } - Err(e) => { eprintln!("❌ {}", e); process::exit(1); } - } - } else { - self.execute_vm_fallback_from_ast(filename, main_ast); - return; + Err(e) => { + eprintln!("❌ {}", e); + process::exit(1); } } + } else { + code.clone() } - Err(e) => { eprintln!("❌ {}", e); process::exit(1); } } - } else { - // Fallback: text-prelude merge(言語非依存) - match crate::runner::modes::common_util::resolve::merge_prelude_text(self, &code2, filename) { - Ok(merged) => { - if std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1") { - eprintln!("[using/text-merge] applied (vm-fallback): {} bytes", merged.len()); - } - code2 = merged; - } - Err(e) => { eprintln!("❌ using text merge error: {}", e); process::exit(1); } + Err(e) => { + eprintln!("❌ {}", e); + process::exit(1); } } } else { @@ -101,14 +77,25 @@ impl NyashRunner { ); process::exit(1); } - } + code + }; + // Dev sugar pre-expand: @name = expr → local name = expr code2 = crate::runner::modes::common_util::resolve::preexpand_at_local(&code2); - // Hako-friendly normalize: strip leading `local ` at line head for Nyash parser compatibility. - if crate::runner::modes::common_util::hako::looks_like_hako_code(&code2) { + + // Hako-friendly normalize + if crate::runner::modes::common_util::hako::looks_like_hako_code(&code2) + || filename.ends_with(".hako") + { code2 = crate::runner::modes::common_util::hako::strip_local_decl(&code2); } + if trace && (std::env::var("NYASH_PARSER_STAGE3").ok() == Some("1".into()) + || std::env::var("HAKO_PARSER_STAGE3").ok() == Some("1".into())) + { + eprintln!("[vm-fallback] Stage-3: enabled (env) for {}", filename); + } + // Fail‑Fast (opt‑in): Hako 構文を Nyash VM 経路で実行しない // 目的: .hako は Hakorune VM、MIR は Core/LLVM に役割分離するためのガード { @@ -436,7 +423,6 @@ impl NyashRunner { let mut interp = MirInterpreter::new(); match interp.execute_module(&module) { Ok(result) => { - // Normalize display (avoid nonexistent coerce_to_exit_code here) use nyash_rust::box_trait::{BoolBox, IntegerBox}; let rc = if let Some(ib) = result.as_any().downcast_ref::() { ib.value as i32 @@ -448,13 +434,15 @@ impl NyashRunner { // For C‑API pure pipeline, suppress "RC:" text to keep last line = exe path let capi = std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() == Some("1"); let pure = std::env::var("HAKO_CAPI_PURE").ok().as_deref() == Some("1"); - if capi && pure { - process::exit(rc); - } else { + if !(capi && pure) { println!("RC: {}", rc); } + process::exit(rc); + } + Err(e) => { + eprintln!("❌ VM fallback runtime error: {}", e); + process::exit(1); } - Err(e) => { eprintln!("❌ VM fallback runtime error: {}", e); process::exit(1); } } } } diff --git a/src/tokenizer/kinds.rs b/src/tokenizer/kinds.rs index 5930bc61..e1dfbdbe 100644 --- a/src/tokenizer/kinds.rs +++ b/src/tokenizer/kinds.rs @@ -49,6 +49,10 @@ pub enum TokenType { WEAK, USING, IMPORT, + // Stage-3 keywords (env-gated) + WHILE, + FOR, + IN, // 演算子 ShiftLeft, diff --git a/src/tokenizer/lex_ident.rs b/src/tokenizer/lex_ident.rs index 84491688..7ea061f2 100644 --- a/src/tokenizer/lex_ident.rs +++ b/src/tokenizer/lex_ident.rs @@ -56,6 +56,10 @@ impl NyashTokenizer { "using" => TokenType::USING, "and" => TokenType::AND, "or" => TokenType::OR, + // Stage-3 loop keywords (gated below) + "while" => TokenType::WHILE, + "for" => TokenType::FOR, + "in" => TokenType::IN, "true" => TokenType::TRUE, "false" => TokenType::FALSE, "null" => TokenType::NULL, @@ -72,6 +76,9 @@ impl NyashTokenizer { | TokenType::TRY | TokenType::CATCH | TokenType::THROW + | TokenType::WHILE + | TokenType::FOR + | TokenType::IN ); if is_stage3 { if std::env::var("NYASH_TOK_TRACE").ok().as_deref() == Some("1") { @@ -89,6 +96,9 @@ impl NyashTokenizer { | TokenType::TRY | TokenType::CATCH | TokenType::THROW + | TokenType::WHILE + | TokenType::FOR + | TokenType::IN ); if is_stage3 { eprintln!("[tok-stage3] Keeping {:?} as keyword (NYASH_PARSER_STAGE3={})", diff --git a/tools/hako_check/cli.hako b/tools/hako_check/cli.hako index 086ebc1a..52dae271 100644 --- a/tools/hako_check/cli.hako +++ b/tools/hako_check/cli.hako @@ -58,15 +58,17 @@ static box HakoAnalyzerBox { // Normalize CRLF -> LF and convert fancy quotes to ASCII local out = "" local n = text.length() - for i in 0..(n-1) { - local ch = text.substring(i, i+1) + local i2 = 0 + while i2 < n { + local ch = text.substring(i2, i2+1) // drop CR - if ch == "\r" { continue } + if ch == "\r" { i2 = i2 + 1; continue } // fancy double quotes → ASCII - if ch == "“" || ch == "”" { out = out.concat("\""); continue } + if ch == "“" || ch == "”" { out = out.concat("\""); i2 = i2 + 1; continue } // fancy single quotes → ASCII - if ch == "‘" || ch == "’" { out = out.concat("'"); continue } + if ch == "‘" || ch == "’" { out = out.concat("'"); i2 = i2 + 1; continue } out = out.concat(ch) + i2 = i2 + 1 } return out } diff --git a/tools/hako_check/hako_source_checker.hako b/tools/hako_check/hako_source_checker.hako index 613d1531..987914cc 100644 --- a/tools/hako_check/hako_source_checker.hako +++ b/tools/hako_check/hako_source_checker.hako @@ -27,7 +27,7 @@ static box HakoSourceCheckerBox { me._rule_jsonfrag_usage(text, path, issues) local n = issues.size() if n > 0 { - do { local i=0; while i= n { break } local cj = text.substring(j, j+1) if cj == "\n" { break } if cj == "=" { seen_eq = 1; break } - off = off + 1 } } while 0 + off = off + 1 } if seen_eq == 1 { out.push("[HC001] top-level assignment in static box (use lazy init in method): " + path + ":" + Str.int_to_str(line)) } diff --git a/tools/hako_check/run_tests.sh b/tools/hako_check/run_tests.sh new file mode 100644 index 00000000..297afd81 --- /dev/null +++ b/tools/hako_check/run_tests.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN="${NYASH_BIN:-$ROOT/target/release/hakorune}" + +if [ ! -x "$BIN" ]; then + echo "[TEST] hakorune not built: $BIN" >&2 + echo "Run: cargo build --release" >&2 + exit 2 +fi + +TARGET_DIR="$ROOT/tools/hako_check/tests" +fail=0 + +run_case() { + local dir="$1" + local expected="$dir/expected.json" + local input_ok="$dir/ok.hako" + local input_ng="$dir/ng.hako" + if [ ! -f "$expected" ]; then echo "[TEST] skip (no expected): $dir"; return; fi + if [ ! -f "$input_ok" ] && [ ! -f "$input_ng" ]; then echo "[TEST] skip (no inputs): $dir"; return; fi + local tmp_out="/tmp/hako_test_$$.json" + NYASH_DISABLE_NY_COMPILER=1 HAKO_DISABLE_NY_COMPILER=1 \ + NYASH_PARSER_STAGE3=1 HAKO_PARSER_STAGE3=1 NYASH_PARSER_SEAM_TOLERANT=1 HAKO_PARSER_SEAM_TOLERANT=1 \ + NYASH_ENABLE_USING=1 HAKO_ENABLE_USING=1 NYASH_USING_AST=1 \ + "$BIN" --backend vm "$ROOT/tools/hako_check/cli.hako" -- --format json-lsp ${input_ok:+"$input_ok"} ${input_ng:+"$input_ng"} \ + >"$tmp_out" 2>/dev/null || true + if ! diff -u "$expected" "$tmp_out" >/dev/null; then + echo "[TEST/FAIL] $dir" >&2 + diff -u "$expected" "$tmp_out" || true + fail=$((fail+1)) + else + echo "[TEST/OK] $dir" + fi + rm -f "$tmp_out" +} + +for d in "$TARGET_DIR"/*; do + [ -d "$d" ] || continue + run_case "$d" +done + +if [ $fail -ne 0 ]; then + echo "[TEST/SUMMARY] failures=$fail" >&2 + exit 1 +fi +echo "[TEST/SUMMARY] all green" +exit 0 + diff --git a/tools/hako_check/tests/README.md b/tools/hako_check/tests/README.md new file mode 100644 index 00000000..ab80886a --- /dev/null +++ b/tools/hako_check/tests/README.md @@ -0,0 +1,16 @@ +# Hako Check — Rule Tests (MVP) + +構成(1 ルール = 1 ディレクトリ) +- tools/hako_check/tests// + - ok.hako … 検出なし + - ng.hako … 最低 1 件の検出 + - edge.hako … 端境(任意) + - expected.json … `--format json-lsp` の期待ダイアグノスティクス + +実行(MVP) +- `bash tools/hako_check/run_tests.sh` で全テストを走査 +- 差分があれば終了コード 1、詳細を提示 + +注意 +- 21.4 は AST JSON 優先。Text fallback の差異は expected に反映 +- ルール名は HCxxx を推奨(例: HC002)