Skip to content

Commit 12daa74

Browse files
committed
Support union parameters
There's ambiguity in mapping due to the flexible nature of JavaScript. Hopefully most types are unambiguous, like a string or am *parser.Node. We need to "probe" each field to see if it's a possible candidate for the JS value. On a perfect match, we stop probing and set the appropriate union field. There are 2 levels of possible matches: candidate and coerce. A "candidate" match has higher precedence. This is necessary because, in JavaScript, a lot of things can be coerced to a lot of other, seemingly wrong, things. For example, say we have this union: a: i32, b: bool, Field `a` is a perfect match for the value 123. And field b is a coerce match (because, yes, 123 can be coerced to a boolean). So we map it to `a`. Field `a` is a candidate match for the value 34.2, because float -> int are both "Numbers" in JavaScript. And field b is a coerce match. So we map it to `a`. Both field `a` and field `b` are coerce matches for "hello". So we map it to `a` because it's declared first (this relies on how Zig currently works, but I don't think the ordering of type declarations is guaranteed, so that's an issue).
1 parent 2910f4f commit 12daa74

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
@@ -2013,7 +2013,7 @@ fn Caller(comptime E: type) type {
20132013
fn jsValueToZig(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !T {
20142014
switch (@typeInfo(T)) {
20152015
.optional => |o| {
2016-
if (js_value.isNull() or js_value.isUndefined()) {
2016+
if (js_value.isNullOrUndefined()) {
20172017
return null;
20182018
}
20192019
return try self.jsValueToZig(named_function, o.child, js_value);
@@ -2031,11 +2031,8 @@ fn Caller(comptime E: type) type {
20312031
return error.InvalidArgument;
20322032
}
20332033
if (@hasField(TypeLookup, @typeName(ptr.child))) {
2034-
const obj = js_value.castTo(v8.Object);
2035-
if (obj.internalFieldCount() == 0) {
2036-
return error.InvalidArgument;
2037-
}
2038-
return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), obj);
2034+
const js_obj = js_value.castTo(v8.Object);
2035+
return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj);
20392036
}
20402037
},
20412038
.slice => {
@@ -2131,58 +2128,50 @@ fn Caller(comptime E: type) type {
21312128
},
21322129
else => {},
21332130
},
2134-
.@"struct" => |s| {
2135-
if (@hasDecl(T, "_CALLBACK_ID_KLUDGE")) {
2136-
if (!js_value.isFunction()) {
2137-
return error.InvalidArgument;
2138-
}
2139-
2140-
const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
2141-
const scope = self.scope;
2142-
try scope.trackCallback(func);
2143-
2144-
return .{
2145-
.func = func,
2146-
.scope = scope,
2147-
.id = js_value.castTo(v8.Object).getIdentityHash(),
2148-
};
2149-
}
2150-
2151-
if (!js_value.isObject()) {
2131+
.@"struct" => {
2132+
return try (self.jsValueToStruct(named_function, T, js_value)) orelse {
21522133
return error.InvalidArgument;
2134+
};
2135+
},
2136+
.@"union" => |u| {
2137+
// see probeJsValueToZig for some explanation of what we're
2138+
// trying to do
2139+
2140+
// the first field that we find which the js_value could be
2141+
// coerced to.
2142+
var coerce_index: ?usize = null;
2143+
2144+
// the first field that we find which the js_Value is
2145+
// compatible with. A compatible field has higher precedence
2146+
// than a coercible, but still isn't a perfect match.
2147+
var compatible_index: ?usize = null;
2148+
2149+
inline for (u.fields, 0..) |field, i| {
2150+
switch (try self.probeJsValueToZig(named_function, field.type, js_value)) {
2151+
.value => |v| return @unionInit(T, field.name, v),
2152+
.ok => {
2153+
// a perfect match like above case, except the probing
2154+
// didn't get the value for us.
2155+
return @unionInit(T, field.name, try self.jsValueToZig(named_function, field.type, js_value));
2156+
},
2157+
.coerce => if (coerce_index == null) {
2158+
coerce_index = i;
2159+
},
2160+
.compatible => if (compatible_index == null) {
2161+
compatible_index = i;
2162+
},
2163+
.invalid => {},
2164+
}
21532165
}
21542166

2155-
const js_obj = js_value.castTo(v8.Object);
2156-
2157-
if (comptime isJsObject(T)) {
2158-
// Caller wants an opaque JsObject. Probably a parameter
2159-
// that it needs to pass back into a callback
2160-
return E.JsObject{
2161-
.js_obj = js_obj,
2162-
.scope = self.scope,
2163-
};
2164-
}
2165-
2166-
const context = self.context;
2167-
const isolate = self.isolate;
2168-
2169-
var value: T = undefined;
2170-
inline for (s.fields) |field| {
2171-
const name = field.name;
2172-
const key = v8.String.initUtf8(isolate, name);
2173-
if (js_obj.has(context, key.toValue())) {
2174-
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(context, key));
2175-
} else if (@typeInfo(field.type) == .optional) {
2176-
@field(value, name) = null;
2177-
} else {
2178-
if (field.defaultValue()) |dflt| {
2179-
@field(value, name) = dflt;
2180-
} else {
2181-
return error.JSWrongObject;
2182-
}
2167+
// We didn't find a perfect match.
2168+
const closest = compatible_index orelse coerce_index orelse return error.InvalidArgument;
2169+
inline for (u.fields, 0..) |field, i| {
2170+
if (i == closest) {
2171+
return @unionInit(T, field.name, try self.jsValueToZig(named_function, field.type, js_value));
21832172
}
21842173
}
2185-
return value;
2174+
unreachable;
21862175
},
21872176
else => {},
21882177
}
@@ -2237,6 +2226,230 @@ fn Caller(comptime E: type) type {
22372226
return error.InvalidArgument;
22382227
}
22392228

2229+
// Extracted so that it can be used in both jsValueToZig and in
2230+
// probeJsValueToZig. Avoids having to duplicate this logic when probing.
2231+
fn jsValueToStruct(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !?T {
2232+
if (@hasDecl(T, "_CALLBACK_ID_KLUDGE")) {
2233+
if (!js_value.isFunction()) {
2234+
return error.InvalidArgument;
2235+
}
2236+
2237+
const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
2238+
const scope = self.scope;
2239+
try scope.trackCallback(func);
2240+
2241+
return .{
2242+
.func = func,
2243+
.scope = scope,
2244+
.id = js_value.castTo(v8.Object).getIdentityHash(),
2245+
};
2246+
}
2247+
2248+
if (!js_value.isObject()) {
2249+
return null;
2250+
}
2251+
2252+
const js_obj = js_value.castTo(v8.Object);
2253+
2254+
if (comptime isJsObject(T)) {
2255+
// Caller wants an opaque JsObject. Probably a parameter
2256+
// that it needs to pass back into a callback
2257+
return E.JsObject{
2258+
.js_obj = js_obj,
2259+
.scope = self.scope,
2260+
};
2261+
}
2262+
2263+
const context = self.context;
2264+
const isolate = self.isolate;
2265+
2266+
var value: T = undefined;
2267+
inline for (@typeInfo(T).@"struct".fields) |field| {
2268+
const name = field.name;
2269+
const key = v8.String.initUtf8(isolate, name);
2270+
if (js_obj.has(context, key.toValue())) {
2271+
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(context, key));
2272+
} else if (@typeInfo(field.type) == .optional) {
2273+
@field(value, name) = null;
2274+
} else {
2275+
const dflt = field.defaultValue() orelse return null;
2276+
@field(value, name) = dflt;
2277+
}
2278+
}
2279+
return value;
2280+
}
2281+
2282+
// Probing is part of trying to map a JS value to a Zig union. There's
2283+
// a lot of ambiguity in this process, in part because some JS values
2284+
// can almost always be coerced. For example, anything can be coerced
2285+
// into an integer (it just becomes 0), or a float (becomes NaN) or a
2286+
// string.
2287+
//
2288+
// The way we'll do this is that, if there's a direct match, we'll use it
2289+
// If there's a potential match, we'll keep looking for a direct match
2290+
// and only use the (first) potential match as a fallback.
2291+
//
2292+
// Finally, I considered adding this probing directly into jsValueToZig
2293+
// but I decided doing this separately was better. However, the goal is
2294+
// obviously that probing is consistent with jsValueToZig.
2295+
fn ProbeResult(comptime T: type) type {
2296+
return union(enum) {
2297+
// The js_value maps directly to T
2298+
value: T,
2299+
2300+
// The value is a T. This is almost the same as returning value: T,
2301+
// but the caller still has to get T by calling jsValueToZig.
2302+
// We prefer returning .{.ok => {}}, to avoid reducing duplication
2303+
// with jsValueToZig, but in some cases where probing has a cost
2304+
// AND yields the value anyways, we'll use .{.value = T}.
2305+
ok: void,
2306+
2307+
// the js_value is compatible with T (i.e. a int -> float),
2308+
compatible: void,
2309+
2310+
// the js_value can be coerced to T (this is a lower precedence
2311+
// than compatible)
2312+
coerce: void,
2313+
2314+
// the js_value cannot be turned into T
2315+
invalid: void,
2316+
};
2317+
}
2318+
fn probeJsValueToZig(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !ProbeResult(T) {
2319+
switch (@typeInfo(T)) {
2320+
.optional => |o| {
2321+
if (js_value.isNullOrUndefined()) {
2322+
return .{ .value = null };
2323+
}
2324+
return self.probeJsValueToZig(named_function, o.child, js_value);
2325+
},
2326+
.float => {
2327+
if (js_value.isNumber() or js_value.isNumberObject()) {
2328+
if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) {
2329+
// int => float is a reasonable match
2330+
return .{ .compatible = {} };
2331+
}
2332+
return .{ .ok = {} };
2333+
}
2334+
// anything can be coerced into a float, it becomes NaN
2335+
return .{ .coerce = {} };
2336+
},
2337+
.int => {
2338+
if (js_value.isNumber() or js_value.isNumberObject()) {
2339+
if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) {
2340+
return .{ .ok = {} };
2341+
}
2342+
// float => int is kind of reasonable, I guess
2343+
return .{ .compatible = {} };
2344+
}
2345+
// anything can be coerced into a int, it becomes 0
2346+
return .{ .coerce = {} };
2347+
},
2348+
.bool => {
2349+
if (js_value.isBoolean() or js_value.isBooleanObject()) {
2350+
return .{ .ok = {} };
2351+
}
2352+
// anything can be coerced into a boolean, it will become
2353+
// true or false based on..some complex rules I don't know.
2354+
return .{ .coerce = {} };
2355+
},
2356+
.pointer => |ptr| switch (ptr.size) {
2357+
.one => {
2358+
if (!js_value.isObject()) {
2359+
return .{ .invalid = {} };
2360+
}
2361+
if (@hasField(TypeLookup, @typeName(ptr.child))) {
2362+
const js_obj = js_value.castTo(v8.Object);
2363+
// There's a bit of overhead in doing this, so instead
2364+
// of having a version of typeTaggedAnyOpaque which
2365+
// returns a boolean or an optional, we rely on the
2366+
// main implementation and just handle the error.
2367+
const attempt = E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj);
2368+
if (attempt) |value| {
2369+
return .{ .value = value };
2370+
} else |_| {
2371+
return .{ .invalid = {} };
2372+
}
2373+
}
2374+
// probably an error, but not for us to deal with
2375+
return .{ .invalid = {} };
2376+
},
2377+
.slice => {
2378+
if (js_value.isTypedArray()) {
2379+
switch (ptr.child) {
2380+
u8 => if (ptr.sentinel() == null) {
2381+
if (js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
2382+
return .{ .ok = {} };
2383+
}
2384+
},
2385+
i8 => if (js_value.isInt8Array()) {
2386+
return .{ .ok = {} };
2387+
},
2388+
u16 => if (js_value.isUint16Array()) {
2389+
return .{ .ok = {} };
2390+
},
2391+
i16 => if (js_value.isInt16Array()) {
2392+
return .{ .ok = {} };
2393+
},
2394+
u32 => if (js_value.isUint32Array()) {
2395+
return .{ .ok = {} };
2396+
},
2397+
i32 => if (js_value.isInt32Array()) {
2398+
return .{ .ok = {} };
2399+
},
2400+
u64 => if (js_value.isBigUint64Array()) {
2401+
return .{ .ok = {} };
2402+
},
2403+
i64 => if (js_value.isBigInt64Array()) {
2404+
return .{ .ok = {} };
2405+
},
2406+
else => {},
2407+
}
2408+
return .{ .invalid = {} };
2409+
}
2410+
2411+
if (ptr.child == u8) {
2412+
if (js_value.isString()) {
2413+
return .{ .ok = {} };
2414+
}
2415+
// anything can be coerced into a string
2416+
return .{ .coerce = {} };
2417+
}
2418+
2419+
if (!js_value.isArray()) {
2420+
return error.InvalidArgument;
2421+
}
2422+
2423+
// This can get tricky.
2424+
const js_arr = js_value.castTo(v8.Array);
2425+
2426+
if (js_arr.length() == 0) {
2427+
// not so tricky in this case.
2428+
return .{ .value = &.{} };
2429+
}
2430+
2431+
// We settle for just probing the first value. Ok, actually
2432+
// not tricky in this case either.
2433+
const context = self.contxt;
2434+
const js_obj = js_arr.castTo(v8.Object);
2435+
return self.probeJsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(context, 0));
2436+
},
2437+
else => {},
2438+
},
2439+
.@"struct" => {
2440+
// We don't want to duplicate the code for this, so we call
2441+
// the actual coversion function.
2442+
const value = (try self.jsValueToStruct(named_function, T, js_value)) orelse {
2443+
return .{ .invalid = {} };
2444+
};
2445+
return .{ .value = value };
2446+
},
2447+
else => {},
2448+
}
2449+
2450+
return .{ .invalid = {} };
2451+
}
2452+
22402453
fn zigValueToJs(self: *const Self, value: anytype) !v8.Value {
22412454
return self.scope.zigValueToJs(value);
22422455
}

0 commit comments

Comments
 (0)