diff --git a/apps/tests/assignment_expr_shortcircuit.hako b/apps/tests/assignment_expr_shortcircuit.hako new file mode 100644 index 00000000..b6e7ef02 --- /dev/null +++ b/apps/tests/assignment_expr_shortcircuit.hako @@ -0,0 +1,12 @@ +// Phase 152-A: Grouped assignment expression with shortcircuit +// Test: (x = expr) can be used in conditional expressions + +static box Main { + main() { + local x = 0 + if ((x = 1) > 0) and true { + return x // Expected: RC 1 + } + return -1 + } +} diff --git a/apps/tests/assignment_expr_simple.hako b/apps/tests/assignment_expr_simple.hako new file mode 100644 index 00000000..0f3eaf92 --- /dev/null +++ b/apps/tests/assignment_expr_simple.hako @@ -0,0 +1,11 @@ +// Phase 152-A: Grouped assignment expression simple test +// Test: (x = expr) returns value and assigns to x + +static box Main { + main() { + local x = 0 + local y = (x = x + 1) + // x should be 1, y should be 1 + return y // Expected: RC 1 + } +} diff --git a/apps/tests/shortcircuit_and_phi_skip.hako b/apps/tests/shortcircuit_and_phi_skip.hako index 6acc44bf..fa032922 100644 --- a/apps/tests/shortcircuit_and_phi_skip.hako +++ b/apps/tests/shortcircuit_and_phi_skip.hako @@ -1,7 +1,8 @@ static box Main { main(args) { local x = 0 - ((x = x + 1) < 0) && ((x = x + 1) < 0) + // Phase 152-A: Use grouped assignment in expression context + local result = ((x = x + 1) < 0) && ((x = x + 1) < 0) // LHS が false → RHS は評価されず x は 1 のまま return x } diff --git a/src/ast.rs b/src/ast.rs index e1b7c8e5..17382802 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -523,6 +523,15 @@ pub enum ASTNode { span: Span, }, + /// Stage-3: 括弧付き代入式: (x = expr) - Phase 152-A + /// 値・型は右辺と同じ、副作用として左辺に代入 + /// 使用例: local y = (x = x + 1), if (x = next()) != null { } + GroupedAssignmentExpr { + lhs: String, // 変数名 + rhs: Box, // 右辺式 + span: Span, + }, + /// メソッド呼び出し: object.method(arguments) MethodCall { object: Box, diff --git a/src/ast/utils.rs b/src/ast/utils.rs index 109afbe9..d93c0da4 100644 --- a/src/ast/utils.rs +++ b/src/ast/utils.rs @@ -52,6 +52,8 @@ impl ASTNode { ASTNode::MapLiteral { .. } => "MapLiteral", // Optional diagnostic-only wrapper ASTNode::ScopeBox { .. } => "ScopeBox", + // Phase 152-A: Grouped assignment expression + ASTNode::GroupedAssignmentExpr { .. } => "GroupedAssignmentExpr", } } @@ -91,6 +93,8 @@ impl ASTNode { ASTNode::ArrayLiteral { .. } => E, ASTNode::MapLiteral { .. } => E, ASTNode::AwaitExpression { .. } => E, + // Phase 152-A: Grouped assignment expression (expression with side effect) + ASTNode::GroupedAssignmentExpr { .. } => E, // Statement nodes - 実行可能なアクション ASTNode::Program { .. } => S, @@ -330,6 +334,10 @@ impl ASTNode { format!("Index(target={:?}, index={:?})", target, index) } ASTNode::ScopeBox { .. } => "ScopeBox".to_string(), + // Phase 152-A: Grouped assignment expression + ASTNode::GroupedAssignmentExpr { lhs, .. } => { + format!("GroupedAssignmentExpr(lhs={})", lhs) + } } } @@ -380,6 +388,8 @@ impl ASTNode { ASTNode::ArrayLiteral { span, .. } => *span, ASTNode::MapLiteral { span, .. } => *span, ASTNode::ScopeBox { span, .. } => *span, + // Phase 152-A: Grouped assignment expression + ASTNode::GroupedAssignmentExpr { span, .. } => *span, } } } diff --git a/src/mir/builder/exprs.rs b/src/mir/builder/exprs.rs index 847ebe59..a04ea0b0 100644 --- a/src/mir/builder/exprs.rs +++ b/src/mir/builder/exprs.rs @@ -134,6 +134,13 @@ impl super::MirBuilder { } } + // Phase 152-A: Grouped assignment expression (x = expr) + // Stage-3 only. Value/type same as rhs, side effect assigns to lhs. + // Reuses existing build_assignment logic, returns the SSA ValueId. + ASTNode::GroupedAssignmentExpr { lhs, rhs, .. } => { + self.build_assignment(lhs.clone(), *rhs.clone()) + } + ASTNode::Index { target, index, .. } => { self.build_index_expression(*target.clone(), *index.clone()) } diff --git a/src/mir/builder/vars.rs b/src/mir/builder/vars.rs index 60691d9e..50d428ba 100644 --- a/src/mir/builder/vars.rs +++ b/src/mir/builder/vars.rs @@ -24,6 +24,10 @@ pub(super) fn collect_free_vars( collect_free_vars(target, used, locals); collect_free_vars(value, used, locals); } + // Phase 152-A: Grouped assignment expression + ASTNode::GroupedAssignmentExpr { rhs, .. } => { + collect_free_vars(rhs, used, locals); + } ASTNode::BinaryOp { left, right, .. } => { collect_free_vars(left, used, locals); collect_free_vars(right, used, locals); diff --git a/src/parser/expr/primary.rs b/src/parser/expr/primary.rs index 22396e05..d70824f7 100644 --- a/src/parser/expr/primary.rs +++ b/src/parser/expr/primary.rs @@ -265,6 +265,12 @@ impl NyashParser { } } TokenType::LPAREN => { + // Phase 152-A: Try grouped assignment first (Stage-3 only) + if let Some(assignment) = self.try_parse_grouped_assignment()? { + return Ok(assignment); + } + + // Fallback: normal grouped expression self.advance(); let expr = self.parse_expression()?; self.consume(TokenType::RPAREN)?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 41c6bb6d..0d59d5c1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -27,6 +27,7 @@ mod expr; mod expr_cursor; // TokenCursorを使用した式パーサー(実験的) mod expressions; mod items; +mod stage3; // Phase 152-A: Stage-3 parser extensions mod statements; // Now uses modular structure in statements/ pub mod sugar; // Phase 12.7-B: desugar pass (basic) pub mod sugar_gate; // thread-local gate for sugar parsing (tests/docs) diff --git a/src/parser/stage3/assignment_expr_parser.rs b/src/parser/stage3/assignment_expr_parser.rs new file mode 100644 index 00000000..3f7a00a9 --- /dev/null +++ b/src/parser/stage3/assignment_expr_parser.rs @@ -0,0 +1,170 @@ +/*! + * Phase 152-A: Grouped Assignment Expression Parser (箱化モジュール化) + * + * Stage-3 構文: (x = expr) を expression として受け入れる + * 仕様: + * - `x = expr` は statement のまま(変更なし) + * - `(x = expr)` のみ expression として受け入れる + * - 値・型は右辺と同じ + * + * 責務: + * 1. Stage-3 gate 確認 + * 2. `(` IDENT `=` expr `)` パターン検出 + * 3. GroupedAssignmentExpr AST ノード生成 + */ + +use crate::ast::{ASTNode, Span}; +use crate::parser::common::ParserUtils; +use crate::parser::{NyashParser, ParseError}; +use crate::tokenizer::TokenType; + +impl NyashParser { + /// Try to parse grouped assignment expression: (x = expr) + /// + /// Returns Some(ASTNode) if pattern matches and Stage-3 is enabled, + /// None otherwise to allow normal grouped expression fallback. + /// + /// This function is designed for 1-line delegation from primary.rs: + /// ``` + /// if let Some(assignment) = self.try_parse_grouped_assignment()? { + /// return Ok(assignment); + /// } + /// ``` + pub(crate) fn try_parse_grouped_assignment(&mut self) -> Result, ParseError> { + // Stage-3 gate check + if !crate::config::env::parser_stage3_enabled() { + return Ok(None); + } + + // Look ahead pattern check: '(' IDENT '=' ... + if !self.is_grouped_assignment_pattern() { + return Ok(None); + } + + // Parse: '(' IDENT '=' expr ')' + self.consume(TokenType::LPAREN)?; + + let ident = match &self.current_token().token_type { + TokenType::IDENTIFIER(name) => { + let var_name = name.clone(); + self.advance(); + var_name + } + _ => { + let line = self.current_token().line; + return Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "identifier in grouped assignment".to_string(), + line, + }); + } + }; + + self.consume(TokenType::ASSIGN)?; + + let rhs = self.parse_expression()?; + + self.consume(TokenType::RPAREN)?; + + Ok(Some(ASTNode::GroupedAssignmentExpr { + lhs: ident, + rhs: Box::new(rhs), + span: Span::unknown(), + })) + } + + /// Check if current token position matches grouped assignment pattern + /// + /// Pattern: '(' IDENT '=' ... + fn is_grouped_assignment_pattern(&self) -> bool { + // Current token should be '(' + if !self.match_token(&TokenType::LPAREN) { + return false; + } + + // Next token should be IDENTIFIER + if self.current() + 1 >= self.tokens().len() { + return false; + } + if !matches!( + &self.tokens()[self.current() + 1].token_type, + TokenType::IDENTIFIER(_) + ) { + return false; + } + + // Token after that should be '=' + if self.current() + 2 >= self.tokens().len() { + return false; + } + matches!( + &self.tokens()[self.current() + 2].token_type, + TokenType::ASSIGN + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tokenizer::Tokenizer; + + #[test] + fn test_grouped_assignment_simple() { + std::env::set_var("NYASH_FEATURES", "stage3"); + + let input = "local y = (x = 42)"; + let tokens = Tokenizer::tokenize(input).unwrap(); + let mut parser = NyashParser::new(tokens); + + // Skip 'local' and 'y' and '=' + parser.advance(); // local + parser.advance(); // y + parser.advance(); // = + + let result = parser.try_parse_grouped_assignment().unwrap(); + assert!(result.is_some()); + + if let Some(ASTNode::GroupedAssignmentExpr { lhs, rhs, .. }) = result { + assert_eq!(lhs, "x"); + // rhs should be Literal(42) + assert!(matches!(*rhs, ASTNode::Literal { .. })); + } else { + panic!("Expected GroupedAssignmentExpr"); + } + } + + #[test] + fn test_grouped_assignment_pattern_detection() { + std::env::set_var("NYASH_FEATURES", "stage3"); + + // Positive case: (x = expr) + let tokens = Tokenizer::tokenize("(x = 42)").unwrap(); + let parser = NyashParser::new(tokens); + assert!(parser.is_grouped_assignment_pattern()); + + // Negative case: (42) - not an identifier + let tokens = Tokenizer::tokenize("(42)").unwrap(); + let parser = NyashParser::new(tokens); + assert!(!parser.is_grouped_assignment_pattern()); + + // Negative case: x = 42 - no parenthesis + let tokens = Tokenizer::tokenize("x = 42").unwrap(); + let parser = NyashParser::new(tokens); + assert!(!parser.is_grouped_assignment_pattern()); + } + + #[test] + fn test_stage3_gate_off() { + std::env::remove_var("NYASH_FEATURES"); + std::env::remove_var("NYASH_PARSER_STAGE3"); + std::env::remove_var("HAKO_PARSER_STAGE3"); + + let input = "(x = 42)"; + let tokens = Tokenizer::tokenize(input).unwrap(); + let mut parser = NyashParser::new(tokens); + + let result = parser.try_parse_grouped_assignment().unwrap(); + assert!(result.is_none()); // Should return None when Stage-3 is off + } +} diff --git a/src/parser/stage3/mod.rs b/src/parser/stage3/mod.rs new file mode 100644 index 00000000..9b2080a2 --- /dev/null +++ b/src/parser/stage3/mod.rs @@ -0,0 +1,8 @@ +/*! + * Stage-3 Parser Extensions + * + * Stage-3 構文の拡張機能群 + * - 括弧付き代入式 (Phase 152-A) + */ + +pub mod assignment_expr_parser;