design icon indicating copy to clipboard operation
design copied to clipboard

Proposal: Additional JS primitive builtins

Open sjrd opened this issue 8 months ago • 11 comments

Motivation

The JS String Builtins proposal introduced an efficient mechanism to manipulate JS strings from WebAssembly code using the JS embedding. It focused on strings as a high-impact first step. Now that infrastructure for JS builtins is firmly in place, this proposal suggests an expanded set of builtins that manipulate JS primitive values.

The motivation is largely the same as for the original JS string builtins. All of the proposed builtins can be expressed today using JS glue code, but we want to avoid the cost of a Wasm-to-JS function call for operations that should be a tight sequence of inline instructions.

Overview

Most of what we could say here has already been very well laid out in the overview of the JS String Builtins proposal. I do not think it is worth repeating them here.

In particular, the goals for builtins remain the same. Builtins should be simple and only provide functionality that is semantically already provided by JavaScript.

The proposed list of builtins is based on experience with the Scala.js-to-Wasm compiler. Through benchmarking, we have identified a number of operations that show up high on profiles for no good reason, other than they require glue code to JavaScript. We have extrapolated to some additional operations that we think are likely relevant to other toolchains.

Here is a quick overview of the builtins we propose. The set is intentionally fairly broad, so that we can discuss what is actually useful and what might be overreaching. In the expanded specifications below, we will mark the functions that we consider particularly important, based on our experience with Scala.js.

Some of those functions can be directly imported, because they do not rely on their this value (ex.: parseFloat, sin, Object.is). These may already be optimized accurately by JS engines if they are used as is. At least, their semantics are such that the engines are allowed to. The purpose of including them here is to have some sort of guarantee that they will be optimized without a round-trip to JS land.

  • String (extensions to the existing wasm:js-string):
    • Conversion from anything as if in string concat: fromAny
    • Conversion from primitive numeric types: fromI32, fromU32, fromI64, fromU64, fromF32, fromF64
    • Unicode-aware case conversions: toLowerCase, toUpperCase
  • Number (wasm:js-number):
    • Type test: test, testF32, testI32, testU32
    • Create from primitive: fromF64, fromF32, fromI32, fromU32
    • Extract to primitive: toF64, toF32, toI32, toU32
    • JS primitive operations: fmod (the % operator), wrapToI32 (x | 0)
    • Math operations: sin, cos, etc. (the ones that have hardware equivalents, at least)
    • Parsing: parse
  • Boolean (wasm:js-boolean):
    • Type test: test
    • (creation is efficiently achieved by importing true and false as globals)
    • Extract to primitive: toI32
  • Undefined (wasm:js-undefined):
    • Type test: test
    • (creation is efficiently achieved by importing void 0 as a global)
  • Symbol (wasm:js-symbol):
    • Type test: test
  • Bigint (wasm:js-bigint):
    • Type test: test
    • Create from primitive: fromF64, fromI64, fromU64
    • Extract to primitive: convertToF64, wrapToI64
    • Operations: add, pow, shl, etc. (the ones corresponding to JS operators)
    • Wrapping operations: asIntN, asUintN
    • Parsing: parse
    • Conversion to string: toString
  • Generic (wasm:js-object):
    • JS same-value: is (Object.is)

About the "universal representation"

Many of the rationales below talk about a "universal representation". This is in the context of a language that compiles to Wasm in a JS embedding, and wants to interoperate with JavaScript at a fairly deep level. In such a language, a universal representation of values must be compatible with JavaScript. For example, primitive numbers stored in a universal representation must appear as JS numbers when seen by JavaScript code.

For more context, you may want to revisit the notes and slides of the Scala.js experience report that we gave during the June 2024 hybrid CG meeting.

Open questions

Are the importable functions worth it?

parseFloat, Object.is, Math.sin et al., can already be imported with acceptable signatures today. Is it worth adding them as builtins? Should we instead "strongly encourage" JS embeddings to recognize them at instantiation time and optimize them accordingly?

Conversions of bigints to/from bit arrays

Some languages with big integers provide direct conversions to/from bit arrays:

  • as i8 arrays, i32 arrays and/or i64 arrays,
  • in little endian and/or big endian, and
  • in two's complement representation or in sign-magnitude representation.

Should we also add all those conversions to wasm:js-bigint?

toString and parsing with radix

Should we expose the integer parsing and formatting methods that take an explicit radix?

Specifications

In this section, we give the specification of the proposed builtins. We follow the same style as the JS String builtins proposal. In particular, we also use the trap() function to specify that a Wasm trap should occur.

We mark with ❗️ the builtins that would be particularly important to Scala.js, with a one-sentence reason.

"wasm:js-string" "fromAny"

func fromAny(
  x: externref
) -> (ref extern) {
  // NOTE: Unlike all the other builtins we propose, this *can* invoke
  // arbitrary JS code.
  return "" + x;
}

"wasm:js-string" "fromI32"

❗️ Primitive string concatenation on an i32

func fromI32(
  x: i32
) -> (ref extern) {
  return "" + x;
}

"wasm:js-string" "fromU32"

Likely as useful as fromI32 for languages that have primitive unsigned integers.

func fromU32(
  x: i32
) -> (ref extern) {
  // NOTE: `x` is interpreted as signed 32-bit integers when converted to a JS
  // value using standard conversions. Reinterpret it as unsigned here.
  x >>>= 0;

  return "" + x;
}

"wasm:js-string" "fromI64"

❗️ Primitive string concatenation on an i64

func fromI64(
  x: i64
) -> (ref extern) {
  // NOTE: `x` is interpreted as a signed JS `bigint`, which provides the
  // expected conversion to string.
  return "" + x;
}

"wasm:js-string" "fromU64"

Likely as useful as fromI64 for languages that have primitive unsigned integers.

func fromU64(
  x: i64
) -> (ref extern) {
  // NOTE: `x` is interpreted as a signed JS `bigint`. Reinterpret it as
  // unsigned here.
  x = BigInt.asUintN(64, x);

  return "" + x;
}

"wasm:js-string" "fromF32"

Can be implemented without additional overhead with f64.promote_f32+fromF64.

func fromF32(
  x: f32
) -> (ref extern) {
  return "" + x;
}

"wasm:js-string" "fromF64"

❗️ Primitive string concatenation on an f64

func fromF64(
  x: f64
) -> (ref extern) {
  return "" + x;
}

"wasm:js-string" "toLowerCase"

func toLowerCase(
  string: externref
) -> (ref extern) {
  if (typeof string !== "string")
    trap();

  return string.toLowerCase();
}

"wasm:js-string" "toUpperCase"

func toUpperCase(
  string: externref
) -> (ref extern) {
  if (typeof string !== "string")
    trap();

  return string.toUpperCase();
}

We suggest specifically toLowerCase() and toUpperCase(), but not the plethora of other methods of strings. Other methods can already be efficiently implemented using the existing builtins in js-string, on the Wasm side. The Unicode-aware case conversions (using the ROOT locale) require to embed a significant subset of the Unicode database. That's not something we want to ship along with our Wasm payloads.

"wasm:js-number" "test"

❗️ Primitive type test for f64 from the universal representation

func test(
  x: externref
) -> i32 {
  if (typeof x !== "number")
    return 0;
  return 1;
}

"wasm:js-number" "testF32"

Could be implemented on the Wasm side using testF64 and toF64.

func testF32(
  x: externref
) -> i32 {
  if (typeof x !== "number")
    return 0;
  if (Math.fround(x) !== x && x === x) // note: do not reject NaN
    return 0;
  return 1;
}

"wasm:js-number" "testI32"

❗️ Primitive type test for i32 from the universal representation

Technically implementable using testF64, toF64, and additional Wasm instructions. However, I suspect there are many cases where engines would first have to convert an integer stored internally into a float before giving it to Wasm.

func testI32(
  x: externref
) -> i32 {
  if (typeof x !== "number")
    return 0;
  if ((x | 0) !== x || Object.is(x, -0))
    return 0;
  return 1;
}

When the result is expected to be 1, in today's engines, it is worth first trying (ref.test (ref i31) x) before falling back on JS glue code.

"wasm:js-number" "testU32"

Likely as useful as testI32 for languages that have primitive unsigned integers.

func testU32(
  x: externref
) -> i32 {
  if (typeof x !== "number")
    return 0;
  if ((x >>> 0) !== x || Object.is(x, -0))
    return 0;
  return 1;
}

"wasm:js-number" "fromF64"

❗️ Box a primitive f64 into the universal representation

func fromF64(
  x: f64
) -> (ref extern) {
  return x;
}

"wasm:js-number" "fromF32"

Could be implemented with f64.promote_f32 and fromF64 without additional overhead.

func fromF32(
  x: f32
) -> (ref extern) {
  return x;
}

"wasm:js-number" "fromI32"

❗️ Box a primitive i32 into the universal representation

func fromI32(
  x: i32
) -> (ref extern) {
  return x;
}

In today's engines, it is worth spending Wasm instructions to test whether x fits in 31 bits. If it does, use (ref.i31 x). Otherwise, fall back on JS glue code.

"wasm:js-number" "fromU32"

Likely as useful as fromI32 for languages that have primitive unsigned integers.

func fromU32(
  x: i32
) -> (ref extern) {
  // NOTE: `x` is interpreted as signed 32-bit integers when converted to a JS
  // value using standard conversions. Reinterpret it as unsigned here.
  x >>>= 0;

  return x;
}

"wasm:js-number" "toF64"

❗️ Unboxing from the universal representation into a primitive f64

func toF64(
  x: externref
) -> f64 {
  if (typeof x !== "number")
    trap();

  return x;
}

"wasm:js-number" "toF32"

Could be implemented with toF64 and additional code on the Wasm side.

func toF32(
  x: externref
) -> f32 {
  if (typeof x !== "number")
    trap();

  // NOTE: alternative semantics would be *not* to perform that test, and let
  // ToWebAssemblyValue demote to f32 instead. But then it is equivalent to
  // doing `toF64`+`f32.demote_f64`, so there is not much point.
  if (Math.fround(x) !== x && x === x)
    trap();

  return x;
}

"wasm:js-number" "toI32"

❗️ Unboxing from the universal representation into a primitive i32

Technically implementable using toF64, and additional Wasm instructions. However, I suspect there are many cases where engines would first have to convert an integer stored internally into a float before giving it to Wasm.

func toI32(
  x: externref
) -> i32 {
  if (typeof x !== "number")
    trap();

  if ((x | 0) !== x || Object.is(x, -0))
    trap();

  return x;
}

In today's engines, it is worth first trying a br_on_cast (ref i31), with a fallback on JS glue code.

"wasm:js-number" "toU32"

Likely as useful as toI32 for languages that have primitive unsigned integers.

func toU32(
  x: externref
) -> i32 {
  if (typeof x !== "number")
    trap();

  if ((x >>> 0) !== x || Object.is(x, -0))
    trap();

  return x;
}

"wasm:js-number" "fmod"

Can be implemented in pure Wasm, although that misses the opportunity to leverage hardware fprem instructions, where they exist.

func fmod(
  x: f64,
  y: f64
) -> f64 {
  return x % y;
}

"wasm:js-number" "wrapToI32"

Can be implemented in pure Wasm, although that misses the opportunity to leverage hardware FJCVTZS instructions, where they exist.

func wrapToI32(
  x: f64
) -> i32 {
  return x | 0;
}

Note that a hypothetical wrapToU32 would be exactly equivalent, and hence is not proposed.

"wasm:js-number" "sin" (and other similar Math operations)

Can be imported as is. Included for "guaranteed" no-glue-code calls.

Can also be implemented in pure Wasm, although that misses the opportunity to leveral relevant hardward support, where available.

func sin(
  x: f64
) -> f64 {
  return Math.sin(x);
}

"wasm:js-number" "parse"

Can be imported as is as parseFloat, except that parseFloat calls ToString() on its argument, rather than rejecting non-strings. The exact behavior can be achieved by calling "wasm:js-string" "cast" before calling parseFloat. Included for "guaranteed" no-glue-code calls.

func parse(
  string: externref
) -> f64 {
  if (typeof string !== "string")
    trap();

  return parseFloat(string);
}

Note: we do not offer parseInt, as it can be efficiently implemented on the Wasm side based on existing JS string builtins. Moreover, languages tend to disagree on the specifics of the format anyway, which makes it a poor common ground. parseFloat is more critical, as efficient implementations require big tables, which we do not want to ship along with our Wasm code. At the same time, its semantics are more widely shared across languages.

"wasm:js-boolean" "test"

❗️ Primitive type test for bool from the universal representation

func test(
  x: externref
) -> i32 {
  if (typeof x !== "boolean")
    return 0;
  return 1;
}

"wasm:js-boolean" "toI32"

❗️ Unboxing from the universal representation into a primitive bool

func toI32(
  x: externref
) -> i32 {
  if (typeof x !== "boolean")
    trap();

  if (x)
    return 1;
  return 0;
}

"wasm:js-undefined" "test"

❗️ Primitive type test for undefined from the universal representation

func test(
  x: externref
) -> i32 {
  if (typeof x !== "undefined")
    return 0;
  return 1;
}

"wasm:js-symbol" "test"

❗️ Primitive type test for symbol from the universal representation

func test(
  x: externref
) -> i32 {
  if (typeof x !== "symbol")
    return 0;
  return 1;
}

"wasm:js-bigint" "test"

❗️ Primitive type test for bigint from the universal representation

func test(
  x: externref
) -> i32 {
  if (typeof x !== "bigint")
    return 0;
  return 1;
}

Other than type testing, Scala.js would probably not benefit much, if at all, from the other operations on bigints. However, I believe that other languages may use them, and they seem to fit in the design space of this proposal.

"wasm:js-bigint" "fromF64"

func fromF64(
  x: f64
) -> (ref extern) {
  // NOTE: BigInt(x) would throw a RangeError in the situation below.
  // Trap instead.
  if (!Number.isInteger(x))
    trap();

  return BigInt(x);
}

"wasm:js-bigint" "fromI64"

func fromI64(
  x: i64
) -> (ref extern) {
  // NOTE: `x` is interpreted as a signed JS `bigint` by the Wasm-to-JS
  // interface, so this appears as a no-op.
  return x;
}

"wasm:js-bigint" "fromU64"

func fromU64(
  x: i64
) -> (ref extern) {
  // NOTE: `x` is interpreted as a signed JS `bigint` by the Wasm-to-JS
  // interface. Reinterpret it as unsigned.
  return BigInt.asUint(64, x);
}

"wasm:js-bigint" "convertToF64"

func convertToF64(
  x: externref
) -> f64 {
  if (typeof x !== "bigint")
    trap();

  return Number(x);
}

"wasm:js-bigint" "wrapToI64"

func wrapToI64(
  x: externref
) -> i64 {
  if (typeof x !== "bigint")
    trap();

  // NOTE: ToWebAssemblyValue specifies a wrapping conversion via ToBigInt64
  return x;
}

"wasm:js-bigint" "add" (and other JS operators)

func add(
  x: externref,
  y: externref
) -> f64 {
  if (typeof x !== "bigint")
    trap();
  if (typeof y !== "bigint")
    trap();

  return x + y;
}

"wasm:js-bigint" "asIntN"

func asIntN(
  bits: i32,
  bigint: externref
) -> f64 {
  if (typeof bigint !== "bigint")
    trap();

  // NOTE: `bits` is interpreted as signed 32-bit integers when converted to a
  // JS value using standard conversions. Reinterpret it as unsigned here.
  bits >>>= 0;

  return BigInt.asIntN(bits, bigint);
}

"wasm:js-bigint" "asUintN"

func asUintN(
  bits: i32,
  bigint: externref
) -> f64 {
  if (typeof bigint !== "bigint")
    trap();

  // NOTE: `bits` is interpreted as signed 32-bit integers when converted to a
  // JS value using standard conversions. Reinterpret it as unsigned here.
  bits >>>= 0;

  return BigInt.asUintN(bits, bigint);
}

"wasm:js-bigint" "parse"

func parse(
  string: externref
) -> f64 {
  if (typeof string !== "string")
    trap();

  // NOTE: ToBitInt(argument) throws a SyntaxError if the string is not a valid
  // integer string. However, that is not easy to test ahead of time. So we
  // catch the SyntaxError instead to turn it into a Wasm trap.
  try {
    return BigInt(string);
  } catch (e) {
    // Assert: e instanceof SyntaxError
    trap();
  }
}

"wasm:js-bigint" "toString"

There might not be much point to this method if "wasm:js-string" "fromAny" exists.

func toString(
  bigint: externref
) -> f64 {
  if (typeof bigint !== "bigint")
    trap();

  return "" + bigint;
}

"wasm:js-object" "is"

❗️ This function is the closest thing to an identity test that applies to the universal representation on a JS host.

Can be imported as is. Included for "guaranteed" no-glue-code calls.

func is(
  x: externref,
  y: externref
) -> i32 {
  if (Object.is(x, y))
    return 1;
  return 0;
}

sjrd avatar Apr 02 '25 12:04 sjrd

I'd be curious to know which of these operations producers other than Scala.js would be interested in. IIUC, Scala.js is fairly unique in how it represents and manipulates even things like numbers as externrefs. As an example, it's hard to imagine something like "wasm:js-boolean" "toI32" being useful for other producers, since they would normally not be storing bools as externrefs.

tlively avatar Apr 03 '25 23:04 tlively

For wasm_of_ocaml, I would be particularly interested in math operations from the Math object (along with the JavaScript remainder operator), as well as primitives for accessing array buffers (that should be covered by #1555), and UTF-8 encoding / decoding support (between JavaScript strings and Wasm byte arrays).

I'm a bit skeptical about generic functions like Object.is. In JavaScript, these functions are optimized through execution tracing and specialized code generation based on actual usage patterns. That kind of optimization probably won't happen if they are direcly inlined directly in Wasm code.

There are a few other primitives I could use, but I don't think they are critical for performance in my case:

  • Conversion functions: between JavaScript numbers and Wasm floats and i32, and between booleans and i32.
  • Float parsing.
  • Operations on Map and WeakMap.
  • BigInt operations.

Conversion functions like the primitive "wasm:js-boolean" "toI32" are useful to interoperate with JavaScript. I'm currently just importing the identity to implement this conversion.

   (import "bindings" "identity" (func $to_bool (param anyref) (result i32)))

vouillon avatar Apr 04 '25 08:04 vouillon

Technically Scala.js uses anyref as its universal representation. We would have a more direct encoding if all the signatures above used anyref, but I used externref to stay in line with the original JS builtins proposal.

I don't think using anyref as a universal representation is that odd. In particular I'm pretty sure several toolchains use ref.i31 to some extent.

The really unusual aspect of Scala.js is that we need our universal representation to be compatible with that of JavaScript. In particular, we want the following diagram to commute, as category theorists say:

         upcast/                             
 ┌─────┐   downcast ┌────────┐               
 │ i32 ◄────────────► anyref │               
 └───▲─┘            └───▲────┘               
     │                  │                    
     │                  │ToJSValue/          
     │                  │  ToWebAssemblyValue
     │              ┌───▼────┐               
     └──────────────►  JS    │               
ToJSValue/          └────────┘               
  ToWebAssemblyValue                         

Although Scala.js might be the first toolchain to really enforce that property (though @vouillon's reply just above suggests that that's not even true), I don't think it will be the last. A language that wants good, polymorphism-resistant interoperability with JavaScript will want their version of that diagram to commute as well.

sjrd avatar Apr 04 '25 08:04 sjrd

I'm a bit skeptical about generic functions like Object.is. In JavaScript, these functions are optimized through execution tracing and specialized code generation based on actual usage patterns. That kind of optimization probably won't happen if they are direcly inlined directly in Wasm code.

For Object.is (and "wasm:js-string" "fromAny"), I'd be happy to have a direct connection to the fallback non-inline function in the engine's runtime, TBH ;) It's the bridge to JS that is really the performance killer.

sjrd avatar Apr 04 '25 08:04 sjrd

@tlively Would it be appropriate for me to send a PR to the meetings repo, to present this proposal at an upcoming CG meeting and perhaps get a Stage 1 vote?

I'm seeing the quick progress on the Custom Descriptors proposal, and in a sense it makes me feel encouraged to push this one forward.

I did reread the process document, as well as the champions.md document. One thing I'm unclear on is who can really be a champion? Can I champion my own proposal? (I am willing to implement it in at least one engine when the time comes.) Or are possible champions part of a more "core" group of people.

Should I first actively seek interest among other toolchains who could be interested?

Sorry for these meta questions. I did look around, but if I missed the answers somewhere, feel free to point them to me or just answer "look harder" ;) Thanks.

sjrd avatar May 13 '25 15:05 sjrd

Yes, scheduling a phase 1 vote would be appropriate, and yes, anyone can be a champion! You can certainly champion your own proposal.

tlively avatar May 13 '25 18:05 tlively

Responding to

the opportunity to leverage hardware fprem instructions, where they exist.

and

"sin" (and other similar Math operations)

Can also be implemented in pure Wasm, although that misses the opportunity to leveral relevant hardward support, where available.

Modern popular CPU architectures don't have fast fprem, sin, or other instructions, other than ones for which Wasm already has instructions, such as sqrt. x86 does have frem, fsin, etc., however they're slow and sometimes inaccurate, and in practice compilers don't use them, even for things like Math.sin etc.

sunfishcode avatar May 14 '25 15:05 sunfishcode

Interesting. V8 does appear to generate hardware fprem for the % operator, though (granted, with a software loop around it, but doing 64 exponents by iteration is still better than doing only 1). I have not investigated the Math functions, though. If we don't generate hardware instructions for those anyway, then it's probably not worth including them, indeed.

sjrd avatar May 14 '25 18:05 sjrd

Interesting. I wasn't aware of that use of fprem. If that code turns out to be a win in practice, that might motivate adding frem as an opcode to core Wasm.

sunfishcode avatar May 15 '25 16:05 sunfishcode

Drive-by:

Conversion from anything as if in string concat: fromAny

Please call this toString to match JavaScript. Even if it doesn't match the naming convention for other operations I think introducing a new name for this extremely widely used operation would be worse than violating that consistency.

bakkot avatar Jun 03 '25 17:06 bakkot

Conversion from anything as if in string concat

Also you probably don't want string concat behaviour here. You probably want String(...) behaviour, since it won't throw on symbols.

michaelficarra avatar Jun 03 '25 20:06 michaelficarra

cc @mkustermann, we were talking about Dart needing some subset of this proposed functionality recently.

@sjrd, this went to phase 1, but I don't see a proposal repo for it yet. Would you like me to create one for you?

tlively avatar Jul 09 '25 21:07 tlively

@sjrd, this went to phase 1, but I don't see a proposal repo for it yet. Would you like me to create one for you?

Ah yes, please. I have been swamped in end-of-semester teaching duties for the past few weeks, but I'll be able to get back to it soon.

sjrd avatar Jul 09 '25 22:07 sjrd

cc @mkustermann, we were talking about Dart needing some subset of this proposed functionality recently.

To give some more information around those things from Dart2Wasm perspective.

We use type tests on JS values for several purposes, e.g.

  • We get an arbitrary JS object and want to (possibly deeply) convert it to a Dart object. The compiler doesn't know statically the exact type or shape of the JS object, so we use type tests to convert it. Currently we call out to JS type testing code which then does various tests and returns an integer representing that type (see externRefType()) then we have the type and can either convert the object (e.g. numbers, bools, ...) or create a Dart wrapper/view (e.g. js typed array).
  • Another use case is verification for soundness: We have declarative JS interop annotations that say a JS functions returns a specific JS type but want to verify it's of that type - to guarantee Dart soundness: If we have String foo = callToJS(), the foo variable in Dart is represented as a wasm struct with an externref field in it. We want to guarantee that the externref we get from callToJS() is actually a JS string.

For JS strings specifically

  • We want fast conversion of primitives to strings and parse strings to primitives (currently we e.g. call an imported Function.prototype.call.bind(Number.prototype.toString) function and hope the engines optimize it)
  • We also are very interested in utf8 encoding/decoding (also mentioned in proposals/js-string-builtins/Overview.md#future-extensions) & creating ascii encoding/decoding (i.e. operations working on i8 arrays) - the current need to always go via i16 arrays is very suboptimal (e.g. we have our own utf8 decoder and we detect ascii case, and even if it's ascii we have to use i16 arrays to create a string via "wasm:js-string":"fromCharCodeArray")

mkustermann avatar Jul 14 '25 08:07 mkustermann

FYI, I populated the proposal repo yesterday: https://github.com/WebAssembly/js-primitive-builtins

There's a PR pending to add it to the main Proposals readme. Once that is merged, I will close this issue.

sjrd avatar Jul 14 '25 08:07 sjrd

[...] We want to guarantee that the externref we get from callToJS() is actually a JS string.

Just to clarify: This is already covered with the js-string-builtins proposal via "wasm:js-string" "test".

Another use case is verification for soundness: We have declarative JS interop annotations that say a JS functions returns a specific JS type but want to verify it's of that type - to guarantee Dart soundness

Wouldn't it be simpler to just emit the type-check on the JS side (if needed by wrapping the callToJS target)? With proper inlining of the JS call target into the JS wrapper this could even remove the checks if the optimizer can infer the JS type?

Liedtke avatar Jul 14 '25 08:07 Liedtke

Just to clarify: This is already covered with the js-string-builtins proposal via "wasm:js-string" "test".

True. I should've taken a different example, e.g. JS typed data, JS array, ...

Wouldn't it be simpler to just emit the type-check on the JS side (if needed by wrapping the callToJS target)?

Yes, we also discussed this before (maybe also here).

Though if we had fast JS type checks via calls to specially recognized/optimized imported functions, we may prefer to use those over emitting bigger JS code.

mkustermann avatar Jul 14 '25 09:07 mkustermann

In order to scope this specific proposal, I chose to limit the proposed set of builtins to manipulate JS primitives (hence the name of the proposal). So although it would give you efficient type tests for undefined, boolean and number (and also bigint and symbol, in addition to string), typed arrays and Arrays would be out of scope. The problem with various types of objects, even if specified in ECMAScript, is that the set quickly grows uncontrollably, and it's hard to set an objective limit.

sjrd avatar Jul 14 '25 09:07 sjrd

The proposal is now listed in the Proposals repo. I've been told that this is the right time to close this issue.

Follow-up discussions should happen on the JS Primitive Builtins Proposal repo.

sjrd avatar Jul 14 '25 18:07 sjrd