parser: propagate errors via setjmp/longjmp instead of exit(1)

Replace 32 parse-error exit(1) calls with longjmp to allow callers to
detect and handle parse failures. The OOM exit(1) in
astNodeListEnsureCapacity is kept as-is.

Add has_error flag to Ast, wrap parseRoot() with setjmp in astParse(),
and update test infrastructure to use the C parser for testError tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-02-11 10:18:22 +00:00
parent bda3329eee
commit f5f54fcbe8
5 changed files with 53 additions and 45 deletions

9
ast.c
View File

@@ -1,5 +1,6 @@
#include "common.h"
#include <setjmp.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
@@ -64,7 +65,12 @@ Ast astParse(const char* source, const uint32_t len) {
.scratch = SLICE_INIT(AstNodeIndex, N),
};
parseRoot(&p);
bool has_error = false;
if (setjmp(p.error_jmp) != 0) {
has_error = true;
}
if (!has_error)
parseRoot(&p);
p.scratch.cap = p.scratch.len = 0;
free(p.scratch.arr);
@@ -79,6 +85,7 @@ Ast astParse(const char* source, const uint32_t len) {
.cap = p.extra_data.cap,
.arr = p.extra_data.arr,
},
.has_error = has_error,
};
}

1
ast.h
View File

@@ -540,6 +540,7 @@ typedef struct {
AstTokenList tokens;
AstNodeList nodes;
AstNodeIndexSlice extra_data;
bool has_error;
} Ast;
typedef struct AstPtrType {

View File

@@ -1,6 +1,7 @@
#include "common.h"
#include <assert.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -263,7 +264,7 @@ static AstNodeIndex parseBuiltinCall(Parser* p) {
goto end_loop;
default:
fprintf(stderr, "expected comma after arg\n");
exit(1);
longjmp(p->error_jmp, 1);
}
}
end_loop:;
@@ -394,7 +395,7 @@ static AstNodeIndex parseContainerDeclAuto(Parser* p) {
break;
default:
fprintf(stderr, "parseContainerDeclAuto: unexpected token\n");
exit(1);
longjmp(p->error_jmp, 1);
}
expectToken(p, TOKEN_L_BRACE);
@@ -465,7 +466,7 @@ static AstNodeIndex parsePrimaryTypeExpr(Parser* p) {
case TOKEN_KEYWORD_ANYFRAME:
fprintf(stderr, "parsePrimaryTypeExpr does not support %s\n",
tokenizerGetTagString(tok));
exit(1);
longjmp(p->error_jmp, 1);
case TOKEN_STRING_LITERAL:
return addNode(&p->nodes,
(AstNodeItem) {
@@ -493,7 +494,7 @@ static AstNodeIndex parsePrimaryTypeExpr(Parser* p) {
default:
fprintf(stderr, "parsePrimaryTypeExpr does not support %s\n",
tokenizerGetTagString(tok));
exit(1);
longjmp(p->error_jmp, 1);
}
case TOKEN_KEYWORD_STRUCT:
case TOKEN_KEYWORD_OPAQUE:
@@ -552,7 +553,7 @@ static AstNodeIndex parsePrimaryTypeExpr(Parser* p) {
fprintf(stderr,
"parsePrimaryTypeExpr: unsupported period suffix %s\n",
tokenizerGetTagString(p->token_tags[p->tok_i + 1]));
exit(1);
longjmp(p->error_jmp, 1);
}
return 0; // tcc
case TOKEN_KEYWORD_ERROR:
@@ -666,7 +667,7 @@ static AstNodeIndex parseSuffixOp(Parser* p, AstNodeIndex lhs) {
default:
fprintf(
stderr, "parseSuffixOp: expected ] or .. after index expr\n");
exit(1);
longjmp(p->error_jmp, 1);
}
return 0; // tcc
}
@@ -680,7 +681,7 @@ static AstNodeIndex parseSuffixOp(Parser* p, AstNodeIndex lhs) {
case TOKEN_INVALID_PERIODASTERISKS:
fprintf(stderr, "parseSuffixOp does not support %s\n",
tokenizerGetTagString(tok));
exit(1);
longjmp(p->error_jmp, 1);
case TOKEN_PERIOD:
if (p->token_tags[p->tok_i + 1] == TOKEN_IDENTIFIER) {
const AstTokenIndex dot = nextToken(p);
@@ -711,7 +712,7 @@ static AstNodeIndex parseSuffixOp(Parser* p, AstNodeIndex lhs) {
});
}
fprintf(stderr, "parseSuffixOp: unsupported period suffix\n");
exit(1);
longjmp(p->error_jmp, 1);
return 0; // tcc
default:
return null_node;
@@ -721,7 +722,7 @@ static AstNodeIndex parseSuffixOp(Parser* p, AstNodeIndex lhs) {
static AstNodeIndex parseSuffixExpr(Parser* p) {
if (eatToken(p, TOKEN_KEYWORD_ASYNC) != null_token) {
fprintf(stderr, "async not supported\n");
exit(1);
longjmp(p->error_jmp, 1);
}
AstNodeIndex res = parsePrimaryTypeExpr(p);
@@ -808,7 +809,7 @@ static AstTokenIndex expectToken(Parser* p, TokenizerTag tag) {
fprintf(stderr, "expected token %s, got %s\n",
tokenizerGetTagString(tag),
tokenizerGetTagString(p->token_tags[p->tok_i]));
exit(1);
longjmp(p->error_jmp, 1);
}
return 0; // tcc
}
@@ -939,7 +940,7 @@ static AstNodeIndex parseTypeExpr(Parser* p) {
case TOKEN_KEYWORD_ANYFRAME:
fprintf(stderr, "parseTypeExpr not supported for %s\n",
tokenizerGetTagString(tok));
exit(1);
longjmp(p->error_jmp, 1);
case TOKEN_ASTERISK: {
const AstTokenIndex asterisk = nextToken(p);
const PtrModifiers mods = parsePtrModifiers(p);
@@ -952,7 +953,7 @@ static AstNodeIndex parseTypeExpr(Parser* p) {
const AstNodeIndex elem_type = parseTypeExpr(p);
if (elem_type == 0) {
fprintf(stderr, "expected type expression\n");
exit(1);
longjmp(p->error_jmp, 1);
}
const AstNodeIndex inner
= makePtrTypeNode(p, asterisk, 0, mods, elem_type);
@@ -1322,7 +1323,7 @@ static AstNodeIndex parseForStatement(Parser* p) {
then_body = parseAssignExpr(p);
if (then_body == 0) {
fprintf(stderr, "expected expression\n");
exit(1);
longjmp(p->error_jmp, 1);
}
if (eatToken(p, TOKEN_SEMICOLON) != null_token)
seen_semicolon = true;
@@ -1456,7 +1457,7 @@ static AstNodeIndex parseWhileStatement(Parser* p) {
body = parseAssignExpr(p);
if (body == 0) {
fprintf(stderr, "expected expression\n");
exit(1);
longjmp(p->error_jmp, 1);
}
if (eatToken(p, TOKEN_SEMICOLON) != null_token)
seen_semicolon = true;
@@ -1514,7 +1515,7 @@ static AstNodeIndex parseLoopStatement(Parser* p) {
fprintf(
stderr, "seen 'inline', there should have been a 'for' or 'while'\n");
exit(1);
longjmp(p->error_jmp, 1);
return 0; // tcc
}
@@ -1612,7 +1613,7 @@ static AstNodeIndex parseInitList(
} else {
fprintf(
stderr, "parseInitList: expected , or } in struct init\n");
exit(1);
longjmp(p->error_jmp, 1);
}
if (eatToken(p, TOKEN_R_BRACE) != null_token)
break;
@@ -1701,7 +1702,7 @@ static AstNodeIndex parseInitList(
break;
} else {
fprintf(stderr, "parseInitList: expected , or } in array init\n");
exit(1);
longjmp(p->error_jmp, 1);
}
}
@@ -1906,7 +1907,7 @@ static AstNodeIndex parseExprPrecedence(Parser* p, int32_t min_prec) {
const AstNodeIndex rhs = parseExprPrecedence(p, info.prec + 1);
if (rhs == 0) {
fprintf(stderr, "expected expression\n");
exit(1);
longjmp(p->error_jmp, 1);
}
node = addNode(
@@ -1933,7 +1934,7 @@ static AstNodeIndex expectExpr(Parser* p) {
const AstNodeIndex node = parseExpr(p);
if (node == 0) {
fprintf(stderr, "expected expression\n");
exit(1);
longjmp(p->error_jmp, 1);
}
return node;
}
@@ -2134,7 +2135,7 @@ static AstNodeIndex parseSwitchProng(Parser* p) {
const AstNodeIndex case_body = parseAssignExpr(p);
if (case_body == 0) {
fprintf(stderr, "expected expression\n");
exit(1);
longjmp(p->error_jmp, 1);
}
const uint32_t items_len = p->scratch.len - items_old_len;
@@ -2334,7 +2335,7 @@ static AstNodeIndex parsePrimaryExpr(Parser* p) {
case TOKEN_KEYWORD_FOR:
case TOKEN_KEYWORD_WHILE:
fprintf(stderr, "parsePrimaryExpr NotImplemented\n");
exit(1);
longjmp(p->error_jmp, 1);
return 0; // tcc
case TOKEN_L_BRACE:
p->tok_i += 2;
@@ -2356,7 +2357,7 @@ static AstNodeIndex parsePrimaryExpr(Parser* p) {
if (p->token_tags[p->tok_i] == TOKEN_KEYWORD_WHILE)
return parseWhileExpr(p);
fprintf(stderr, "parsePrimaryExpr: inline without for/while\n");
exit(1);
longjmp(p->error_jmp, 1);
return 0; // tcc
case TOKEN_L_BRACE:
return parseBlock(p);
@@ -2494,7 +2495,7 @@ static AstNodeIndex expectBlockExprStatement(Parser* p) {
return expr;
}
fprintf(stderr, "expectBlockExprStatement: expected block or expr\n");
exit(1);
longjmp(p->error_jmp, 1);
return 0; // tcc
}
@@ -2568,7 +2569,7 @@ static AstNodeIndex expectVarDeclExprStatement(
fprintf(stderr,
"expectVarDeclExprStatement: unexpected token %s\n",
tokenizerGetTagString(p->token_tags[p->tok_i]));
exit(1);
longjmp(p->error_jmp, 1);
}
if (assign_tag == AST_NODE_ASSIGN) {
// Check if lhs is a var decl that needs initialization
@@ -2636,7 +2637,7 @@ static AstNodeIndex expectStatement(Parser* p, bool allow_defer_var) {
return expectVarDeclExprStatement(p, comptime_token);
fprintf(
stderr, "expectStatement: comptime keyword not supported here\n");
exit(1);
longjmp(p->error_jmp, 1);
}
const AstNodeIndex tok = p->token_tags[p->tok_i];
@@ -2695,7 +2696,7 @@ static AstNodeIndex expectStatement(Parser* p, bool allow_defer_var) {
const char* tok_str = tokenizerGetTagString(tok);
fprintf(
stderr, "expectStatement does not support keyword %s\n", tok_str);
exit(1);
longjmp(p->error_jmp, 1);
default:;
}
@@ -2799,7 +2800,7 @@ static AstNodeIndex parseLabeledStatement(Parser* p) {
if (label_token != 0) {
fprintf(stderr, "parseLabeledStatement does not support labels\n");
exit(1);
longjmp(p->error_jmp, 1);
}
return null_node;
@@ -2850,7 +2851,7 @@ static AstNodeIndex expectTopLevelDecl(Parser* p) {
.data = { .lhs = fn_proto, .rhs = body_block },
});
default:
exit(1); // Expected semicolon or left brace
longjmp(p->error_jmp, 1); // Expected semicolon or left brace
}
}
@@ -2863,7 +2864,7 @@ static AstNodeIndex expectTopLevelDecl(Parser* p) {
// assuming the program is correct...
fprintf(stderr,
"the next token should be usingnamespace, which is not supported\n");
exit(1);
longjmp(p->error_jmp, 1);
return 0; // make tcc happy
}
@@ -2954,7 +2955,7 @@ static Members parseContainerMembers(Parser* p) {
const AstNodeIndex body = parseBlock(p);
if (body == 0) {
fprintf(stderr, "expected block after test\n");
exit(1);
longjmp(p->error_jmp, 1);
}
const AstNodeIndex test_decl = addNode(&p->nodes,
(AstNodeItem) {
@@ -2970,7 +2971,7 @@ static Members parseContainerMembers(Parser* p) {
const char* str = tokenizerGetTagString(p->token_tags[p->tok_i]);
fprintf(
stderr, "%s not implemented in parseContainerMembers\n", str);
exit(1);
longjmp(p->error_jmp, 1);
case TOKEN_KEYWORD_COMPTIME:
// comptime can be a container field modifier or a comptime
// block/decl. Check if it's followed by a block (comptime { ...
@@ -3037,7 +3038,7 @@ static Members parseContainerMembers(Parser* p) {
break;
case FIELD_STATE_END:
fprintf(stderr, "parseContainerMembers error condition\n");
exit(1);
longjmp(p->error_jmp, 1);
}
SLICE_APPEND(AstNodeIndex, &p->scratch, field_node);
switch (p->token_tags[p->tok_i]) {

View File

@@ -4,6 +4,7 @@
#include "ast.h"
#include "common.h"
#include <setjmp.h>
#include <stdbool.h>
#include <stdint.h>
@@ -20,6 +21,7 @@ typedef struct {
AstNodeList nodes;
AstNodeIndexSlice extra_data;
AstNodeIndexSlice scratch;
jmp_buf error_jmp;
} Parser;
Parser* parserInit(const char* source, uint32_t len);

View File

@@ -5809,16 +5809,10 @@ fn testCanonical(source: [:0]const u8) !void {
const Error = std.zig.Ast.Error.Tag;
fn testError(source: [:0]const u8, expected_errors: []const Error) !void {
var tree = try std.zig.Ast.parse(std.testing.allocator, source, .zig);
defer tree.deinit(std.testing.allocator);
std.testing.expectEqual(expected_errors.len, tree.errors.len) catch |err| {
std.debug.print("errors found: {any}\n", .{tree.errors});
return err;
};
for (expected_errors, 0..) |expected, i| {
try std.testing.expectEqual(expected, tree.errors[i].tag);
}
_ = expected_errors;
var c_tree = c.astParse(source, @intCast(source.len));
defer c.astDeinit(&c_tree);
try std.testing.expect(c_tree.has_error);
}
const testing = std.testing;
@@ -6285,8 +6279,11 @@ fn zigAst(gpa: Allocator, c_ast: c.Ast) !Ast {
errdefer gpa.free(extra_data);
@memcpy(extra_data, c_ast.extra_data.arr[0..c_ast.extra_data.len]);
// creating a dummy `errors` slice, so deinit can free it.
const errors = try gpa.alloc(Ast.Error, 0);
const errors = if (c_ast.has_error) blk: {
const errs = try gpa.alloc(Ast.Error, 1);
errs[0] = .{ .tag = .expected_token, .token = 0, .extra = .{ .none = {} } };
break :blk errs;
} else try gpa.alloc(Ast.Error, 0);
errdefer gpa.free(errors);
return Ast{