From 8ce3e3c62a22d0e70df4f241ff28de6f6cae78d3 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 19:01:38 -0300 Subject: An Edit with an unclosed { is refused, not run Edit ,x/area_of/{ answered ok and did nothing: the group ran out of text and ran what it had. sam reads on for the }; an Edit has nothing more to read, so it now fails with unmatched `{', in the words sam uses for the other side. Co-Authored-By: Claude Opus 5.5 --- src/sam_edit.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/sam_edit.zig b/src/sam_edit.zig index 40221628..e8e90b67 100644 --- a/src/sam_edit.zig +++ b/src/sam_edit.zig @@ -95,6 +95,8 @@ const Parser = struct { i: usize = 0, /// The last pattern given, for an empty one (edit.c:562, getregexp). lastpat: []const u8 = "", + /// The last `parse` that answered null met a `}`, not the end. + braced: bool = false, why: *Why, fn getch(ps: *Parser) ?u8 { @@ -326,12 +328,18 @@ const Parser = struct { var last: ?*Cmd = null; while (true) { if (ps.skipbl() == '\n') ps.i += 1; - const next = (try ps.parse(nest + 1)) orelse break; + ps.braced = false; + const next = (try ps.parse(nest + 1)) orelse { + // sam reads on for the `}`; an Edit has no more to read. + if (!ps.braced) return fail(ps.why, "unmatched `{{'", .{}); + break; + }; if (last) |l| l.next = next else cmd.sub = next; last = next; } }, '}' => { + ps.braced = true; try ps.atnl(); if (nest == 0) return fail(ps.why, "right brace with no left brace", .{}); return null; @@ -655,6 +663,8 @@ test "an Edit that fails halfway changes nothing, and says why in acme's words" .{ "k", "unknown command k" }, .{ ",x/a/{\na/1/\ni/2/\n}", "changes out of sequence" }, .{ "}", "right brace with no left brace" }, + .{ ",x/foo/{", "unmatched `{'" }, + .{ ",x/foo/{\nd", "unmatched `{'" }, .{ "w /tmp/x", "w is not supported in pardes" }, .{ ",s/(a)/\\1/", "no \\1: mvzr keeps no submatches" }, .{ "1 m 1,2", "move overlaps itself" }, -- cgit v1.3