From cc59dd64b9ccf7f5ccf3a77b73b25a97d357f58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Wed, 13 May 2026 12:40:07 +0200 Subject: [PATCH] script: make ScriptIterator.next fallible --- src/agent/Agent.zig | 7 ++++- src/script.zig | 8 +++--- src/script/Command.zig | 58 +++++++++++++++++++++--------------------- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 48dcbfdd..689ac112 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -520,7 +520,12 @@ fn runScript(self: *Self, path: []const u8) bool { var last_comment: ?[]const u8 = null; var replacements: std.ArrayList(Replacement) = .empty; - while (iter.next()) |entry| { + while (true) { + const entry = (iter.next() catch |err| { + self.terminal.printErrorFmt("line {d}: {s} parsing script", .{ iter.line_num, @errorName(err) }); + self.flushReplacements(path, content, replacements.items); + return false; + }) orelse break; switch (entry.command) { .comment => { // Recorded scripts prefix LLM-generated commands with the diff --git a/src/script.zig b/src/script.zig index 2fa6b761..05d5f3f8 100644 --- a/src/script.zig +++ b/src/script.zig @@ -324,14 +324,14 @@ test "applyReplacements: heals a multi-line EVAL block using iterator span" { "CLICK '#after'\n"; var iter: Command.ScriptIterator = .init(std.testing.allocator, content); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .goto); - const e2 = iter.next().?; + const e2 = (try iter.next()).?; try std.testing.expect(e2.command == .eval_js); defer std.testing.allocator.free(e2.command.eval_js); - const e3 = iter.next().?; + const e3 = (try iter.next()).?; try std.testing.expect(e3.command == .click); - try std.testing.expect(iter.next() == null); + try std.testing.expect((try iter.next()) == null); const replacements = [_]Replacement{.{ .original_span = e2.raw_span, diff --git a/src/script/Command.zig b/src/script/Command.zig index f0386f20..9dc15d78 100644 --- a/src/script/Command.zig +++ b/src/script/Command.zig @@ -350,7 +350,7 @@ pub const ScriptIterator = struct { }; /// Multi-line EVAL / EXTRACT blocks are assembled into a single command. - pub fn next(self: *ScriptIterator) ?Entry { + pub fn next(self: *ScriptIterator) std.mem.Allocator.Error!?Entry { while (self.lines.next()) |line| { self.line_num += 1; const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); @@ -360,7 +360,7 @@ pub const ScriptIterator = struct { if (BlockKeyword.fromOpener(trimmed)) |opener| { const start_line = self.line_num; - const body_or_null = self.collectMultiLineBlock(opener.quote_type); + const body_or_null = try self.collectMultiLineBlock(opener.quote_type); const span_end = self.lines.index orelse self.lines.buffer.len; const cmd: Command = switch (opener.kind) { .eval => if (body_or_null) |body| .{ .eval_js = body } else .{ .natural_language = "unterminated EVAL block" }, @@ -400,7 +400,7 @@ pub const ScriptIterator = struct { } }; - fn collectMultiLineBlock(self: *ScriptIterator, quote_type: QuoteType) ?[]const u8 { + fn collectMultiLineBlock(self: *ScriptIterator, quote_type: QuoteType) std.mem.Allocator.Error!?[]const u8 { const closer = quote_type.toLiteral(); var parts: std.ArrayList(u8) = .empty; // toOwnedSlice empties `parts`, so this defer is a no-op on success. @@ -409,12 +409,12 @@ pub const ScriptIterator = struct { self.line_num += 1; const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); if (std.mem.eql(u8, trimmed, closer)) { - return parts.toOwnedSlice(self.allocator) catch null; + return try parts.toOwnedSlice(self.allocator); } if (parts.items.len > 0) { - parts.append(self.allocator, '\n') catch return null; + try parts.append(self.allocator, '\n'); } - parts.appendSlice(self.allocator, line) catch return null; + try parts.appendSlice(self.allocator, line); } return null; } @@ -1000,17 +1000,17 @@ test "ScriptIterator basic commands" { ; var iter: ScriptIterator = .init(std.testing.allocator, script); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expectEqualStrings("https://example.com", e1.command.goto); try std.testing.expectEqual(@as(u32, 1), e1.line_num); - const e2 = iter.next().?; + const e2 = (try iter.next()).?; try std.testing.expect(e2.command == .tree); - const e3 = iter.next().?; + const e3 = (try iter.next()).?; try std.testing.expectEqualStrings("Login", e3.command.click); - try std.testing.expect(iter.next() == null); + try std.testing.expect((try iter.next()) == null); } test "ScriptIterator skips blank lines and comments" { @@ -1023,19 +1023,19 @@ test "ScriptIterator skips blank lines and comments" { ; var iter: ScriptIterator = .init(std.testing.allocator, script); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .comment); - const e2 = iter.next().?; + const e2 = (try iter.next()).?; try std.testing.expect(e2.command == .goto); - const e3 = iter.next().?; + const e3 = (try iter.next()).?; try std.testing.expect(e3.command == .comment); - const e4 = iter.next().?; + const e4 = (try iter.next()).?; try std.testing.expect(e4.command == .tree); - try std.testing.expect(iter.next() == null); + try std.testing.expect((try iter.next()) == null); } test "ScriptIterator multi-line EVAL" { @@ -1050,19 +1050,19 @@ test "ScriptIterator multi-line EVAL" { ; var iter: ScriptIterator = .init(std.testing.allocator, script); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .goto); - const e2 = iter.next().?; + const e2 = (try iter.next()).?; try std.testing.expect(e2.command == .eval_js); try std.testing.expect(std.mem.indexOf(u8, e2.command.eval_js, "const x = 1;") != null); try std.testing.expect(std.mem.indexOf(u8, e2.command.eval_js, "return x + y;") != null); defer std.testing.allocator.free(e2.command.eval_js); - const e3 = iter.next().?; + const e3 = (try iter.next()).?; try std.testing.expect(e3.command == .tree); - try std.testing.expect(iter.next() == null); + try std.testing.expect((try iter.next()) == null); } test "ScriptIterator unterminated EVAL" { @@ -1072,7 +1072,7 @@ test "ScriptIterator unterminated EVAL" { ; var iter: ScriptIterator = .init(std.testing.allocator, script); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .natural_language); try std.testing.expectEqualStrings("unterminated EVAL block", e1.command.natural_language); } @@ -1086,15 +1086,15 @@ test "ScriptIterator inline triple-quoted EVAL stays single-line" { ; var iter: ScriptIterator = .init(std.testing.allocator, script); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .eval_js); try std.testing.expectEqualStrings("console.log(\"x\")", e1.command.eval_js); - const e2 = iter.next().?; + const e2 = (try iter.next()).?; try std.testing.expect(e2.command == .click); try std.testing.expectEqualStrings(".btn", e2.command.click); - try std.testing.expect(iter.next() == null); + try std.testing.expect((try iter.next()) == null); } test "ScriptIterator multi-line EXTRACT" { @@ -1110,19 +1110,19 @@ test "ScriptIterator multi-line EXTRACT" { ; var iter: ScriptIterator = .init(std.testing.allocator, script); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .goto); - const e2 = iter.next().?; + const e2 = (try iter.next()).?; try std.testing.expect(e2.command == .extract); try std.testing.expect(std.mem.indexOf(u8, e2.command.extract, "\"title\": \"h1\"") != null); try std.testing.expect(std.mem.indexOf(u8, e2.command.extract, "\"items\": [\".item\"]") != null); defer std.testing.allocator.free(e2.command.extract); - const e3 = iter.next().?; + const e3 = (try iter.next()).?; try std.testing.expect(e3.command == .tree); - try std.testing.expect(iter.next() == null); + try std.testing.expect((try iter.next()) == null); } test "ScriptIterator unterminated EXTRACT" { @@ -1132,7 +1132,7 @@ test "ScriptIterator unterminated EXTRACT" { ; var iter: ScriptIterator = .init(std.testing.allocator, script); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .natural_language); try std.testing.expectEqualStrings("unterminated EXTRACT block", e1.command.natural_language); } @@ -1146,7 +1146,7 @@ test "ScriptIterator multi-line EVAL mismatched triple quote" { ; var iter: ScriptIterator = .init(std.testing.allocator, script); - const e1 = iter.next().?; + const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .eval_js); try std.testing.expectEqualStrings(" const s = \" ''' \";\n console.log(s);", e1.command.eval_js); std.testing.allocator.free(e1.command.eval_js);