aboutsummaryrefslogtreecommitdiff
path: root/src/z/parser/js/parser.zig
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2026-07-30 10:24:19 +0200
committerNathan Reiner <nathan@nathanreiner.xyz>2026-07-30 10:24:19 +0200
commit00920ddd36a8ebb851b52c3c048c0017753176bd (patch)
tree8498e65ca6c055333aa95b6c3a0d28c8370cb0ef /src/z/parser/js/parser.zig
parente8259360516078517a2fbeddbf781f701f35d0d0 (diff)
complete js parser + minifierjs-parser
Diffstat (limited to 'src/z/parser/js/parser.zig')
-rw-r--r--src/z/parser/js/parser.zig553
1 files changed, 553 insertions, 0 deletions
diff --git a/src/z/parser/js/parser.zig b/src/z/parser/js/parser.zig
new file mode 100644
index 0000000..09118dd
--- /dev/null
+++ b/src/z/parser/js/parser.zig
@@ -0,0 +1,553 @@
+const std = @import("std");
+const lex = @import("lexical_grammar.zig");
+const Token = lex.Token;
+const TokenType = lex.TokenType;
+const ast = @import("ast/core.zig");
+
+pub const ParseError = error{
+ UnexpectedToken,
+ ExpectedToken,
+ ExpectedExpression,
+ ExpectedStatement,
+ ExpectedIdentifier,
+ UnclosedString,
+ InvalidNumber,
+ InvalidEscape,
+ UnterminatedComment,
+ ExpectedParameterName,
+ ExpectedPropertyName,
+ UnterminatedTemplate,
+ InvalidAssignmentTarget,
+ TooManyTokens,
+};
+
+pub const BP = struct {
+ pub const comma = 1;
+ pub const assign = 2;
+ pub const yield = 3;
+ pub const conditional = 4;
+ pub const coalesce = 5;
+ pub const log_or = 6;
+ pub const log_and = 7;
+ pub const bit_or = 8;
+ pub const bit_xor = 9;
+ pub const bit_and = 10;
+ pub const equality = 11;
+ pub const relational = 12;
+ pub const shift = 13;
+ pub const additive = 14;
+ pub const multiplicative = 15;
+ pub const exponentiation = 16;
+ pub const unary = 17;
+ pub const update = 18;
+ pub const call = 19;
+ pub const member = 20;
+};
+
+pub const NodeArena = struct {
+ exprs: [8192]ast.Expr = undefined,
+ expr_count: usize = 0,
+ stmts: [8192]ast.Stmt = undefined,
+ stmt_count: usize = 0,
+ decls: [1024]ast.Decl = undefined,
+ decl_count: usize = 0,
+ pats: [1024]ast.Pat = undefined,
+ pat_count: usize = 0,
+ lits: [1024]ast.Lit = undefined,
+ lit_count: usize = 0,
+ string_buf: [65536]u8 = undefined,
+ string_pos: usize = 0,
+
+ pub fn exprPtr(self: *NodeArena, data: ast.ExprType, loc: ast.Loc) *const ast.Expr {
+ const idx = self.expr_count;
+ self.exprs[idx] = .{ .loc = loc, .data = data };
+ self.expr_count += 1;
+ return &self.exprs[idx];
+ }
+
+ pub fn stmtPtr(self: *NodeArena, data: ast.StmtType, loc: ast.Loc) *const ast.Stmt {
+ const idx = self.stmt_count;
+ self.stmts[idx] = .{ .loc = loc, .data = data };
+ self.stmt_count += 1;
+ return &self.stmts[idx];
+ }
+
+ pub fn declPtr(self: *NodeArena, data: ast.DeclType, loc: ast.Loc) *const ast.Decl {
+ const idx = self.decl_count;
+ self.decls[idx] = .{ .loc = loc, .data = data };
+ self.decl_count += 1;
+ return &self.decls[idx];
+ }
+
+ pub fn patPtr(self: *NodeArena, data: ast.PatType, loc: ast.Loc) *const ast.Pat {
+ const idx = self.pat_count;
+ self.pats[idx] = .{ .loc = loc, .data = data };
+ self.pat_count += 1;
+ return &self.pats[idx];
+ }
+
+ pub fn litPtr(self: *NodeArena, data: ast.LitType, loc: ast.Loc) *const ast.Lit {
+ const idx = self.lit_count;
+ self.lits[idx] = .{ .loc = loc, .data = data };
+ self.lit_count += 1;
+ return &self.lits[idx];
+ }
+};
+
+pub const Parser = struct {
+ tokens: []const Token,
+ pos: usize,
+ source: []const u8,
+ arena: *NodeArena,
+
+ pub fn init(tokens: []const Token, source: []const u8, arena: *NodeArena) @This() {
+ return .{ .tokens = tokens, .pos = 0, .source = source, .arena = arena };
+ }
+
+ pub fn tok(self: *@This()) Token {
+ return self.tokens[self.pos];
+ }
+
+ pub fn peek(self: *@This()) TokenType {
+ return self.tokens[self.pos].kind;
+ }
+
+ pub fn peekAt(self: *@This(), offset: usize) TokenType {
+ const idx = self.pos + offset;
+ if (idx >= self.tokens.len) return .eof;
+ return self.tokens[idx].kind;
+ }
+
+ pub fn advance(self: *@This()) void {
+ self.pos += 1;
+ }
+
+ pub fn advanceTok(self: *@This()) Token {
+ const t = self.tokens[self.pos];
+ self.pos += 1;
+ return t;
+ }
+
+ pub fn expect(self: *@This(), kind: TokenType) ParseError!void {
+ if (self.peek() != kind) return ParseError.ExpectedToken;
+ self.advance();
+ }
+
+ pub fn expectTok(self: *@This(), kind: TokenType) ParseError!Token {
+ if (self.peek() != kind) return ParseError.ExpectedToken;
+ return self.advanceTok();
+ }
+
+ pub fn match(self: *@This(), kind: TokenType) bool {
+ if (self.peek() == kind) {
+ _ = self.advanceTok();
+ return true;
+ }
+ return false;
+ }
+
+ pub fn matchKeyword(self: *@This(), kw: TokenType) bool {
+ if (self.peek() == kw) {
+ _ = self.advanceTok();
+ return true;
+ }
+ return false;
+ }
+
+ pub fn isLineTerminatorBeforeNext(self: *@This()) bool {
+ if (self.pos == 0) return false;
+ const prev_token = self.tokens[self.pos - 1];
+ const next_token = self.tokens[self.pos];
+ var i = prev_token.loc.end;
+ while (i < next_token.loc.start) : (i += 1) {
+ if (i >= self.source.len) break;
+ const c = self.source[i];
+ switch (c) {
+ '\n', '\r' => return true,
+ else => {},
+ }
+ }
+ return false;
+ }
+
+ pub fn as_identifier(self: *@This(), t: Token) []const u8 {
+ return self.source[t.loc.start..t.loc.end];
+ }
+
+ pub fn tokenSlice(self: *@This(), t: Token) []const u8 {
+ return self.source[t.loc.start..t.loc.end];
+ }
+
+ pub fn parseForDeclaration(self: *@This()) ParseError!*const ast.Decl {
+ const kind_tok = self.advanceTok();
+ var decls: [128]ast.VarDeclarator = undefined;
+ var count: usize = 0;
+
+ while (true) {
+ const pat = try ast.Pat.parse(self);
+ var decl_init: ?*const ast.Expr = null;
+ if (self.match(.@"=")) {
+ decl_init = try ast.Expr.parse(self, 0);
+ }
+ decls[count] = .{ .loc = pat.loc, .id = pat, .init = decl_init };
+ count += 1;
+ if (!self.match(.comma)) break;
+ }
+
+ const kind: ast.VarKind = switch (kind_tok.kind) {
+ .@"var" => .@"var",
+ .let => .let,
+ .@"const" => .@"const",
+ else => return ParseError.UnexpectedToken,
+ };
+
+ return self.arena.declPtr(.{ .@"var" = .{ .loc = .{ .start = kind_tok.loc.start, .end = decls[count - 1].loc.end }, .kind = kind, .decls = decls[0..count] } }, .{ .start = kind_tok.loc.start, .end = decls[count - 1].loc.end });
+ }
+
+ pub fn parseParams(self: *@This()) ParseError![]const ast.Pat {
+ try self.expect(.lparen);
+ var params: [128]ast.Pat = undefined;
+ var count: usize = 0;
+
+ if (self.peek() != .rparen) {
+ while (true) {
+ if (self.peek() == .@"...") {
+ self.advance();
+ const pat = try ast.Pat.parse(self);
+ params[count] = ast.Pat{ .loc = pat.loc, .data = .{ .rest = .{ .loc = pat.loc, .arg = self.arena.patPtr(pat.data, pat.loc) } } };
+ count += 1;
+ break;
+ }
+ const pat = try ast.Pat.parse(self);
+ if (self.match(.@"=")) {
+ const expr = try ast.Expr.parse(self, 0);
+ params[count] = ast.Pat{ .loc = .{ .start = pat.loc.start, .end = expr.loc.end }, .data = .{ .assign = .{ .loc = .{ .start = pat.loc.start, .end = expr.loc.end }, .left = self.arena.patPtr(pat.data, pat.loc), .right = expr } } };
+ } else {
+ params[count] = pat;
+ }
+ count += 1;
+ if (!self.match(.comma)) break;
+ if (self.peek() == .@"...") {
+ self.advance();
+ const rest_pat = try ast.Pat.parse(self);
+ params[count] = ast.Pat{ .loc = rest_pat.loc, .data = .{ .rest = .{ .loc = rest_pat.loc, .arg = self.arena.patPtr(rest_pat.data, rest_pat.loc) } } };
+ count += 1;
+ break;
+ }
+ }
+ }
+
+ try self.expect(.rparen);
+ return params[0..count];
+ }
+
+ pub fn parseFunctionBody(self: *@This()) ParseError!ast.BlockStmt {
+ return ast.BlockStmt.parse(self);
+ }
+
+ pub fn parsePrimaryExpr(self: *@This()) ParseError!*const ast.Expr {
+ const start = self.tok().loc.start;
+
+ switch (self.peek()) {
+ .this => {
+ const t = self.advanceTok();
+ return self.arena.exprPtr(.{ .this = {} }, .{ .start = start, .end = t.loc.end });
+ },
+ .super => {
+ const t = self.advanceTok();
+ return self.arena.exprPtr(.{ .super = {} }, .{ .start = start, .end = t.loc.end });
+ },
+ .null => {
+ const t = self.advanceTok();
+ return self.arena.exprPtr(.{ .null = {} }, .{ .start = start, .end = t.loc.end });
+ },
+ .true => {
+ const t = self.advanceTok();
+ return self.arena.exprPtr(.{ .bool = true }, .{ .start = start, .end = t.loc.end });
+ },
+ .false => {
+ const t = self.advanceTok();
+ return self.arena.exprPtr(.{ .bool = false }, .{ .start = start, .end = t.loc.end });
+ },
+ .number => {
+ const t = self.advanceTok();
+ const val = std.fmt.parseFloat(f64, self.tokenSlice(t)) catch 0.0;
+ return self.arena.exprPtr(.{ .number = val }, .{ .start = start, .end = t.loc.end });
+ },
+ .string => {
+ const t = self.advanceTok();
+ return self.arena.exprPtr(.{ .string = self.tokenSlice(t) }, .{ .start = start, .end = t.loc.end });
+ },
+ .bigint => {
+ const t = self.advanceTok();
+ return self.arena.exprPtr(.{ .number = 0 }, .{ .start = start, .end = t.loc.end });
+ },
+ .regex => {
+ const t = self.advanceTok();
+ const slice = self.tokenSlice(t);
+ const slash_pos = std.mem.indexOfScalar(u8, slice[1..], '/') orelse 0;
+ const pattern = slice[1 .. slash_pos + 1];
+ const flags = slice[slash_pos + 2 ..];
+ return self.arena.exprPtr(
+ .{ .regex = .{ .pattern = pattern, .flags = flags } },
+ .{ .start = start, .end = t.loc.end },
+ );
+ },
+ .identifier => {
+ const t = self.advanceTok();
+ const name = self.tokenSlice(t);
+ return self.arena.exprPtr(.{ .identifier = name }, .{ .start = start, .end = t.loc.end });
+ },
+ .private_identifier => {
+ const t = self.advanceTok();
+ const name = self.tokenSlice(t);
+ return self.arena.exprPtr(.{ .private_ident = name }, .{ .start = start, .end = t.loc.end });
+ },
+ .lbrace => return ast.ObjectExpr.parse(self),
+ .lbracket => return ast.ArrayExpr.parse(self),
+ .lparen => {
+ self.advance();
+ if (self.peek() == .rparen) {
+ self.advance();
+ if (self.peek() == .@"=>") {
+ return ast.ArrowFn.parse(self, .{ .loc = .{ .start = start, .end = self.tok().loc.start }, .params = &.{} });
+ }
+ return self.arena.exprPtr(.{ .identifier = "" }, .{ .start = start, .end = self.tok().loc.start });
+ }
+ if (self.peek() == .@"...") {
+ self.advance();
+ const arg = try ast.Expr.parse(self, 0);
+ try self.expect(.rparen);
+ if (self.peek() == .@"=>") {
+ return ast.ArrowFn.parse(self, .{ .loc = .{ .start = start, .end = arg.loc.end }, .params = &.{} });
+ }
+ const loc: ast.Loc = .{ .start = start, .end = self.tok().loc.start };
+ return self.arena.exprPtr(.{ .spread = .{ .loc = loc, .arg = arg } }, loc);
+ }
+ const expr = try ast.Expr.parse(self, 0);
+ if (self.peek() == .comma) {
+ var params: [128]ast.Pat = undefined;
+ var count: usize = 0;
+ params[count] = ast.Pat{ .loc = expr.loc, .data = .{ .ident = expr.data.identifier } };
+ count += 1;
+ while (self.match(.comma)) {
+ const p = try ast.Pat.parse(self);
+ params[count] = p;
+ count += 1;
+ }
+ try self.expect(.rparen);
+ if (self.peek() == .@"=>") {
+ return ast.ArrowFn.parse(self, .{ .loc = .{ .start = start, .end = self.tok().loc.start }, .params = params[0..count] });
+ }
+ return self.arena.exprPtr(.{ .identifier = "" }, .{ .start = start, .end = self.tok().loc.start });
+ }
+ try self.expect(.rparen);
+ if (self.peek() == .@"=>") {
+ const pat = ast.Pat{ .loc = expr.loc, .data = .{ .ident = expr.data.identifier } };
+ return ast.ArrowFn.parse(self, .{ .loc = .{ .start = start, .end = self.tok().loc.start }, .params = &.{pat} });
+ }
+ return self.arena.exprPtr(.{ .parenthesized = expr }, .{ .start = start, .end = self.tok().loc.start });
+ },
+ .function => return ast.FnExpr.parse(self),
+ .class => return ast.ClassExpr.parse(self),
+ .async => {
+ self.advance();
+ if (self.peek() == .function) {
+ return ast.FnExpr.parse(self);
+ }
+ const id_tok = try self.expectTok(.identifier);
+ const name = self.tokenSlice(id_tok);
+ if (self.peek() == .@"=>") {
+ const pat = ast.Pat{ .loc = .{ .start = start, .end = id_tok.loc.end }, .data = .{ .ident = name } };
+ return ast.ArrowFn.parse(self, .{ .loc = .{ .start = start, .end = id_tok.loc.end }, .params = &.{pat} });
+ }
+ return self.arena.exprPtr(.{ .identifier = name }, .{ .start = start, .end = id_tok.loc.end });
+ },
+ .new => {
+ self.advance();
+ const callee = try ast.Expr.parse(self, BP.member);
+ var args: [256]ast.Expr = undefined;
+ var arg_count: usize = 0;
+ if (self.peek() == .lparen) {
+ self.advance();
+ if (self.peek() != .rparen) {
+ while (true) {
+ const arg = try ast.Expr.parse(self, 0);
+ args[arg_count] = arg.*;
+ arg_count += 1;
+ if (!self.match(.comma)) break;
+ }
+ }
+ try self.expect(.rparen);
+ }
+ const loc: ast.Loc = .{ .start = start, .end = if (arg_count > 0) args[arg_count - 1].loc.end else callee.loc.end };
+ return self.arena.exprPtr(.{ .new = .{ .loc = loc, .callee = callee, .args = args[0..arg_count] } }, loc);
+ },
+ .delete, .typeof, .void, .@"!", .@"~", .@"+", .@"-" => {
+ const op = self.unaryOpFromToken();
+ self.advance();
+ const arg = try ast.Expr.parse(self, BP.unary);
+ const loc: ast.Loc = .{ .start = start, .end = arg.loc.end };
+ return self.arena.exprPtr(.{ .unary = .{ .loc = loc, .op = op, .arg = arg, .prefix = true } }, loc);
+ },
+ .@"++" => {
+ self.advance();
+ const arg = try ast.Expr.parse(self, BP.update);
+ const loc: ast.Loc = .{ .start = start, .end = arg.loc.end };
+ return self.arena.exprPtr(.{ .update = .{ .loc = loc, .op = .@"++", .arg = arg, .prefix = true } }, loc);
+ },
+ .@"--" => {
+ self.advance();
+ const arg = try ast.Expr.parse(self, BP.update);
+ const loc: ast.Loc = .{ .start = start, .end = arg.loc.end };
+ return self.arena.exprPtr(.{ .update = .{ .loc = loc, .op = .@"--", .arg = arg, .prefix = true } }, loc);
+ },
+ .await => {
+ self.advance();
+ const arg = try ast.Expr.parse(self, BP.unary);
+ const loc: ast.Loc = .{ .start = start, .end = arg.loc.end };
+ return self.arena.exprPtr(.{ .await = .{ .loc = loc, .arg = arg } }, loc);
+ },
+ .yield => {
+ self.advance();
+ var delegate = false;
+ var arg: ?*const ast.Expr = null;
+ if (!self.isLineTerminatorBeforeNext()) {
+ if (self.match(.@"*")) {
+ delegate = true;
+ }
+ if (self.peek() != .rbrace and self.peek() != .rparen and self.peek() != .rbracket and self.peek() != .semicolon and self.peek() != .comma and self.peek() != .@":" and self.peek() != .eof) {
+ arg = try ast.Expr.parse(self, 0);
+ }
+ }
+ const loc: ast.Loc = .{ .start = start, .end = if (arg) |a| a.loc.end else start + 5 };
+ return self.arena.exprPtr(.{ .yield = .{ .loc = loc, .arg = arg, .delegate = delegate } }, loc);
+ },
+ .template_head, .no_sub_template => return self.arena.exprPtr(.{ .template = try ast.TemplateLit.parse(self) }, .{ .start = start, .end = self.tok().loc.start }),
+ .import => {
+ self.advance();
+ try self.expect(.lparen);
+ _ = try ast.Expr.parse(self, 0);
+ try self.expect(.rparen);
+ return self.arena.exprPtr(.{ .identifier = "import" }, .{ .start = start, .end = self.tok().loc.start });
+ },
+ .@"#" => {
+ self.advance();
+ const id_tok = try self.expectTok(.identifier);
+ return self.arena.exprPtr(.{ .private_ident = self.tokenSlice(id_tok) }, .{ .start = start, .end = id_tok.loc.end });
+ },
+ else => {
+ if (self.peek().isIdentifier()) {
+ const t = self.advanceTok();
+ const name = self.tokenSlice(t);
+ if (self.peek() == .@"=>") {
+ const pat = ast.Pat{ .loc = .{ .start = t.loc.start, .end = t.loc.end }, .data = .{ .ident = name } };
+ return ast.ArrowFn.parse(self, .{ .loc = .{ .start = t.loc.start, .end = t.loc.end }, .params = &.{pat} });
+ }
+ return self.arena.exprPtr(.{ .identifier = name }, .{ .start = t.loc.start, .end = t.loc.end });
+ }
+ return ParseError.ExpectedExpression;
+ },
+ }
+ }
+
+ pub fn parsePropKey(self: *@This()) ParseError!ast.PropKey {
+ switch (self.peek()) {
+ .identifier, .let, .static, .get, .set, .async => {
+ const t = self.advanceTok();
+ return ast.PropKey{ .ident = self.tokenSlice(t) };
+ },
+ .string => {
+ const t = self.advanceTok();
+ return ast.PropKey{ .string = self.tokenSlice(t) };
+ },
+ .number => {
+ const t = self.advanceTok();
+ const val = std.fmt.parseFloat(f64, self.tokenSlice(t)) catch 0.0;
+ return ast.PropKey{ .number = val };
+ },
+ .lbracket => {
+ self.advance();
+ const expr = try ast.Expr.parse(self, 0);
+ try self.expect(.rbracket);
+ return ast.PropKey{ .computed = expr };
+ },
+ .private_identifier => {
+ const t = self.advanceTok();
+ return ast.PropKey{ .private = self.tokenSlice(t) };
+ },
+ else => return ParseError.ExpectedPropertyName,
+ }
+ }
+
+ pub fn bp(_: *@This(), kind: TokenType) u32 {
+ return switch (kind) {
+ .comma => BP.comma,
+ .@"=", .@"+=", .@"-=", .@"*=", .@"/=", .@"%=", .@"**=", .@"<<=", .@">>=", .@">>>=", .@"&=", .@"|=", .@"^=", .@"&&=", .@"||=", .@"??=" => BP.assign,
+ .@"?" => BP.conditional,
+ .@"??" => BP.coalesce,
+ .@"||" => BP.log_or,
+ .@"&&" => BP.log_and,
+ .@"|" => BP.bit_or,
+ .@"^" => BP.bit_xor,
+ .@"&" => BP.bit_and,
+ .@"==", .@"!=", .@"===", .@"!==" => BP.equality,
+ .@"<", .@">", .@"<=", .@">=", .in, .instanceof => BP.relational,
+ .@"<<", .@">>", .@">>>" => BP.shift,
+ .@"+", .@"-" => BP.additive,
+ .@"*", .@"/", .@"%" => BP.multiplicative,
+ .@"**" => BP.exponentiation,
+ .lparen => BP.call,
+ .lbracket, .@".", .@"?." => BP.member,
+ .@"++", .@"--" => BP.update,
+ .template_head, .no_sub_template => BP.member,
+ else => 0,
+ };
+ }
+
+ pub fn unaryOpFromToken(self: *@This()) ast.UnaryOp {
+ return switch (self.tok().kind) {
+ .@"+" => .@"+",
+ .@"-" => .@"-",
+ .@"!" => .@"!",
+ .@"~" => .@"~",
+ .typeof => .typeof,
+ .void => .void,
+ .delete => .delete,
+ else => .@"+",
+ };
+ }
+
+ pub fn assignOpFromToken(self: *@This()) ast.AssignOp {
+ return switch (self.tokens[self.pos - 1].kind) {
+ .@"=" => .@"=",
+ .@"+=" => .@"+=",
+ .@"-=" => .@"-=",
+ .@"*=" => .@"*=",
+ .@"/=" => .@"/=",
+ .@"%=" => .@"%=",
+ .@"**=" => .@"**=",
+ .@"<<=" => .@"<<=",
+ .@">>=" => .@">>=",
+ .@">>>=" => .@">>>=",
+ .@"&=" => .@"&=",
+ .@"|=" => .@"|=",
+ .@"^=" => .@"^=",
+ .@"&&=" => .@"&&=",
+ .@"||=" => .@"||=",
+ .@"??=" => .@"??=",
+ else => .@"=",
+ };
+ }
+
+ pub fn semicolon(self: *@This()) void {
+ if (self.peek() == .semicolon) {
+ self.advance();
+ }
+ }
+
+ pub fn statementPointer(self: *@This()) ParseError!*const ast.Stmt {
+ const s = try ast.Stmt.parse(self);
+ return self.arena.stmtPtr(s.data, s.loc);
+ }
+};