feat(gql): add GQL to Zig code generator

Add codegen.zig that transpiles GQL AST to programmatic Zig code:
- MATCH statements → graph traversal code
- CREATE statements → addNode/addEdge calls
- DELETE statements → remove operations
- RETURN statements → result collection
- Expression generation (literals, comparisons, binary ops)

7/7 tests passing (codegen + lexer + parser)
This commit is contained in:
Markus Maiwald 2026-02-03 12:49:56 +01:00
parent 7077e37c06
commit 19577e99f8
2 changed files with 321 additions and 0 deletions

View File

@ -8,6 +8,7 @@ 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");
pub const codegen = @import("gql/codegen.zig");
/// Parse GQL query string into AST
pub fn parse(allocator: std.mem.Allocator, query: []const u8) !ast.Query {
@ -40,3 +41,6 @@ pub const ReturnStatement = ast.ReturnStatement;
pub const GraphPattern = ast.GraphPattern;
pub const NodePattern = ast.NodePattern;
pub const EdgePattern = ast.EdgePattern;
// Re-export code generator
pub const generateZig = codegen.generate;

View File

@ -0,0 +1,317 @@
//! GQL to Zig Code Generator
//!
//! Transpiles GQL AST to Zig programmatic API calls.
//! Turns declarative graph queries into imperative Zig code.
const std = @import("std");
const ast = @import("ast.zig");
const Query = ast.Query;
const Statement = ast.Statement;
const MatchStatement = ast.MatchStatement;
const CreateStatement = ast.CreateStatement;
const GraphPattern = ast.GraphPattern;
const PathPattern = ast.PathPattern;
const NodePattern = ast.NodePattern;
const EdgePattern = ast.EdgePattern;
const Expression = ast.Expression;
/// Code generation context
pub const CodeGenContext = struct {
allocator: std.mem.Allocator,
indent_level: usize = 0,
output: std.ArrayList(u8),
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.allocator = allocator,
.indent_level = 0,
.output = std.ArrayList(u8){},
};
}
pub fn deinit(self: *Self) void {
self.output.deinit(self.allocator);
}
pub fn getCode(self: *Self) ![]const u8 {
return self.output.toOwnedSlice(self.allocator);
}
fn write(self: *Self, text: []const u8) !void {
try self.output.appendSlice(self.allocator, text);
}
fn writeln(self: *Self, text: []const u8) !void {
try self.writeIndent();
try self.write(text);
try self.write("\n");
}
fn writeIndent(self: *Self) !void {
for (0..self.indent_level) |_| {
try self.write(" ");
}
}
fn indent(self: *Self) void {
self.indent_level += 1;
}
fn dedent(self: *Self) void {
if (self.indent_level > 0) {
self.indent_level -= 1;
}
}
};
/// Generate Zig code from GQL query
pub fn generate(allocator: std.mem.Allocator, query: Query) ![]const u8 {
var ctx = CodeGenContext.init(allocator);
errdefer ctx.deinit();
// Header
try ctx.writeln("// Auto-generated from GQL query");
try ctx.writeln("// Libertaria QVL Programmatic API");
try ctx.writeln("");
try ctx.writeln("const std = @import(\"std\");");
try ctx.writeln("const qvl = @import(\"qvl\");");
try ctx.writeln("");
try ctx.writeln("pub fn execute(graph: *qvl.HybridGraph) !void {");
ctx.indent();
// Generate code for each statement
for (query.statements) |stmt| {
try generateStatement(&ctx, stmt);
}
ctx.dedent();
try ctx.writeln("}");
return ctx.getCode();
}
fn generateStatement(ctx: *CodeGenContext, stmt: Statement) !void {
switch (stmt) {
.match => |m| try generateMatch(ctx, m),
.create => |c| try generateCreate(ctx, c),
.delete => |d| try generateDelete(ctx, d),
.return_stmt => |r| try generateReturn(ctx, r),
}
}
fn generateMatch(ctx: *CodeGenContext, match: MatchStatement) !void {
try ctx.writeln("");
try ctx.writeln("// MATCH statement");
// Generate path traversal for each pattern
for (match.pattern.paths) |path| {
try generatePathTraversal(ctx, path);
}
// Generate WHERE clause if present
if (match.where) |where| {
try ctx.write(" // WHERE ");
try generateExpression(ctx, where);
try ctx.write("\n");
}
}
fn generatePathTraversal(ctx: *CodeGenContext, path: PathPattern) !void {
// Path pattern: (a)-[r]->(b)-[s]->(c)
// Generate: traverse from start node following edges
if (path.elements.len == 0) return;
// Get start node
const start_node = path.elements[0].node;
const start_var = start_node.variable orelse ast.Identifier{ .name = "_" };
try ctx.write(" // Traverse from ");
try ctx.write(start_var.name);
try ctx.write("\n");
// For simple 1-hop: getOutgoing and filter
if (path.elements.len == 3) {
// (a)-[r]->(b)
const edge = path.elements[1].edge;
const end_node = path.elements[2].node;
const edge_var = edge.variable orelse ast.Identifier{ .name = "edge" };
const end_var = end_node.variable orelse ast.Identifier{ .name = "target" };
try ctx.write(" var ");
try ctx.write(edge_var.name);
try ctx.write(" = try graph.getOutgoing(");
try ctx.write(start_var.name);
try ctx.write(");\n");
// Filter by edge type if specified
if (edge.types.len > 0) {
try ctx.write(" // Filter by type: ");
for (edge.types) |t| {
try ctx.write(t.name);
try ctx.write(" ");
}
try ctx.write("\n");
}
try ctx.write(" var ");
try ctx.write(end_var.name);
try ctx.write(" = ");
try ctx.write(edge_var.name);
try ctx.write(".to;\n");
}
}
fn generateCreate(ctx: *CodeGenContext, create: CreateStatement) !void {
try ctx.writeln("");
try ctx.writeln("// CREATE statement");
for (create.pattern.paths) |path| {
// Create nodes and edges
for (path.elements) |elem| {
switch (elem) {
.node => |n| {
if (n.variable) |v| {
try ctx.write(" const ");
try ctx.write(v.name);
try ctx.write(" = try graph.addNode(.{ .id = \"");
try ctx.write(v.name);
try ctx.write("\" });\n");
}
},
.edge => |e| {
if (e.variable) |v| {
try ctx.write(" try graph.addEdge(");
try ctx.write(v.name);
try ctx.write(");\n");
}
},
}
}
}
}
fn generateDelete(ctx: *CodeGenContext, delete: ast.DeleteStatement) !void {
try ctx.writeln("");
try ctx.writeln("// DELETE statement");
for (delete.targets) |target| {
try ctx.write(" try graph.removeNode(");
try ctx.write(target.name);
try ctx.write(");\n");
}
}
fn generateReturn(ctx: *CodeGenContext, ret: ast.ReturnStatement) !void {
try ctx.writeln("");
try ctx.writeln("// RETURN statement");
try ctx.writeln(" var results = std.ArrayList(Result).init(allocator);");
try ctx.writeln(" defer results.deinit();");
for (ret.items) |item| {
try ctx.write(" try results.append(");
try generateExpression(ctx, item.expression);
try ctx.write(");\n");
}
}
fn generateExpression(ctx: *CodeGenContext, expr: Expression) !void {
switch (expr) {
.identifier => |i| try ctx.write(i.name),
.literal => |l| try generateLiteral(ctx, l),
.property_access => |p| {
try ctx.write(p.object.name);
try ctx.write(".");
try ctx.write(p.property.name);
},
.comparison => |c| {
try generateExpression(ctx, c.left.*);
try ctx.write(" ");
try ctx.write(comparisonOpToString(c.op));
try ctx.write(" ");
try generateExpression(ctx, c.right.*);
},
.binary_op => |b| {
try generateExpression(ctx, b.left.*);
try ctx.write(" ");
try ctx.write(binaryOpToString(b.op));
try ctx.write(" ");
try generateExpression(ctx, b.right.*);
},
else => try ctx.write("/* complex expression */"),
}
}
fn generateLiteral(ctx: *CodeGenContext, literal: ast.Literal) !void {
switch (literal) {
.string => |s| {
try ctx.write("\"");
try ctx.write(s);
try ctx.write("\"");
},
.integer => |i| {
var buf: [32]u8 = undefined;
const str = try std.fmt.bufPrint(&buf, "{d}", .{i});
try ctx.write(str);
},
.float => |f| {
var buf: [32]u8 = undefined;
const str = try std.fmt.bufPrint(&buf, "{d}", .{f});
try ctx.write(str);
},
.boolean => |b| try ctx.write(if (b) "true" else "false"),
.null => try ctx.write("null"),
}
}
fn comparisonOpToString(op: ast.ComparisonOperator) []const u8 {
return switch (op) {
.eq => "==",
.neq => "!=",
.lt => "<",
.lte => "<=",
.gt => ">",
.gte => ">=",
};
}
fn binaryOpToString(op: ast.BinaryOperator) []const u8 {
return switch (op) {
.add => "+",
.sub => "-",
.mul => "*",
.div => "/",
.mod => "%",
.and_op => "and",
.or_op => "or",
};
}
// ============================================================================
// TESTS
// ============================================================================
test "Codegen: simple MATCH" {
const allocator = std.testing.allocator;
const gql = "MATCH (n:Identity) RETURN n";
var lex = @import("lexer.zig").Lexer.init(gql, allocator);
const tokens = try lex.tokenize();
defer allocator.free(tokens);
var parser = @import("parser.zig").Parser.init(tokens, allocator);
var query = try parser.parse();
defer query.deinit();
const code = try generate(allocator, query);
defer allocator.free(code);
// Check that generated code contains expected patterns
const code_str = code;
try std.testing.expect(std.mem.indexOf(u8, code_str, "execute") != null);
try std.testing.expect(std.mem.indexOf(u8, code_str, "HybridGraph") != null);
}