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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The functions every program can call: `@lower(s)`, `@join(xs, ",")` and//! the rest. None of them takes a function as an argument, so none of them//! can be made to loop.
const std = @import("std");
const Node = @import("ast.zig").Node;const eval = @import("eval.zig");const Evaluator = eval.Evaluator;const Error = eval.Error;const Value = @import("value.zig").Value;
pub const Builtin = enum { lower, upper, trim, len, contains, startsWith, endsWith, join, replace, split, min, max, int, float, string,
pub const Arity = struct { min: u8, max: u8 };
pub fn arity(b: Builtin) Arity { return switch (b) { .lower, .upper, .trim, .len, .int, .float, .string => .{ .min = 1, .max = 1 }, .contains, .startsWith, .endsWith, .join, .split => .{ .min = 2, .max = 2 }, .replace => .{ .min = 3, .max = 3 }, .min, .max => .{ .min = 1, .max = std.math.maxInt(u8) }, }; }};
pub fn call(e: *Evaluator, b: Builtin, pos: u32, nodes: []const *const Node, args: []const Value) Error!Value { const a = Args{ .e = e, .b = b, .nodes = nodes, .args = args }; switch (b) { .lower, .upper => { const s = try a.string(0); const out = try e.arena.alloc(u8, s.len); _ = if (b == .lower) std.ascii.lowerString(out, s) else std.ascii.upperString(out, s); return .{ .string = out }; }, .trim => return .{ .string = std.mem.trim(u8, try a.string(0), " \t\r\n") }, .len => { const v = args[0]; if (v == .string) return .{ .int = @intCast(v.string.len) }; if (v.itemCount()) |n| return .{ .int = @intCast(n) }; if (v.fieldCount()) |n| return .{ .int = @intCast(n) }; return a.wrong(0, "a string, list or struct"); }, .contains => { const hay = args[0]; if (hay == .string) { return .{ .bool = std.mem.find(u8, hay.string, try a.string(1)) != null }; } if (hay.itemCount() == null) return a.wrong(0, "a string or a list"); for (try e.items(nodes[0].pos, hay)) |item| { if (try e.eql(pos, item, args[1])) return .{ .bool = true }; } return .{ .bool = false }; }, .startsWith => return .{ .bool = std.mem.startsWith(u8, try a.string(0), try a.string(1)) }, .endsWith => return .{ .bool = std.mem.endsWith(u8, try a.string(0), try a.string(1)) }, .join => { if (args[0].itemCount() == null) return a.wrong(0, "a list"); const sep = try a.string(1); var buf: std.ArrayList(u8) = .empty; for (try e.items(nodes[0].pos, args[0]), 0..) |item, i| { if (i > 0) { try buf.appendSlice(e.arena, sep); try e.checkString(pos, buf.items.len); } try e.appendText(&buf, nodes[0].pos, item); } return .{ .string = buf.items }; }, .replace => { const s = try a.string(0); const from = try a.string(1); const to = try a.string(2); if (from.len == 0) return e.fail(nodes[1].pos, "@replace cannot replace the empty string", .{}); const size = std.mem.replacementSize(u8, s, from, to); try e.checkString(pos, size); const out = try e.arena.alloc(u8, size); _ = std.mem.replace(u8, s, from, to, out); return .{ .string = out }; }, .split => { const s = try a.string(0); const sep = try a.string(1); if (sep.len == 0) return e.fail(nodes[1].pos, "@split cannot split on the empty string", .{}); try e.checkList(pos, std.mem.count(u8, s, sep) + 1); var out: std.ArrayList(Value) = .empty; var it = std.mem.splitSequence(u8, s, sep); while (it.next()) |part| try out.append(e.arena, .{ .string = part }); return .{ .list = out.items }; }, .min, .max => { const values = if (args.len == 1) blk: { if (args[0].itemCount() == null) return a.wrong(0, "a list, or two or more values"); break :blk try e.items(nodes[0].pos, args[0]); } else args; if (values.len == 0) return e.fail(nodes[0].pos, "@{t} of an empty list", .{b}); var best = values[0]; for (values[1..]) |v| { const order = try e.compare(pos, if (b == .min) .lt else .gt, v, best); if (order == (if (b == .min) std.math.Order.lt else .gt)) best = v; } // A single value is its own min and max, but it still has to be // something that could have been compared. if (values.len == 1) _ = try e.compare(pos, .lt, best, best); return best; }, .int => switch (args[0]) { .int => return args[0], .bool => |x| return .{ .int = @intFromBool(x) }, .float => |f| { const t = @trunc(f); if (std.math.isNan(f) or t < -0x1p63 or t >= 0x1p63) { return e.fail(nodes[0].pos, "{d} does not fit in an i64", .{f}); } return .{ .int = @intFromFloat(t) }; }, .string => |s| return .{ .int = std.fmt.parseInt(i64, std.mem.trim(u8, s, " \t\r\n"), 0) catch return e.fail(nodes[0].pos, "\"{f}\" is not an integer", .{std.zig.fmtString(s)}) }, else => return a.wrong(0, "a number, a bool or a string"), }, .float => switch (args[0]) { .int => |i| return .{ .float = @floatFromInt(i) }, .float => return args[0], .string => |s| return .{ .float = std.fmt.parseFloat(f64, std.mem.trim(u8, s, " \t\r\n")) catch return e.fail(nodes[0].pos, "\"{f}\" is not a number", .{std.zig.fmtString(s)}) }, else => return a.wrong(0, "a number or a string"), }, .string => { var buf: std.ArrayList(u8) = .empty; try e.appendText(&buf, nodes[0].pos, args[0]); return .{ .string = buf.items }; }, }}
const Args = struct { e: *Evaluator, b: Builtin, nodes: []const *const Node, args: []const Value,
fn string(self: Args, i: usize) Error![]const u8 { if (self.args[i] == .string) return self.args[i].string; return self.wrong(i, "a string"); }
fn wrong(self: Args, i: usize, comptime wanted: []const u8) error{Eval} { return self.e.fail(self.nodes[i].pos, "@{t} needs " ++ wanted ++ " here, not {s}", .{ self.b, self.args[i].kindName() }); }};