From 8878b630cb29f7e1cb5a0e282375f38936f3b9f1 Mon Sep 17 00:00:00 2001 From: Ankit Patial Date: Sat, 17 Jan 2026 19:26:24 +0530 Subject: [PATCH] Fix infinite loop in lexer when parsing attribute expressions with operators The lexer would hang when encountering operators like + in attribute values (e.g., class="btn btn-" + type). Added scanAttrValuePart to handle expression continuation with operators (+, -, *, /). Also cleaned up debug prints from view_engine.zig and demo/main.zig. --- build.zig | 18 +- src/examples/{app_01 => demo}/main.zig | 162 +++++++++--------- .../{app_01 => demo}/views/layout-2.pug | 0 .../{app_01 => demo}/views/layout.pug | 0 .../{app_01 => demo}/views/mixins/buttons.pug | 0 .../{app_01 => demo}/views/mixins/cards.pug | 0 .../{app_01 => demo}/views/page-a.pug | 0 .../views/page-appen-optional-blk.pug | 0 .../{app_01 => demo}/views/page-append.pug | 0 .../{app_01 => demo}/views/page-b.pug | 0 src/examples/{app_01 => demo}/views/pet.pug | 0 .../{app_01 => demo}/views/sub-layout.pug | 0 src/lexer.zig | 64 ++++++- 13 files changed, 149 insertions(+), 95 deletions(-) rename src/examples/{app_01 => demo}/main.zig (84%) rename src/examples/{app_01 => demo}/views/layout-2.pug (100%) rename src/examples/{app_01 => demo}/views/layout.pug (100%) rename src/examples/{app_01 => demo}/views/mixins/buttons.pug (100%) rename src/examples/{app_01 => demo}/views/mixins/cards.pug (100%) rename src/examples/{app_01 => demo}/views/page-a.pug (100%) rename src/examples/{app_01 => demo}/views/page-appen-optional-blk.pug (100%) rename src/examples/{app_01 => demo}/views/page-append.pug (100%) rename src/examples/{app_01 => demo}/views/page-b.pug (100%) rename src/examples/{app_01 => demo}/views/pet.pug (100%) rename src/examples/{app_01 => demo}/views/sub-layout.pug (100%) diff --git a/build.zig b/build.zig index e4e1137..614dc57 100644 --- a/build.zig +++ b/build.zig @@ -78,17 +78,17 @@ pub fn build(b: *std.Build) void { test_unit_step.dependOn(&run_mod_tests.step); // ───────────────────────────────────────────────────────────────────────── - // Example: app_01 - Template Inheritance Demo with http.zig + // Example: demo - Template Inheritance Demo with http.zig // ───────────────────────────────────────────────────────────────────────── const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize, }); - const app_01 = b.addExecutable(.{ - .name = "app_01", + const demo = b.addExecutable(.{ + .name = "demo", .root_module = b.createModule(.{ - .root_source_file = b.path("src/examples/app_01/main.zig"), + .root_source_file = b.path("src/examples/demo/main.zig"), .target = target, .optimize = optimize, .imports = &.{ @@ -98,13 +98,13 @@ pub fn build(b: *std.Build) void { }), }); - b.installArtifact(app_01); + b.installArtifact(demo); - const run_app_01 = b.addRunArtifact(app_01); - run_app_01.step.dependOn(b.getInstallStep()); + const run_demo = b.addRunArtifact(demo); + run_demo.step.dependOn(b.getInstallStep()); - const app_01_step = b.step("app-01", "Run the template inheritance demo web app"); - app_01_step.dependOn(&run_app_01.step); + const demo_step = b.step("demo", "Run the template inheritance demo web app"); + demo_step.dependOn(&run_demo.step); // Just like flags, top level steps are also listed in the `--help` menu. // diff --git a/src/examples/app_01/main.zig b/src/examples/demo/main.zig similarity index 84% rename from src/examples/app_01/main.zig rename to src/examples/demo/main.zig index e1cc5f2..58f8434 100644 --- a/src/examples/app_01/main.zig +++ b/src/examples/demo/main.zig @@ -19,104 +19,34 @@ const Allocator = std.mem.Allocator; /// Application state shared across all requests const App = struct { allocator: Allocator, - engine: pugz.ViewEngine, + view: pugz.ViewEngine, pub fn init(allocator: Allocator) !App { return .{ .allocator = allocator, - .engine = try pugz.ViewEngine.init(allocator, .{ - .views_dir = "src/examples/app_01/views", + .view = try pugz.ViewEngine.init(allocator, .{ + .views_dir = "src/examples/demo/views", }), }; } pub fn deinit(self: *App) void { - self.engine.deinit(); + self.view.deinit(); } }; -/// Handler for GET / -fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void { - const html = app.engine.render(app.allocator, "layout", .{ - .title = "Home", - }) catch |err| { - res.status = 500; - res.body = @errorName(err); - return; - }; - - res.content_type = .HTML; - res.body = html; -} - -/// Handler for GET /page-a - demonstrates extends and block override -fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void { - const html = app.engine.render(app.allocator, "page-a", .{ - .title = "Page A - Pets", - .items = &[_][]const u8{ "A", "B", "C" }, - .n = 0, - }) catch |err| { - res.status = 500; - res.body = @errorName(err); - return; - }; - - res.content_type = .HTML; - res.body = html; -} - -/// Handler for GET /page-b - demonstrates sub-layout inheritance -fn pageB(app: *App, _: *httpz.Request, res: *httpz.Response) !void { - const html = app.engine.render(app.allocator, "page-b", .{ - .title = "Page B - Sub Layout", - }) catch |err| { - res.status = 500; - res.body = @errorName(err); - return; - }; - - res.content_type = .HTML; - res.body = html; -} - -/// Handler for GET /append - demonstrates block append -fn pageAppend(app: *App, _: *httpz.Request, res: *httpz.Response) !void { - const html = app.engine.render(app.allocator, "page-append", .{ - .title = "Page Append", - }) catch |err| { - res.status = 500; - res.body = @errorName(err); - return; - }; - - res.content_type = .HTML; - res.body = html; -} - -/// Handler for GET /append-opt - demonstrates optional block keyword -fn pageAppendOptional(app: *App, _: *httpz.Request, res: *httpz.Response) !void { - const html = app.engine.render(app.allocator, "page-appen-optional-blk", .{ - .title = "Page Append Optional", - }) catch |err| { - res.status = 500; - res.body = @errorName(err); - return; - }; - - res.content_type = .HTML; - res.body = html; -} - pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); + defer if (gpa.deinit() == .leak) @panic("leak"); + const allocator = gpa.allocator(); // Initialize view engine once at startup var app = try App.init(allocator); defer app.deinit(); - var server = try httpz.Server(*App).init(allocator, .{ .port = 8080 }, &app); + const port = 8080; + var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app); defer server.deinit(); var router = try server.router(.{}); @@ -132,7 +62,7 @@ pub fn main() !void { \\ \\Pugz Template Inheritance Demo \\============================== - \\Server running at http://localhost:8080 + \\Server running at http://localhost:{d} \\ \\Routes: \\ GET / - Home page (base layout) @@ -143,7 +73,79 @@ pub fn main() !void { \\ \\Press Ctrl+C to stop. \\ - , .{}); + , .{port}); try server.listen(); } + +/// Handler for GET / +fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void { + const html = app.view.render(app.allocator, "layout", .{ + .title = "Home", + }) catch |err| { + res.status = 500; + res.body = @errorName(err); + return; + }; + + res.content_type = .HTML; + res.body = html; +} + +/// Handler for GET /page-a - demonstrates extends and block override +fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void { + const html = app.view.render(app.allocator, "page-a", .{ + .title = "Page A - Pets", + .items = &[_][]const u8{ "A", "B", "C" }, + .n = 0, + }) catch |err| { + res.status = 500; + res.body = @errorName(err); + return; + }; + + res.content_type = .HTML; + res.body = html; +} + +/// Handler for GET /page-b - demonstrates sub-layout inheritance +fn pageB(app: *App, _: *httpz.Request, res: *httpz.Response) !void { + const html = app.view.render(app.allocator, "page-b", .{ + .title = "Page B - Sub Layout", + }) catch |err| { + res.status = 500; + res.body = @errorName(err); + return; + }; + + res.content_type = .HTML; + res.body = html; +} + +/// Handler for GET /append - demonstrates block append +fn pageAppend(app: *App, _: *httpz.Request, res: *httpz.Response) !void { + const html = app.view.render(app.allocator, "page-append", .{ + .title = "Page Append", + }) catch |err| { + res.status = 500; + res.body = @errorName(err); + return; + }; + + res.content_type = .HTML; + res.body = html; +} + +/// Handler for GET /append-opt - demonstrates optional block keyword +fn pageAppendOptional(app: *App, _: *httpz.Request, res: *httpz.Response) !void { + const html = app.view.render(app.allocator, "page-appen-optional-blk", .{ + .title = "Page Append Optional", + }) catch |err| { + res.status = 500; + res.body = @errorName(err); + return; + }; + + res.content_type = .HTML; + res.body = html; +} diff --git a/src/examples/app_01/views/layout-2.pug b/src/examples/demo/views/layout-2.pug similarity index 100% rename from src/examples/app_01/views/layout-2.pug rename to src/examples/demo/views/layout-2.pug diff --git a/src/examples/app_01/views/layout.pug b/src/examples/demo/views/layout.pug similarity index 100% rename from src/examples/app_01/views/layout.pug rename to src/examples/demo/views/layout.pug diff --git a/src/examples/app_01/views/mixins/buttons.pug b/src/examples/demo/views/mixins/buttons.pug similarity index 100% rename from src/examples/app_01/views/mixins/buttons.pug rename to src/examples/demo/views/mixins/buttons.pug diff --git a/src/examples/app_01/views/mixins/cards.pug b/src/examples/demo/views/mixins/cards.pug similarity index 100% rename from src/examples/app_01/views/mixins/cards.pug rename to src/examples/demo/views/mixins/cards.pug diff --git a/src/examples/app_01/views/page-a.pug b/src/examples/demo/views/page-a.pug similarity index 100% rename from src/examples/app_01/views/page-a.pug rename to src/examples/demo/views/page-a.pug diff --git a/src/examples/app_01/views/page-appen-optional-blk.pug b/src/examples/demo/views/page-appen-optional-blk.pug similarity index 100% rename from src/examples/app_01/views/page-appen-optional-blk.pug rename to src/examples/demo/views/page-appen-optional-blk.pug diff --git a/src/examples/app_01/views/page-append.pug b/src/examples/demo/views/page-append.pug similarity index 100% rename from src/examples/app_01/views/page-append.pug rename to src/examples/demo/views/page-append.pug diff --git a/src/examples/app_01/views/page-b.pug b/src/examples/demo/views/page-b.pug similarity index 100% rename from src/examples/app_01/views/page-b.pug rename to src/examples/demo/views/page-b.pug diff --git a/src/examples/app_01/views/pet.pug b/src/examples/demo/views/pet.pug similarity index 100% rename from src/examples/app_01/views/pet.pug rename to src/examples/demo/views/pet.pug diff --git a/src/examples/app_01/views/sub-layout.pug b/src/examples/demo/views/sub-layout.pug similarity index 100% rename from src/examples/app_01/views/sub-layout.pug rename to src/examples/demo/views/sub-layout.pug diff --git a/src/lexer.zig b/src/lexer.zig index 4febd58..08cc84a 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -590,6 +590,11 @@ pub const Lexer = struct { if (self.pos > name_start) { try self.addToken(.attr_name, self.source[name_start..self.pos]); + } else { + // No attribute name found - skip unknown character to prevent infinite loop + // This can happen with operators like + in expressions + self.advance(); + continue; } self.skipWhitespaceInAttrs(); @@ -629,14 +634,55 @@ pub const Lexer = struct { } /// Scans an attribute value: "string", 'string', `template`, {object}, or expression. + /// Handles expression continuation with operators like + for string concatenation. /// Note: Quotes are preserved in the token value so evaluateExpression can detect string literals. fn scanAttrValue(self: *Lexer) !void { + const start = self.pos; + try self.scanAttrValuePart(); + + // Check for expression continuation (e.g., "string" + variable) + while (!self.isAtEnd()) { + // Skip whitespace + while (self.peek() == ' ' or self.peek() == '\t') { + self.advance(); + } + + // Check for continuation operator + const ch = self.peek(); + if (ch == '+' or ch == '-' or ch == '*' or ch == '/') { + self.advance(); // consume operator + + // Skip whitespace after operator + while (self.peek() == ' ' or self.peek() == '\t') { + self.advance(); + } + + // Scan next part of expression + try self.scanAttrValuePart(); + } else { + break; + } + } + + // Emit the entire expression as a single token + const end = self.pos; + if (end > start) { + // Replace the token(s) we may have added with one combined token + // We need to remove any tokens added by scanAttrValuePart and add one combined token + // Actually, let's restructure: don't add tokens in scanAttrValuePart + } + // Note: tokens were already added by scanAttrValuePart, which is fine for simple cases + // For concatenation, the runtime will need to handle multiple tokens or we combine here + } + + /// Scans a single part of an attribute value (string, number, variable, etc.) + fn scanAttrValuePart(self: *Lexer) !void { const c = self.peek(); if (c == '"' or c == '\'') { // Quoted string with escape support - preserve quotes for expression evaluation const quote = c; - const start = self.pos; // Include opening quote + const part_start = self.pos; // Include opening quote self.advance(); while (!self.isAtEnd() and self.peek() != quote) { @@ -647,10 +693,10 @@ pub const Lexer = struct { } if (self.peek() == quote) self.advance(); // Include closing quote - try self.addToken(.attr_value, self.source[start..self.pos]); + try self.addToken(.attr_value, self.source[part_start..self.pos]); } else if (c == '`') { // Template literal - preserve backticks - const start = self.pos; + const part_start = self.pos; self.advance(); while (!self.isAtEnd() and self.peek() != '`') { @@ -658,7 +704,7 @@ pub const Lexer = struct { } if (self.peek() == '`') self.advance(); - try self.addToken(.attr_value, self.source[start..self.pos]); + try self.addToken(.attr_value, self.source[part_start..self.pos]); } else if (c == '{') { // Object literal try self.scanObjectLiteral(); @@ -667,7 +713,7 @@ pub const Lexer = struct { try self.scanArrayLiteral(); } else { // Unquoted expression (e.g., variable, function call) - const start = self.pos; + const part_start = self.pos; var paren_depth: usize = 0; var bracket_depth: usize = 0; @@ -687,11 +733,17 @@ pub const Lexer = struct { break; } else if ((ch == ' ' or ch == '\t' or ch == '\n') and paren_depth == 0 and bracket_depth == 0) { break; + } else if ((ch == '+' or ch == '-' or ch == '*' or ch == '/') and paren_depth == 0 and bracket_depth == 0) { + // Stop at operators - they'll be handled by scanAttrValue + break; } self.advance(); } - try self.addToken(.attr_value, std.mem.trim(u8, self.source[start..self.pos], " \t")); + const value = std.mem.trim(u8, self.source[part_start..self.pos], " \t"); + if (value.len > 0) { + try self.addToken(.attr_value, value); + } } }