diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-05 20:21:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | de4def3548a6729b0dfd2120495a61beef8c8c2c (patch) | |
| tree | 12144f0f64dc96bb4741459c1f9db57f44349330 /tutor_gen.zig | |
| parent | 7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff) | |
| download | pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip | |
pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options).
Diffstat (limited to 'tutor_gen.zig')
| -rw-r--r-- | tutor_gen.zig | 386 |
1 files changed, 0 insertions, 386 deletions
diff --git a/tutor_gen.zig b/tutor_gen.zig deleted file mode 100644 index 09875e64..00000000 --- a/tutor_gen.zig +++ /dev/null @@ -1,386 +0,0 @@ -const std = @import("std"); -const modal = @import("modal.zig"); - -// tutor_gen: a separate binary (NOT comptime) that parses tutor.txt and writes -// tutor_cases.zig — a file of standard Zig `test` blocks, one per practice block. -// `zig build tutor-gen` runs it (writing the file); `zig build unit-test` then -// compiles+runs those tests against modal.stepFile. -// -// Format of tutor.txt practice blocks (all directive lines start with "# "): -// # keys: l l l i X esc <- space-separated keystroke tokens -// # at: 0,4 <- optional starting cursor "row,col" (default 0,0) -// # before <- the lines below are the starting buffer -// <clean line> -// # after <- the lines below are the expected buffer -// <clean line> -// Everything not under a # before/# after is prose (ignored). A `# keys:` starts -// a new case; cases end at the next `# keys:` or a non-practice line. -// -// Keystroke tokens: each char of a token is typed in turn, EXCEPT the named -// specials `esc`, `enter`, `bs` (backspace), `left/right/up/down`, `home/end`, -// `pgup/pgdn`. `Ctrl-d`/`Ctrl-u`/`Ctrl-f`/`Ctrl-b` map to half/full page. - -const Case = struct { - keys: []const u8, - at: ?[]const u8 = null, - before: []const u8, - after: []const u8, -}; - -fn parseCases(alloc: std.mem.Allocator, src: []const u8) ![]Case { - var cases: std.ArrayList(Case) = .empty; - var it = std.mem.splitScalar(u8, src, '\n'); - // accumulate the current case - var cur_keys: ?[]const u8 = null; - var cur_at: ?[]const u8 = null; - var before: ?std.ArrayList(u8) = null; - var after: ?std.ArrayList(u8) = null; - // which section is actively collecting content (so # after stops before from - // absorbing the after lines). `none` = neither. - var section: enum { none, before, after } = .none; - defer if (before) |*b| b.deinit(alloc); - defer if (after) |*a| a.deinit(alloc); - // NOTE: cur_keys / cur_at ownership is handled explicitly at each flush/drop - // (moved into a case, or freed + nulled). We do NOT defer-free them here: - // a defer would double-free a value that was moved into a case (the move - // leaves the local pointing at the now-case-owned memory until it's nulled, - // and a function-exit defer can't tell). - - while (it.next()) |raw_line| { - // Practice-block lines in tutor.txt are indented by one leading space - // (the tutor's prose style). Strip exactly that one space so directives - // match AND content lines don't carry the indent into the buffer. - var line = raw_line; - if (line.len > 0 and line[0] == ' ') line = line[1..]; - line = std.mem.trimEnd(u8, line, "\r"); - if (std.mem.startsWith(u8, line, "# keys:")) { - // flush previous case - if (cur_keys != null and before != null and after != null) { - try cases.append(alloc, .{ - .keys = cur_keys.?, - .at = cur_at, - .before = try before.?.toOwnedSlice(alloc), - .after = try after.?.toOwnedSlice(alloc), - }); - before = null; - after = null; - } else if (cur_keys != null) { - // a # keys: with no before/after — drop it (incomplete): free the - // owned keys/at so they don't leak. - if (cur_keys) |k| alloc.free(k); - if (cur_at) |at| alloc.free(at); - before = null; - after = null; - } - cur_keys = try alloc.dupe(u8, std.mem.trim(u8, line["# keys:".len..], " ")); - // cur_at from a prior case was either moved (flushed) or freed (drop); - // start fresh. - cur_at = null; - before = null; - after = null; - section = .none; - continue; - } - if (std.mem.startsWith(u8, line, "# at:")) { - if (cur_at) |old| alloc.free(old); - cur_at = try alloc.dupe(u8, std.mem.trim(u8, line["# at:".len..], " ")); - continue; - } - if (std.mem.startsWith(u8, line, "# before")) { - before = .empty; - after = null; - section = .before; - continue; - } - if (std.mem.startsWith(u8, line, "# after")) { - after = .empty; - section = .after; - continue; - } - // any other "# ..." directive that isn't keys/at/before/after ends a case - if (std.mem.startsWith(u8, line, "#")) { - if (cur_keys != null and before != null and after != null and after.?.items.len > 0) { - try cases.append(alloc, .{ - .keys = cur_keys.?, - .at = cur_at, - .before = try before.?.toOwnedSlice(alloc), - .after = try after.?.toOwnedSlice(alloc), - }); - } else { - // incomplete: free the owned keys/at + any partial content - if (cur_keys) |k| alloc.free(k); - if (cur_at) |at| alloc.free(at); - if (before) |*b| b.deinit(alloc); - if (after) |*a| a.deinit(alloc); - } - cur_keys = null; - cur_at = null; - before = null; - after = null; - section = .none; - continue; - } - // a blank line: if we have a complete case (keys + before + after with - // content), flush it now — the practice block is delimited by blank lines - // in the tutor, so prose after the blank line must NOT be absorbed. - if (line.len == 0) { - if (cur_keys != null and before != null and after != null and after.?.items.len > 0) { - try cases.append(alloc, .{ - .keys = cur_keys.?, - .at = cur_at, - .before = try before.?.toOwnedSlice(alloc), - .after = try after.?.toOwnedSlice(alloc), - }); - cur_keys = null; - before = null; - after = null; - section = .none; - } else { - section = .none; // a blank line closes the active section - } - continue; - } - // a content line: belongs to the ACTIVE section only - if (section == .before) { - if (before) |*b| { - if (b.items.len > 0) try b.append(alloc, '\n'); - try b.appendSlice(alloc, line); - } - continue; - } - if (section == .after) { - if (after) |*a| { - if (a.items.len > 0) try a.append(alloc, '\n'); - try a.appendSlice(alloc, line); - } - continue; - } - // prose line, ignore - } - // flush trailing case - if (cur_keys != null and before != null and after != null and after.?.items.len > 0) { - try cases.append(alloc, .{ - .keys = cur_keys.?, - .at = cur_at, - .before = try before.?.toOwnedSlice(alloc), - .after = try after.?.toOwnedSlice(alloc), - }); - // ownership transferred; clear so the defers above don't double-free. - cur_keys = null; - cur_at = null; - before = null; - after = null; - } - return cases.toOwnedSlice(alloc); -} - -// Emit a Zig string literal for `s`, escaping per Zig string syntax. -fn emitStringLiteral(out: *std.ArrayList(u8), alloc: std.mem.Allocator, s: []const u8) !void { - try out.append(alloc, '"'); - for (s) |c| { - switch (c) { - '"' => try out.appendSlice(alloc, "\\\""), - '\\' => try out.appendSlice(alloc, "\\\\"), - '\n' => try out.appendSlice(alloc, "\\n"), - '\r' => try out.appendSlice(alloc, "\\r"), - '\t' => try out.appendSlice(alloc, "\\t"), - else => { - if (c >= 0x20 and c < 0x7f) { - try out.append(alloc, c); - } else { - try out.print(alloc, "\\x{x:0>2}", .{c}); - } - }, - } - } - try out.append(alloc, '"'); -} - -fn writeCases(io: std.Io, alloc: std.mem.Allocator, cases: []const Case, out_path: []const u8) !void { - // Build the generated source in a buffer, then write it in one shot. - var out: std.ArrayList(u8) = .empty; - defer out.deinit(alloc); - - try out.appendSlice(alloc, "// AUTO-GENERATED by tutor_gen from tutor.txt. DO NOT EDIT.\n"); - try out.appendSlice(alloc, "// One `test` block per practice block in the tutor. Re-run: zig build tutor-gen\n"); - try out.appendSlice(alloc, "const std = @import(\"std\");\n"); - try out.appendSlice(alloc, "const modal = @import(\"modal\");\n\n"); - - for (cases, 0..) |c, i| { - // Derive a short human label from the keys for the test name. Sanitize - // it to a valid Zig identifier fragment (spaces/specials -> '_'). - var name_buf: [128]u8 = undefined; - const printed = std.fmt.bufPrint(&name_buf, "tutor_case_{d:0>2}_{s}", .{ i, c.keys }) catch "tutor_case"; - var san_buf: [128]u8 = undefined; - const san_len = @min(printed.len, san_buf.len); - for (printed[0..san_len], 0..) |ch, j| { - san_buf[j] = switch (ch) { - 'a'...'z', 'A'...'Z', '0'...'9', '_' => ch, - else => '_', - }; - } - const name_san = san_buf[0..san_len]; - try out.print(alloc, "test \"{s}\" {{\n", .{name_san}); - try out.appendSlice(alloc, " const a = std.testing.allocator;\n"); - try out.appendSlice(alloc, " var yank: ?[]u8 = null;\n"); - try out.appendSlice(alloc, " defer if (yank) |y| a.free(y);\n"); - try out.appendSlice(alloc, " var scroll: usize = 0;\n"); - try out.appendSlice(alloc, " var st = modal.FileState{};\n"); - try out.appendSlice(alloc, " const view = modal.View{ .rows = 200, .scroll = &scroll };\n"); - - // starting content - try out.appendSlice(alloc, " var content: []u8 = try a.dupe(u8, "); - try emitStringLiteral(&out, alloc, c.before); - try out.appendSlice(alloc, ");\n"); - try out.appendSlice(alloc, " defer a.free(content);\n"); - - // starting cursor - if (c.at) |at| { - try out.print(alloc, " st.cur = .{{ .row = {s}, .col = {s} }};\n", .{ - splitField(at, 0), - splitField(at, 1), - }); - } - - // keystrokes - var tit = std.mem.tokenizeAny(u8, c.keys, " "); - while (tit.next()) |tok| { - try emitKey(&out, alloc, tok); - } - - // assertion - try out.appendSlice(alloc, " try std.testing.expectEqualStrings("); - try emitStringLiteral(&out, alloc, c.after); - try out.appendSlice(alloc, ", content);\n"); - try out.appendSlice(alloc, "}\n\n"); - } - try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out_path, .data = out.items }); -} - -// "row,col" -> the row part (idx 0) or col part (idx 1). -fn splitField(at: []const u8, idx: usize) []const u8 { - var it = std.mem.splitScalar(u8, at, ','); - var i: usize = 0; - while (it.next()) |part| : (i += 1) { - if (i == idx) return std.mem.trim(u8, part, " "); - } - return "0"; -} - -// Emit the stepFile calls for one keystroke token. -// One stepFile call, as a generated line. `key_expr` is the Zig Key payload -// expression (e.g. `.esc`, `.ch = 'x'`). -fn emitStep(out: *std.ArrayList(u8), alloc: std.mem.Allocator, key_expr: []const u8) !void { - try out.print(alloc, " {{ const n = try modal.stepFile(a, content, &st, view, {s}, &yank); if (n) |nn| {{ a.free(content); content = nn; }} }}\n", .{key_expr}); -} - -fn emitKey(out: *std.ArrayList(u8), alloc: std.mem.Allocator, tok: []const u8) !void { - // named specials -> a single stepFile with that key - if (std.mem.eql(u8, tok, "esc")) return emitStep(out, alloc, ".esc"); - if (std.mem.eql(u8, tok, "enter")) return emitStep(out, alloc, ".enter"); - if (std.mem.eql(u8, tok, "bs")) return emitStep(out, alloc, ".backspace"); - if (std.mem.eql(u8, tok, "left")) return emitStep(out, alloc, ".left"); - if (std.mem.eql(u8, tok, "right")) return emitStep(out, alloc, ".right"); - if (std.mem.eql(u8, tok, "up")) return emitStep(out, alloc, ".up"); - if (std.mem.eql(u8, tok, "down")) return emitStep(out, alloc, ".down"); - if (std.mem.eql(u8, tok, "home")) return emitStep(out, alloc, ".home"); - if (std.mem.eql(u8, tok, "end")) return emitStep(out, alloc, ".end"); - if (std.mem.eql(u8, tok, "pgdn") or std.mem.eql(u8, tok, "pagedown")) return emitStep(out, alloc, ".page_down"); - if (std.mem.eql(u8, tok, "pgup") or std.mem.eql(u8, tok, "pageup")) return emitStep(out, alloc, ".page_up"); - if (std.mem.eql(u8, tok, "Ctrl-d")) return emitStep(out, alloc, ".half_down"); - if (std.mem.eql(u8, tok, "Ctrl-u")) return emitStep(out, alloc, ".half_up"); - if (std.mem.eql(u8, tok, "Ctrl-f")) return emitStep(out, alloc, ".page_down"); - if (std.mem.eql(u8, tok, "Ctrl-b")) return emitStep(out, alloc, ".page_up"); - // otherwise: each byte of the token is a typed char (in order) - for (tok) |c| { - var expr_buf: [32]u8 = undefined; - const expr = std.fmt.bufPrint(&expr_buf, ".{{ .ch = {d} }}", .{c}) catch return; - try emitStep(out, alloc, expr); - } -} - -pub fn main(init: std.process.Init) !void { - const io = init.io; - const gpa = init.gpa; - const args = try init.minimal.args.toSlice(init.arena.allocator()); - const in_path = if (args.len > 1) args[1] else "tutor.txt"; - const out_path = if (args.len > 2) args[2] else "tutor_cases.zig"; - - const src = try std.Io.Dir.cwd().readFileAlloc(io, in_path, gpa, .limited(1 << 20)); - defer gpa.free(src); - - const cases = try parseCases(gpa, src); - defer { - for (cases) |c| { - gpa.free(c.keys); - if (c.at) |at| gpa.free(at); - gpa.free(c.before); - gpa.free(c.after); - } - gpa.free(cases); - } - - try writeCases(io, gpa, cases, out_path); - std.debug.print("tutor_gen: wrote {d} test cases to {s}\n", .{ cases.len, out_path }); -} - -// ---- self-test: the parser on tiny fixtures ---- -test "parseCases extracts a before/after block" { - const a = std.testing.allocator; - const src = - \\# keys: l l l - \\# before - \\abcdef - \\# after - \\abcdef - \\ - ; - const cases = try parseCases(a, src); - defer { - for (cases) |c| { - a.free(c.keys); - if (c.at) |at| a.free(at); - a.free(c.before); - a.free(c.after); - } - a.free(cases); - } - try std.testing.expectEqual(@as(usize, 1), cases.len); - try std.testing.expectEqualStrings("l l l", cases[0].keys); - try std.testing.expectEqualStrings("abcdef", cases[0].before); - try std.testing.expectEqualStrings("abcdef", cases[0].after); -} - -test "parseCases handles # at: and multiple cases" { - const a = std.testing.allocator; - const src = - \\# keys: b i Z esc - \\# at: 0,4 - \\# before - \\foo bar - \\# after - \\Zfoo bar - \\ - \\# keys: x d - \\# before - \\keep - \\gone - \\# after - \\keep - \\ - ; - const cases = try parseCases(a, src); - defer { - for (cases) |c| { - a.free(c.keys); - if (c.at) |at| a.free(at); - a.free(c.before); - a.free(c.after); - } - a.free(cases); - } - try std.testing.expectEqual(@as(usize, 2), cases.len); - try std.testing.expectEqualStrings("0,4", cases[0].at.?); - try std.testing.expectEqualStrings("keep\ngone", cases[1].before); - try std.testing.expectEqualStrings("keep", cases[1].after); -} |
