Skip to content

Commit 74eaee5

Browse files
authored
Merge pull request #585 from lightpanda-io/union_params
Support union parameters
2 parents 312189f + 20e4261 commit 74eaee5

File tree

5 files changed

+305
-57
lines changed

5 files changed

+305
-57
lines changed

.github/actions/install/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ inputs:
1717
zig-v8:
1818
description: 'zig v8 version to install'
1919
required: false
20-
default: 'v0.1.19'
20+
default: 'v0.1.20'
2121
v8:
2222
description: 'v8 version to install'
2323
required: false

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ ARG ZIG=0.14.0
55
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
66
ARG ARCH=x86_64
77
ARG V8=11.1.134
8-
ARG ZIG_V8=v0.1.19
8+
ARG ZIG_V8=v0.1.20
99

1010
RUN apt-get update -yq && \
1111
apt-get install -yq xz-utils \

build.zig.zon

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
1414
},
1515
.v8 = .{
16-
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/363e2899e6d782ad999edbfae048228871230467.tar.gz",
17-
.hash = "v8-0.0.0-xddH6wHzIAARDy1uFvPqqBpTXzhlnEGDTuX9IAUQz3oU",
16+
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/f0c7eaaffe39f2f1a224fbe97e550daca0ca1801.tar.gz",
17+
.hash = "v8-0.0.0-xddH62T4IADchAHFgo4nx79w1VedNDhIVErtSNgup-Tk",
1818
},
1919
//.v8 = .{ .path = "../zig-v8-fork" },
2020
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },

src/runtime/js.zig

Lines changed: 266 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2075,7 +2075,7 @@ fn Caller(comptime E: type) type {
20752075
fn jsValueToZig(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !T {
20762076
switch (@typeInfo(T)) {
20772077
.optional => |o| {
2078-
if (js_value.isNull() or js_value.isUndefined()) {
2078+
if (js_value.isNullOrUndefined()) {
20792079
return null;
20802080
}
20812081
return try self.jsValueToZig(named_function, o.child, js_value);
@@ -2093,11 +2093,8 @@ fn Caller(comptime E: type) type {
20932093
return error.InvalidArgument;
20942094
}
20952095
if (@hasField(TypeLookup, @typeName(ptr.child))) {
2096-
const obj = js_value.castTo(v8.Object);
2097-
if (obj.internalFieldCount() == 0) {
2098-
return error.InvalidArgument;
2099-
}
2100-
return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), obj);
2096+
const js_obj = js_value.castTo(v8.Object);
2097+
return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj);
21012098
}
21022099
},
21032100
.slice => {
@@ -2193,58 +2190,50 @@ fn Caller(comptime E: type) type {
21932190
},
21942191
else => {},
21952192
},
2196-
.@"struct" => |s| {
2197-
if (@hasDecl(T, "_CALLBACK_ID_KLUDGE")) {
2198-
if (!js_value.isFunction()) {
2199-
return error.InvalidArgument;
2200-
}
2201-
2202-
const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
2203-
const scope = self.scope;
2204-
try scope.trackCallback(func);
2205-
2206-
return .{
2207-
.func = func,
2208-
.scope = scope,
2209-
.id = js_value.castTo(v8.Object).getIdentityHash(),
2210-
};
2211-
}
2212-
2213-
const js_obj = js_value.castTo(v8.Object);
2214-
2215-
if (comptime isJsObject(T)) {
2216-
// Caller wants an opaque JsObject. Probably a parameter
2217-
// that it needs to pass back into a callback
2218-
return E.JsObject{
2219-
.js_obj = js_obj,
2220-
.scope = self.scope,
2221-
};
2222-
}
2223-
2224-
if (!js_value.isObject()) {
2193+
.@"struct" => {
2194+
return try (self.jsValueToStruct(named_function, T, js_value)) orelse {
22252195
return error.InvalidArgument;
2196+
};
2197+
},
2198+
.@"union" => |u| {
2199+
// see probeJsValueToZig for some explanation of what we're
2200+
// trying to do
2201+
2202+
// the first field that we find which the js_value could be
2203+
// coerced to.
2204+
var coerce_index: ?usize = null;
2205+
2206+
// the first field that we find which the js_Value is
2207+
// compatible with. A compatible field has higher precedence
2208+
// than a coercible, but still isn't a perfect match.
2209+
var compatible_index: ?usize = null;
2210+
2211+
inline for (u.fields, 0..) |field, i| {
2212+
switch (try self.probeJsValueToZig(named_function, field.type, js_value)) {
2213+
.value => |v| return @unionInit(T, field.name, v),
2214+
.ok => {
2215+
// a perfect match like above case, except the probing
2216+
// didn't get the value for us.
2217+
return @unionInit(T, field.name, try self.jsValueToZig(named_function, field.type, js_value));
2218+
},
2219+
.coerce => if (coerce_index == null) {
2220+
coerce_index = i;
2221+
},
2222+
.compatible => if (compatible_index == null) {
2223+
compatible_index = i;
2224+
},
2225+
.invalid => {},
2226+
}
22262227
}
22272228

2228-
const context = self.context;
2229-
const isolate = self.isolate;
2230-
2231-
var value: T = undefined;
2232-
inline for (s.fields) |field| {
2233-
const name = field.name;
2234-
const key = v8.String.initUtf8(isolate, name);
2235-
if (js_obj.has(context, key.toValue())) {
2236-
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(context, key));
2237-
} else if (@typeInfo(field.type) == .optional) {
2238-
@field(value, name) = null;
2239-
} else {
2240-
if (field.defaultValue()) |dflt| {
2241-
@field(value, name) = dflt;
2242-
} else {
2243-
return error.JSWrongObject;
2244-
}
2229+
// We didn't find a perfect match.
2230+
const closest = compatible_index orelse coerce_index orelse return error.InvalidArgument;
2231+
inline for (u.fields, 0..) |field, i| {
2232+
if (i == closest) {
2233+
return @unionInit(T, field.name, try self.jsValueToZig(named_function, field.type, js_value));
22452234
}
22462235
}
2247-
return value;
2236+
unreachable;
22482237
},
22492238
else => {},
22502239
}
@@ -2299,6 +2288,230 @@ fn Caller(comptime E: type) type {
22992288
return error.InvalidArgument;
23002289
}
23012290

2291+
// Extracted so that it can be used in both jsValueToZig and in
2292+
// probeJsValueToZig. Avoids having to duplicate this logic when probing.
2293+
fn jsValueToStruct(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !?T {
2294+
if (@hasDecl(T, "_CALLBACK_ID_KLUDGE")) {
2295+
if (!js_value.isFunction()) {
2296+
return error.InvalidArgument;
2297+
}
2298+
2299+
const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
2300+
const scope = self.scope;
2301+
try scope.trackCallback(func);
2302+
2303+
return .{
2304+
.func = func,
2305+
.scope = scope,
2306+
.id = js_value.castTo(v8.Object).getIdentityHash(),
2307+
};
2308+
}
2309+
2310+
const js_obj = js_value.castTo(v8.Object);
2311+
2312+
if (comptime isJsObject(T)) {
2313+
// Caller wants an opaque JsObject. Probably a parameter
2314+
// that it needs to pass back into a callback
2315+
return E.JsObject{
2316+
.js_obj = js_obj,
2317+
.scope = self.scope,
2318+
};
2319+
}
2320+
2321+
if (!js_value.isObject()) {
2322+
return null;
2323+
}
2324+
2325+
const context = self.context;
2326+
const isolate = self.isolate;
2327+
2328+
var value: T = undefined;
2329+
inline for (@typeInfo(T).@"struct".fields) |field| {
2330+
const name = field.name;
2331+
const key = v8.String.initUtf8(isolate, name);
2332+
if (js_obj.has(context, key.toValue())) {
2333+
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(context, key));
2334+
} else if (@typeInfo(field.type) == .optional) {
2335+
@field(value, name) = null;
2336+
} else {
2337+
const dflt = field.defaultValue() orelse return null;
2338+
@field(value, name) = dflt;
2339+
}
2340+
}
2341+
return value;
2342+
}
2343+
2344+
// Probing is part of trying to map a JS value to a Zig union. There's
2345+
// a lot of ambiguity in this process, in part because some JS values
2346+
// can almost always be coerced. For example, anything can be coerced
2347+
// into an integer (it just becomes 0), or a float (becomes NaN) or a
2348+
// string.
2349+
//
2350+
// The way we'll do this is that, if there's a direct match, we'll use it
2351+
// If there's a potential match, we'll keep looking for a direct match
2352+
// and only use the (first) potential match as a fallback.
2353+
//
2354+
// Finally, I considered adding this probing directly into jsValueToZig
2355+
// but I decided doing this separately was better. However, the goal is
2356+
// obviously that probing is consistent with jsValueToZig.
2357+
fn ProbeResult(comptime T: type) type {
2358+
return union(enum) {
2359+
// The js_value maps directly to T
2360+
value: T,
2361+
2362+
// The value is a T. This is almost the same as returning value: T,
2363+
// but the caller still has to get T by calling jsValueToZig.
2364+
// We prefer returning .{.ok => {}}, to avoid reducing duplication
2365+
// with jsValueToZig, but in some cases where probing has a cost
2366+
// AND yields the value anyways, we'll use .{.value = T}.
2367+
ok: void,
2368+
2369+
// the js_value is compatible with T (i.e. a int -> float),
2370+
compatible: void,
2371+
2372+
// the js_value can be coerced to T (this is a lower precedence
2373+
// than compatible)
2374+
coerce: void,
2375+
2376+
// the js_value cannot be turned into T
2377+
invalid: void,
2378+
};
2379+
}
2380+
fn probeJsValueToZig(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !ProbeResult(T) {
2381+
switch (@typeInfo(T)) {
2382+
.optional => |o| {
2383+
if (js_value.isNullOrUndefined()) {
2384+
return .{ .value = null };
2385+
}
2386+
return self.probeJsValueToZig(named_function, o.child, js_value);
2387+
},
2388+
.float => {
2389+
if (js_value.isNumber() or js_value.isNumberObject()) {
2390+
if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) {
2391+
// int => float is a reasonable match
2392+
return .{ .compatible = {} };
2393+
}
2394+
return .{ .ok = {} };
2395+
}
2396+
// anything can be coerced into a float, it becomes NaN
2397+
return .{ .coerce = {} };
2398+
},
2399+
.int => {
2400+
if (js_value.isNumber() or js_value.isNumberObject()) {
2401+
if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) {
2402+
return .{ .ok = {} };
2403+
}
2404+
// float => int is kind of reasonable, I guess
2405+
return .{ .compatible = {} };
2406+
}
2407+
// anything can be coerced into a int, it becomes 0
2408+
return .{ .coerce = {} };
2409+
},
2410+
.bool => {
2411+
if (js_value.isBoolean() or js_value.isBooleanObject()) {
2412+
return .{ .ok = {} };
2413+
}
2414+
// anything can be coerced into a boolean, it will become
2415+
// true or false based on..some complex rules I don't know.
2416+
return .{ .coerce = {} };
2417+
},
2418+
.pointer => |ptr| switch (ptr.size) {
2419+
.one => {
2420+
if (!js_value.isObject()) {
2421+
return .{ .invalid = {} };
2422+
}
2423+
if (@hasField(TypeLookup, @typeName(ptr.child))) {
2424+
const js_obj = js_value.castTo(v8.Object);
2425+
// There's a bit of overhead in doing this, so instead
2426+
// of having a version of typeTaggedAnyOpaque which
2427+
// returns a boolean or an optional, we rely on the
2428+
// main implementation and just handle the error.
2429+
const attempt = E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj);
2430+
if (attempt) |value| {
2431+
return .{ .value = value };
2432+
} else |_| {
2433+
return .{ .invalid = {} };
2434+
}
2435+
}
2436+
// probably an error, but not for us to deal with
2437+
return .{ .invalid = {} };
2438+
},
2439+
.slice => {
2440+
if (js_value.isTypedArray()) {
2441+
switch (ptr.child) {
2442+
u8 => if (ptr.sentinel() == null) {
2443+
if (js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
2444+
return .{ .ok = {} };
2445+
}
2446+
},
2447+
i8 => if (js_value.isInt8Array()) {
2448+
return .{ .ok = {} };
2449+
},
2450+
u16 => if (js_value.isUint16Array()) {
2451+
return .{ .ok = {} };
2452+
},
2453+
i16 => if (js_value.isInt16Array()) {
2454+
return .{ .ok = {} };
2455+
},
2456+
u32 => if (js_value.isUint32Array()) {
2457+
return .{ .ok = {} };
2458+
},
2459+
i32 => if (js_value.isInt32Array()) {
2460+
return .{ .ok = {} };
2461+
},
2462+
u64 => if (js_value.isBigUint64Array()) {
2463+
return .{ .ok = {} };
2464+
},
2465+
i64 => if (js_value.isBigInt64Array()) {
2466+
return .{ .ok = {} };
2467+
},
2468+
else => {},
2469+
}
2470+
return .{ .invalid = {} };
2471+
}
2472+
2473+
if (ptr.child == u8) {
2474+
if (js_value.isString()) {
2475+
return .{ .ok = {} };
2476+
}
2477+
// anything can be coerced into a string
2478+
return .{ .coerce = {} };
2479+
}
2480+
2481+
if (!js_value.isArray()) {
2482+
return error.InvalidArgument;
2483+
}
2484+
2485+
// This can get tricky.
2486+
const js_arr = js_value.castTo(v8.Array);
2487+
2488+
if (js_arr.length() == 0) {
2489+
// not so tricky in this case.
2490+
return .{ .value = &.{} };
2491+
}
2492+
2493+
// We settle for just probing the first value. Ok, actually
2494+
// not tricky in this case either.
2495+
const context = self.contxt;
2496+
const js_obj = js_arr.castTo(v8.Object);
2497+
return self.probeJsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(context, 0));
2498+
},
2499+
else => {},
2500+
},
2501+
.@"struct" => {
2502+
// We don't want to duplicate the code for this, so we call
2503+
// the actual coversion function.
2504+
const value = (try self.jsValueToStruct(named_function, T, js_value)) orelse {
2505+
return .{ .invalid = {} };
2506+
};
2507+
return .{ .value = value };
2508+
},
2509+
else => {},
2510+
}
2511+
2512+
return .{ .invalid = {} };
2513+
}
2514+
23022515
fn zigValueToJs(self: *const Self, value: anytype) !v8.Value {
23032516
return self.scope.zigValueToJs(value);
23042517
}

0 commit comments

Comments
 (0)