├── .gitattributes ├── .gitignore ├── res ├── en_US.def └── fi_FI.def ├── .github └── workflows │ └── ci.yml ├── README.md ├── examples └── generate_defs.zig ├── LICENSE └── src ├── value.zig ├── test.zig ├── lib.zig ├── Code.zig ├── Context.zig └── Parser.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ -------------------------------------------------------------------------------- /res/en_US.def: -------------------------------------------------------------------------------- 1 | # member is either "member function " or empty 2 | # variadic is either "at least " or empty 3 | def "{%member}expected {%variadic}{%expected} argument(s), found {%actual}" 4 | if %expected = 1 5 | "{%member}expected {%variadic}one argument, found {%actual}" 6 | else 7 | "{%member}expected {%variadic}{%expected} arguments, found {%actual}" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /res/fi_FI.def: -------------------------------------------------------------------------------- 1 | # member is either "member function " or empty 2 | # variadic is either "at least " or empty 3 | def "{%member}expected {%variadic}{%expected} argument(s), found {%actual}" 4 | if %member != "" 5 | set %member to "jäsen " 6 | end 7 | if %variadic != "" 8 | set %variadic to "vähintään" 9 | end 10 | "{%member}funktio ottaa {%variadic}{%expected} argumenttia, mutta sai {%actual}" 11 | end 12 | 13 | def "foo {%0} {{%foo}}" 14 | "bar {%0} bar" 15 | end 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - tmp 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | submodules: true 20 | - uses: mlugg/setup-zig@v1 21 | with: 22 | version: master 23 | 24 | - name: Fmt 25 | run: zig fmt . --check 26 | if: matrix.os == 'ubuntu-latest' 27 | 28 | - name: Run Tests 29 | run: zig build test 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i18n experiment 2 | 3 | An experiment at creating an translation library for use with Zig. 4 | 5 | One goal is that starting to use this should be as easy as 6 | replacing all instances like `writer.print("...", .{...})` 7 | with `i18n.format(writer, "...", .{...})` 8 | where the format string becomes the key for translation. 9 | 10 | Translations are specified in `$LOCALE.def` files so for code like 11 | ```zig 12 | i18n.format(writer, "Hello {s}!", .{name}); 13 | ``` 14 | A finnish translation file `fi_FI.def` would contain something like: 15 | ``` 16 | # Comment explaining something about this translation 17 | def "Hello {%name}!" 18 | "Moikka {%name}!" 19 | end 20 | ``` -------------------------------------------------------------------------------- /examples/generate_defs.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const i18n = @import("i18n"); 3 | 4 | pub fn main() !void { 5 | var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; 6 | defer _ = gpa_state.deinit(); 7 | const gpa = gpa_state.allocator(); 8 | 9 | const input = 10 | \\def "Hello {%name}!" 11 | \\ "Moikka {%name}!" 12 | \\end 13 | \\ 14 | ; 15 | 16 | var ctx = i18n.Context{ .arena = std.heap.ArenaAllocator.init(gpa) }; 17 | defer ctx.deinit(); 18 | try i18n.parse(&ctx, input); 19 | 20 | var out_buf = std.ArrayList(u8).init(gpa); 21 | defer out_buf.deinit(); 22 | 23 | try ctx.format(out_buf.writer(), "Hello foo {s}!", .{"Veikka"}); 24 | try ctx.format(out_buf.writer(), "Hello baz {s}!", .{"Veikka"}); 25 | try ctx.format(out_buf.writer(), "Hello bar {s}!", .{"Veikka"}); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Veikka Tuominen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/value.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const lib = @import("lib.zig"); 3 | 4 | pub const Value = union(enum) { 5 | bool: bool, 6 | int: i64, 7 | float: f64, 8 | str: []const u8, 9 | preformatted: []const u8, 10 | none, 11 | 12 | pub fn from( 13 | arena: *std.heap.ArenaAllocator, 14 | value: anytype, 15 | comptime fmt: []const u8, 16 | options: std.fmt.FormatOptions, 17 | ) !Value { 18 | const T = @TypeOf(value); 19 | switch (@typeInfo(T)) { 20 | .bool => { 21 | if (fmt.len != 0) std.fmt.invalidFmtError(fmt, value); 22 | return .{ .bool = value }; 23 | }, 24 | .pointer => |ptr_info| { 25 | if (comptime std.mem.eql(u8, fmt, "s") and ptr_info.child == u8) { 26 | return .{ .str = value }; 27 | } 28 | }, 29 | .comptime_int, .int => { 30 | if ((fmt.len == 1 and switch (fmt[0]) { 31 | 'd', 'c', 'u', 'b', 'x', 'X', 'o' => false, 32 | else => true, 33 | }) or fmt.len > 1) std.fmt.invalidFmtError(fmt, value); 34 | if (std.math.cast(i64, value)) |some| { 35 | return .{ .int = some }; 36 | } 37 | }, 38 | .comptime_float, .float => { 39 | if ((fmt.len == 1 and switch (fmt[0]) { 40 | 'e', 'd', 'x' => true, 41 | else => true, 42 | }) or fmt.len > 1) std.fmt.invalidFmtError(fmt, value); 43 | return .{ .int = value }; 44 | }, 45 | else => {}, 46 | } 47 | var out_buf = std.ArrayList(u8).init(arena.child_allocator); 48 | defer out_buf.deinit(); 49 | try std.fmt.formatType(value, fmt, options, out_buf.writer(), std.options.fmt_max_depth); 50 | 51 | return .{ .preformatted = try arena.allocator().dupe(u8, out_buf.items) }; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const lib = @import("lib.zig"); 3 | 4 | test "simple translation" { 5 | try testFormat( 6 | \\# Comment explaining something about this translation 7 | \\def "Hello {%name}!" 8 | \\ "Moikka {%name}!" 9 | \\end 10 | , "Hello {s}!", .{.{ 11 | .{"Veikka"}, 12 | "Moikka Veikka!", 13 | }}); 14 | } 15 | 16 | test "use of undefined argument" { 17 | try testFormat( 18 | \\def "Bye {%name}!" 19 | \\ "Heippa {%foo}!" 20 | \\end 21 | , "Bye {s}!", .{.{ 22 | .{"Veikka"}, 23 | "Heippa [UNDEFINED ARGUMENT %foo]!", 24 | }}); 25 | } 26 | 27 | test "complex value" { 28 | try testFormat( 29 | \\def "This is a tuple {%0}!" 30 | \\ "Tämä on monikko {%0}!" 31 | \\end 32 | , "This is a tuple {}!", .{.{ 33 | .{.{ .hello_world, 1 }}, 34 | "Tämä on monikko { .hello_world, 1 }!", 35 | }}); 36 | } 37 | 38 | test "numbers in binary" { 39 | try testFormat( 40 | \\def "{%dec} in binary is {%bin}" 41 | \\ "binääri {%bin} on {%dec}" 42 | \\end 43 | , "{[0]} in binary is {[0]b}", .{.{ 44 | .{12}, 45 | "binääri 1100 on 12", 46 | }}); 47 | } 48 | 49 | test "set argument" { 50 | try testFormat( 51 | \\def "this is {%bool}" 52 | \\ set %bool to false 53 | \\ "this is {%bool}" 54 | \\end 55 | , "this is {}", .{.{ 56 | .{true}, 57 | "this is false", 58 | }}); 59 | } 60 | 61 | test "set variable" { 62 | try testFormat( 63 | \\def "no arguments here" 64 | \\ set %my_var to true 65 | \\ "but my var is {%my_var}" 66 | \\end 67 | , "no arguments here", .{.{ 68 | .{}, 69 | "but my var is true", 70 | }}); 71 | } 72 | 73 | test "simple if" { 74 | try testFormat( 75 | \\def "{%bool1} {%bool2}" 76 | \\ if %bool1 77 | \\ "bool1" 78 | \\ elseif %bool2 79 | \\ "bool2" 80 | \\ else 81 | \\ "neither" 82 | \\ end 83 | \\end 84 | , "{} {}", .{ 85 | .{ 86 | .{ false, false }, 87 | "neither", 88 | }, 89 | .{ 90 | .{ true, false }, 91 | "bool1", 92 | }, 93 | .{ 94 | .{ false, true }, 95 | "bool2", 96 | }, 97 | }); 98 | } 99 | 100 | fn testFormat(input: [:0]const u8, comptime fmt: []const u8, comptime tests: anytype) !void { 101 | const a = std.testing.allocator; 102 | var ctx = lib.Context{ .arena = std.heap.ArenaAllocator.init(a) }; 103 | defer ctx.deinit(); 104 | 105 | try lib.parse(&ctx, input); 106 | 107 | var out_buf = std.ArrayList(u8).init(a); 108 | defer out_buf.deinit(); 109 | 110 | inline for (tests) |@"test"| { 111 | try ctx.format(out_buf.writer(), fmt, @"test"[0]); 112 | try std.testing.expectEqualStrings(@as([]const u8, @"test"[1]), out_buf.items); 113 | out_buf.items.len = 0; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Step = std.Build.Step; 3 | 4 | pub const Code = @import("Code.zig"); 5 | pub const Context = @import("Context.zig"); 6 | pub const Value = @import("value.zig").Value; 7 | pub const parse = @import("Parser.zig").parse; 8 | 9 | pub const GenerateDefsStep = struct { 10 | step: Step, 11 | compile_step: *Step.Compile, 12 | 13 | pub const base_id = .custom; 14 | 15 | pub const Options = struct { 16 | compile_step: *Step.Compile, 17 | }; 18 | 19 | pub fn create(owner: *std.Build, options: Options) *GenerateDefsStep { 20 | const self = owner.allocator.create(GenerateDefsStep) catch @panic("OOM"); 21 | self.* = .{ 22 | .step = Step.init(.{ 23 | .id = base_id, 24 | .name = "GenerateDefsStep", 25 | .owner = owner, 26 | .makeFn = make, 27 | }), 28 | .compile_step = options.compile_step, 29 | }; 30 | 31 | const new_options = owner.addOptions(); 32 | new_options.addOption(bool, "log_fmts", true); 33 | const i18n_module = options.compile_step.root_module.import_table.get("i18n").?; 34 | i18n_module.import_table.values()[0] = new_options.createModule(); 35 | self.step.dependOn(&new_options.step); 36 | return self; 37 | } 38 | 39 | fn make(step: *Step, options: std.Build.Step.MakeOptions) !void { 40 | const self: *GenerateDefsStep = @fieldParentPtr("step", step); 41 | // Make the compilation step as usual. 42 | self.compile_step.step.make(options) catch {}; 43 | const log_txt = self.compile_step.step.result_error_bundle.getCompileLogOutput(); 44 | 45 | var list = std.ArrayList([]const u8).init(step.owner.allocator); 46 | defer list.deinit(); 47 | var start: usize = 0; 48 | while (true) { 49 | start = std.mem.indexOfScalarPos(u8, log_txt, start, '"') orelse break; 50 | const end = std.mem.indexOfScalarPos(u8, log_txt, start, '\n') orelse log_txt.len; 51 | try list.append(log_txt[start .. end - 1]); 52 | start = end; 53 | } 54 | const lessThan = struct { 55 | pub fn lessThan(_: void, rhs: []const u8, lhs: []const u8) bool { 56 | return std.mem.order(u8, lhs, rhs).compare(.lt); 57 | } 58 | }.lessThan; 59 | std.mem.sort([]const u8, list.items, {}, lessThan); 60 | 61 | const file = blk: { 62 | var dir = try step.owner.build_root.handle.makeOpenPath("res", .{}); 63 | defer dir.close(); 64 | break :blk try dir.createFile("base.def", .{}); 65 | }; 66 | defer file.close(); 67 | var buf = std.io.bufferedWriter(file.writer()); 68 | const w = buf.writer(); 69 | for (list.items) |item| { 70 | try w.print("# def {s}\n", .{item}); 71 | } 72 | try buf.flush(); 73 | } 74 | }; 75 | 76 | pub fn addTo(mod: *std.Build.Module, path: std.Build.LazyPath) void { 77 | const b = mod.owner; 78 | const options = b.addOptions(); 79 | options.addOption(bool, "log_fmts", false); 80 | const module = b.createModule(.{ 81 | .root_source_file = path, 82 | .imports = &.{.{ 83 | .name = "options", 84 | .module = options.createModule(), 85 | }}, 86 | }); 87 | return mod.addImport("i18n", module); 88 | } 89 | 90 | test { 91 | _ = @import("test.zig"); 92 | } 93 | -------------------------------------------------------------------------------- /src/Code.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const expect = std.testing.expect; 4 | const log = std.log.scoped(.i18n); 5 | const lib = @import("lib.zig"); 6 | 7 | const Code = @This(); 8 | 9 | pub const Program = struct { body: u32 }; 10 | 11 | pub const Inst = struct { 12 | op: Op, 13 | data: Data, 14 | 15 | pub const Op = enum(u8) { 16 | end, 17 | set_arg, 18 | set_var, 19 | @"if", 20 | arg, 21 | @"var", 22 | str, 23 | bool, 24 | int, 25 | float, 26 | 27 | eq, 28 | neq, 29 | lt, 30 | lte, 31 | gt, 32 | gte, 33 | @"and", 34 | @"or", 35 | not, 36 | 37 | pub fn Data(comptime op: Op) type { 38 | return switch (op) { 39 | .end => void, 40 | .arg => u5, 41 | .set_arg => SetArg, 42 | .set_var => SetVar, 43 | .@"if" => If, 44 | .@"var", .str => []const u8, 45 | .bool => bool, 46 | .int => i64, 47 | .float => f64, 48 | .not => Ref, 49 | else => Bin, 50 | }; 51 | } 52 | }; 53 | pub const Data = Bin; 54 | 55 | pub const SetVar = struct { 56 | @"var": []const u8, 57 | operand: Inst.Ref, 58 | }; 59 | 60 | pub const SetArg = struct { 61 | pos: u5, 62 | operand: Inst.Ref, 63 | }; 64 | 65 | pub const If = struct { 66 | cond: Inst.Ref, 67 | then_body: u32, 68 | else_body: u32, 69 | }; 70 | 71 | pub const Bin = extern struct { 72 | lhs: Inst.Ref, 73 | rhs: Inst.Ref, 74 | }; 75 | 76 | pub const Ref = enum(u32) { _ }; 77 | }; 78 | 79 | pub const Vm = struct { 80 | ctx: *lib.Context, 81 | args: []lib.Context.Argument, 82 | vars: std.StringHashMapUnmanaged(lib.Value) = .{}, 83 | 84 | pub fn deinit(vm: *Vm) void { 85 | vm.vars.deinit(vm.ctx.arena.child_allocator); 86 | vm.* = undefined; 87 | } 88 | 89 | pub fn run(vm: *Vm, program: Program) !lib.Value { 90 | const body: []Inst.Ref = @ptrCast(vm.ctx.code.extra.items[program.body..]); 91 | return vm.evalBody(body); 92 | } 93 | 94 | fn evalBody(vm: *Vm, body: []Inst.Ref) !lib.Value { 95 | var i: usize = 0; 96 | const ops = vm.ctx.code.insts.items(.op); 97 | while (true) { 98 | const inst = body[i]; 99 | i += 1; 100 | switch (ops[@intFromEnum(inst)]) { 101 | .set_var => { 102 | const set = vm.ctx.code.getExtra(.set_var, inst); 103 | const val = try vm.evalExpr(set.operand); 104 | try vm.vars.put(vm.ctx.arena.child_allocator, set.@"var", val); 105 | }, 106 | .set_arg => { 107 | const set = vm.ctx.code.getExtra(.set_arg, inst); 108 | const val = try vm.evalExpr(set.operand); 109 | vm.args[set.pos].val = val; 110 | }, 111 | .@"if" => { 112 | const @"if" = vm.ctx.code.getExtra(.@"if", inst); 113 | const cond = try vm.evalExpr(@"if".cond); 114 | const cond_bool = switch (cond) { 115 | .bool => |b| b, 116 | .int => |int| int != 0, 117 | .float => |float| float != 0, 118 | else => { 119 | log.warn("ignoring 'if' with non-boolean condition", .{}); 120 | continue; 121 | }, 122 | }; 123 | const then_body: []Inst.Ref = @ptrCast(vm.ctx.code.extra.items[@"if".then_body..]); 124 | const else_body: []Inst.Ref = @ptrCast(vm.ctx.code.extra.items[@"if".else_body..]); 125 | const body_res = if (cond_bool) 126 | try vm.evalBody(then_body) 127 | else if (@"if".else_body != 0) 128 | try vm.evalBody(else_body) 129 | else 130 | .none; 131 | if (body_res == .str) return body_res; 132 | }, 133 | .str => return .{ .str = vm.ctx.code.getExtra(.str, inst) }, 134 | .end => return .none, 135 | else => unreachable, 136 | } 137 | } 138 | } 139 | 140 | fn evalExpr(vm: *Vm, inst: Inst.Ref) !lib.Value { 141 | const ops = vm.ctx.code.insts.items(.op); 142 | switch (ops[@intFromEnum(inst)]) { 143 | .bool => return .{ .bool = vm.ctx.code.getExtra(.bool, inst) }, 144 | .int => return .{ .int = vm.ctx.code.getExtra(.int, inst) }, 145 | .float => return .{ .float = vm.ctx.code.getExtra(.float, inst) }, 146 | .arg => return vm.args[vm.ctx.code.getExtra(.arg, inst)].val, 147 | .set_arg, .set_var, .@"if", .end => unreachable, 148 | else => |op| std.debug.panic("TODO eval {}", .{op}), 149 | } 150 | } 151 | }; 152 | 153 | insts: std.MultiArrayList(Inst) = .{}, 154 | extra: std.ArrayListUnmanaged(u32) = .{}, 155 | strings: std.ArrayListUnmanaged(u8) = .{}, 156 | 157 | pub fn deinit(c: *Code, gpa: Allocator) void { 158 | c.insts.deinit(gpa); 159 | c.extra.deinit(gpa); 160 | c.strings.deinit(gpa); 161 | c.* = undefined; 162 | } 163 | 164 | pub fn getExtra(c: *Code, comptime op: Inst.Op, ref: Inst.Ref) op.Data() { 165 | const data = c.insts.items(.data)[@intFromEnum(ref)]; 166 | switch (op) { 167 | .end => {}, 168 | .set_var => { 169 | const extra_index = @intFromEnum(data.lhs); 170 | const offset = c.extra.items[extra_index]; 171 | const len = c.extra.items[extra_index + 1]; 172 | return .{ 173 | .@"var" = c.strings.items[offset..][0..len], 174 | .operand = data.rhs, 175 | }; 176 | }, 177 | .set_arg => return .{ 178 | .pos = @intCast(@intFromEnum(data.lhs)), 179 | .operand = data.rhs, 180 | }, 181 | .@"if" => { 182 | const extra_index = @intFromEnum(data.rhs); 183 | return .{ 184 | .then_body = c.extra.items[extra_index], 185 | .else_body = c.extra.items[extra_index + 1], 186 | .cond = data.lhs, 187 | }; 188 | }, 189 | .@"var", .str => { 190 | const offset = @intFromEnum(data.lhs); 191 | const len = @intFromEnum(data.rhs); 192 | return c.strings.items[offset..][0..len]; 193 | }, 194 | .arg => return @intCast(@intFromEnum(data.lhs)), 195 | .bool => return @intFromEnum(data.lhs) != 0, 196 | .int => return @bitCast(data), 197 | .float => return @bitCast(data), 198 | .not => return data.lhs, 199 | else => return data, 200 | } 201 | } 202 | 203 | pub fn addInst(c: *Code, gpa: Allocator, comptime op: Inst.Op, input: op.Data()) !Inst.Ref { 204 | const ref: Inst.Ref = @enumFromInt(c.insts.len); 205 | try c.insts.append(gpa, .{ 206 | .op = op, 207 | .data = switch (op) { 208 | .end => undefined, 209 | .set_arg => .{ 210 | .lhs = @enumFromInt(input.pos), 211 | .rhs = input.operand, 212 | }, 213 | .set_var => blk: { 214 | const offset = c.strings.items.len; 215 | try c.strings.appendSlice(gpa, input.@"var"); 216 | 217 | const extra_index = c.extra.items.len; 218 | try c.extra.append(gpa, @intCast(offset)); 219 | try c.extra.append(gpa, @intCast(input.@"var".len)); 220 | break :blk .{ 221 | .lhs = @enumFromInt(extra_index), 222 | .rhs = input.operand, 223 | }; 224 | }, 225 | .@"if" => blk: { 226 | const extra_index = c.extra.items.len; 227 | try c.extra.append(gpa, input.then_body); 228 | try c.extra.append(gpa, input.else_body); 229 | break :blk .{ 230 | .lhs = input.cond, 231 | .rhs = @enumFromInt(extra_index), 232 | }; 233 | }, 234 | .@"var", .str => blk: { 235 | const offset = c.strings.items.len; 236 | try c.strings.appendSlice(gpa, input); 237 | break :blk .{ 238 | .lhs = @enumFromInt(offset), 239 | .rhs = @enumFromInt(input.len), 240 | }; 241 | }, 242 | .arg => .{ .lhs = @enumFromInt(input), .rhs = undefined }, 243 | .bool => .{ .lhs = @enumFromInt(@intFromBool(input)), .rhs = undefined }, 244 | .int => @bitCast(input), 245 | .float => @bitCast(input), 246 | .not => .{ .lhs = input, .rhs = undefined }, 247 | else => input, 248 | }, 249 | }); 250 | return ref; 251 | } 252 | -------------------------------------------------------------------------------- /src/Context.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArenaAllocator = std.heap.ArenaAllocator; 4 | const assert = std.debug.assert; 5 | const expect = std.testing.expect; 6 | const expectEqualStrings = std.testing.expectEqualStrings; 7 | const log = std.log.scoped(.i18n); 8 | const lib = @import("lib.zig"); 9 | 10 | const Context = @This(); 11 | 12 | pub const Argument = struct { 13 | val: lib.Value, 14 | fmt_options: std.fmt.FormatOptions, 15 | kind: enum { decimal, scientific, hex }, 16 | case: std.fmt.Case, 17 | base: u8, 18 | }; 19 | const max_format_args = @typeInfo(std.fmt.ArgSetType).int.bits; 20 | 21 | defs: std.StringHashMapUnmanaged(lib.Code.Program) = .{}, 22 | code: lib.Code = .{}, 23 | arena: ArenaAllocator, 24 | 25 | pub fn deinit(ctx: *Context) void { 26 | const gpa = ctx.arena.child_allocator; 27 | var it = ctx.defs.keyIterator(); 28 | while (it.next()) |some| gpa.free(some.*); 29 | ctx.defs.deinit(gpa); 30 | ctx.code.deinit(gpa); 31 | ctx.arena.deinit(); 32 | ctx.* = undefined; 33 | } 34 | 35 | pub fn format( 36 | ctx: *Context, 37 | writer: anytype, 38 | comptime fmt: []const u8, 39 | args: anytype, 40 | ) !void { 41 | const ArgsType = @TypeOf(args); 42 | const args_type_info = @typeInfo(ArgsType); 43 | if (args_type_info != .@"struct") { 44 | @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); 45 | } 46 | 47 | const fields_info = args_type_info.@"struct".fields; 48 | if (fields_info.len > max_format_args) { 49 | @compileError("32 arguments max are supported per format call"); 50 | } 51 | 52 | _ = ctx.arena.reset(.{ .retain_with_limit = 4069 }); 53 | var options: [max_format_args]Argument = undefined; 54 | 55 | @setEvalBranchQuota(2000000); 56 | comptime var arg_state: std.fmt.ArgState = .{ .args_len = fields_info.len }; 57 | comptime var i = 0; 58 | comptime var options_i = 0; 59 | comptime var query_str: []const u8 = ""; 60 | inline while (i < fmt.len) { 61 | const start_index = i; 62 | 63 | inline while (i < fmt.len) : (i += 1) { 64 | switch (fmt[i]) { 65 | '{', '}' => break, 66 | else => {}, 67 | } 68 | } 69 | 70 | comptime var end_index = i; 71 | comptime var unescape_brace = false; 72 | 73 | // Handle {{ and }}, those are un-escaped as single braces 74 | if (i + 1 < fmt.len and fmt[i + 1] == fmt[i]) { 75 | unescape_brace = true; 76 | // Make the first brace part of the literal... 77 | end_index += 1; 78 | // ...and skip both 79 | i += 2; 80 | } 81 | 82 | // Write out the literal 83 | if (start_index != end_index) { 84 | query_str = query_str ++ fmt[start_index..end_index]; 85 | } 86 | 87 | // We've already skipped the other brace, restart the loop 88 | if (unescape_brace) continue; 89 | 90 | if (i >= fmt.len) break; 91 | 92 | if (fmt[i] == '}') { 93 | @compileError("missing opening {"); 94 | } 95 | 96 | // Get past the { 97 | comptime assert(fmt[i] == '{'); 98 | i += 1; 99 | 100 | const fmt_begin = i; 101 | // Find the closing brace 102 | inline while (i < fmt.len and fmt[i] != '}') : (i += 1) {} 103 | const fmt_end = i; 104 | 105 | if (i >= fmt.len) { 106 | @compileError("missing closing }"); 107 | } 108 | 109 | // Get past the } 110 | comptime assert(fmt[i] == '}'); 111 | i += 1; 112 | 113 | const placeholder = comptime std.fmt.Placeholder.parse(fmt[fmt_begin..fmt_end].*); 114 | const arg_pos = comptime switch (placeholder.arg) { 115 | .none => null, 116 | .number => |pos| pos, 117 | .named => |arg_name| std.meta.fieldIndex(ArgsType, arg_name) orelse 118 | @compileError("no argument with name '" ++ arg_name ++ "'"), 119 | }; 120 | 121 | const width = comptime switch (placeholder.width) { 122 | .none => null, 123 | .number => |v| v, 124 | .named => |arg_name| blk: { 125 | const arg_i = std.meta.fieldIndex(ArgsType, arg_name) orelse 126 | @compileError("no argument with name '" ++ arg_name ++ "'"); 127 | _ = arg_state.nextArg(arg_i) orelse @compileError("too few arguments"); 128 | break :blk @field(args, arg_name); 129 | }, 130 | }; 131 | 132 | const precision = comptime switch (placeholder.precision) { 133 | .none => null, 134 | .number => |v| v, 135 | .named => |arg_name| blk: { 136 | const arg_i = std.meta.fieldIndex(ArgsType, arg_name) orelse 137 | @compileError("no argument with name '" ++ arg_name ++ "'"); 138 | _ = arg_state.nextArg(arg_i) orelse @compileError("too few arguments"); 139 | break :blk @field(args, arg_name); 140 | }, 141 | }; 142 | 143 | const arg_to_print = comptime arg_state.nextArg(arg_pos) orelse 144 | @compileError("too few arguments"); 145 | 146 | query_str = query_str ++ "{}"; 147 | const fmt_options = std.fmt.FormatOptions{ 148 | .fill = placeholder.fill, 149 | .alignment = placeholder.alignment, 150 | .width = width, 151 | .precision = precision, 152 | }; 153 | const spec = placeholder.specifier_arg; 154 | options[options_i] = .{ 155 | .val = try lib.Value.from(&ctx.arena, @field(args, fields_info[arg_to_print].name), spec, fmt_options), 156 | .fmt_options = fmt_options, 157 | .case = comptime if (std.mem.eql(u8, spec, "X")) .upper else .lower, 158 | .base = comptime if (std.mem.eql(u8, spec, "b")) 159 | 2 160 | else if (std.mem.eql(u8, spec, "o")) 161 | 8 162 | else if (std.mem.eql(u8, spec, "x") or std.mem.eql(u8, spec, "X")) 163 | 16 164 | else 165 | 10, 166 | .kind = comptime if (std.mem.eql(u8, spec, "d")) 167 | .decimal 168 | else if (std.mem.eql(u8, spec, "x")) 169 | .hex 170 | else 171 | .scientific, 172 | }; 173 | options_i += 1; 174 | } 175 | 176 | if (comptime arg_state.hasUnusedArgs()) { 177 | const missing_count = arg_state.args_len - @popCount(arg_state.used_args); 178 | switch (missing_count) { 179 | 0 => unreachable, 180 | 1 => @compileError("unused argument in '" ++ fmt ++ "'"), 181 | else => @compileError(std.fmt.comptimePrint("{d}", .{missing_count}) ++ " unused arguments in '" ++ fmt ++ "'"), 182 | } 183 | } 184 | 185 | if (!@import("builtin").is_test and @import("options").log_fmts) { 186 | const S = struct { 187 | fn logFmt(comptime a: anytype) void { 188 | @compileLog(a); 189 | } 190 | }; 191 | S.logFmt(query_str[0..query_str.len].*); 192 | } 193 | 194 | var vm = lib.Code.Vm{ .ctx = ctx, .args = options[0..options_i] }; 195 | defer vm.deinit(); 196 | const rule = (try query(&vm, query_str)) orelse query_str; 197 | try render(rule, &vm, writer); 198 | } 199 | 200 | pub fn query(vm: *lib.Code.Vm, key: []const u8) !?[]const u8 { 201 | const program = vm.ctx.defs.get(key) orelse return null; 202 | const res = try vm.run(program); 203 | switch (res) { 204 | .str => |str| return str, 205 | else => { 206 | log.err("definition for '{s}' did not result in a format string", .{key}); 207 | return null; 208 | }, 209 | } 210 | } 211 | 212 | pub fn render(rule: []const u8, vm: *lib.Code.Vm, writer: anytype) !void { 213 | var i: usize = 0; 214 | var arg_i: u8 = 0; 215 | while (i < rule.len) { 216 | const start_index = i; 217 | while (i < rule.len) : (i += 1) { 218 | switch (rule[i]) { 219 | '{', '}' => break, 220 | else => {}, 221 | } 222 | } 223 | // Handle {{ and }}, those are un-escaped as single braces 224 | var unescape = false; 225 | if (i + 1 < rule.len and rule[i + 1] == rule[i]) { 226 | unescape = true; 227 | i += 1; 228 | } 229 | try writer.writeAll(rule[start_index..i]); 230 | if (unescape) { 231 | i += 1; 232 | continue; 233 | } 234 | 235 | if (i >= rule.len) break; 236 | 237 | // The parser validates these. 238 | assert(rule[i] == '{'); 239 | i += 1; 240 | 241 | const name_start = i; 242 | while (rule[i] != '}') : (i += 1) {} 243 | const name = rule[name_start..i]; 244 | assert(rule[i] == '}'); 245 | i += 1; 246 | 247 | const options = if (name.len == 0) 248 | vm.args[arg_i] 249 | else if (name.len == 1 and name[0] < max_format_args) 250 | vm.args[name[0]] 251 | else 252 | Argument{ 253 | .fmt_options = .{}, 254 | .kind = .decimal, 255 | .case = .lower, 256 | .base = 10, 257 | .val = vm.vars.get(name) orelse { 258 | try writer.print("[USE OF UNDEFINED VARIABLE %{s}]", .{name}); 259 | continue; 260 | }, 261 | }; 262 | 263 | arg_i += 1; 264 | switch (options.val) { 265 | .str, .preformatted => |str| try writer.writeAll(str), 266 | .bool => |b| try std.fmt.formatBuf(if (b) "true" else "false", options.fmt_options, writer), 267 | .int => |int| { 268 | try std.fmt.formatInt(int, options.base, options.case, options.fmt_options, writer); 269 | }, 270 | .float => |float| { 271 | var buf: [std.fmt.format_float.bufferSize(.decimal, f64)]u8 = undefined; 272 | 273 | const s = switch (options.kind) { 274 | .decimal => std.fmt.formatFloat(&buf, float, .{ .mode = .decimal, .precision = options.fmt_options.precision }) catch |err| switch (err) { 275 | error.BufferTooSmall => "(float)", 276 | }, 277 | .scientific => std.fmt.formatFloat(&buf, float, .{ .mode = .scientific, .precision = options.fmt_options.precision }) catch |err| switch (err) { 278 | error.BufferTooSmall => "(float)", 279 | }, 280 | .hex => hex: { 281 | var buf_stream = std.io.fixedBufferStream(&buf); 282 | std.fmt.formatFloatHexadecimal(float, options.fmt_options, buf_stream.writer()) catch |err| switch (err) { 283 | error.NoSpaceLeft => unreachable, 284 | }; 285 | break :hex buf_stream.getWritten(); 286 | }, 287 | }; 288 | return std.fmt.formatBuf(s, options.fmt_options, writer); 289 | }, 290 | .none => unreachable, 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Parser.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const assert = std.debug.assert; 4 | const log = std.log.scoped(.i18n); 5 | const math = std.math; 6 | const mem = std.mem; 7 | const lib = @import("lib.zig"); 8 | const Inst = lib.Code.Inst; 9 | 10 | const Parser = @This(); 11 | 12 | const max_format_args = @typeInfo(std.fmt.ArgSetType).int.bits; 13 | const ArgPos = enum(u8) { 14 | @"var" = 0xFF, 15 | _, 16 | 17 | inline fn toInt(ap: ArgPos) u5 { 18 | return @intCast(@intFromEnum(ap)); 19 | } 20 | }; 21 | 22 | ctx: *lib.Context, 23 | inst_buf: std.ArrayListUnmanaged(Inst.Ref) = .{}, 24 | arg_names: std.StringHashMapUnmanaged(ArgPos) = .{}, 25 | 26 | input: [:0]const u8, 27 | index: usize = 0, 28 | line: usize = 1, 29 | col: usize = 1, 30 | gpa: Allocator, 31 | 32 | pub fn parse(ctx: *lib.Context, input: [:0]const u8) !void { 33 | const gpa = ctx.arena.child_allocator; 34 | var parser = Parser{ .ctx = ctx, .gpa = gpa, .input = input }; 35 | defer { 36 | parser.inst_buf.deinit(gpa); 37 | parser.arg_names.deinit(gpa); 38 | } 39 | 40 | var warned = false; 41 | while (true) { 42 | parser.skipWhitespace(); 43 | if (parser.input[parser.index] == 0 and parser.index == parser.input.len) break; 44 | 45 | if (try parser.def()) { 46 | warned = false; 47 | } else { 48 | if (!warned) parser.warn("ignoring unexpected input", .{}); 49 | parser.col += 1; 50 | parser.index += 1; 51 | } 52 | } 53 | } 54 | 55 | fn addInst(p: *Parser, comptime op: Inst.Op, input: op.Data()) !Inst.Ref { 56 | return p.ctx.code.addInst(p.gpa, op, input); 57 | } 58 | 59 | fn skipWhitespace(p: *Parser) void { 60 | while (true) switch (p.input[p.index]) { 61 | '\n' => { 62 | p.col = 1; 63 | p.line += 1; 64 | p.index += 1; 65 | }, 66 | ' ', '\t', '\r' => { 67 | p.col += 1; 68 | p.index += 1; 69 | }, 70 | '#' => while (true) switch (p.input[p.index]) { 71 | 0 => return, 72 | '\n' => { 73 | p.col = 1; 74 | p.line += 1; 75 | p.index += 1; 76 | break; 77 | }, 78 | else => { 79 | p.col += 1; 80 | p.index += 1; 81 | }, 82 | }, 83 | else => return, 84 | }; 85 | } 86 | 87 | fn warn(p: Parser, comptime fmt: []const u8, args: anytype) void { 88 | log.warn(fmt ++ " at line {d}, column {d}", args ++ .{ p.line, p.col }); 89 | } 90 | 91 | fn word(p: *Parser, s: []const u8) bool { 92 | if (!mem.startsWith(u8, p.input[p.index..], s)) 93 | return false; 94 | switch (p.input[p.index + s.len]) { 95 | 'a'...'z', 'A'...'Z', '0'...'9', '_' => return false, 96 | else => {}, 97 | } 98 | p.index += s.len; 99 | p.col += s.len; 100 | p.skipWhitespace(); 101 | return true; 102 | } 103 | 104 | fn def(p: *Parser) !bool { 105 | if (!p.word("def")) return false; 106 | 107 | const strings = &p.ctx.code.strings; 108 | const start_len = strings.items.len; 109 | var opt_def_name = try p.str(.collect); 110 | var gop: @TypeOf(p.ctx.defs).GetOrPutResult = undefined; 111 | if (opt_def_name) |def_name| { 112 | // TODO handle this better 113 | const name_str = blk: { 114 | const data = p.ctx.code.insts.items(.data)[@intFromEnum(def_name)]; 115 | const offset = @intFromEnum(data.lhs); 116 | const len = @intFromEnum(data.rhs); 117 | const name_str = strings.items[offset..][0..len]; 118 | break :blk try p.gpa.dupe(u8, name_str); 119 | }; 120 | 121 | strings.items.len = start_len; 122 | gop = try p.ctx.defs.getOrPut(p.gpa, name_str); 123 | if (gop.found_existing) { 124 | p.warn("ignoring duplicate definition for '{s}'", .{name_str}); 125 | opt_def_name = null; 126 | p.gpa.free(name_str); 127 | } 128 | } else { 129 | p.warn("ignoring unnamed definition", .{}); 130 | } 131 | errdefer if (opt_def_name) |_| p.ctx.defs.removeByPtr(gop.key_ptr); 132 | 133 | p.inst_buf.items.len = 0; 134 | var warned = false; 135 | while (true) { 136 | p.skipWhitespace(); 137 | if (p.input[p.index] == 0 and p.index == p.input.len) { 138 | p.warn("unexpected EOF inside definition", .{}); 139 | break; 140 | } 141 | if (p.word("end")) break; 142 | if (try p.stmt()) continue; 143 | if (!warned) { 144 | warned = true; 145 | p.warn("ignoring unexpected statement", .{}); 146 | } 147 | p.col += 1; 148 | p.index += 1; 149 | } 150 | if (opt_def_name == null) return true; 151 | 152 | const end_inst = try p.addInst(.end, {}); 153 | try p.inst_buf.append(p.gpa, end_inst); 154 | const body: u32 = @intCast(p.ctx.code.extra.items.len); 155 | try p.ctx.code.extra.appendSlice(p.gpa, @ptrCast(p.inst_buf.items)); 156 | gop.value_ptr.* = .{ .body = body }; 157 | return true; 158 | } 159 | 160 | const ArgMode = enum { collect, check }; 161 | 162 | fn str(p: *Parser, arg_mode: ArgMode) !?Inst.Ref { 163 | if (arg_mode == .collect) p.arg_names.clearRetainingCapacity(); 164 | if (p.input[p.index] != '"') return null; 165 | 166 | const start = p.index + 1; 167 | var escape = true; 168 | while (true) { 169 | const c = p.input[p.index]; 170 | if (c == 0) { 171 | p.warn("invalid null byte in string", .{}); 172 | return null; 173 | } 174 | p.index += 1; 175 | if (escape) { 176 | escape = false; 177 | } else if (c == '\\') { 178 | escape = true; 179 | } else if (c == '"') { 180 | break; 181 | } 182 | } 183 | 184 | const strings = &p.ctx.code.strings; 185 | const offset = strings.items.len; 186 | const slice = p.input[start..p.index]; 187 | try strings.ensureUnusedCapacity(p.gpa, slice.len); 188 | var i: usize = 0; 189 | while (true) { 190 | switch (slice[i]) { 191 | '\\' => { 192 | const escape_char_index = i + 1; 193 | const result = std.zig.string_literal.parseEscapeSequence(slice, &i); 194 | p.col += (i - escape_char_index) + 1; 195 | switch (result) { 196 | .success => |codepoint| { 197 | if (slice[escape_char_index] == 'u') { 198 | var buf: [4]u8 = undefined; 199 | const len = std.unicode.utf8Encode(codepoint, &buf) catch { 200 | p.warn("invalid unicode codepoint in escape sequence", .{}); 201 | continue; 202 | }; 203 | strings.appendSliceAssumeCapacity(buf[0..len]); 204 | } else { 205 | strings.appendAssumeCapacity(@intCast(codepoint)); 206 | } 207 | }, 208 | .failure => { 209 | p.warn("invalid escape sequence", .{}); 210 | continue; 211 | }, 212 | } 213 | }, 214 | '\n' => { 215 | strings.appendAssumeCapacity('\n'); 216 | p.line += 1; 217 | p.col = 1; 218 | i += 1; 219 | }, 220 | '"' => break, 221 | '{', '}' => try p.strArg(slice, &i, arg_mode), 222 | else => |c| { 223 | strings.appendAssumeCapacity(c); 224 | p.col += 1; 225 | i += 1; 226 | }, 227 | } 228 | } 229 | 230 | const ref: Inst.Ref = @enumFromInt(p.ctx.code.insts.len); 231 | try p.ctx.code.insts.append(p.gpa, .{ 232 | .op = .str, 233 | .data = .{ 234 | .lhs = @enumFromInt(offset), 235 | .rhs = @enumFromInt(strings.items.len - offset), 236 | }, 237 | }); 238 | return ref; 239 | } 240 | 241 | fn strArg(p: *Parser, slice: []const u8, offset: *usize, arg_mode: ArgMode) !void { 242 | const strings = &p.ctx.code.strings; 243 | const c = slice[offset.*]; 244 | if (c == slice[offset.* + 1]) { 245 | strings.appendSliceAssumeCapacity("{{"); 246 | p.col += 2; 247 | offset.* += 2; 248 | return; 249 | } 250 | if (c == '}') { 251 | p.warn("unescaped '}}' in string", .{}); 252 | strings.appendSliceAssumeCapacity("{{"); 253 | p.col += 1; 254 | offset.* += 1; 255 | return; 256 | } 257 | if (slice[offset.* + 1] != '%') { 258 | p.warn("expected '%' after '{{'", .{}); 259 | strings.appendSliceAssumeCapacity("{{"); 260 | p.col += 1; 261 | offset.* += 1; 262 | return; 263 | } 264 | offset.* += 2; 265 | const start = offset.*; 266 | while (true) switch (slice[offset.*]) { 267 | '0'...'9', 'a'...'z', 'A'...'Z', '_' => { 268 | offset.* += 1; 269 | p.col += 1; 270 | }, 271 | '}' => break, 272 | else => { 273 | p.warn("invalid character in argument name", .{}); 274 | return; 275 | }, 276 | }; 277 | const arg_name = slice[start..offset.*]; 278 | offset.* += 1; 279 | p.col += 1; 280 | 281 | if (arg_mode == .collect) { 282 | if (p.arg_names.size >= max_format_args) { 283 | p.warn("too many arguments, max {d}", .{max_format_args}); 284 | return; 285 | } 286 | 287 | const pos: ArgPos = @enumFromInt(p.arg_names.size); 288 | const gop = try p.arg_names.getOrPut(p.gpa, arg_name); 289 | if (gop.found_existing) { 290 | p.warn("redeclaration of argument '%{s}'", .{arg_name}); 291 | } else { 292 | gop.value_ptr.* = pos; 293 | } 294 | strings.appendSliceAssumeCapacity("{}"); 295 | return; 296 | } else if (!p.arg_names.contains(arg_name)) { 297 | p.warn("use of undefined argument '%{s}'", .{arg_name}); 298 | 299 | const undef = "[UNDEFINED ARGUMENT %"; 300 | try strings.ensureTotalCapacity(p.gpa, strings.capacity + undef.len); 301 | 302 | strings.appendSliceAssumeCapacity(undef); 303 | strings.appendSliceAssumeCapacity(arg_name); 304 | strings.appendSliceAssumeCapacity("]"); 305 | return; 306 | } 307 | 308 | strings.appendAssumeCapacity('{'); 309 | if (p.arg_names.get(arg_name)) |pos| if (pos != .@"var") { 310 | strings.appendAssumeCapacity(@intFromEnum(pos)); 311 | strings.appendAssumeCapacity('}'); 312 | return; 313 | }; 314 | strings.appendSliceAssumeCapacity(arg_name); 315 | strings.appendAssumeCapacity('}'); 316 | } 317 | 318 | fn arg(p: *Parser) !?[]const u8 { 319 | if (p.input[p.index] != '%') return null; 320 | p.col += 1; 321 | p.index += 1; 322 | 323 | const start = p.index; 324 | while (true) switch (p.input[p.index]) { 325 | '0'...'9', 'a'...'z', 'A'...'Z', '_' => { 326 | p.col += 1; 327 | p.index += 1; 328 | }, 329 | else => break, 330 | }; 331 | if (start == p.index) { 332 | p.warn("expected argument name after '%'", .{}); 333 | return null; 334 | } 335 | return p.input[start..p.index]; 336 | } 337 | 338 | fn stmt(p: *Parser) Allocator.Error!bool { 339 | if (p.word("set")) { 340 | const dest = try p.arg(); 341 | var pos: ArgPos = .@"var"; 342 | if (dest) |some| { 343 | const gop = try p.arg_names.getOrPut(p.gpa, some); 344 | if (!gop.found_existing) { 345 | gop.value_ptr.* = pos; 346 | } else { 347 | pos = gop.value_ptr.*; 348 | } 349 | } else { 350 | p.warn("expected argument name after 'set'", .{}); 351 | } 352 | p.skipWhitespace(); 353 | if (!p.word("to")) { 354 | p.warn("expected 'to' after argument to set", .{}); 355 | } 356 | const val = (try p.expr()) orelse { 357 | p.warn("expected expression after 'to'", .{}); 358 | return false; 359 | }; 360 | if (dest == null) { 361 | return false; 362 | } 363 | const inst = if (pos == .@"var") try p.addInst(.set_var, .{ 364 | .@"var" = dest.?, 365 | .operand = val, 366 | }) else try p.addInst(.set_arg, .{ 367 | .pos = pos.toInt(), 368 | .operand = val, 369 | }); 370 | try p.inst_buf.append(p.gpa, inst); 371 | return true; 372 | } else if (p.word("if")) { 373 | return p.ifBody(); 374 | } else if (try p.str(.check)) |inst| { 375 | try p.inst_buf.append(p.gpa, inst); 376 | return true; 377 | } else { 378 | return false; 379 | } 380 | } 381 | 382 | fn ifBody(p: *Parser) !bool { 383 | const cond = (try p.expr()) orelse { 384 | p.warn("expected if condition", .{}); 385 | return false; 386 | }; 387 | var start = p.inst_buf.items.len; 388 | defer p.inst_buf.items.len = start; 389 | 390 | var warned = false; 391 | var then_body: ?u32 = null; 392 | while (true) { 393 | p.skipWhitespace(); 394 | if (p.input[p.index] == 0 and p.index == p.input.len) { 395 | p.warn("unexpected EOF inside 'if'", .{}); 396 | break; 397 | } 398 | if (p.word("end")) break; 399 | if (p.word("elseif")) { 400 | then_body = try p.finishBody(start); 401 | _ = try p.ifBody(); 402 | break; 403 | } 404 | if (p.word("else")) { 405 | if (then_body != null) { 406 | p.warn("ignoring duplicate 'else'", .{}); 407 | continue; 408 | } 409 | then_body = try p.finishBody(start); 410 | } 411 | if (try p.stmt()) continue; 412 | if (!warned) { 413 | warned = true; 414 | p.warn("ignoring unexpected statement", .{}); 415 | } 416 | p.col += 1; 417 | p.index += 1; 418 | } 419 | const else_body = try p.finishBody(start); 420 | const @"if" = try p.addInst(.@"if", .{ 421 | .cond = cond, 422 | .then_body = then_body orelse else_body, 423 | .else_body = if (then_body != null) else_body else 0, 424 | }); 425 | try p.inst_buf.append(p.gpa, @"if"); 426 | start += 1; 427 | return true; 428 | } 429 | 430 | fn finishBody(p: *Parser, start: usize) !u32 { 431 | const index: u32 = @intCast(p.ctx.code.extra.items.len); 432 | try p.inst_buf.append(p.gpa, try p.addInst(.end, {})); 433 | try p.ctx.code.extra.appendSlice(p.gpa, @ptrCast(p.inst_buf.items[start..])); 434 | p.inst_buf.items.len = start; 435 | return index; 436 | } 437 | 438 | fn expr(p: *Parser) !?Inst.Ref { 439 | if (p.word("true")) { 440 | return try p.addInst(.bool, true); 441 | } else if (p.word("false")) { 442 | return try p.addInst(.bool, false); 443 | } else if (try p.str(.check)) |some| { 444 | return some; 445 | } else if (try p.arg()) |arg_name| { 446 | const pos = p.arg_names.get(arg_name) orelse { 447 | p.warn("use of undefined argument '%{s}'", .{arg_name}); 448 | return null; 449 | }; 450 | if (pos == .@"var") { 451 | return try p.addInst(.@"var", arg_name); 452 | } else { 453 | return try p.addInst(.arg, pos.toInt()); 454 | } 455 | } else { 456 | // TODO numbers 457 | return null; 458 | } 459 | } 460 | --------------------------------------------------------------------------------