diff --git a/l1-identity/qvl.zig b/l1-identity/qvl.zig index 756e5ce..600f4c5 100644 --- a/l1-identity/qvl.zig +++ b/l1-identity/qvl.zig @@ -16,6 +16,7 @@ pub const inference = @import("qvl/inference.zig"); pub const pop = @import("qvl/pop_integration.zig"); pub const storage = @import("qvl/storage.zig"); pub const integration = @import("qvl/integration.zig"); +pub const gql = @import("qvl/gql.zig"); pub const RiskEdge = types.RiskEdge; pub const NodeId = types.NodeId; @@ -24,6 +25,11 @@ pub const PersistentGraph = storage.PersistentGraph; pub const HybridGraph = integration.HybridGraph; pub const GraphTransaction = integration.GraphTransaction; +// GQL exports +pub const GQLQuery = gql.Query; +pub const GQLStatement = gql.Statement; +pub const parseGQL = gql.parse; + test { @import("std").testing.refAllDecls(@This()); } diff --git a/l1-identity/qvl/gql.zig b/l1-identity/qvl/gql.zig new file mode 100644 index 0000000..beabd25 --- /dev/null +++ b/l1-identity/qvl/gql.zig @@ -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; diff --git a/l1-identity/qvl/gql/ast.zig b/l1-identity/qvl/gql/ast.zig new file mode 100644 index 0000000..5364e15 --- /dev/null +++ b/l1-identity/qvl/gql/ast.zig @@ -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); + } +}; diff --git a/l1-identity/qvl/gql/lexer.zig b/l1-identity/qvl/gql/lexer.zig new file mode 100644 index 0000000..315a3d1 --- /dev/null +++ b/l1-identity/qvl/gql/lexer.zig @@ -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); +} diff --git a/l1-identity/qvl/gql/parser.zig b/l1-identity/qvl/gql/parser.zig new file mode 100644 index 0000000..a3d81a8 --- /dev/null +++ b/l1-identity/qvl/gql/parser.zig @@ -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); +}