Something went wrong. Try again.
A small configuration language for Zig: ZON with let, if and templates over a context, evaluated to a struct, always finishing.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Why a program would not parse or would not evaluate, in words, and where.//!//! Filled in rather than logged, so that the library says nothing on its own//! and a caller decides what a person sees. The message is kept in the struct//! itself: a diagnostic that allocated would need freeing on every error path,//! and the one thing an error path should not do is fail again.
const std = @import("std");
const Diagnostic = @This();
/// A place in a program's source. Both are one-based.////// `column` counts characters rather than bytes -- a UTF-8 continuation byte/// does not move it -- so that a caret printed under a line of the source/// lands under the character it means.pub const Pos = struct { line: u32 = 1, column: u32 = 1,
/// The position of byte `offset` in `source`. An offset past the end is /// the position just after the last character. pub fn of(source: []const u8, offset: usize) Pos { var pos: Pos = .{}; for (source[0..@min(offset, source.len)]) |c| { if (c == '\n') { pos.line += 1; pos.column = 1; } else if (c & 0xc0 != 0x80) { pos.column += 1; } } return pos; }};
/// One file of a program: the one it was parsed from, or one it includes.////// A position in a parsed program is a byte offset, and the offsets of a/// program's files do not overlap: each file starts at its `base`, past the/// end of the one before. So an offset alone says which file it is in.pub const Source = struct { /// The file's name, or null for a program parsed with no `filename`. name: ?[]const u8, text: []const u8, base: u32,
/// The file in `sources` that holds `offset`, which are in order of /// `base`. pub fn find(sources: []const Source, offset: u32) ?Source { var found: ?Source = null; for (sources) |s| { if (s.base > offset) break; found = s; } return found; }};
pos: Pos = .{},buf: [240]u8 = undefined,len: usize = 0,file_buf: [240]u8 = undefined,/// Null when the diagnostic names no file.file_len: ?usize = null,
pub fn message(self: *const Diagnostic) []const u8 { return self.buf[0..self.len];}
/// The file the diagnostic is in, if the program's files have names.pub fn file(self: *const Diagnostic) ?[]const u8 { return self.file_buf[0 .. self.file_len orelse return null];}
/// Record a message at `pos`, in no file in particular. A message too long/// for the buffer is cut short rather than lost.pub fn set(self: *Diagnostic, pos: Pos, comptime fmt: []const u8, args: anytype) void { self.pos = pos; self.file_len = null; var w: std.Io.Writer = .fixed(&self.buf); w.print(fmt, args) catch {}; self.len = w.end;}
/// Record a message at `pos` in the file called `name`, which is copied: a/// diagnostic outlives the program it describes. A name too long for the/// buffer keeps its end, which is the part that says which file it is.pub fn setIn(self: *Diagnostic, name: ?[]const u8, pos: Pos, comptime fmt: []const u8, args: anytype) void { self.set(pos, fmt, args); const n = name orelse return; const tail = n[n.len - @min(n.len, self.file_buf.len) ..]; @memcpy(self.file_buf[0..tail.len], tail); self.file_len = tail.len;}
/// Record a message at `offset`, a position in a parsed program, in/// whichever of the program's files holds it.pub fn setAt(self: *Diagnostic, sources: []const Source, offset: u32, comptime fmt: []const u8, args: anytype) void { const s = Source.find(sources, offset) orelse return self.set(.{}, fmt, args); self.setIn(s.name, .of(s.text, offset - s.base), fmt, args);}
/// `file:line:column: message`, or `line:column: message` when there is no/// file name: the shape an editor jumps to.pub fn format(self: *const Diagnostic, w: *std.Io.Writer) std.Io.Writer.Error!void { if (self.file()) |name| try w.print("{s}:", .{name}); try w.print("{d}:{d}: {s}", .{ self.pos.line, self.pos.column, self.message() });}
test "a diagnostic says where and what" { var d: Diagnostic = .{}; d.set(.{ .line = 3, .column = 7 }, "expected '{s}'", .{";"}); var buf: [64]u8 = undefined; const text = try std.fmt.bufPrint(&buf, "{f}", .{&d}); try std.testing.expectEqualStrings("3:7: expected ';'", text);}
test "a diagnostic names its file when it has one" { var d: Diagnostic = .{}; d.setIn("conf/app.struthio", .{ .line = 2, .column = 1 }, "oops", .{}); var buf: [64]u8 = undefined; try std.testing.expectEqualStrings("conf/app.struthio:2:1: oops", try std.fmt.bufPrint(&buf, "{f}", .{&d})); d.set(.{}, "again", .{}); try std.testing.expectEqual(null, d.file());}
test "an offset is found in the file that holds it" { const sources = [_]Source{ .{ .name = "a", .text = "1 +\n2", .base = 0 }, .{ .name = "b", .text = "x", .base = 6 }, }; var d: Diagnostic = .{}; d.setAt(&sources, 4, "in a", .{}); try std.testing.expectEqualStrings("a", d.file().?); try std.testing.expectEqual(Pos{ .line = 2, .column = 1 }, d.pos); d.setAt(&sources, 6, "in b", .{}); try std.testing.expectEqualStrings("b", d.file().?); try std.testing.expectEqual(Pos{ .line = 1, .column = 1 }, d.pos);}
test "a message too long for the buffer is cut short, not lost" { var d: Diagnostic = .{}; d.set(.{}, "{s}", .{&@as([400]u8, @splat('x'))}); try std.testing.expectEqual(d.buf.len, d.message().len);}
test "positions count lines and characters" { const src = "ab\ncé\nx"; try std.testing.expectEqual(Pos{ .line = 1, .column = 1 }, Pos.of(src, 0)); try std.testing.expectEqual(Pos{ .line = 2, .column = 1 }, Pos.of(src, 3)); // `é` is two bytes and one column. try std.testing.expectEqual(Pos{ .line = 2, .column = 3 }, Pos.of(src, 6)); try std.testing.expectEqual(Pos{ .line = 3, .column = 1 }, Pos.of(src, 7)); try std.testing.expectEqual(Pos{ .line = 3, .column = 2 }, Pos.of(src, 99));}