From 2b7286db4e9926c2dbc49bb082a3ef3cf9073f07 Mon Sep 17 00:00:00 2001 From: Ankit Patial Date: Tue, 10 Feb 2026 12:35:09 +0530 Subject: [PATCH] fix: handle optional primitive types in @TypeOf annotations - Treat []const u8 as string primitive, not array (fixes ?[]const u8) - Optional primitives generate ?T = null (e.g., ?[]const u8 = null) - Added test for optional string type annotation --- build.zig.zon | 2 +- src/tpl_compiler/zig_codegen.zig | 64 +++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b478699..81c7342 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .pugz, - .version = "0.3.14", + .version = "0.3.15", .fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .dependencies = .{}, diff --git a/src/tpl_compiler/zig_codegen.zig b/src/tpl_compiler/zig_codegen.zig index 8e427f1..39d0293 100644 --- a/src/tpl_compiler/zig_codegen.zig +++ b/src/tpl_compiler/zig_codegen.zig @@ -814,17 +814,23 @@ pub const Codegen = struct { try self.write(" } = .{}"); } else if (type_info.primitive_type) |prim| { // Primitive type with appropriate default - try self.write(prim); - if (std.mem.eql(u8, prim, "[]const u8")) { - try self.write(" = \"\""); - } else if (std.mem.eql(u8, prim, "bool")) { - try self.write(" = false"); - } else if (std.mem.startsWith(u8, prim, "i") or std.mem.startsWith(u8, prim, "u")) { - try self.write(" = 0"); - } else if (std.mem.startsWith(u8, prim, "f")) { - try self.write(" = 0.0"); + if (type_info.is_optional) { + try self.write("?"); + try self.write(prim); + try self.write(" = null"); } else { - // Unknown type, no default + try self.write(prim); + if (std.mem.eql(u8, prim, "[]const u8")) { + try self.write(" = \"\""); + } else if (std.mem.eql(u8, prim, "bool")) { + try self.write(" = false"); + } else if (std.mem.startsWith(u8, prim, "i") or std.mem.startsWith(u8, prim, "u")) { + try self.write(" = 0"); + } else if (std.mem.startsWith(u8, prim, "f")) { + try self.write(" = 0.0"); + } else { + // Unknown type, no default + } } } else { // Fallback: string @@ -1103,6 +1109,12 @@ pub fn parseTypeHintSpec(allocator: Allocator, spec: []const u8) !TypeInfo { remaining = remaining[1..]; } + // Treat []const u8 as a string primitive, not an array + if (std.mem.eql(u8, remaining, "[]const u8")) { + info.primitive_type = remaining; + return info; + } + // Check for array prefix [] if (std.mem.startsWith(u8, remaining, "[]")) { info.is_array = true; @@ -1336,3 +1348,35 @@ test "zig_codegen - @TypeOf with non-optional @import type" { try std.testing.expect(std.mem.indexOf(u8, zig_code, "data.config.title") != null); try std.testing.expect(std.mem.indexOf(u8, zig_code, "config_title") == null); } + +test "zig_codegen - @TypeOf with optional string type" { + const allocator = std.testing.allocator; + + const source = + \\//- @TypeOf(_logo_url): ?[]const u8 + \\if _logo_url + \\ img(src=_logo_url) + ; + + var parse_result = try template.parseWithSource(allocator, source); + defer parse_result.deinit(allocator); + + const fields = try extractFieldNames(allocator, parse_result.ast); + defer { + for (fields) |field| allocator.free(field); + allocator.free(fields); + } + + try std.testing.expectEqual(@as(usize, 1), fields.len); + try std.testing.expectEqualStrings("_logo_url", fields[0]); + + var cg = Codegen.init(allocator); + defer cg.deinit(); + + const zig_code = try cg.generate(parse_result.ast, "render", fields, null); + defer allocator.free(zig_code); + + // Should be ?[]const u8 = null, not []const const u8 = &.{} + try std.testing.expect(std.mem.indexOf(u8, zig_code, "?[]const u8 = null") != null); + try std.testing.expect(std.mem.indexOf(u8, zig_code, "[]const const") == null); +}