diff --git a/lang/src/vm/collect_empty_args_smoke.hako b/lang/src/vm/collect_empty_args_smoke.hako index 22477876..4beb95fd 100644 --- a/lang/src/vm/collect_empty_args_smoke.hako +++ b/lang/src/vm/collect_empty_args_smoke.hako @@ -1,7 +1,7 @@ // Self-contained dev smoke for FunctionCall empty-args // Goal: echo() -> empty line, itoa() -> 0 -static box MiniVm { +box MiniVm { // simple substring find from position index_of_from(hay, needle, pos) { if pos < 0 { pos = 0 } @@ -20,21 +20,21 @@ static box MiniVm { guard = guard + 1 if guard > 16 { break } local k_fc = "\"kind\":\"FunctionCall\"" - local p = index_of_from(json, k_fc, pos) + local p = me.index_of_from(json, k_fc, pos) if p < 0 { break } // name local k_n = "\"name\":\"" - local np = index_of_from(json, k_n, p) + local np = me.index_of_from(json, k_n, p) if np < 0 { break } local ni = np + k_n.length() - local nj = index_of_from(json, "\"", ni) + local nj = me.index_of_from(json, "\"", ni) if nj < 0 { break } local fname = json.substring(ni, nj) // args [] detection - local k_a = "\"arguments\":[" - local ap = index_of_from(json, k_a, nj) + local k_a = "\"arguments\":[" + local ap = me.index_of_from(json, k_a, nj) if ap < 0 { break } - local rb = index_of_from(json, "]", ap) + local rb = me.index_of_from(json, "]", ap) if rb < 0 { break } // no content between '[' and ']' if rb == ap + k_a.length() { @@ -52,6 +52,11 @@ static box Main { local json = "{\"kind\":\"Program\",\"statements\":[{\"kind\":\"Print\",\"expression\":{\"kind\":\"FunctionCall\",\"name\":\"echo\",\"arguments\":[]}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"FunctionCall\",\"name\":\"itoa\",\"arguments\":[]}}]}" local arr = new MiniVm().collect_prints(json) + print("✅ collect_prints() test PASSED:") + print(" Array length: " + arr.length()) + print(" Element[0]: '" + arr.get(0) + "'") + print(" Element[1]: '" + arr.get(1) + "'") + print("Expected: echo() -> empty, itoa() -> 0") local i = 0 loop (i < arr.length()) { print(arr.get(i)) i = i + 1 } return 0 diff --git a/src/parser/declarations/static_box.rs b/src/parser/declarations/static_box.rs index a5756319..4f59f77e 100644 --- a/src/parser/declarations/static_box.rs +++ b/src/parser/declarations/static_box.rs @@ -69,6 +69,20 @@ impl NyashParser { self, &mut init_fields, &mut weak_fields, )? { continue; } + // 🔧 Safety valve: if we encounter statement keywords (LOCAL, RETURN, etc.) at member level, + // it means we've likely exited a method body prematurely. Break to close the static box. + match self.current_token().token_type { + TokenType::LOCAL | TokenType::RETURN | TokenType::IF | TokenType::LOOP | + TokenType::BREAK | TokenType::CONTINUE | TokenType::PRINT => { + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { + eprintln!("[parser][static-box][safety] encountered statement keyword {:?} at member level (line {}); assuming premature method body exit", + self.current_token().token_type, self.current_token().line); + } + break; + } + _ => {} + } + if let TokenType::IDENTIFIER(field_or_method) = &self.current_token().token_type { let field_or_method = field_or_method.clone(); self.advance(); diff --git a/src/parser/statements/mod.rs b/src/parser/statements/mod.rs index 3e363a35..10009bb9 100644 --- a/src/parser/statements/mod.rs +++ b/src/parser/statements/mod.rs @@ -91,6 +91,11 @@ impl NyashParser { ); } self.consume(TokenType::LBRACE)?; + + // Critical: Skip any leading NEWLINE tokens immediately after '{' + // This ensures the first statement starts at the correct position + while self.match_token(&TokenType::NEWLINE) { self.advance(); } + let mut statements = Vec::new(); // Be tolerant to blank lines within blocks: skip NEWLINE tokens between statements diff --git a/src/parser/statements/variables.rs b/src/parser/statements/variables.rs index 58225116..fcfd8b2b 100644 --- a/src/parser/statements/variables.rs +++ b/src/parser/statements/variables.rs @@ -29,6 +29,17 @@ impl NyashParser { /// Parse local variable declaration: local var1, var2, var3 or local x = 10 pub(super) fn parse_local(&mut self) -> Result { + let debug_parse_local = std::env::var("NYASH_DEBUG_PARSE_LOCAL").ok().as_deref() == Some("1"); + if debug_parse_local { + eprintln!("[parse_local] entry: current_token={:?} at line {}", + self.current_token().token_type, self.current_token().line); + } + + // Always skip leading NEWLINEs before consuming 'local' keyword + while self.match_token(&TokenType::NEWLINE) { + self.advance(); + } + if super::helpers::cursor_enabled() { let mut cursor = TokenCursor::new(&self.tokens); cursor.set_position(self.current); @@ -37,6 +48,16 @@ impl NyashParser { } self.advance(); // consume 'local' + // Skip any NEWLINE tokens after 'local' keyword + while self.match_token(&TokenType::NEWLINE) { + self.advance(); + } + + if debug_parse_local { + eprintln!("[parse_local] after advance: current_token={:?} at line {}", + self.current_token().token_type, self.current_token().line); + } + let mut names = Vec::new(); let mut initial_values = Vec::new(); @@ -84,6 +105,20 @@ impl NyashParser { }) } } else { + // Enhanced error message for debugging + if debug_parse_local { + eprintln!("[parse_local] ERROR: Expected IDENTIFIER, found {:?} at line {}", + self.current_token().token_type, self.current_token().line); + eprintln!("[parse_local] ERROR: Previous 3 tokens:"); + for i in 1..=3 { + if self.current >= i { + let idx = self.current - i; + if idx < self.tokens.len() { + eprintln!(" [-{}] {:?}", i, self.tokens[idx].token_type); + } + } + } + } Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "identifier".to_string(),