ReviewOS

also looking at this

craft-native/craft

fix(native): interaction, audio, document and checkpoint gaps stranded after #401

#415
Merged glennmichael123 wants to merge codex/issues-388-400-344 into main
19 files +987 -50

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/zig/src/js_backend_null.zigmodified+0-65
Changes to packages/zig/src/js_backend_null.zig
@@ -16,28 +16,6 @@ pub const available = false;
1616/// Matches the zig-js backend's error set, so `js_runtime` handles one shape.
1717pub const Error = error{ OutOfMemory, Unavailable, Throw, Parse };
1818
19pub const Argument = union(enum) {
20 number: f64,
21 integer: i64,
22 boolean: bool,
23 text: []const u8,
24 buffer: *const Persistent,
25};
26
27pub const Result = union(enum) {
28 undefined,
29 null,
30 boolean: bool,
31 number: f64,
32 text: []const u8,
33};
34
35pub const Persistent = struct {
36 pub fn deinit(self: *Persistent) void {
37 self.* = .{};
38 }
39};
40
4119pub const Context = struct {
4220 pub fn create(allocator: std.mem.Allocator) Error!*Context {
4321 _ = allocator;
@@ -54,47 +32,4 @@ pub const Context = struct {
5432 _ = source;
5533 return Error.Unavailable;
5634 }
57
58 pub fn persistFunction(self: *Context, source: []const u8) Error!Persistent {
59 _ = self;
60 _ = source;
61 return Error.Unavailable;
62 }
63
64 pub fn createUint8Buffer(self: *Context, bytes: []const u8) Error!Persistent {
65 _ = self;
66 _ = bytes;
67 return Error.Unavailable;
68 }
69
70 pub fn readUint8Buffer(self: *Context, buffer: *const Persistent, arena: std.mem.Allocator) Error![]const u8 {
71 _ = self;
72 _ = buffer;
73 _ = arena;
74 return Error.Unavailable;
75 }
76
77 pub fn writeUint8Buffer(self: *Context, buffer: *const Persistent, bytes: []const u8) Error!void {
78 _ = self;
79 _ = buffer;
80 _ = bytes;
81 return Error.Unavailable;
82 }
83
84 pub fn call(self: *Context, callback: *const Persistent, arena: std.mem.Allocator, args: []const Argument) Error!Result {
85 _ = self;
86 _ = callback;
87 _ = arena;
88 _ = args;
89 return Error.Unavailable;
90 }
91
92 pub fn compact(self: *Context) void {
93 _ = self;
94 }
95
96 pub fn lastError(self: *const Context) []const u8 {
97 _ = self;
98 return "JavaScript runtime unavailable";
99 }
10035};
packages/zig/src/js_backend_zigjs.zigmodified+7-211
Changes to packages/zig/src/js_backend_zigjs.zig
@@ -15,89 +15,16 @@ const js = ;
1515
1616pub const available = true;
1717
18pub const Error = error{
19 OutOfMemory,
20 Unavailable,
21 Throw,
22 Parse,
23 InvalidHandle,
24 WrongContext,
25 TypeMismatch,
26 BufferSizeMismatch,
27 BufferTooLarge,
28 TooManyArguments,
29 UnsupportedResult,
30};
31
32const max_buffer_bytes: usize = 64 * 1024 * 1024;
33const max_call_arguments: usize = 64;
34
35pub const Argument = union(enum) {
36 number: f64,
37 integer: i64,
38 boolean: bool,
39 text: []const u8,
40 buffer: *const Persistent,
41};
42
43pub const Result = union(enum) {
44 undefined,
45 null,
46 boolean: bool,
47 number: f64,
48 text: []const u8,
49};
50
51const PersistentKind = enum { function, uint8_buffer };
52
53const PersistentHandle = struct {
54 owner: ?*Context,
55 root: ?*js.Context.ProtectedValue,
56 allocator: std.mem.Allocator,
57 kind: PersistentKind,
58
59 fn dispose(self: *PersistentHandle) void {
60 const allocator = self.allocator;
61 if (self.owner) |owner| {
62 if (self.root) |root| _ = owner.inner.unprotectValue(root);
63 for (owner.handles.items, 0..) |candidate, index| {
64 if (candidate == self) {
65 _ = owner.handles.swapRemove(index);
66 break;
67 }
68 }
69 }
70 self.owner = null;
71 self.root = null;
72 allocator.destroy(self);
73 }
74};
75
76pub const Persistent = struct {
77 handle: ?*PersistentHandle = null,
78
79 pub fn deinit(self: *Persistent) void {
80 const handle = self.handle orelse return;
81 self.handle = null;
82 handle.dispose();
83 }
84};
18pub const Error = error{ OutOfMemory, Unavailable, Throw, Parse };
8519
8620pub const Context = struct {
8721 inner: *js.Context,
8822 // Kept so `destroy` can free the wrapper it allocated. Without it the
8923 // wrapper leaked on every evaluation.
9024 allocator: std.mem.Allocator,
91 handles: std.ArrayListUnmanaged(*PersistentHandle) = .empty,
92 last_error: [256]u8 = undefined,
93 last_error_len: usize = 0,
9425
9526 pub fn create(allocator: std.mem.Allocator) Error!*Context {
96 return createWithOptions(allocator, false);
97 }
98
99 pub fn createWithOptions(allocator: std.mem.Allocator, enable_gc: bool) Error!*Context {
100 const inner = js.Context.createWith(allocator, .{ .enable_gc = enable_gc }) catch return Error.OutOfMemory;
27 const inner = js.Context.create(allocator) catch return Error.OutOfMemory;
10128 errdefer inner.destroy();
10229
10330 const self = allocator.create(Context) catch return Error.OutOfMemory;
@@ -106,155 +33,24 @@ pub const Context = struct {
10633 }
10734
10835 pub fn destroy(self: *Context) void {
109 for (self.handles.items) |handle| {
110 handle.owner = null;
111 handle.root = null;
112 }
113 self.handles.deinit(self.allocator);
11436 const allocator = self.allocator;
11537 self.inner.destroy();
11638 allocator.destroy(self);
11739 }
11840
119 fn rememberError(self: *Context, message: []const u8) void {
120 const count = @min(message.len, self.last_error.len);
121 @memcpy(self.last_error[0..count], message[0..count]);
122 self.last_error_len = count;
123 }
124
125 fn fail(self: *Context, comptime err: Error, message: []const u8) Error {
126 self.rememberError(message);
127 return err;
128 }
129
130 pub fn lastError(self: *const Context) []const u8 {
131 return self.last_error[0..self.last_error_len];
132 }
133
13441 /// Evaluate and render the result as text.
13542 ///
13643 /// `arena` rather than a general allocator: zig-js's `toString` allocates
13744 /// internally and the slice it returns is not a single freeable
13845 /// allocation, so freeing it individually aborts on invalid free.
13946 pub fn evalToString(self: *Context, arena: std.mem.Allocator, source: []const u8) Error![]const u8 {
140 self.last_error_len = 0;
141 const evaluated = self.inner.evaluate(source) catch |err| return switch (err) {
142 error.OutOfMemory => self.fail(Error.OutOfMemory, "JavaScript runtime ran out of memory"),
47 const value = self.inner.evaluate(source) catch |err| return switch (err) {
48 error.OutOfMemory => Error.OutOfMemory,
14349 // A JS-level throw and a parse failure are different problems for
14450 // the caller: one is the script's logic, the other is its syntax.
145 error.Throw => self.fail(Error.Throw, "JavaScript evaluation threw"),
146 else => self.fail(Error.Parse, "JavaScript source failed to parse"),
147 };
148 return evaluated.toString(arena) catch self.fail(Error.OutOfMemory, "JavaScript result could not be rendered");
149 }
150
151 fn mapEvalError(self: *Context, err: anyerror) Error {
152 return switch (err) {
153 error.OutOfMemory => self.fail(Error.OutOfMemory, "JavaScript runtime ran out of memory"),
154 error.Throw => self.fail(Error.Throw, "JavaScript callback threw"),
155 else => self.fail(Error.Throw, "JavaScript callback failed"),
156 };
157 }
158
159 fn retain(self: *Context, retained: js.Value, kind: PersistentKind) Error!Persistent {
160 const root = self.inner.protectValue(retained) catch return self.fail(Error.OutOfMemory, "could not retain JavaScript value");
161 errdefer _ = self.inner.unprotectValue(root);
162 const handle = self.allocator.create(PersistentHandle) catch return self.fail(Error.OutOfMemory, "could not allocate persistent handle");
163 errdefer self.allocator.destroy(handle);
164 handle.* = .{ .owner = self, .root = root, .allocator = self.allocator, .kind = kind };
165 self.handles.append(self.allocator, handle) catch return self.fail(Error.OutOfMemory, "could not register persistent handle");
166 return .{ .handle = handle };
167 }
168
169 fn getValue(self: *Context, handle: *const Persistent, expected: PersistentKind) Error!js.Value {
170 const record = handle.handle orelse return self.fail(Error.InvalidHandle, "persistent handle has been disposed");
171 if (record.owner == null or record.root == null) return self.fail(Error.InvalidHandle, "persistent handle belongs to a disposed runtime");
172 if (record.owner.? != self) return self.fail(Error.WrongContext, "persistent handle belongs to another runtime");
173 if (record.kind != expected) return self.fail(Error.TypeMismatch, "persistent handle has the wrong type");
174 return record.root.?.get();
175 }
176
177 pub fn persistFunction(self: *Context, source: []const u8) Error!Persistent {
178 self.last_error_len = 0;
179 const function = self.inner.evaluate(source) catch |err| return self.mapEvalError(err);
180 if (!function.isCallable()) return self.fail(Error.TypeMismatch, "persistent source did not evaluate to a function");
181 return self.retain(function, .function);
182 }
183
184 pub fn createUint8Buffer(self: *Context, bytes: []const u8) Error!Persistent {
185 self.last_error_len = 0;
186 if (bytes.len > max_buffer_bytes) return self.fail(Error.BufferTooLarge, "typed buffer exceeds the 64 MiB host limit");
187
188 const gc_saved = js.gc.setActiveContext(self.inner);
189 defer js.gc.restoreActiveContext(gc_saved);
190 var machine = self.inner.interpreter();
191 self.inner.pushActiveInterpreter(&machine) catch return self.fail(Error.OutOfMemory, "could not enter JavaScript runtime");
192 defer self.inner.popActiveInterpreter(&machine);
193 const created = machine.makeUninitializedTypedArray(.u8, bytes.len) catch |err| return self.mapEvalError(err);
194 const typed = created.asObj().typedArray() orelse return self.fail(Error.TypeMismatch, "runtime did not create a Uint8Array");
195 const backing = typed.buffer.arrayBuffer() orelse return self.fail(Error.TypeMismatch, "Uint8Array has no ArrayBuffer backing");
196 @memcpy(backing.bytes()[typed.byte_offset..][0..bytes.len], bytes);
197 return self.retain(created, .uint8_buffer);
198 }
199
200 pub fn readUint8Buffer(self: *Context, buffer: *const Persistent, arena: std.mem.Allocator) Error![]const u8 {
201 const buffer_value = try self.getValue(buffer, .uint8_buffer);
202 const typed = buffer_value.asObj().typedArray() orelse return self.fail(Error.TypeMismatch, "persistent value is not a typed buffer");
203 const length = typed.currentLength() orelse return self.fail(Error.InvalidHandle, "typed buffer is detached or out of bounds");
204 const bytes = typed.buffer.arrayBuffer().?.bytes();
205 if (typed.byte_offset > bytes.len or length > bytes.len - typed.byte_offset)
206 return self.fail(Error.InvalidHandle, "typed buffer range is invalid");
207 return arena.dupe(u8, bytes[typed.byte_offset..][0..length]) catch self.fail(Error.OutOfMemory, "could not copy typed buffer");
208 }
209
210 pub fn writeUint8Buffer(self: *Context, buffer: *const Persistent, bytes: []const u8) Error!void {
211 const buffer_value = try self.getValue(buffer, .uint8_buffer);
212 const typed = buffer_value.asObj().typedArray() orelse return self.fail(Error.TypeMismatch, "persistent value is not a typed buffer");
213 const length = typed.currentLength() orelse return self.fail(Error.InvalidHandle, "typed buffer is detached or out of bounds");
214 if (bytes.len != length) return self.fail(Error.BufferSizeMismatch, "typed buffer writes must match the view length");
215 const backing = typed.buffer.arrayBuffer().?;
216 const destination = backing.bytes();
217 if (typed.byte_offset > destination.len or length > destination.len - typed.byte_offset)
218 return self.fail(Error.InvalidHandle, "typed buffer range is invalid");
219 @memcpy(destination[typed.byte_offset..][0..length], bytes);
220 }
221
222 pub fn call(self: *Context, callback: *const Persistent, arena: std.mem.Allocator, args: []const Argument) Error!Result {
223 self.last_error_len = 0;
224 if (args.len > max_call_arguments) return self.fail(Error.TooManyArguments, "persistent calls accept at most 64 arguments");
225 const function = try self.getValue(callback, .function);
226
227 var values: std.ArrayListUnmanaged(js.Value) = .empty;
228 try values.ensureTotalCapacity(self.inner.arena(), args.len);
229 for (args) |arg| {
230 const converted = switch (arg) {
231 .number => |n| js.Value.num(n),
232 .integer => |n| js.Value.num(@floatFromInt(n)),
233 .boolean => |b| js.Value.boolVal(b),
234 .text => |text| js.Value.strAlloc(self.inner.arena(), text) catch return self.fail(Error.OutOfMemory, "could not allocate callback text argument"),
235 .buffer => |buffer| try self.getValue(buffer, .uint8_buffer),
236 };
237 values.appendAssumeCapacity(converted);
238 }
239
240 const gc_saved = js.gc.setActiveContext(self.inner);
241 defer js.gc.restoreActiveContext(gc_saved);
242 var machine = self.inner.interpreter();
243 self.inner.pushActiveInterpreter(&machine) catch return self.fail(Error.OutOfMemory, "could not enter JavaScript runtime");
244 defer self.inner.popActiveInterpreter(&machine);
245 const result = machine.callValue(function, values.items) catch |err| return self.mapEvalError(err);
246 return switch (result.kind()) {
247 .undefined => .undefined,
248 .null => .null,
249 .boolean => .{ .boolean = result.asBool() },
250 .number => .{ .number = result.asNum() },
251 .string => .{ .text = arena.dupe(u8, result.asWtf8(arena) catch return self.fail(Error.OutOfMemory, "could not copy callback result")) catch return self.fail(Error.OutOfMemory, "could not copy callback result") },
252 .object => self.fail(Error.UnsupportedResult, "persistent calls return only primitive values"),
51 error.Throw => Error.Throw,
52 else => Error.Parse,
25353 };
254 }
255
256 pub fn compact(self: *Context) void {
257 if (!self.inner.requestGarbageCompaction()) return;
258 _ = self.inner.compactGarbage();
54 return value.toString(arena) catch Error.OutOfMemory;
25955 }
26056};
packages/zig/src/js_runtime.zigmodified+0-98
Changes to packages/zig/src/js_runtime.zig
@@ -31,15 +31,6 @@ const backend = ;
3131pub const available = backend.available;
3232
3333pub const Error = backend.Error;
34pub const Argument = backend.Argument;
35pub const Result = backend.Result;
36pub const Persistent = backend.Persistent;
37
38pub const Options = struct {
39 /// Enable zig-js's precise moving collector. Keep this on for runtimes
40 /// that retain callbacks or typed buffers across host calls.
41 enable_gc: bool = false,
42};
4334
4435/// Told to the user rather than logged: an unavailable runtime is a build
4536/// choice, and the fix is a flag they can pass.
@@ -58,10 +49,6 @@ pub const Runtime = struct {
5849 return .{ .ctx = try backend.Context.create(allocator), .allocator = allocator };
5950 }
6051
61 pub fn initWithOptions(allocator: std.mem.Allocator, options: Options) Error!Runtime {
62 return .{ .ctx = try backend.Context.createWithOptions(allocator, options.enable_gc), .allocator = allocator };
63 }
64
6552 pub fn deinit(self: *Runtime) void {
6653 self.ctx.destroy();
6754 }
@@ -74,41 +61,6 @@ pub const Runtime = struct {
7461 pub fn evalToString(self: *Runtime, arena: std.mem.Allocator, source: []const u8) Error![]const u8 {
7562 return self.ctx.evalToString(arena, source);
7663 }
77
78 /// Evaluate a function expression once and retain its identity in the
79 /// realm. Calls use the engine's callable path; they do not generate a
80 /// fresh source string for every frame.
81 pub fn persistFunction(self: *Runtime, source: []const u8) Error!Persistent {
82 return self.ctx.persistFunction(source);
83 }
84
85 /// Create a rooted Uint8Array whose bytes remain owned by the JavaScript
86 /// realm until the returned handle is disposed.
87 pub fn createUint8Buffer(self: *Runtime, bytes: []const u8) Error!Persistent {
88 return self.ctx.createUint8Buffer(bytes);
89 }
90
91 pub fn readUint8Buffer(self: *Runtime, buffer: *const Persistent, arena: std.mem.Allocator) Error![]const u8 {
92 return self.ctx.readUint8Buffer(buffer, arena);
93 }
94
95 pub fn writeUint8Buffer(self: *Runtime, buffer: *const Persistent, bytes: []const u8) Error!void {
96 return self.ctx.writeUint8Buffer(buffer, bytes);
97 }
98
99 pub fn call(self: *Runtime, callback: *const Persistent, arena: std.mem.Allocator, args: []const Argument) Error!Result {
100 return self.ctx.call(callback, arena, args);
101 }
102
103 /// Request one moving collection and run it at a quiescent host boundary.
104 /// Persistent handles are the only values that may safely cross it.
105 pub fn compact(self: *Runtime) void {
106 self.ctx.compact();
107 }
108
109 pub fn lastError(self: *const Runtime) []const u8 {
110 return self.ctx.lastError();
111 }
11264};
11365
11466/// Evaluate one script and return its result — the whole lifecycle, for a
@@ -246,53 +198,3 @@ test "runs without any browser globals" {
246198 const kind = try evalOnce(testing.allocator, arena.allocator(), "typeof window");
247199 try testing.expectEqualStrings("undefined", kind);
248200}
249
250test "persistent callbacks retain typed buffers across compaction" {
251 if (!available) return error.SkipZigTest;
252
253 var arena = std.heap.ArenaAllocator.init(testing.allocator);
254 defer arena.deinit();
255
256 var runtime = try Runtime.initWithOptions(testing.allocator, .{ .enable_gc = true });
257 defer runtime.deinit();
258 var callback = try runtime.persistFunction("function (base, bytes) { return base + bytes[0] + bytes[1]; }");
259 defer callback.deinit();
260 var buffer = try runtime.createUint8Buffer(&.{ 2, 3 });
261 defer buffer.deinit();
262
263 runtime.compact();
264 const first = try runtime.call(&callback, arena.allocator(), &.{
265 .{ .integer = 37 },
266 .{ .buffer = &buffer },
267 });
268 try testing.expectEqual(@as(f64, 42), first.number);
269
270 try runtime.writeUint8Buffer(&buffer, &.{ 4, 5 });
271 runtime.compact();
272 const second = try runtime.call(&callback, arena.allocator(), &.{
273 .{ .number = 33 },
274 .{ .buffer = &buffer },
275 });
276 try testing.expectEqual(@as(f64, 42), second.number);
277 try testing.expectEqualSlices(u8, &.{ 4, 5 }, try runtime.readUint8Buffer(&buffer, arena.allocator()));
278}
279
280test "persistent handles reject disposal and cross-runtime use" {
281 if (!available) return error.SkipZigTest;
282
283 var first = try Runtime.init(testing.allocator);
284 defer first.deinit();
285 var second = try Runtime.init(testing.allocator);
286 defer second.deinit();
287
288 var callback = try first.persistFunction("function () { return 1; }");
289 try std.testing.expectError(
290 Error.WrongContext,
291 second.call(&callback, testing.allocator, &.{}),
292 );
293 callback.deinit();
294 try std.testing.expectError(
295 Error.InvalidHandle,
296 first.call(&callback, testing.allocator, &.{}),
297 );
298}