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:
parent
59e1f10f7a
commit
c944e08202
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue