feat(parser): Phase 285A1.4 & A1.5 - Weak field sugar + Parser hang fix

A1.4: Add sugar syntax `public weak parent` ≡ `public { weak parent }`
A1.5: Fix parser hang on unsupported `param: Type` syntax

Key changes:
- A1.4: Extend visibility parser to handle weak modifier (fields.rs)
- A1.5: Shared helper `parse_param_name_list()` with progress-zero detection
- A1.5: Fix 6 vulnerable parameter parsing loops (methods, constructors, functions)
- Tests: Sugar syntax (OK/NG), parser hang (timeout-based)
- Docs: lifecycle.md, EBNF.md, phase-285a1-boxification.md

Additional changes:
- weak() builtin implementation (handlers/weak.rs)
- Leak tracking improvements (leak_tracker.rs)
- Documentation updates (lifecycle, types, memory-finalization, etc.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-24 07:44:50 +09:00
parent a47f850d02
commit ab76e39036
60 changed files with 2099 additions and 454 deletions

View File

@ -5,6 +5,8 @@
* Extracted from parser/mod.rs as part of modularization
*/
pub(crate) mod params;
use super::ParseError;
use crate::ast::Span;
use crate::tokenizer::{Token, TokenType};

View File

@ -0,0 +1,92 @@
/*!
* Parameter Parsing Utilities
*
* Phase 285A1.5: Common helper for parameter name list parsing
* Prevents infinite parser hangs on unsupported parameter type annotations
*/
use super::ParserUtils;
use crate::parser::{NyashParser, ParseError};
use crate::tokenizer::TokenType;
/// Parse parameter name list with Fail-Fast on unexpected tokens
///
/// Parses: IDENT (',' IDENT)*
/// Rejects: Type annotations, unexpected tokens, malformed comma sequences
///
/// # Arguments
/// * `p` - Mutable reference to NyashParser
/// * `context` - Context string for error messages ("method", "constructor", "function", etc.)
///
/// # Returns
/// * `Ok(Vec<String>)` - List of parameter names
/// * `Err(ParseError)` - Parse error with context-aware message
///
/// # Features
/// * Progress-zero detection: Tracks token position, errors if stuck
/// * Explicit token handling: All token types explicitly matched
/// * Fail-Fast: Either advances or errors (no infinite loop possible)
/// * Unified error messages: Single source of truth for error text
pub(crate) fn parse_param_name_list(
p: &mut NyashParser,
context: &str,
) -> Result<Vec<String>, ParseError> {
let mut params = Vec::new();
let mut last_token_position: Option<(usize, usize)> = None;
while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() {
// Progress-zero detection: same position twice → infinite loop risk
let current_position = p.current_position();
if let Some(last_pos) = last_token_position {
if current_position == last_pos {
return Err(ParseError::InfiniteLoop {
location: format!("{} parameter list", context),
token: p.current_token().token_type.clone(),
line: p.current_token().line,
});
}
}
last_token_position = Some(current_position);
match &p.current_token().token_type {
TokenType::IDENTIFIER(param) => {
params.push(param.clone());
p.advance();
if p.match_token(&TokenType::COMMA) {
p.advance();
} else if !p.match_token(&TokenType::RPAREN) {
return Err(ParseError::UnexpectedToken {
found: p.current_token().token_type.clone(),
expected: format!(
"',' or ')' in {} parameter list. \
Note: Parameter type annotations are not supported.",
context
),
line: p.current_token().line,
});
}
}
TokenType::COMMA => {
return Err(ParseError::UnexpectedToken {
found: TokenType::COMMA,
expected: format!("parameter name in {} parameter list", context),
line: p.current_token().line,
});
}
_ => {
return Err(ParseError::UnexpectedToken {
found: p.current_token().token_type.clone(),
expected: format!(
"parameter name or ')' in {} parameter list. \
Note: Parameter type annotations are not supported.",
context
),
line: p.current_token().line,
});
}
}
}
Ok(params)
}

View File

@ -23,17 +23,10 @@ pub(crate) fn try_parse_constructor(
let name = "init".to_string();
p.advance(); // consume 'init'
p.consume(TokenType::LPAREN)?;
let mut params = Vec::new();
while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() {
crate::must_advance!(p, _unused, "constructor parameter parsing");
if let TokenType::IDENTIFIER(param) = &p.current_token().token_type {
params.push(param.clone());
p.advance();
}
if p.match_token(&TokenType::COMMA) {
p.advance();
}
}
// Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations
let params = crate::parser::common::params::parse_param_name_list(p, "constructor (init)")?;
p.consume(TokenType::RPAREN)?;
let mut body = p.parse_block_statements()?;
// Optional postfix catch/cleanup (method-level gate)
@ -97,17 +90,10 @@ pub(crate) fn try_parse_constructor(
let name = "pack".to_string();
p.advance(); // consume 'pack'
p.consume(TokenType::LPAREN)?;
let mut params = Vec::new();
while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() {
crate::must_advance!(p, _unused, "pack parameter parsing");
if let TokenType::IDENTIFIER(param) = &p.current_token().token_type {
params.push(param.clone());
p.advance();
}
if p.match_token(&TokenType::COMMA) {
p.advance();
}
}
// Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations
let params = crate::parser::common::params::parse_param_name_list(p, "constructor (pack)")?;
p.consume(TokenType::RPAREN)?;
let body = p.parse_block_statements()?;
let node = ASTNode::FunctionDeclaration {
@ -134,17 +120,10 @@ pub(crate) fn try_parse_constructor(
let name = "birth".to_string();
p.advance(); // consume 'birth'
p.consume(TokenType::LPAREN)?;
let mut params = Vec::new();
while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() {
crate::must_advance!(p, _unused, "birth parameter parsing");
if let TokenType::IDENTIFIER(param) = &p.current_token().token_type {
params.push(param.clone());
p.advance();
}
if p.match_token(&TokenType::COMMA) {
p.advance();
}
}
// Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations
let params = crate::parser::common::params::parse_param_name_list(p, "constructor (birth)")?;
p.consume(TokenType::RPAREN)?;
let body = p.parse_block_statements()?;
let node = ASTNode::FunctionDeclaration {

View File

@ -151,6 +151,35 @@ pub(crate) fn try_parse_visibility_block_or_single(
p.consume(TokenType::RBRACE)?;
return Ok(true);
}
// Phase 285A1.4: Sugar syntax - public weak parent, private weak parent
if p.match_token(&TokenType::WEAK) {
p.advance(); // consume WEAK only
// Read field name (reuse existing pattern)
if let TokenType::IDENTIFIER(fname) = &p.current_token().token_type {
let fname = fname.clone();
p.advance(); // consume IDENTIFIER
// Delegate to existing weak field parser (handles type annotation, etc.)
parse_weak_field(p, fname.clone(), methods, fields, weak_fields)?;
// Register with visibility tracking
if visibility == "public" {
public_fields.push(fname);
} else {
private_fields.push(fname);
}
*last_method_name = None; // Reset method context (Phase 285A1.4)
return Ok(true);
} else {
return Err(ParseError::UnexpectedToken {
expected: "field name after 'weak' in visibility context".to_string(),
found: p.current_token().token_type.clone(),
line: p.current_token().line,
});
}
}
if let TokenType::IDENTIFIER(n) = &p.current_token().token_type {
let fname = n.clone();
p.advance();

View File

@ -17,17 +17,8 @@ pub(crate) fn try_parse_method(
}
p.advance(); // consume '('
let mut params = Vec::new();
while !p.match_token(&TokenType::RPAREN) && !p.is_at_end() {
crate::must_advance!(p, _unused, "method parameter parsing");
if let TokenType::IDENTIFIER(param) = &p.current_token().token_type {
params.push(param.clone());
p.advance();
}
if p.match_token(&TokenType::COMMA) {
p.advance();
}
}
// Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations
let params = crate::parser::common::params::parse_param_name_list(p, "method")?;
p.consume(TokenType::RPAREN)?;
let mut body = p.parse_block_statements()?;

View File

@ -313,6 +313,19 @@ impl NyashParser {
span: Span::unknown(),
})
}
// Phase 285A0: weak(expr) → WeakRef creation
// SSOT: docs/reference/language/lifecycle.md:171
TokenType::WEAK => {
self.advance();
self.consume(TokenType::LPAREN)?;
let argument = self.parse_expression()?;
self.consume(TokenType::RPAREN)?;
Ok(ASTNode::FunctionCall {
name: "weak".to_string(),
arguments: vec![argument],
span: Span::unknown(),
})
}
_ => {
let line = self.current_token().line;
Err(ParseError::InvalidExpression { line })

View File

@ -29,27 +29,9 @@ impl NyashParser {
// パラメータリストをパース
self.consume(TokenType::LPAREN)?;
let mut params = Vec::new();
while !self.match_token(&TokenType::RPAREN) && !self.is_at_end() {
must_advance!(self, _unused, "function declaration parameter parsing");
if let TokenType::IDENTIFIER(param) = &self.current_token().token_type {
params.push(param.clone());
self.advance();
if self.match_token(&TokenType::COMMA) {
self.advance();
}
} else if !self.match_token(&TokenType::RPAREN) {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "parameter name".to_string(),
line,
});
}
}
// Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations
let params = crate::parser::common::params::parse_param_name_list(self, "function")?;
self.consume(TokenType::RPAREN)?;

View File

@ -67,27 +67,9 @@ impl NyashParser {
// パラメータリストをパース
self.consume(TokenType::LPAREN)?;
let mut params = Vec::new();
while !self.match_token(&TokenType::RPAREN) && !self.is_at_end() {
must_advance!(self, _unused, "static function parameter parsing");
if let TokenType::IDENTIFIER(param) = &self.current_token().token_type {
params.push(param.clone());
self.advance();
if self.match_token(&TokenType::COMMA) {
self.advance();
}
} else if !self.match_token(&TokenType::RPAREN) {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "parameter name".to_string(),
line,
});
}
}
// Phase 285A1.5: Use shared helper to prevent parser hangs on unsupported type annotations
let params = crate::parser::common::params::parse_param_name_list(self, "static method")?;
self.consume(TokenType::RPAREN)?;