From d4a836ad27b3cb0a373c5d8904670bf11d1666f7 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 22:07:28 +0200 Subject: [PATCH] feat: add Parsed and deinit --- src/expr.zig | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/expr.zig diff --git a/src/expr.zig b/src/expr.zig new file mode 100644 index 0000000..8593d67 --- /dev/null +++ b/src/expr.zig @@ -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(); +