Implement Stage-3 grouped assignment expression `(x = expr)` following the 箱化モジュール化 (modular box) pattern established in Phase 133/134. **Implementation**: - AssignmentExprParser module (Rust: 183 lines) - src/parser/stage3/assignment_expr_parser.rs (+183 lines) - src/parser/stage3/mod.rs (+9 lines) - AST node addition: GroupedAssignmentExpr - src/ast.rs (+7 lines) - src/ast/utils.rs (+9 lines) - MIR lowering via 1-line delegation - src/mir/builder/exprs.rs (+5 lines) - src/mir/builder/vars.rs (+4 lines) - Parser integration via 1-line delegation - src/parser/expr/primary.rs (+6 lines) - src/parser/mod.rs (+1 line) **Test Results**: 3/3 PASS - assignment_expr_simple.hako: RC 1 ✅ - assignment_expr_shortcircuit.hako: RC 1 ✅ - shortcircuit_and_phi_skip.hako: RC 1 ✅ (updated to use expression context) **Stage-3 Gate**: No impact on Stage-2/legacy - NYASH_FEATURES=stage3 required - Pattern: '(' IDENT '=' expr ')' - Value/type same as rhs, side effect assigns to lhs **箱化モジュール化パターン**: - Dedicated module for assignment expression parsing - Clear responsibility separation - 1-line delegation for integration - Testability improvement - Follows Phase 133/134-A/134-B pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
323 lines
14 KiB
Rust
323 lines
14 KiB
Rust
use crate::ast::{ASTNode, LiteralValue, Span};
|
||
use crate::parser::common::ParserUtils;
|
||
use crate::parser::{NyashParser, ParseError};
|
||
use crate::tokenizer::TokenType;
|
||
|
||
impl NyashParser {
|
||
pub(crate) fn expr_parse_primary(&mut self) -> Result<ASTNode, ParseError> {
|
||
match &self.current_token().token_type {
|
||
TokenType::LBRACK => {
|
||
let sugar_on = crate::parser::sugar_gate::is_enabled()
|
||
|| std::env::var("NYASH_ENABLE_ARRAY_LITERAL").ok().as_deref() == Some("1");
|
||
if !sugar_on {
|
||
let line = self.current_token().line;
|
||
return Err(ParseError::UnexpectedToken {
|
||
found: self.current_token().token_type.clone(),
|
||
expected: "enable NYASH_SYNTAX_SUGAR_LEVEL=basic|full or NYASH_ENABLE_ARRAY_LITERAL=1".to_string(),
|
||
line,
|
||
});
|
||
}
|
||
self.advance();
|
||
let mut elems: Vec<ASTNode> = Vec::new();
|
||
while !self.match_token(&TokenType::RBRACK) && !self.is_at_end() {
|
||
crate::must_advance!(self, _unused, "array literal element parsing");
|
||
let el = self.parse_expression()?;
|
||
elems.push(el);
|
||
if self.match_token(&TokenType::COMMA) {
|
||
self.advance();
|
||
}
|
||
}
|
||
self.consume(TokenType::RBRACK)?;
|
||
Ok(ASTNode::ArrayLiteral {
|
||
elements: elems,
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::LBRACE => {
|
||
let sugar_on = crate::parser::sugar_gate::is_enabled()
|
||
|| std::env::var("NYASH_ENABLE_MAP_LITERAL").ok().as_deref() == Some("1");
|
||
if !sugar_on {
|
||
let line = self.current_token().line;
|
||
return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "enable NYASH_SYNTAX_SUGAR_LEVEL=basic|full or NYASH_ENABLE_MAP_LITERAL=1".to_string(), line });
|
||
}
|
||
self.advance();
|
||
let mut entries: Vec<(String, ASTNode)> = Vec::new();
|
||
let sugar_level = std::env::var("NYASH_SYNTAX_SUGAR_LEVEL").ok();
|
||
// デフォルトでIDENTIFIERキーを許可(basicが明示的に指定された場合のみ無効)
|
||
let ident_key_on = std::env::var("NYASH_ENABLE_MAP_IDENT_KEY").ok().as_deref()
|
||
== Some("1")
|
||
|| sugar_level.as_deref() != Some("basic"); // basic以外は全て許可(デフォルト含む)
|
||
// skip_newlines削除: brace_depth > 0なので自動スキップされる
|
||
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
|
||
// skip_newlines削除: brace_depth > 0なので自動スキップされる
|
||
let key = match &self.current_token().token_type {
|
||
TokenType::STRING(s) => {
|
||
let v = s.clone();
|
||
self.advance();
|
||
v
|
||
}
|
||
TokenType::IDENTIFIER(id) if ident_key_on => {
|
||
let v = id.clone();
|
||
self.advance();
|
||
v
|
||
}
|
||
_ => {
|
||
let line = self.current_token().line;
|
||
return Err(ParseError::UnexpectedToken {
|
||
found: self.current_token().token_type.clone(),
|
||
expected: if ident_key_on {
|
||
"string or identifier key in map literal".to_string()
|
||
} else {
|
||
"string key in map literal (set NYASH_SYNTAX_SUGAR_LEVEL=full for identifier keys)".to_string()
|
||
},
|
||
line,
|
||
});
|
||
}
|
||
};
|
||
// skip_newlines削除: brace_depth > 0なので自動スキップされる
|
||
self.consume(TokenType::COLON)?;
|
||
// skip_newlines削除: brace_depth > 0なので自動スキップされる
|
||
let value_expr = self.parse_expression()?;
|
||
entries.push((key, value_expr));
|
||
// skip_newlines削除: brace_depth > 0なので自動スキップされる
|
||
if self.match_token(&TokenType::COMMA) {
|
||
self.advance();
|
||
// skip_newlines削除: brace_depth > 0なので自動スキップされる
|
||
}
|
||
}
|
||
// skip_newlines削除: brace_depth > 0なので自動スキップされる
|
||
self.consume(TokenType::RBRACE)?;
|
||
Ok(ASTNode::MapLiteral {
|
||
entries,
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::STRING(s) => {
|
||
let value = s.clone();
|
||
self.advance();
|
||
Ok(ASTNode::Literal {
|
||
value: LiteralValue::String(value),
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::NUMBER(n) => {
|
||
let value = *n;
|
||
self.advance();
|
||
Ok(ASTNode::Literal {
|
||
value: LiteralValue::Integer(value),
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::FLOAT(f) => {
|
||
let value = *f;
|
||
self.advance();
|
||
Ok(ASTNode::Literal {
|
||
value: LiteralValue::Float(value),
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::TRUE => {
|
||
self.advance();
|
||
Ok(ASTNode::Literal {
|
||
value: LiteralValue::Bool(true),
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::FALSE => {
|
||
self.advance();
|
||
Ok(ASTNode::Literal {
|
||
value: LiteralValue::Bool(false),
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::NULL => {
|
||
self.advance();
|
||
Ok(ASTNode::Literal {
|
||
value: LiteralValue::Null,
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::THIS => {
|
||
if std::env::var("NYASH_DEPRECATE_THIS").ok().as_deref() == Some("1") {
|
||
eprintln!(
|
||
"[deprecate:this] 'this' is deprecated; use 'me' instead (line {})",
|
||
self.current_token().line
|
||
);
|
||
}
|
||
self.advance();
|
||
Ok(ASTNode::Me {
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::ME => {
|
||
self.advance();
|
||
Ok(ASTNode::Me {
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
TokenType::NEW => {
|
||
self.advance();
|
||
if let TokenType::IDENTIFIER(class_name) = &self.current_token().token_type {
|
||
let class = class_name.clone();
|
||
self.advance();
|
||
let mut type_arguments: Vec<String> = Vec::new();
|
||
if self.match_token(&TokenType::LESS) {
|
||
self.advance();
|
||
loop {
|
||
if let TokenType::IDENTIFIER(tn) = &self.current_token().token_type {
|
||
type_arguments.push(tn.clone());
|
||
self.advance();
|
||
} else {
|
||
let line = self.current_token().line;
|
||
return Err(ParseError::UnexpectedToken {
|
||
found: self.current_token().token_type.clone(),
|
||
expected: "type argument".to_string(),
|
||
line,
|
||
});
|
||
}
|
||
if self.match_token(&TokenType::COMMA) {
|
||
self.advance();
|
||
continue;
|
||
}
|
||
self.consume(TokenType::GREATER)?;
|
||
break;
|
||
}
|
||
}
|
||
self.consume(TokenType::LPAREN)?;
|
||
let mut arguments = Vec::new();
|
||
while !self.match_token(&TokenType::RPAREN) && !self.is_at_end() {
|
||
crate::must_advance!(self, _unused, "new expression argument parsing");
|
||
arguments.push(self.parse_expression()?);
|
||
if self.match_token(&TokenType::COMMA) {
|
||
self.advance();
|
||
}
|
||
}
|
||
self.consume(TokenType::RPAREN)?;
|
||
Ok(ASTNode::New {
|
||
class,
|
||
arguments,
|
||
type_arguments,
|
||
span: Span::unknown(),
|
||
})
|
||
} else {
|
||
let line = self.current_token().line;
|
||
Err(ParseError::UnexpectedToken {
|
||
found: self.current_token().token_type.clone(),
|
||
expected: "class name".to_string(),
|
||
line,
|
||
})
|
||
}
|
||
}
|
||
TokenType::FROM => self.parse_from_call(),
|
||
TokenType::IDENTIFIER(name) => {
|
||
let parent = name.clone();
|
||
self.advance();
|
||
if self.match_token(&TokenType::DoubleColon) {
|
||
self.advance();
|
||
let method = match &self.current_token().token_type {
|
||
TokenType::IDENTIFIER(m) => {
|
||
let s = m.clone();
|
||
self.advance();
|
||
s
|
||
}
|
||
TokenType::INIT => {
|
||
self.advance();
|
||
"init".to_string()
|
||
}
|
||
TokenType::PACK => {
|
||
self.advance();
|
||
"pack".to_string()
|
||
}
|
||
TokenType::BIRTH => {
|
||
self.advance();
|
||
"birth".to_string()
|
||
}
|
||
_ => {
|
||
let line = self.current_token().line;
|
||
return Err(ParseError::UnexpectedToken {
|
||
found: self.current_token().token_type.clone(),
|
||
expected: "method name".to_string(),
|
||
line,
|
||
});
|
||
}
|
||
};
|
||
self.consume(TokenType::LPAREN)?;
|
||
let mut arguments = Vec::new();
|
||
while !self.match_token(&TokenType::RPAREN) && !self.is_at_end() {
|
||
crate::must_advance!(self, _unused, "Parent::method call argument parsing");
|
||
arguments.push(self.parse_expression()?);
|
||
if self.match_token(&TokenType::COMMA) {
|
||
self.advance();
|
||
}
|
||
}
|
||
self.consume(TokenType::RPAREN)?;
|
||
Ok(ASTNode::FromCall {
|
||
parent,
|
||
method,
|
||
arguments,
|
||
span: Span::unknown(),
|
||
})
|
||
} else {
|
||
Ok(ASTNode::Variable {
|
||
name: parent,
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
}
|
||
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)?;
|
||
Ok(expr)
|
||
}
|
||
TokenType::FN => {
|
||
self.advance();
|
||
let mut params: Vec<String> = Vec::new();
|
||
if self.match_token(&TokenType::LPAREN) {
|
||
self.advance();
|
||
while !self.match_token(&TokenType::RPAREN) && !self.is_at_end() {
|
||
if let TokenType::IDENTIFIER(p) = &self.current_token().token_type {
|
||
params.push(p.clone());
|
||
self.advance();
|
||
if self.match_token(&TokenType::COMMA) {
|
||
self.advance();
|
||
}
|
||
} else {
|
||
let line = self.current_token().line;
|
||
return Err(ParseError::UnexpectedToken {
|
||
found: self.current_token().token_type.clone(),
|
||
expected: "parameter name".to_string(),
|
||
line,
|
||
});
|
||
}
|
||
}
|
||
self.consume(TokenType::RPAREN)?;
|
||
}
|
||
self.consume(TokenType::LBRACE)?;
|
||
let mut body: Vec<ASTNode> = Vec::new();
|
||
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
|
||
if !self.match_token(&TokenType::RBRACE) {
|
||
body.push(self.parse_statement()?);
|
||
}
|
||
}
|
||
self.consume(TokenType::RBRACE)?;
|
||
Ok(ASTNode::Lambda {
|
||
params,
|
||
body,
|
||
span: Span::unknown(),
|
||
})
|
||
}
|
||
_ => {
|
||
let line = self.current_token().line;
|
||
Err(ParseError::InvalidExpression { line })
|
||
}
|
||
}
|
||
}
|
||
}
|