Skip to content

[Strings] Handle fromCharCode and cast imports in StringLifting/Lowering - #9373

Open
jeremy-code wants to merge 6 commits into
WebAssembly:mainfrom
jeremy-code:dev/string-cast-from-char-code
Open

jeremy-code wants to merge 6 commits into
WebAssembly:mainfrom
jeremy-code:dev/string-cast-from-char-code

Conversation

@jeremy-code

Copy link
Copy Markdown

StringLifting: fromCharCode(x) becomes string.from_code_point(x & 0xFFFF) and cast becomes non-nullable ref.cast to string.

StringLowering: string.from_code_point(x & 0xFFFF) or equivalent becomes fromCharCode(x) and non-nullable ref.cast to string becomes cast(). Nullable ref.cast to string checks ref.is_null beforehand.

@jeremy-code
jeremy-code requested a review from a team as a code owner November 9, 2026 08:38

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support and optimizations for fromCharCode and cast operations in both the StringLifting and StringLowering passes. In StringLifting, fromCharCode is mapped to fromCodePoint with a 0xFFFF mask, and cast is lifted to a ref.cast to string. In StringLowering, casts to string are lowered to imported cast calls, and fromCodePoint with a 0xFFFF mask is optimized back to fromCharCode. The feedback suggests optimizing the lowering of nullable casts by skipping the redundant null check when the input operand is already known to be non-nullable.

Comment thread src/passes/StringLowering.cpp Resolved
Comment thread src/passes/StringLifting.cpp Resolved

@tlively tlively left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

Comment thread src/passes/StringLowering.cpp Resolved
Comment thread src/passes/StringLowering.cpp Resolved
Comment thread src/passes/StringLowering.cpp Resolved
Comment thread src/passes/StringLifting.cpp Resolved
Comment thread src/passes/StringLifting.cpp Resolved
Comment thread src/passes/StringLifting.cpp Resolved
Comment thread src/passes/StringLowering.cpp Resolved
Comment thread src/passes/StringLowering.cpp Resolved
jeremy-code and others added 2 commits November 9, 2026 19:53
StringLifting: fromCharCode(x) becomes string.from_code_point(x & 0xFFFF) and cast becomes non-nullable ref.cast to string.

StringLowering: string.from_code_point(x & 0xFFFF) or equivalent becomes fromCharCode(x) and non-nullable ref.cast to string becomes cast(). Nullable ref.cast to string checks ref.is_null beforehand.
…-instructions.wast

Co-authored-by: Thomas Lively <tlively123@gmail.com>
@jeremy-code
jeremy-code force-pushed the dev/string-cast-from-char-code branch from c26b4b0 to e55e999 Compare November 10, 2026 03:54
…st behavior

Add a StringNewFromCharCode IR operation. The equivalent stringref operation is
string.from_code_point where the codePoint is masked (x & 0xFFFF).

Allow ref.cast with strings when strings are enabled, otherwise GC is required.

StringLifting: Refinalize IR whenever a call has been replaced (i.e. externref
-> stringref).

StringLowering: Move replaceInstructions before updateTypes and add visitRefCast in Replacer.
Comment thread src/passes/StringLowering.cpp Resolved
Comment thread src/wasm/wasm-stack.cpp Outdated
Comment on lines +2998 to +3005
if (curr->op == StringNewFromCharCode) {
// This is not part of the spec, so use a bitmask to get the expected output
// String.fromCharCode(0x10000) => '\x00'
// String.fromCodePoint(0x10000) => '𐀀'
// Hence, String.fromCharCode(x) == String.fromCodePoint(x & 0xFFFF)
o << int8_t(BinaryConsts::I32Const) << S32LEB(0xFFFF)
<< int8_t(BinaryConsts::I32And);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than try to emit valid stringref instructions here, let's just invent a new opcode for this operation. The stringref proposal is not active, so we don't need to worry about compatibility, and using a dedicated opcode will prevent these masks from bloating the IR when we round-trip through a binary.

Does that sound reasonable to you, too, @kripken?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly either way. My hope is that we end up with a toolchain-level spec at some point (c.f. the bignums discussion). In that case, we would write a new spec and can add this new instruction then, I suppose; but I'd be ok the other way too (keeping to the current spec). Either seem fine. So if you prefer a new instruction, sgtm.

@jeremy-code jeremy-code Nov 11, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I mention this in the README for the Binaryen IR? Not sure if that would be excessively verbose.

Binaryen supports a stringref type. This is similar to the currently-inactive [stringref proposal] except that the string type is a subtype of externref rather than anyref and that it has a fromCharCode operation.

Comment thread src/wasm/wasm-validator.cpp Outdated
Comment on lines +3252 to +3256
bool hasStringCasts =
curr->type.isRef() &&
curr->type.getHeapType().isMaybeShared(HeapType::string);
shouldBeTrue(getModule()->features.hasGC() ||
(hasStringCasts && getModule()->features.hasStrings()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool hasStringCasts =
curr->type.isRef() &&
curr->type.getHeapType().isMaybeShared(HeapType::string);
shouldBeTrue(getModule()->features.hasGC() ||
(hasStringCasts && getModule()->features.hasStrings()),
bool isStringCast = curr->type.isString();
shouldBeTrue(getModule()->features.hasGC() ||
(isStringCast && getModule()->features.hasStrings()),

I wonder if we will need to allow casts to nullexternref without GC enabled as well. Can you run the fuzzer (scripts/monitor_fuzz.py) to find out?

Please also add validation tests for this new special case.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the fuzzer is set to disable strings whenever GC is disabled:

'--disable-gc': ['--disable-strings', '--disable-stack-switching', '--disable-custom-descriptors'],


;; CHECK: (import "wasm:js-string" "substring" (func $substring (type $11) (param externref i32 i32) (result (ref extern))))

;; CHECK: (import "wasm:js-string" "fromCharCode" (func $fromCharCode_14 (type $0) (param i32) (result (ref extern))))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's probably out of scope for this PR, but it seems bad that we end up with duplicate imports after the round trip.

jeremy-code and others added 3 commits November 10, 2026 15:07
Co-authored-by: Thomas Lively <tlively123@gmail.com>
Co-authored-by: Thomas Lively <tlively123@gmail.com>
…ble or string bottom type to be cast without GC

StringFromCharCode now has opcode 0xab. Support unreachable casts and casts to the string bottom type (noextern) without requiring GC.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants