feat: add Parsed and deinit

This commit is contained in:
dualfroz committed 2026-09-02 22:07:28 +02:00
1 parent 3480459c98
commit d4a836ad27
1 file changed
+157
+157
View File
@@ -0,0 +1,157 @@
const std = @import("std");
const rowmod = @import("row.zig");
const idx = @import("index.zig");
const querymod = @import("query.zig");
const dbmod = @import("db.zig");
const Allocator = std.mem.Allocator;
const Predicate = querymod.Predicate;
const Condition = querymod.Condition;
/// Errors a caller can act on when a `table select` expression is rejected.
/// Parsing never panics on malformed input - every failure mode a user can
/// trigger from the CLI maps to one of these.
pub const ParseError = error{
UnexpectedToken,
UnexpectedEnd,
InvalidIdentifier,
UnknownOperator,
InvalidValue,
} || Allocator.Error;
/// Owns every allocation a parsed Predicate tree depends on (field-name and
/// text-value copies, and_/or_ slices, not_ boxes) in one arena, so freeing
/// is a single `deinit()` regardless of how deep the tree is.
pub const Parsed = struct {
arena: std.heap.ArenaAllocator,
predicate: Predicate,
pub fn deinit(self: *Parsed) void {
self.arena.deinit();
}
};
const TokenKind = enum { lparen, rparen, word, eof };
const Token = struct {
kind: TokenKind,
text: []const u8,
};
const Lexer = struct {
src: []const u8,
pos: usize = 0,
fn next(self: *Lexer) Token {
while (self.pos < self.src.len and std.ascii.isWhitespace(self.src[self.pos])) : (self.pos += 1) {}
if (self.pos >= self.src.len) return .{ .kind = .eof, .text = "" };
const c = self.src[self.pos];
if (c == '(') {
self.pos += 1;
return .{ .kind = .lparen, .text = "(" };
}
if (c == ')') {
self.pos += 1;
return .{ .kind = .rparen, .text = ")" };
}
const start = self.pos;
while (self.pos < self.src.len) {
const ch = self.src[self.pos];
if (std.ascii.isWhitespace(ch) or ch == '(' or ch == ')') break;
self.pos += 1;
}
return .{ .kind = .word, .text = self.src[start..self.pos] };
}
};
fn isKeyword(tok: Token, kw: []const u8) bool {
return tok.kind == .word and std.mem.eql(u8, tok.text, kw);
}
/// Recursive-descent parser for the `table select` predicate language:
///
/// expr := and ("or" and)*
/// and := unary ("and" unary)*
/// unary := "not" unary | primary
/// primary := "(" expr ")" | field op value | field ("is_null" | "is_not_null")
/// op := eq | ne | lt | lte | gt | gte | like
///
/// Precedence (loosest to tightest) is or < and < not, matching every other
/// C-family boolean expression language, so `a eq 1 or b eq 2 and not c eq 3`
/// parses as `a eq 1 or (b eq 2 and (not (c eq 3)))`.
const Parser = struct {
lexer: Lexer,
allocator: Allocator,
cur: Token,
fn advance(self: *Parser) void {
self.cur = self.lexer.next();
}
fn parseOr(self: *Parser) ParseError!Predicate {
var list = std.ArrayList(Predicate).init(self.allocator);
try list.append(try self.parseAnd());
while (isKeyword(self.cur, "or")) {
self.advance();
try list.append(try self.parseAnd());
}
if (list.items.len == 1) return list.items[0];
return .{ .or_ = try list.toOwnedSlice() };
}
fn parseAnd(self: *Parser) ParseError!Predicate {
var list = std.ArrayList(Predicate).init(self.allocator);
try list.append(try self.parseUnary());
while (isKeyword(self.cur, "and")) {
self.advance();
try list.append(try self.parseUnary());
}
if (list.items.len == 1) return list.items[0];
return .{ .and_ = try list.toOwnedSlice() };
}
fn parseUnary(self: *Parser) ParseError!Predicate {
if (isKeyword(self.cur, "not")) {
self.advance();
const inner = try self.parseUnary();
const boxed = try self.allocator.create(Predicate);
boxed.* = inner;
return .{ .not_ = boxed };
}
return self.parsePrimary();
}
fn parsePrimary(self: *Parser) ParseError!Predicate {
if (self.cur.kind == .lparen) {
self.advance();
const inner = try self.parseOr();
if (self.cur.kind != .rparen) return error.UnexpectedToken;
self.advance();
return inner;
}
return self.parseCondition();
}
fn parseCondition(self: *Parser) ParseError!Predicate {
if (self.cur.kind != .word) return if (self.cur.kind == .eof) error.UnexpectedEnd else error.UnexpectedToken;
const field = self.cur.text;
if (!idx.validIdentifier(field)) return error.InvalidIdentifier;
const fieldCopy = try self.allocator.dupe(u8, field);
self.advance();
if (self.cur.kind != .word) return error.UnexpectedEnd;
const op = self.cur.text;
self.advance();
if (std.mem.eql(u8, op, "in")) return self.parseIn(fieldCopy);
if (std.mem.eql(u8, op, "between")) return self.parseBetween(fieldCopy);
if (std.mem.eql(u8, op, "is_null")) return .{ .is_null = fieldCopy };
if (std.mem.eql(u8, op, "is_not_null")) return .{ .is_not_null = fieldCopy };
if (self.cur.kind != .word) return error.UnexpectedEnd;
const rawValue = rowmod.Value.parseCli(self.cur.text) catch return error.InvalidValue;
const value = try rawValue.dupe(self.allocator);
self.advance();