Skip to content

Commit 6e992b4

Browse files
committed
threads: make mapped arguments severing atomic
1 parent d74fa86 commit 6e992b4

5 files changed

Lines changed: 86 additions & 5 deletions

File tree

‎src/builtins.zig‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,7 +1715,7 @@ pub fn defineOneResult(self: *Interpreter, target: *value.Object, key: []const u
17151715
try target.setAttr(self.arena, key, attr);
17161716
target.has_indexed_property.store(true, .monotonic);
17171717
// Redefining a mapped index as non-writable severs the parameter link.
1718-
if (am_mapped and !attr.writable) target.arg_map_names[i] = "";
1718+
if (am_mapped and !attr.writable) interpreter.argMapSever(target, i);
17191719
target.extendArrayLengthFloor(i + 1);
17201720
return true;
17211721
}
@@ -1802,7 +1802,7 @@ pub fn defineOneResult(self: *Interpreter, target: *value.Object, key: []const u
18021802
// `length`; 2^32 - 1 and above are ordinary properties.
18031803
if (i < 4294967295 and i + 1 > target.arrayLength()) target.extendArrayLengthFloor(i + 1);
18041804
// Defining a mapped arguments index as an accessor severs its link.
1805-
if (target.is_arguments and (get != null or set != null) and i < target.arg_map_names.len) target.arg_map_names[i] = "";
1805+
if (target.is_arguments and (get != null or set != null)) interpreter.argMapSever(target, i);
18061806
}
18071807
}
18081808
return true;

‎src/context.zig‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9662,6 +9662,63 @@ test "parallel_js: TypedArray set snapshots array-like source length under no-GI
96629662
try std.testing.expect(result.asBool());
96639663
}
96649664

9665+
test "parallel_js: mapped arguments severing is atomic under no-GIL readers" {
9666+
if (builtin.single_threaded) return error.SkipZigTest;
9667+
const ctx = try Context.createWithTestingOptions(std.testing.allocator, .{
9668+
.enable_threads = true,
9669+
.enable_gc = true,
9670+
.parallel_gc = true,
9671+
.parallel_js = true,
9672+
});
9673+
defer ctx.destroy();
9674+
9675+
const result = try ctx.evaluate(
9676+
\\(() => {
9677+
\\ if ($vm.useThreadGIL() !== false)
9678+
\\ throw new Error("main still holds the thread GIL");
9679+
\\ function mint(a, b, c) { return arguments; }
9680+
\\ const shared = { slot: null, stop: 0, started: 0, go: 0 };
9681+
\\ const sentinel = "value-one";
9682+
\\ const readers = [];
9683+
\\ for (let r = 0; r < 2; ++r) {
9684+
\\ readers.push(new Thread((shared, sentinel) => {
9685+
\\ if ($vm.useThreadGIL() !== false)
9686+
\\ throw new Error("worker still holds the thread GIL");
9687+
\\ Atomics.add(shared, "started", 1);
9688+
\\ while (Atomics.load(shared, "go") === 0)
9689+
\\ Atomics.wait(shared, "go", 0, 100);
9690+
\\ let failures = 0;
9691+
\\ while (Atomics.load(shared, "stop") === 0) {
9692+
\\ const args = shared.slot;
9693+
\\ if (args === null) continue;
9694+
\\ if (args[1] !== sentinel) ++failures;
9695+
\\ const v0 = args[0];
9696+
\\ if (!(v0 === 7 || v0 === undefined)) ++failures;
9697+
\\ const len = args.length;
9698+
\\ if (!(len === 3 || len === 99)) ++failures;
9699+
\\ }
9700+
\\ return failures;
9701+
\\ }, shared, sentinel));
9702+
\\ }
9703+
\\ while (Atomics.load(shared, "started") !== readers.length)
9704+
\\ Atomics.wait(shared, "started", Atomics.load(shared, "started"), 100);
9705+
\\ Atomics.store(shared, "go", 1);
9706+
\\ Atomics.notify(shared, "go", readers.length);
9707+
\\ for (let i = 0; i < 750; ++i) {
9708+
\\ const args = mint(7, sentinel, true);
9709+
\\ shared.slot = args;
9710+
\\ delete args[0];
9711+
\\ args.length = 99;
9712+
\\ if ((i & 31) === 0) Atomics.wait(shared, "go", 1, 1);
9713+
\\ }
9714+
\\ Atomics.store(shared, "stop", 1);
9715+
\\ Atomics.notify(shared, "stop", readers.length);
9716+
\\ return readers.every(t => t.join() === 0);
9717+
\\})()
9718+
);
9719+
try std.testing.expect(result.asBool());
9720+
}
9721+
96659722
test "TypedArray set coerces offset before detached buffer checks" {
96669723
try std.testing.expect((try evalIn(
96679724
\\class ExpectedError extends Error {}

‎src/gc.zig‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,11 @@ fn finalizeObjectBacking(o: *Object, a: std.mem.Allocator) usize {
355355
o.arg_map_names = &.{};
356356
released += 1;
357357
}
358+
if (flags.arg_map_severed) {
359+
a.free(o.arg_map_severed);
360+
o.arg_map_severed = &.{};
361+
released += 1;
362+
}
358363

359364
o.backing_flags = .{};
360365
o.backing_allocator = null;

‎src/interpreter.zig‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5956,7 +5956,9 @@ pub const Interpreter = struct {
59565956
if (!func.is_strict and !non_simple_params and func.params.len > 0) {
59575957
const n = @min(args.len, func.params.len);
59585958
const names = try args_obj.asObj().argMapNamesAllocator(self.arena).alloc([]const u8, n);
5959+
const severed = try args_obj.asObj().argMapSeveredAllocator(self.arena).alloc(std.atomic.Value(bool), n);
59595960
for (names, 0..) |*nm, i| nm.* = func.params[i].name;
5961+
for (severed) |*flag| flag.* = .init(false);
59605962
// A duplicated parameter name maps only its last index.
59615963
for (names, 0..) |nm, i| {
59625964
var j = i + 1;
@@ -5967,6 +5969,7 @@ pub const Interpreter = struct {
59675969
}
59685970
args_obj.asObj().arg_map_env = @ptrCast(call_env);
59695971
args_obj.asObj().arg_map_names = names;
5972+
args_obj.asObj().arg_map_severed = severed;
59705973
}
59715974
return args_obj;
59725975
}
@@ -10704,7 +10707,7 @@ pub const Interpreter = struct {
1070410707
if (o.getOwn(key) == null and o.denseElementInBounds(i)) {
1070510708
if (o.attrsMap() != null and !o.getAttr(key).configurable) return false;
1070610709
// Deleting a mapped arguments index severs its parameter link.
10707-
if (o.is_arguments and i < o.arg_map_names.len) o.arg_map_names[i] = "";
10710+
if (o.is_arguments) argMapSever(o, i);
1070810711
_ = try o.deleteDenseElement(self.arena, i);
1070910712
return true;
1071010713
}
@@ -39028,10 +39031,17 @@ pub fn hasProperty(o: *value.Object, name: []const u8) bool {
3902839031
/// mapped arguments object or index `i` is unmapped (out of range / severed).
3902939032
pub fn argMapName(o: *value.Object, i: usize) ?[]const u8 {
3903039033
if (o.arg_map_env == null or i >= o.arg_map_names.len) return null;
39034+
if (i < o.arg_map_severed.len and o.arg_map_severed[i].load(.acquire)) return null;
3903139035
const nm = o.arg_map_names[i];
3903239036
return if (nm.len == 0) null else nm;
3903339037
}
3903439038

39039+
/// Atomically sever a mapped-arguments index from its parameter binding.
39040+
pub fn argMapSever(o: *value.Object, i: usize) void {
39041+
if (i >= o.arg_map_severed.len) return;
39042+
o.arg_map_severed[i].store(true, .release);
39043+
}
39044+
3903539045
/// Read a mapped index's parameter binding (null if unmapped).
3903639046
pub fn argMapGet(o: *value.Object, i: usize) ?Value {
3903739047
const nm = argMapName(o, i) orelse return null;

‎src/value.zig‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ pub const ObjectBackingFlags = packed struct {
614614
data_view: bool = false,
615615
temporal: bool = false,
616616
arg_map_names: bool = false,
617+
arg_map_severed: bool = false,
617618
};
618619

619620
pub const ObjectPrivateDataTag = enum(u8) {
@@ -815,10 +816,14 @@ pub const Object = struct {
815816
/// A mapped (sloppy-mode, simple-parameter) arguments object's
816817
/// `[[ParameterMap]]`: the call's environment record (type-erased
817818
/// `*Environment`) and the parameter name each index maps to (`""` = not
818-
/// mapped). A mapped index reads/writes the parameter binding; defining it as
819-
/// an accessor or non-writable, or deleting it, severs the mapping.
819+
/// initially mapped). A mapped index reads/writes the parameter binding;
820+
/// defining it as an accessor or non-writable, or deleting it, atomically
821+
/// severs the mapping in `arg_map_severed`. The names slice is immutable once
822+
/// the arguments object is published so no-GIL readers cannot tear a slice
823+
/// pair while another thread severs an index.
820824
arg_map_env: ?*anyopaque = null,
821825
arg_map_names: [][]const u8 = &.{},
826+
arg_map_severed: []std.atomic.Value(bool) = &.{},
822827
/// `Map`/`Set` instances. A Map keeps `[key,value]` pair-arrays in
823828
/// `elements`; a Set keeps values directly. `size` is a maintained property.
824829
is_map: bool = false,
@@ -1026,6 +1031,10 @@ pub const Object = struct {
10261031
return self.backingFor(fallback, "arg_map_names");
10271032
}
10281033

1034+
pub fn argMapSeveredAllocator(self: *Object, fallback: std.mem.Allocator) std.mem.Allocator {
1035+
return self.backingFor(fallback, "arg_map_severed");
1036+
}
1037+
10291038
pub fn lockProperties(self: *const Object) void {
10301039
var spins: usize = 0;
10311040
const mutex = &@constCast(self).property_lock;

0 commit comments

Comments
 (0)