chore: bump version to 0.2.1

This commit is contained in:
2026-01-23 22:08:53 +05:30
parent 0d4aa9ff90
commit af949f3a7f
7 changed files with 565 additions and 78 deletions

View File

@@ -36,7 +36,9 @@
//! ```
const std = @import("std");
const Lexer = @import("lexer.zig").Lexer;
const lexer_mod = @import("lexer.zig");
const Lexer = lexer_mod.Lexer;
const Diagnostic = lexer_mod.Diagnostic;
const Parser = @import("parser.zig").Parser;
const ast = @import("ast.zig");
@@ -316,6 +318,17 @@ fn generateSingleFile(
try file.writeAll(out.items);
}
/// Logs a diagnostic error with file location in compiler-style format.
fn logDiagnostic(file_path: []const u8, diag: Diagnostic) void {
std.log.err("{s}:{d}:{d}: {s}", .{ file_path, diag.line, diag.column, diag.message });
if (diag.source_line) |src_line| {
std.log.err(" | {s}", .{src_line});
}
if (diag.suggestion) |hint| {
std.log.err(" = hint: {s}", .{hint});
}
}
/// Compiles a single .pug template into a Zig function.
/// Handles three cases:
/// - Empty templates: return ""
@@ -332,13 +345,21 @@ fn compileTemplate(
var lexer = Lexer.init(allocator, source);
defer lexer.deinit();
const tokens = lexer.tokenize() catch |err| {
std.log.err("Tokenize error in '{s}': {}", .{ name, err });
if (lexer.getDiagnostic()) |diag| {
logDiagnostic(name, diag);
} else {
std.log.err("Tokenize error in '{s}': {}", .{ name, err });
}
return err;
};
var parser = Parser.init(allocator, tokens);
var parser = Parser.initWithSource(allocator, tokens, source);
const doc = parser.parse() catch |err| {
std.log.err("Parse error in '{s}': {}", .{ name, err });
if (parser.getDiagnostic()) |diag| {
logDiagnostic(name, diag);
} else {
std.log.err("Parse error in '{s}': {}", .{ name, err });
}
return err;
};
@@ -487,6 +508,86 @@ fn nodeHasDynamic(node: ast.Node) bool {
};
}
/// Checks if a mixin body references `attributes` (for &attributes pass-through).
/// Used to avoid emitting unused mixin_attrs struct in generated code.
fn mixinUsesAttributes(nodes: []const ast.Node) bool {
for (nodes) |node| {
switch (node) {
.element => |e| {
// Check spread_attributes field
if (e.spread_attributes != null) return true;
// Check attribute values for 'attributes' reference
for (e.attributes) |attr| {
if (attr.value) |val| {
if (exprReferencesAttributes(val)) return true;
}
}
// Check inline text for interpolated attributes reference
if (e.inline_text) |segs| {
if (textSegmentsReferenceAttributes(segs)) return true;
}
// Check buffered code
if (e.buffered_code) |bc| {
if (exprReferencesAttributes(bc.expression)) return true;
}
// Recurse into children
if (mixinUsesAttributes(e.children)) return true;
},
.text => |t| {
if (textSegmentsReferenceAttributes(t.segments)) return true;
},
.conditional => |c| {
for (c.branches) |br| {
if (mixinUsesAttributes(br.children)) return true;
}
},
.each => |e| {
if (mixinUsesAttributes(e.children)) return true;
if (mixinUsesAttributes(e.else_children)) return true;
},
.case => |c| {
for (c.whens) |when| {
if (mixinUsesAttributes(when.children)) return true;
}
if (mixinUsesAttributes(c.default_children)) return true;
},
.block => |b| {
if (mixinUsesAttributes(b.children)) return true;
},
else => {},
}
}
return false;
}
/// Checks if an expression string references 'attributes' (e.g., "attributes.class").
fn exprReferencesAttributes(expr: []const u8) bool {
// Check for 'attributes' as standalone or prefix (attributes.class, attributes.id, etc.)
if (std.mem.startsWith(u8, expr, "attributes")) {
// Must be exactly "attributes" or "attributes." followed by more
if (expr.len == 10) return true; // exactly "attributes"
if (expr.len > 10 and expr[10] == '.') return true; // "attributes.something"
}
return false;
}
/// Checks if text segments contain interpolations referencing 'attributes'.
fn textSegmentsReferenceAttributes(segs: []const ast.TextSegment) bool {
for (segs) |seg| {
switch (seg) {
.interp_escaped, .interp_unescaped => |expr| {
if (exprReferencesAttributes(expr)) return true;
},
else => {},
}
}
return false;
}
/// Zig reserved keywords - field names matching these must be escaped with @"..."
/// when used in generated code (e.g., @"type" instead of type)
const zig_keywords = std.StaticStringMap(void).initComptime(.{
@@ -575,7 +676,6 @@ const Compiler = struct {
uses_data: bool, // True if template accesses the data parameter 'd'
mixin_depth: usize, // Nesting level for generating unique mixin variable names
current_attrs_var: ?[]const u8, // Variable name for current mixin's &attributes
used_attrs_var: bool, // True if current mixin accessed its attributes
fn init(
allocator: std.mem.Allocator,
@@ -597,7 +697,6 @@ const Compiler = struct {
.uses_data = false,
.mixin_depth = 0,
.current_attrs_var = null,
.used_attrs_var = false,
};
}
@@ -689,13 +788,21 @@ const Compiler = struct {
var lexer = Lexer.init(self.allocator, source);
const tokens = lexer.tokenize() catch |err| {
std.log.err("Tokenize error in included template '{s}': {}", .{ path, err });
if (lexer.getDiagnostic()) |diag| {
logDiagnostic(path, diag);
} else {
std.log.err("Tokenize error in included template '{s}': {}", .{ path, err });
}
return err;
};
var parser = Parser.init(self.allocator, tokens);
var parser = Parser.initWithSource(self.allocator, tokens, source);
return parser.parse() catch |err| {
std.log.err("Parse error in included template '{s}': {}", .{ path, err });
if (parser.getDiagnostic()) |diag| {
logDiagnostic(path, diag);
} else {
std.log.err("Parse error in included template '{s}': {}", .{ path, err });
}
return err;
};
}
@@ -1195,7 +1302,6 @@ const Compiler = struct {
// Special case: attributes.X should use current mixin's attributes variable
if (std.mem.eql(u8, base, "attributes")) {
if (self.current_attrs_var) |attrs_var| {
self.used_attrs_var = true;
return std.fmt.bufPrint(buf, "{s}.{s}", .{ attrs_var, rest }) catch expr;
}
}
@@ -1215,7 +1321,6 @@ const Compiler = struct {
// Special case: 'attributes' alone should use current mixin's attributes variable
if (std.mem.eql(u8, expr, "attributes")) {
if (self.current_attrs_var) |attrs_var| {
self.used_attrs_var = true;
return attrs_var;
}
}
@@ -1573,71 +1678,67 @@ const Compiler = struct {
try self.writer.writeAll("};\n");
}
// Handle mixin call attributes: +mixin(args)(class="foo", data-id="bar")
// Create an 'attributes' struct with optional fields that the mixin body can access
// Use unique name based on mixin depth to avoid shadowing in nested mixin calls
self.mixin_depth += 1;
const current_depth = self.mixin_depth;
// Check if mixin body actually uses &attributes before emitting the struct
const uses_attributes = mixinUsesAttributes(mixin_def.children);
// Save previous attrs var and restore after mixin body
const prev_attrs_var = self.current_attrs_var;
const prev_used_attrs = self.used_attrs_var;
self.used_attrs_var = false;
defer self.current_attrs_var = prev_attrs_var;
// Generate unique attribute variable name for this mixin depth
var attr_var_buf: [32]u8 = undefined;
const attr_var_name = std.fmt.bufPrint(&attr_var_buf, "mixin_attrs_{d}", .{current_depth}) catch "mixin_attrs";
// Only emit attributes struct if the mixin actually uses it
if (uses_attributes) {
// Use unique name based on mixin depth to avoid shadowing in nested mixin calls
self.mixin_depth += 1;
const current_depth = self.mixin_depth;
// Set current attrs var for buildAccessor to use
self.current_attrs_var = attr_var_name;
var attr_var_buf: [32]u8 = undefined;
const attr_var_name = std.fmt.bufPrint(&attr_var_buf, "mixin_attrs_{d}", .{current_depth}) catch "mixin_attrs";
try self.mixin_params.append(self.allocator, attr_var_name);
try self.writeIndent();
try self.writer.print("const {s}: struct {{\n", .{attr_var_name});
self.depth += 1;
// Define fields as optional with defaults
try self.writeIndent();
try self.writer.writeAll("class: []const u8 = \"\",\n");
try self.writeIndent();
try self.writer.writeAll("id: []const u8 = \"\",\n");
try self.writeIndent();
try self.writer.writeAll("style: []const u8 = \"\",\n");
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("} = .{\n");
self.depth += 1;
for (call.attributes) |attr| {
// Only emit known attributes (class, id, style for now)
if (std.mem.eql(u8, attr.name, "class") or
std.mem.eql(u8, attr.name, "id") or
std.mem.eql(u8, attr.name, "style"))
{
try self.writeIndent();
try self.writer.print(".{s} = ", .{attr.name});
if (attr.value) |val| {
// Check if it's a string literal
if (val.len >= 2 and (val[0] == '"' or val[0] == '\'')) {
try self.writer.print("{s},\n", .{val});
self.current_attrs_var = attr_var_name;
try self.mixin_params.append(self.allocator, attr_var_name);
try self.writeIndent();
try self.writer.print("const {s}: struct {{\n", .{attr_var_name});
self.depth += 1;
try self.writeIndent();
try self.writer.writeAll("class: []const u8 = \"\",\n");
try self.writeIndent();
try self.writer.writeAll("id: []const u8 = \"\",\n");
try self.writeIndent();
try self.writer.writeAll("style: []const u8 = \"\",\n");
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("} = .{\n");
self.depth += 1;
for (call.attributes) |attr| {
if (std.mem.eql(u8, attr.name, "class") or
std.mem.eql(u8, attr.name, "id") or
std.mem.eql(u8, attr.name, "style"))
{
try self.writeIndent();
try self.writer.print(".{s} = ", .{attr.name});
if (attr.value) |val| {
if (val.len >= 2 and (val[0] == '"' or val[0] == '\'')) {
try self.writer.print("{s},\n", .{val});
} else {
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(val, &accessor_buf);
try self.writer.print("{s},\n", .{accessor});
}
} else {
// It's a variable reference
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(val, &accessor_buf);
try self.writer.print("{s},\n", .{accessor});
try self.writer.writeAll("\"\",\n");
}
} else {
try self.writer.writeAll("\"\",\n");
}
}
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("};\n");
}
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("};\n");
// Emit mixin body
// Note: block content (call.block_children) is handled by mixin_block nodes
// For now, we'll inline the mixin body directly
for (mixin_def.children) |child| {
// Handle mixin_block specially - replace with call's block_children
if (child == .mixin_block) {
for (call.block_children) |block_child| {
try self.emitNode(block_child);
@@ -1647,22 +1748,15 @@ const Compiler = struct {
}
}
// Suppress unused variable warning if attributes wasn't used
if (!self.used_attrs_var) {
try self.writeIndent();
try self.writer.print("_ = {s};\n", .{attr_var_name});
}
// Close scope block
try self.flush();
self.depth -= 1;
try self.writeIndent();
try self.writer.writeAll("}\n");
// Restore previous state
self.current_attrs_var = prev_attrs_var;
self.used_attrs_var = prev_used_attrs;
self.mixin_depth -= 1;
if (uses_attributes) {
self.mixin_depth -= 1;
}
}
/// Attempts to load a mixin from the mixins/ subdirectory.

253
src/diagnostic.zig Normal file
View File

@@ -0,0 +1,253 @@
//! Diagnostic - Rich error reporting for Pug template parsing.
//!
//! Provides structured error information including:
//! - Line and column numbers
//! - Source code snippet showing the error location
//! - Descriptive error messages
//! - Optional fix suggestions
//!
//! ## Usage
//! ```zig
//! var lexer = Lexer.init(allocator, source);
//! const tokens = lexer.tokenize() catch |err| {
//! if (lexer.getDiagnostic()) |diag| {
//! std.debug.print("{}\n", .{diag});
//! }
//! return err;
//! };
//! ```
const std = @import("std");
/// Severity level for diagnostics.
pub const Severity = enum {
@"error",
warning,
hint,
pub fn toString(self: Severity) []const u8 {
return switch (self) {
.@"error" => "error",
.warning => "warning",
.hint => "hint",
};
}
};
/// A diagnostic message with rich context about an error or warning.
pub const Diagnostic = struct {
/// Severity level (error, warning, hint)
severity: Severity = .@"error",
/// 1-based line number where the error occurred
line: u32,
/// 1-based column number where the error occurred
column: u32,
/// Length of the problematic span (0 if unknown)
length: u32 = 0,
/// Human-readable error message
message: []const u8,
/// Source line containing the error (for snippet display)
source_line: ?[]const u8 = null,
/// Optional suggestion for fixing the error
suggestion: ?[]const u8 = null,
/// Optional error code for programmatic handling
code: ?[]const u8 = null,
/// Formats the diagnostic for display.
/// Output format:
/// ```
/// error[E001]: Unterminated string
/// --> template.pug:5:12
/// |
/// 5 | p Hello #{name
/// | ^^^^ unterminated interpolation
/// |
/// = hint: Add closing }
/// ```
pub fn format(
self: Diagnostic,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
// Header: error[CODE]: message
try writer.print("{s}", .{self.severity.toString()});
if (self.code) |code| {
try writer.print("[{s}]", .{code});
}
try writer.print(": {s}\n", .{self.message});
// Location: --> file:line:column
try writer.print(" --> line {d}:{d}\n", .{ self.line, self.column });
// Source snippet with caret pointer
if (self.source_line) |src| {
const line_num_width = digitCount(self.line);
// Empty line with gutter
try writer.writeByteNTimes(' ', line_num_width + 1);
try writer.writeAll("|\n");
// Source line
try writer.print("{d} | {s}\n", .{ self.line, src });
// Caret line pointing to error
try writer.writeByteNTimes(' ', line_num_width + 1);
try writer.writeAll("| ");
// Spaces before caret (account for tabs)
var col: u32 = 1;
for (src) |c| {
if (col >= self.column) break;
if (c == '\t') {
try writer.writeAll(" "); // 4-space tab
} else {
try writer.writeByte(' ');
}
col += 1;
}
// Carets for the error span
const caret_count = if (self.length > 0) self.length else 1;
try writer.writeByteNTimes('^', caret_count);
try writer.writeByte('\n');
}
// Suggestion hint
if (self.suggestion) |hint| {
try writer.print(" = hint: {s}\n", .{hint});
}
}
/// Creates a simple diagnostic without source context.
pub fn simple(line: u32, column: u32, message: []const u8) Diagnostic {
return .{
.line = line,
.column = column,
.message = message,
};
}
/// Creates a diagnostic with full context.
pub fn withContext(
line: u32,
column: u32,
message: []const u8,
source_line: []const u8,
suggestion: ?[]const u8,
) Diagnostic {
return .{
.line = line,
.column = column,
.message = message,
.source_line = source_line,
.suggestion = suggestion,
};
}
};
/// Returns the number of digits in a number (for alignment).
fn digitCount(n: u32) usize {
if (n == 0) return 1;
var count: usize = 0;
var val = n;
while (val > 0) : (val /= 10) {
count += 1;
}
return count;
}
/// Extracts a line from source text given a position.
/// Returns the line content and updates line_start to the beginning of the line.
pub fn extractSourceLine(source: []const u8, position: usize) ?[]const u8 {
if (position >= source.len) return null;
// Find line start
var line_start: usize = position;
while (line_start > 0 and source[line_start - 1] != '\n') {
line_start -= 1;
}
// Find line end
var line_end: usize = position;
while (line_end < source.len and source[line_end] != '\n') {
line_end += 1;
}
return source[line_start..line_end];
}
/// Calculates line and column from a byte position in source.
pub fn positionToLineCol(source: []const u8, position: usize) struct { line: u32, column: u32 } {
var line: u32 = 1;
var col: u32 = 1;
var i: usize = 0;
while (i < position and i < source.len) : (i += 1) {
if (source[i] == '\n') {
line += 1;
col = 1;
} else {
col += 1;
}
}
return .{ .line = line, .column = col };
}
// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────
test "Diagnostic formatting" {
const diag = Diagnostic{
.line = 5,
.column = 12,
.message = "Unterminated interpolation",
.source_line = "p Hello #{name",
.suggestion = "Add closing }",
.code = "E001",
};
var buf: [512]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
try diag.format("", .{}, fbs.writer());
const output = fbs.getWritten();
try std.testing.expect(std.mem.indexOf(u8, output, "error[E001]") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "Unterminated interpolation") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "line 5:12") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "p Hello #{name") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "hint: Add closing }") != null);
}
test "extractSourceLine" {
const source = "line one\nline two\nline three";
// Position in middle of "line two"
const line = extractSourceLine(source, 12);
try std.testing.expect(line != null);
try std.testing.expectEqualStrings("line two", line.?);
}
test "positionToLineCol" {
const source = "ab\ncde\nfghij";
// Position 0 = line 1, col 1
var pos = positionToLineCol(source, 0);
try std.testing.expectEqual(@as(u32, 1), pos.line);
try std.testing.expectEqual(@as(u32, 1), pos.column);
// Position 4 = line 2, col 2 (the 'd' in "cde")
pos = positionToLineCol(source, 4);
try std.testing.expectEqual(@as(u32, 2), pos.line);
try std.testing.expectEqual(@as(u32, 2), pos.column);
// Position 7 = line 3, col 1 (the 'f' in "fghij")
pos = positionToLineCol(source, 7);
try std.testing.expectEqual(@as(u32, 3), pos.line);
try std.testing.expectEqual(@as(u32, 1), pos.column);
}

View File

@@ -3,8 +3,23 @@
//! The lexer handles indentation-based nesting (emitting indent/dedent tokens),
//! Pug-specific syntax (tags, classes, IDs, attributes), and text content
//! including interpolation markers.
//!
//! ## Error Diagnostics
//! When tokenization fails, call `getDiagnostic()` to get rich error info:
//! ```zig
//! var lexer = Lexer.init(allocator, source);
//! const tokens = lexer.tokenize() catch |err| {
//! if (lexer.getDiagnostic()) |diag| {
//! std.debug.print("{}\n", .{diag});
//! }
//! return err;
//! };
//! ```
const std = @import("std");
const diagnostic = @import("diagnostic.zig");
pub const Diagnostic = diagnostic.Diagnostic;
/// All possible token types produced by the lexer.
pub const TokenType = enum {
@@ -136,6 +151,8 @@ pub const Lexer = struct {
in_raw_block: bool,
raw_block_indent: usize,
raw_block_started: bool,
/// Last error diagnostic (populated on error)
last_diagnostic: ?Diagnostic,
/// Creates a new lexer for the given source.
/// Does not allocate; allocations happen during tokenize().
@@ -153,9 +170,27 @@ pub const Lexer = struct {
.in_raw_block = false,
.raw_block_indent = 0,
.raw_block_started = false,
.last_diagnostic = null,
};
}
/// Returns the last error diagnostic, if any.
/// Call this after tokenize() returns an error to get detailed error info.
pub fn getDiagnostic(self: *const Lexer) ?Diagnostic {
return self.last_diagnostic;
}
/// Sets a diagnostic error with full context.
fn setDiagnostic(self: *Lexer, message: []const u8, suggestion: ?[]const u8) void {
self.last_diagnostic = Diagnostic.withContext(
@intCast(self.line),
@intCast(self.column),
message,
diagnostic.extractSourceLine(self.source, self.pos) orelse "",
suggestion,
);
}
/// Releases all allocated memory (tokens and indent stack).
/// Call this when done with the lexer, typically via defer.
pub fn deinit(self: *Lexer) void {
@@ -201,6 +236,7 @@ pub const Lexer = struct {
self.column = 1;
self.at_line_start = true;
self.current_indent = 0;
self.last_diagnostic = null;
}
/// Appends a token to the output list.
@@ -858,7 +894,10 @@ pub const Lexer = struct {
brace_depth += 1;
} else if (c == '}') {
if (brace_depth == 0) {
// Unmatched closing brace - shouldn't happen if called correctly
self.setDiagnostic(
"Unmatched closing brace '}'",
"Remove the extra '}' or add a matching '{'",
);
return LexerError.UnmatchedBrace;
}
brace_depth -= 1;
@@ -872,6 +911,10 @@ pub const Lexer = struct {
// Check for unterminated object literal
if (brace_depth > 0) {
self.setDiagnostic(
"Unterminated object literal - missing closing '}'",
"Add a closing '}' to complete the object",
);
return LexerError.UnterminatedString;
}
@@ -889,6 +932,10 @@ pub const Lexer = struct {
bracket_depth += 1;
} else if (c == ']') {
if (bracket_depth == 0) {
self.setDiagnostic(
"Unmatched closing bracket ']'",
"Remove the extra ']' or add a matching '['",
);
return LexerError.UnmatchedBrace;
}
bracket_depth -= 1;
@@ -901,6 +948,10 @@ pub const Lexer = struct {
}
if (bracket_depth > 0) {
self.setDiagnostic(
"Unterminated array literal - missing closing ']'",
"Add a closing ']' to complete the array",
);
return LexerError.UnterminatedString;
}

View File

@@ -6,10 +6,23 @@
//! - Element construction (tag, classes, id, attributes)
//! - Control flow (if/else, each, while)
//! - Mixins, includes, and template inheritance
//!
//! ## Error Diagnostics
//! When parsing fails, call `getDiagnostic()` to get rich error info:
//! ```zig
//! var parser = Parser.init(allocator, tokens);
//! const doc = parser.parse() catch |err| {
//! if (parser.getDiagnostic()) |diag| {
//! std.debug.print("{}\n", .{diag});
//! }
//! return err;
//! };
//! ```
const std = @import("std");
const lexer = @import("lexer.zig");
const ast = @import("ast.zig");
const diagnostic = @import("diagnostic.zig");
const Token = lexer.Token;
const TokenType = lexer.TokenType;
@@ -17,6 +30,8 @@ const Node = ast.Node;
const Attribute = ast.Attribute;
const TextSegment = ast.TextSegment;
pub const Diagnostic = diagnostic.Diagnostic;
/// Errors that can occur during parsing.
pub const ParserError = error{
UnexpectedToken,
@@ -42,6 +57,10 @@ pub const Parser = struct {
tokens: []const Token,
pos: usize,
allocator: std.mem.Allocator,
/// Original source text (for error snippets)
source: ?[]const u8,
/// Last error diagnostic (populated on error)
last_diagnostic: ?Diagnostic,
/// Creates a new parser for the given tokens.
pub fn init(allocator: std.mem.Allocator, tokens: []const Token) Parser {
@@ -49,6 +68,53 @@ pub const Parser = struct {
.tokens = tokens,
.pos = 0,
.allocator = allocator,
.source = null,
.last_diagnostic = null,
};
}
/// Creates a parser with source text for better error messages.
pub fn initWithSource(allocator: std.mem.Allocator, tokens: []const Token, source: []const u8) Parser {
return .{
.tokens = tokens,
.pos = 0,
.allocator = allocator,
.source = source,
.last_diagnostic = null,
};
}
/// Returns the last error diagnostic, if any.
/// Call this after parse() returns an error to get detailed error info.
pub fn getDiagnostic(self: *const Parser) ?Diagnostic {
return self.last_diagnostic;
}
/// Sets a diagnostic error with context from the current token.
fn setDiagnostic(self: *Parser, message: []const u8, suggestion: ?[]const u8) void {
const token = if (self.pos < self.tokens.len) self.tokens[self.pos] else self.tokens[self.tokens.len - 1];
const source_line = if (self.source) |src|
diagnostic.extractSourceLine(src, 0) // Would need position mapping
else
null;
self.last_diagnostic = .{
.line = @intCast(token.line),
.column = @intCast(token.column),
.message = message,
.source_line = source_line,
.suggestion = suggestion,
};
}
/// Sets a diagnostic error for a specific token.
fn setDiagnosticAtToken(self: *Parser, token: Token, message: []const u8, suggestion: ?[]const u8) void {
self.last_diagnostic = .{
.line = @intCast(token.line),
.column = @intCast(token.column),
.message = message,
.source_line = null,
.suggestion = suggestion,
};
}
@@ -562,6 +628,10 @@ pub const Parser = struct {
value_name = before_in;
}
} else {
self.setDiagnostic(
"Missing collection in 'each' loop - expected 'in' keyword",
"Use syntax: each item in collection",
);
return ParserError.MissingCollection;
}
} else if (self.check(.tag)) {
@@ -584,6 +654,10 @@ pub const Parser = struct {
// Parse collection expression
collection = try self.parseRestOfLine();
} else {
self.setDiagnostic(
"Missing iterator variable in 'each' loop",
"Use syntax: each item in collection",
);
return ParserError.MissingIterator;
}
@@ -774,6 +848,10 @@ pub const Parser = struct {
if (self.check(.tag)) {
name = self.advance().value;
} else {
self.setDiagnostic(
"Missing mixin name after 'mixin' keyword",
"Use syntax: mixin name(params)",
);
return ParserError.MissingMixinName;
}
@@ -973,6 +1051,10 @@ pub const Parser = struct {
// No name - this is a mixin block placeholder
return .{ .mixin_block = {} };
} else {
self.setDiagnostic(
"Missing block name after 'block' keyword",
"Use syntax: block name",
);
return ParserError.MissingBlockName;
}
@@ -1005,6 +1087,10 @@ pub const Parser = struct {
} else if (self.check(.text)) {
name = std.mem.trim(u8, self.advance().value, " \t");
} else {
self.setDiagnostic(
"Missing block name after 'append' or 'prepend'",
"Use syntax: append blockname or prepend blockname",
);
return ParserError.MissingBlockName;
}

View File

@@ -34,6 +34,7 @@ pub const parser = @import("parser.zig");
pub const codegen = @import("codegen.zig");
pub const runtime = @import("runtime.zig");
pub const view_engine = @import("view_engine.zig");
pub const diagnostic = @import("diagnostic.zig");
// Re-export main types for convenience
pub const Lexer = lexer.Lexer;