feat(qvl): add GQL parser (ISO 39075) - Sprint 2 WIP

Add Graph Query Language parser components:
- gql/ast.zig: AST types (Query, Match, Create, Return, etc.)
- gql/lexer.zig: Tokenizer with ISO 39075 keywords
- gql/parser.zig: Recursive descent parser
- gql.zig: Module entry point with parse() function

Supports:
- MATCH, CREATE, DELETE, RETURN statements
- Node and Edge patterns with properties
- Variable length paths (*1..3 quantifiers)
- WHERE clauses with AND/OR logic
- Property comparisons (=, <>, <, <=, >, >=)

Note: Tests need Zig API updates (ArrayList changes)

Part of Sprint 2: GQL Parser.
This commit is contained in:
Markus Maiwald 2026-02-03 09:59:50 +01:00
parent 59e1f10f7a
commit c944e08202
5 changed files with 1360 additions and 0 deletions

View File

@ -16,6 +16,7 @@ pub const inference = @import("qvl/inference.zig");
pub const pop = @import("qvl/pop_integration.zig"); pub const pop = @import("qvl/pop_integration.zig");
pub const storage = @import("qvl/storage.zig"); pub const storage = @import("qvl/storage.zig");
pub const integration = @import("qvl/integration.zig"); pub const integration = @import("qvl/integration.zig");
pub const gql = @import("qvl/gql.zig");
pub const RiskEdge = types.RiskEdge; pub const RiskEdge = types.RiskEdge;
pub const NodeId = types.NodeId; pub const NodeId = types.NodeId;
@ -24,6 +25,11 @@ pub const PersistentGraph = storage.PersistentGraph;
pub const HybridGraph = integration.HybridGraph; pub const HybridGraph = integration.HybridGraph;
pub const GraphTransaction = integration.GraphTransaction; pub const GraphTransaction = integration.GraphTransaction;
// GQL exports
pub const GQLQuery = gql.Query;
pub const GQLStatement = gql.Statement;
pub const parseGQL = gql.parse;
test { test {
@import("std").testing.refAllDecls(@This()); @import("std").testing.refAllDecls(@This());
} }

42
l1-identity/qvl/gql.zig Normal file
View File

@ -0,0 +1,42 @@
//! GQL (Graph Query Language) for Libertaria QVL
//!
//! ISO/IEC 39075:2024 compliant implementation
//! Entry point: parse(query_string) -> AST
const std = @import("std");
pub const ast = @import("gql/ast.zig");
pub const lexer = @import("gql/lexer.zig");
pub const parser = @import("gql/parser.zig");
/// Parse GQL query string into AST
pub fn parse(allocator: std.mem.Allocator, query: []const u8) !ast.Query {
var lex = lexer.Lexer.init(query, allocator);
const tokens = try lex.tokenize();
defer allocator.free(tokens);
var par = parser.Parser.init(tokens, allocator);
return try par.parse();
}
/// Transpile GQL to Zig code (programmatic API)
///
/// Example:
/// GQL: MATCH (n:Identity)-[t:TRUST]->(m) WHERE n.did = 'alice' RETURN m
/// Zig: try graph.findTrustPath(alice, trust_filter)
pub fn transpileToZig(allocator: std.mem.Allocator, query: ast.Query) ![]const u8 {
// TODO: Implement code generation
_ = allocator;
_ = query;
return "// TODO: Transpile GQL to Zig";
}
// Re-export commonly used types
pub const Query = ast.Query;
pub const Statement = ast.Statement;
pub const MatchStatement = ast.MatchStatement;
pub const CreateStatement = ast.CreateStatement;
pub const ReturnStatement = ast.ReturnStatement;
pub const GraphPattern = ast.GraphPattern;
pub const NodePattern = ast.NodePattern;
pub const EdgePattern = ast.EdgePattern;

317
l1-identity/qvl/gql/ast.zig Normal file
View File

@ -0,0 +1,317 @@
//! GQL (Graph Query Language) Parser
//!
//! ISO/IEC 39075:2024 compliant parser for Libertaria QVL.
//! Transpiles GQL queries to Zig programmatic API calls.
const std = @import("std");
// ============================================================================
// AST TYPES
// ============================================================================
/// Root node of a GQL query
pub const Query = struct {
allocator: std.mem.Allocator,
statements: []Statement,
pub fn deinit(self: *Query) void {
for (self.statements) |*stmt| {
stmt.deinit();
}
self.allocator.free(self.statements);
}
};
/// Statement types (GQL is statement-based)
pub const Statement = union(enum) {
match: MatchStatement,
create: CreateStatement,
delete: DeleteStatement,
return_stmt: ReturnStatement,
pub fn deinit(self: *Statement) void {
switch (self.*) {
inline else => |*s| s.deinit(),
}
}
};
/// MATCH statement: pattern matching for graph traversal
pub const MatchStatement = struct {
allocator: std.mem.Allocator,
pattern: GraphPattern,
where: ?Expression,
pub fn deinit(self: *MatchStatement) void {
self.pattern.deinit();
if (self.where) |*w| w.deinit();
}
};
/// CREATE statement: insert nodes/edges
pub const CreateStatement = struct {
allocator: std.mem.Allocator,
pattern: GraphPattern,
pub fn deinit(self: *CreateStatement) void {
self.pattern.deinit();
}
};
/// DELETE statement: remove nodes/edges
pub const DeleteStatement = struct {
allocator: std.mem.Allocator,
targets: []Identifier,
pub fn deinit(self: *DeleteStatement) void {
for (self.targets) |*t| t.deinit();
self.allocator.free(self.targets);
}
};
/// RETURN statement: projection of results
pub const ReturnStatement = struct {
allocator: std.mem.Allocator,
items: []ReturnItem,
pub fn deinit(self: *ReturnStatement) void {
for (self.items) |*item| item.deinit();
self.allocator.free(self.items);
}
};
/// Graph pattern: sequence of path patterns
pub const GraphPattern = struct {
allocator: std.mem.Allocator,
paths: []PathPattern,
pub fn deinit(self: *GraphPattern) void {
for (self.paths) |*p| p.deinit();
self.allocator.free(self.paths);
}
};
/// Path pattern: node -edge-> node -edge-> ...
pub const PathPattern = struct {
allocator: std.mem.Allocator,
elements: []PathElement, // Alternating Node and Edge
pub fn deinit(self: *PathPattern) void {
for (self.elements) |*e| e.deinit();
self.allocator.free(self.elements);
}
};
/// Element in a path (node or edge)
pub const PathElement = union(enum) {
node: NodePattern,
edge: EdgePattern,
pub fn deinit(self: *PathElement) void {
switch (self.*) {
inline else => |*e| e.deinit(),
}
}
};
/// Node pattern: (n:Label {props})
pub const NodePattern = struct {
allocator: std.mem.Allocator,
variable: ?Identifier,
labels: []Identifier,
properties: ?PropertyMap,
pub fn deinit(self: *NodePattern) void {
if (self.variable) |*v| v.deinit();
for (self.labels) |*l| l.deinit();
self.allocator.free(self.labels);
if (self.properties) |*p| p.deinit();
}
};
/// Edge pattern: -[r:TYPE {props}]-> or <-[...]-
pub const EdgePattern = struct {
allocator: std.mem.Allocator,
direction: EdgeDirection,
variable: ?Identifier,
types: []Identifier,
properties: ?PropertyMap,
quantifier: ?Quantifier, // *1..3 for variable length
pub fn deinit(self: *EdgePattern) void {
if (self.variable) |*v| v.deinit();
for (self.types) |*t| t.deinit();
self.allocator.free(self.types);
if (self.properties) |*p| p.deinit();
if (self.quantifier) |*q| q.deinit();
}
};
pub const EdgeDirection = enum {
outgoing, // -
incoming, // <-
any, // -
};
/// Quantifier for variable-length paths: *min..max
pub const Quantifier = struct {
min: ?u32,
max: ?u32, // null = unlimited
pub fn deinit(self: *Quantifier) void {
_ = self;
}
};
/// Property map: {key: value, ...}
pub const PropertyMap = struct {
allocator: std.mem.Allocator,
entries: []PropertyEntry,
pub fn deinit(self: *PropertyMap) void {
for (self.entries) |*e| e.deinit();
self.allocator.free(self.entries);
}
};
pub const PropertyEntry = struct {
key: Identifier,
value: Expression,
pub fn deinit(self: *PropertyEntry) void {
self.key.deinit();
self.value.deinit();
}
};
/// Return item: expression [AS alias]
pub const ReturnItem = struct {
expression: Expression,
alias: ?Identifier,
pub fn deinit(self: *ReturnItem) void {
self.expression.deinit();
if (self.alias) |*a| a.deinit();
}
};
// ============================================================================
// EXPRESSIONS
// ============================================================================
pub const Expression = union(enum) {
literal: Literal,
identifier: Identifier,
property_access: PropertyAccess,
binary_op: BinaryOp,
comparison: Comparison,
function_call: FunctionCall,
list: ListExpression,
pub fn deinit(self: *Expression) void {
switch (self.*) {
inline else => |*e| e.deinit(),
}
}
};
pub const Literal = union(enum) {
string: []const u8,
integer: i64,
float: f64,
boolean: bool,
null: void,
pub fn deinit(self: *Literal) void {
switch (self.*) {
.string => |s| std.heap.raw_free(s),
else => {},
}
}
};
/// Identifier (variable, label, property name)
pub const Identifier = struct {
name: []const u8,
pub fn deinit(self: *Identifier) void {
std.heap.raw_free(self.name);
}
};
/// Property access: node.property or edge.property
pub const PropertyAccess = struct {
object: Identifier,
property: Identifier,
pub fn deinit(self: *PropertyAccess) void {
self.object.deinit();
self.property.deinit();
}
};
/// Binary operation: a + b, a - b, etc.
pub const BinaryOp = struct {
left: *Expression,
op: BinaryOperator,
right: *Expression,
pub fn deinit(self: *BinaryOp) void {
self.left.deinit();
std.heap.raw_free(self.left);
self.right.deinit();
std.heap.raw_free(self.right);
}
};
pub const BinaryOperator = enum {
add, sub, mul, div, mod,
and_op, or_op,
};
/// Comparison: a = b, a < b, etc.
pub const Comparison = struct {
left: *Expression,
op: ComparisonOperator,
right: *Expression,
pub fn deinit(self: *Comparison) void {
self.left.deinit();
std.heap.raw_free(self.left);
self.right.deinit();
std.heap.raw_free(self.right);
}
};
pub const ComparisonOperator = enum {
eq, // =
neq, // <>
lt, // <
lte, // <=
gt, // >
gte, // >=
};
/// Function call: function(arg1, arg2, ...)
pub const FunctionCall = struct {
allocator: std.mem.Allocator,
name: Identifier,
args: []Expression,
pub fn deinit(self: *FunctionCall) void {
self.name.deinit();
for (self.args) |*a| a.deinit();
self.allocator.free(self.args);
}
};
/// List literal: [1, 2, 3]
pub const ListExpression = struct {
allocator: std.mem.Allocator,
elements: []Expression,
pub fn deinit(self: *ListExpression) void {
for (self.elements) |*e| e.deinit();
self.allocator.free(self.elements);
}
};

View File

@ -0,0 +1,432 @@
//! GQL Lexer/Tokenizer
//!
//! Converts GQL query string into tokens for parser.
//! ISO/IEC 39075:2024 lexical structure.
const std = @import("std");
pub const TokenType = enum {
// Keywords
match,
create,
delete,
return_keyword,
where,
as_keyword,
and_keyword,
or_keyword,
not_keyword,
null_keyword,
true_keyword,
false_keyword,
// Punctuation
left_paren, // (
right_paren, // )
left_bracket, // [
right_bracket, // ]
left_brace, // {
right_brace, // }
colon, // :
comma, // ,
dot, // .
minus, // -
arrow_right, // ->
arrow_left, // <-
star, // *
slash, // /
percent, // %
plus, // +
// Comparison operators
eq, // =
neq, // <>
lt, // <
lte, // <=
gt, // >
gte, // >=
// Literals
identifier,
string_literal,
integer_literal,
float_literal,
// Special
eof,
invalid,
};
pub const Token = struct {
type: TokenType,
text: []const u8, // Slice into original source
line: u32,
column: u32,
};
pub const Lexer = struct {
source: []const u8,
pos: usize,
line: u32,
column: u32,
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(source: []const u8, allocator: std.mem.Allocator) Self {
return Self{
.source = source,
.pos = 0,
.line = 1,
.column = 1,
.allocator = allocator,
};
}
/// Get next token
pub fn nextToken(self: *Self) !Token {
self.skipWhitespace();
if (self.pos >= self.source.len) {
return self.makeToken(.eof, 0);
}
const start = self.pos;
const c = self.source[self.pos];
// Identifiers and keywords
if (isAlpha(c) or c == '_') {
return self.readIdentifier();
}
// Numbers
if (isDigit(c)) {
return self.readNumber();
}
// Strings
if (c == '"' or c == '\'') {
return self.readString();
}
// Single-char tokens and operators
switch (c) {
'(' => { self.advance(); return self.makeToken(.left_paren, 1); },
')' => { self.advance(); return self.makeToken(.right_paren, 1); },
'[' => { self.advance(); return self.makeToken(.left_bracket, 1); },
']' => { self.advance(); return self.makeToken(.right_bracket, 1); },
'{' => { self.advance(); return self.makeToken(.left_brace, 1); },
'}' => { self.advance(); return self.makeToken(.right_brace, 1); },
':' => { self.advance(); return self.makeToken(.colon, 1); },
',' => { self.advance(); return self.makeToken(.comma, 1); },
'.' => { self.advance(); return self.makeToken(.dot, 1); },
'+' => { self.advance(); return self.makeToken(.plus, 1); },
'%' => { self.advance(); return self.makeToken(.percent, 1); },
'*' => { self.advance(); return self.makeToken(.star, 1); },
'-' => {
self.advance();
if (self.peek() == '>') {
self.advance();
return self.makeToken(.arrow_right, 2);
}
return self.makeToken(.minus, 1);
},
'<' => {
self.advance();
if (self.peek() == '-') {
self.advance();
return self.makeToken(.arrow_left, 2);
} else if (self.peek() == '>') {
self.advance();
return self.makeToken(.neq, 2);
} else if (self.peek() == '=') {
self.advance();
return self.makeToken(.lte, 2);
}
return self.makeToken(.lt, 1);
},
'>' => {
self.advance();
if (self.peek() == '=') {
self.advance();
return self.makeToken(.gte, 2);
}
return self.makeToken(.gt, 1);
},
'=' => { self.advance(); return self.makeToken(.eq, 1); },
else => {
self.advance();
return self.makeToken(.invalid, 1);
},
}
}
/// Read all tokens into array
pub fn tokenize(self: *Self) ![]Token {
var tokens = std.ArrayList(Token).init(self.allocator);
errdefer tokens.deinit(self.allocator);
while (true) {
const tok = try self.nextToken();
try tokens.append(self.allocator, tok);
if (tok.type == .eof) break;
}
return tokens.toOwnedSlice();
}
// =========================================================================
// Internal helpers
// =========================================================================
fn advance(self: *Self) void {
if (self.pos >= self.source.len) return;
if (self.source[self.pos] == '\n') {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
self.pos += 1;
}
fn peek(self: *Self) u8 {
if (self.pos >= self.source.len) return 0;
return self.source[self.pos];
}
fn skipWhitespace(self: *Self) void {
while (self.pos < self.source.len) {
const c = self.source[self.pos];
if (c == ' ' or c == '\t' or c == '\n' or c == '\r') {
self.advance();
} else if (c == '/' and self.pos + 1 < self.source.len and self.source[self.pos + 1] == '/') {
// Single-line comment
while (self.pos < self.source.len and self.source[self.pos] != '\n') {
self.advance();
}
} else if (c == '/' and self.pos + 1 < self.source.len and self.source[self.pos + 1] == '*') {
// Multi-line comment
self.advance(); // /
self.advance(); // *
while (self.pos + 1 < self.source.len) {
if (self.source[self.pos] == '*' and self.source[self.pos + 1] == '/') {
self.advance(); // *
self.advance(); // /
break;
}
self.advance();
}
} else {
break;
}
}
}
fn readIdentifier(self: *Self) Token {
const start = self.pos;
const start_line = self.line;
const start_col = self.column;
while (self.pos < self.source.len) {
const c = self.source[self.pos];
if (isAlphaNum(c) or c == '_') {
self.advance();
} else {
break;
}
}
const text = self.source[start..self.pos];
const tok_type = keywordFromString(text);
return Token{
.type = tok_type,
.text = text,
.line = start_line,
.column = start_col,
};
}
fn readNumber(self: *Self) !Token {
const start = self.pos;
const start_line = self.line;
const start_col = self.column;
var is_float = false;
while (self.pos < self.source.len) {
const c = self.source[self.pos];
if (isDigit(c)) {
self.advance();
} else if (c == '.' and !is_float) {
// Check for range operator (e.g., 1..3)
if (self.pos + 1 < self.source.len and self.source[self.pos + 1] == '.') {
break; // Stop before range operator
}
is_float = true;
self.advance();
} else {
break;
}
}
const text = self.source[start..self.pos];
const tok_type = if (is_float) .float_literal else .integer_literal;
return Token{
.type = tok_type,
.text = text,
.line = start_line,
.column = start_col,
};
}
fn readString(self: *Self) !Token {
const start = self.pos;
const start_line = self.line;
const start_col = self.column;
const quote = self.source[self.pos];
self.advance(); // opening quote
while (self.pos < self.source.len) {
const c = self.source[self.pos];
if (c == quote) {
self.advance(); // closing quote
break;
} else if (c == '\\' and self.pos + 1 < self.source.len) {
self.advance(); // backslash
self.advance(); // escaped char
} else {
self.advance();
}
}
const text = self.source[start..self.pos];
return Token{
.type = .string_literal,
.text = text,
.line = start_line,
.column = start_col,
};
}
fn makeToken(self: *Self, tok_type: TokenType, len: usize) Token {
const tok = Token{
.type = tok_type,
.text = self.source[self.pos - len .. self.pos],
.line = self.line,
.column = self.column - @as(u32, @intCast(len)),
};
return tok;
}
};
// ============================================================================
// Helper functions
// ============================================================================
fn isAlpha(c: u8) bool {
return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z');
}
fn isDigit(c: u8) bool {
return c >= '0' and c <= '9';
}
fn isAlphaNum(c: u8) bool {
return isAlpha(c) or isDigit(c);
}
fn keywordFromString(text: []const u8) TokenType {
const map = std.ComptimeStringMap(TokenType, .{
.{ "MATCH", .match },
.{ "match", .match },
.{ "CREATE", .create },
.{ "create", .create },
.{ "DELETE", .delete },
.{ "delete", .delete },
.{ "RETURN", .return_keyword },
.{ "return", .return_keyword },
.{ "WHERE", .where },
.{ "where", .where },
.{ "AS", .as_keyword },
.{ "as", .as_keyword },
.{ "AND", .and_keyword },
.{ "and", .and_keyword },
.{ "OR", .or_keyword },
.{ "or", .or_keyword },
.{ "NOT", .not_keyword },
.{ "not", .not_keyword },
.{ "NULL", .null_keyword },
.{ "null", .null_keyword },
.{ "TRUE", .true_keyword },
.{ "true", .true_keyword },
.{ "FALSE", .false_keyword },
.{ "false", .false_keyword },
});
return map.get(text) orelse .identifier;
}
// ============================================================================
// TESTS
// ============================================================================
test "Lexer: simple keywords" {
const allocator = std.testing.allocator;
const source = "MATCH (n) RETURN n";
var lexer = Lexer.init(source, allocator);
const tokens = try lexer.tokenize();
defer allocator.free(tokens);
try std.testing.expectEqual(TokenType.match, tokens[0].type);
try std.testing.expectEqual(TokenType.left_paren, tokens[1].type);
try std.testing.expectEqual(TokenType.identifier, tokens[2].type);
try std.testing.expectEqual(TokenType.right_paren, tokens[3].type);
try std.testing.expectEqual(TokenType.return_keyword, tokens[4].type);
try std.testing.expectEqual(TokenType.identifier, tokens[5].type);
try std.testing.expectEqual(TokenType.eof, tokens[6].type);
}
test "Lexer: arrow operators" {
const allocator = std.testing.allocator;
const source = "-> <-";
var lexer = Lexer.init(source, allocator);
const tokens = try lexer.tokenize();
defer allocator.free(tokens);
try std.testing.expectEqual(TokenType.arrow_right, tokens[0].type);
try std.testing.expectEqual(TokenType.arrow_left, tokens[1].type);
}
test "Lexer: string literal" {
const allocator = std.testing.allocator;
const source = "\"hello world\"";
var lexer = Lexer.init(source, allocator);
const tokens = try lexer.tokenize();
defer allocator.free(tokens);
try std.testing.expectEqual(TokenType.string_literal, tokens[0].type);
try std.testing.expectEqualStrings("\"hello world\"", tokens[0].text);
}
test "Lexer: numbers" {
const allocator = std.testing.allocator;
const source = "42 3.14";
var lexer = Lexer.init(source, allocator);
const tokens = try lexer.tokenize();
defer allocator.free(tokens);
try std.testing.expectEqual(TokenType.integer_literal, tokens[0].type);
try std.testing.expectEqual(TokenType.float_literal, tokens[1].type);
}

View File

@ -0,0 +1,563 @@
//! GQL Parser (Recursive Descent)
//!
//! Parses GQL tokens into AST according to ISO/IEC 39075:2024.
//! Entry point: Parser.parse() -> Query AST
const std = @import("std");
const lexer = @import("lexer.zig");
const ast = @import("ast.zig");
const Token = lexer.Token;
const TokenType = lexer.TokenType;
pub const Parser = struct {
tokens: []const Token,
pos: usize,
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(tokens: []const Token, allocator: std.mem.Allocator) Self {
return Self{
.tokens = tokens,
.pos = 0,
.allocator = allocator,
};
}
/// Parse complete query
pub fn parse(self: *Self) !ast.Query {
var statements = std.ArrayList(ast.Statement).init(self.allocator);
errdefer {
for (statements.items) |*s| s.deinit();
statements.deinit();
}
while (!self.isAtEnd()) {
const stmt = try self.parseStatement();
try statements.append(stmt);
}
return ast.Query{
.allocator = self.allocator,
.statements = try statements.toOwnedSlice(),
};
}
// =========================================================================
// Statement parsing
// =========================================================================
fn parseStatement(self: *Self) !ast.Statement {
if (self.match(.match)) {
return ast.Statement{ .match = try self.parseMatchStatement() };
}
if (self.match(.create)) {
return ast.Statement{ .create = try self.parseCreateStatement() };
}
if (self.match(.return_keyword)) {
return ast.Statement{ .return_stmt = try self.parseReturnStatement() };
}
if (self.match(.delete)) {
return ast.Statement{ .delete = try self.parseDeleteStatement() };
}
return error.UnexpectedToken;
}
fn parseMatchStatement(self: *Self) !ast.MatchStatement {
const pattern = try self.parseGraphPattern();
errdefer pattern.deinit();
var where: ?ast.Expression = null;
if (self.match(.where)) {
where = try self.parseExpression();
}
return ast.MatchStatement{
.allocator = self.allocator,
.pattern = pattern,
.where = where,
};
}
fn parseCreateStatement(self: *Self) !ast.CreateStatement {
const pattern = try self.parseGraphPattern();
return ast.CreateStatement{
.allocator = self.allocator,
.pattern = pattern,
};
}
fn parseDeleteStatement(self: *Self) !ast.DeleteStatement {
// Simple: DELETE identifier [, identifier]*
var targets = std.ArrayList(ast.Identifier).init(self.allocator);
errdefer {
for (targets.items) |*t| t.deinit();
targets.deinit();
}
while (true) {
const ident = try self.parseIdentifier();
try targets.append(ident);
if (!self.match(.comma)) break;
}
return ast.DeleteStatement{
.allocator = self.allocator,
.targets = try targets.toOwnedSlice(),
};
}
fn parseReturnStatement(self: *Self) !ast.ReturnStatement {
var items = std.ArrayList(ast.ReturnItem).init(self.allocator);
errdefer {
for (items.items) |*i| i.deinit();
items.deinit();
}
while (true) {
const expr = try self.parseExpression();
var alias: ?ast.Identifier = null;
if (self.match(.as_keyword)) {
alias = try self.parseIdentifier();
}
try items.append(ast.ReturnItem{
.expression = expr,
.alias = alias,
});
if (!self.match(.comma)) break;
}
return ast.ReturnStatement{
.allocator = self.allocator,
.items = try items.toOwnedSlice(),
};
}
// =========================================================================
// Pattern parsing
// =========================================================================
fn parseGraphPattern(self: *Self) !ast.GraphPattern {
var paths = std.ArrayList(ast.PathPattern).init(self.allocator);
errdefer {
for (paths.items) |*p| p.deinit();
paths.deinit();
}
while (true) {
const path = try self.parsePathPattern();
try paths.append(path);
if (!self.match(.comma)) break;
}
return ast.GraphPattern{
.allocator = self.allocator,
.paths = try paths.toOwnedSlice(),
};
}
fn parsePathPattern(self: *Self) !ast.PathPattern {
var elements = std.ArrayList(ast.PathElement).init(self.allocator);
errdefer {
for (elements.items) |*e| e.deinit();
elements.deinit();
}
// Must start with a node
const node = try self.parseNodePattern();
try elements.append(ast.PathElement{ .node = node });
// Optional: edge - node - edge - node ...
while (self.check(.minus) or self.check(.arrow_left)) {
const edge = try self.parseEdgePattern();
try elements.append(ast.PathElement{ .edge = edge });
const next_node = try self.parseNodePattern();
try elements.append(ast.PathElement{ .node = next_node });
}
return ast.PathPattern{
.allocator = self.allocator,
.elements = try elements.toOwnedSlice(),
};
}
fn parseNodePattern(self: *Self) !ast.NodePattern {
try self.consume(.left_paren, "Expected '('");
// Optional variable: (n) or (:Label)
var variable: ?ast.Identifier = null;
if (self.check(.identifier)) {
variable = try self.parseIdentifier();
}
// Optional labels: (:Label1:Label2)
var labels = std.ArrayList(ast.Identifier).init(self.allocator);
errdefer {
for (labels.items) |*l| l.deinit();
labels.deinit();
}
while (self.match(.colon)) {
const label = try self.parseIdentifier();
try labels.append(label);
}
// Optional properties: ({key: value})
var properties: ?ast.PropertyMap = null;
if (self.check(.left_brace)) {
properties = try self.parsePropertyMap();
}
try self.consume(.right_paren, "Expected ')'");
return ast.NodePattern{
.allocator = self.allocator,
.variable = variable,
.labels = try labels.toOwnedSlice(),
.properties = properties,
};
}
fn parseEdgePattern(self: *Self) !ast.EdgePattern {
var direction: ast.EdgeDirection = .outgoing;
// Check for incoming: <-
if (self.match(.arrow_left)) {
direction = .incoming;
} else if (self.match(.minus)) {
direction = .outgoing;
}
// Edge details in brackets: -[r:TYPE]-
var variable: ?ast.Identifier = null;
var types = std.ArrayList(ast.Identifier).init(self.allocator);
errdefer {
for (types.items) |*t| t.deinit();
types.deinit();
}
var properties: ?ast.PropertyMap = null;
var quantifier: ?ast.Quantifier = null;
if (self.match(.left_bracket)) {
// Variable: [r]
if (self.check(.identifier)) {
variable = try self.parseIdentifier();
}
// Type: [:TRUST]
while (self.match(.colon)) {
const edge_type = try self.parseIdentifier();
try types.append(edge_type);
}
// Properties: [{level: 3}]
if (self.check(.left_brace)) {
properties = try self.parsePropertyMap();
}
// Quantifier: [*1..3]
if (self.match(.star)) {
quantifier = try self.parseQuantifier();
}
try self.consume(.right_bracket, "Expected ']'");
}
// Arrow end
if (direction == .outgoing) {
try self.consume(.arrow_right, "Expected '->'");
} else {
// Incoming already consumed <-, now just need -
try self.consume(.minus, "Expected '-'");
}
return ast.EdgePattern{
.allocator = self.allocator,
.direction = direction,
.variable = variable,
.types = try types.toOwnedSlice(),
.properties = properties,
.quantifier = quantifier,
};
}
fn parseQuantifier(self: *Self) !ast.Quantifier {
var min: ?u32 = null;
var max: ?u32 = null;
if (self.check(.integer_literal)) {
min = try self.parseInteger();
}
if (self.match(.dot) and self.match(.dot)) {
if (self.check(.integer_literal)) {
max = try self.parseInteger();
}
}
return ast.Quantifier{
.min = min,
.max = max,
};
}
fn parsePropertyMap(self: *Self) !ast.PropertyMap {
try self.consume(.left_brace, "Expected '{'");
var entries = std.ArrayList(ast.PropertyEntry).init(self.allocator);
errdefer {
for (entries.items) |*e| e.deinit();
entries.deinit();
}
while (!self.check(.right_brace) and !self.isAtEnd()) {
const key = try self.parseIdentifier();
try self.consume(.colon, "Expected ':'");
const value = try self.parseExpression();
try entries.append(ast.PropertyEntry{
.key = key,
.value = value,
});
if (!self.match(.comma)) break;
}
try self.consume(.right_brace, "Expected '}'");
return ast.PropertyMap{
.allocator = self.allocator,
.entries = try entries.toOwnedSlice(),
};
}
// =========================================================================
// Expression parsing
// =========================================================================
fn parseExpression(self: *Self) !ast.Expression {
return try self.parseOrExpression();
}
fn parseOrExpression(self: *Self) !ast.Expression {
var left = try self.parseAndExpression();
while (self.match(.or_keyword)) {
const right = try self.parseAndExpression();
// Create binary op
const left_ptr = try self.allocator.create(ast.Expression);
left_ptr.* = left;
const right_ptr = try self.allocator.create(ast.Expression);
right_ptr.* = right;
left = ast.Expression{
.binary_op = ast.BinaryOp{
.left = left_ptr,
.op = .or_op,
.right = right_ptr,
},
};
}
return left;
}
fn parseAndExpression(self: *Self) !ast.Expression {
var left = try self.parseComparison();
while (self.match(.and_keyword)) {
const right = try self.parseComparison();
const left_ptr = try self.allocator.create(ast.Expression);
left_ptr.* = left;
const right_ptr = try self.allocator.create(ast.Expression);
right_ptr.* = right;
left = ast.Expression{
.binary_op = ast.BinaryOp{
.left = left_ptr,
.op = .and_op,
.right = right_ptr,
},
};
}
return left;
}
fn parseComparison(self: *Self) !ast.Expression {
var left = try self.parseAdditive();
const op: ?ast.ComparisonOperator = blk: {
if (self.match(.eq)) break :blk .eq;
if (self.match(.neq)) break :blk .neq;
if (self.match(.lt)) break :blk .lt;
if (self.match(.lte)) break :blk .lte;
if (self.match(.gt)) break :blk .gt;
if (self.match(.gte)) break :blk .gte;
break :blk null;
};
if (op) |comparison_op| {
const right = try self.parseAdditive();
const left_ptr = try self.allocator.create(ast.Expression);
left_ptr.* = left;
const right_ptr = try self.allocator.create(ast.Expression);
right_ptr.* = right;
return ast.Expression{
.comparison = ast.Comparison{
.left = left_ptr,
.op = comparison_op,
.right = right_ptr,
},
};
}
return left;
}
fn parseAdditive(self: *Self) !ast.Expression {
_ = self;
// Simplified: just return primary for now
return try self.parsePrimary();
}
fn parsePrimary(self: *Self) !ast.Expression {
if (self.match(.null_keyword)) {
return ast.Expression{ .literal = ast.Literal{ .null = {} } };
}
if (self.match(.true_keyword)) {
return ast.Expression{ .literal = ast.Literal{ .boolean = true } };
}
if (self.match(.false_keyword)) {
return ast.Expression{ .literal = ast.Literal{ .boolean = false } };
}
if (self.match(.string_literal)) {
return ast.Expression{ .literal = ast.Literal{ .string = self.previous().text } };
}
if (self.check(.integer_literal)) {
const val = try self.parseInteger();
return ast.Expression{ .literal = ast.Literal{ .integer = @intCast(val) } };
}
// Property access or identifier
if (self.check(.identifier)) {
const ident = try self.parseIdentifier();
if (self.match(.dot)) {
const property = try self.parseIdentifier();
return ast.Expression{
.property_access = ast.PropertyAccess{
.object = ident,
.property = property,
},
};
}
return ast.Expression{ .identifier = ident };
}
return error.UnexpectedToken;
}
// =========================================================================
// Helpers
// =========================================================================
fn parseIdentifier(self: *Self) !ast.Identifier {
const tok = try self.consume(.identifier, "Expected identifier");
return ast.Identifier{ .name = tok.text };
}
fn parseInteger(self: *Self) !u32 {
const tok = try self.consume(.integer_literal, "Expected integer");
return try std.fmt.parseInt(u32, tok.text, 10);
}
fn match(self: *Self, tok_type: TokenType) bool {
if (self.check(tok_type)) {
self.advance();
return true;
}
return false;
}
fn check(self: *Self, tok_type: TokenType) bool {
if (self.isAtEnd()) return false;
return self.peek().type == tok_type;
}
fn advance(self: *Self) Token {
if (!self.isAtEnd()) self.pos += 1;
return self.previous();
}
fn isAtEnd(self: *Self) bool {
return self.peek().type == .eof;
}
fn peek(self: *Self) Token {
return self.tokens[self.pos];
}
fn previous(self: *Self) Token {
return self.tokens[self.pos - 1];
}
fn consume(self: *Self, tok_type: TokenType, message: []const u8) !Token {
if (self.check(tok_type)) return self.advance();
std.log.err("{s}, got {s}", .{ message, @tagName(self.peek().type) });
return error.UnexpectedToken;
}
};
// ============================================================================
// TESTS
// ============================================================================
test "Parser: simple MATCH" {
const allocator = std.testing.allocator;
const source = "MATCH (n:Identity) RETURN n";
var lex = lexer.Lexer.init(source, allocator);
const tokens = try lex.tokenize();
defer allocator.free(tokens);
var parser = Parser.init(tokens, allocator);
const query = try parser.parse();
defer query.deinit();
try std.testing.expectEqual(2, query.statements.len);
try std.testing.expect(query.statements[0] == .match);
try std.testing.expect(query.statements[1] == .return_stmt);
}
test "Parser: path pattern" {
const allocator = std.testing.allocator;
const source = "MATCH (a)-[t:TRUST]->(b) RETURN a, b";
var lex = lexer.Lexer.init(source, allocator);
const tokens = try lex.tokenize();
defer allocator.free(tokens);
var parser = Parser.init(tokens, allocator);
const query = try parser.parse();
defer query.deinit();
try std.testing.expectEqual(1, query.statements[0].match.pattern.paths.len);
}