[dart2wasm] Start using WebAssembly.String imports

Refactor `JSStringImpl` to use the js-string-builtins API[1].

When the module `WebAssembly.String` is not available we define a
"polyfill" with the previous imports, so this change is backwards
compatible.

Also updates some of the methods to use avoid multiple `this.length`
calls (which do an FFI call), and use unchecked getters when possible.

A new library `dart:_error_utils` is introduced for faster range and
index checks when the length (or max value) is known to be positive
(e.g. when it's the length of a string).

For now only `JSStringImpl` and `operator []` and `operator []=` of
`JSArrayImpl` are updated to use the new range and index checks. Rest of
the libraries will be updated separately.

[1]: https://github.com/WebAssembly/js-string-builtins

CoreLibraryReviewExempt: dart2wasm specific library change.
Change-Id: I9436def0cfe59c631f6f4e15ea06cc18a47a738e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/335043
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
This commit is contained in:
Ömer Sinan Ağacan 2023-12-08 10:04:41 +00:00 committed by Commit Queue
parent d27b887fec
commit 7cceaebd49
13 changed files with 242 additions and 103 deletions

View file

@ -370,6 +370,11 @@ class Intrinsifier {
codeGen.wrap(node.arguments.positional[0], w.NumType.i64); codeGen.wrap(node.arguments.positional[0], w.NumType.i64);
b.i64_le_u(); b.i64_le_u();
return boolType; return boolType;
case "ltU":
codeGen.wrap(receiver, w.NumType.i64);
codeGen.wrap(node.arguments.positional[0], w.NumType.i64);
b.i64_lt_u();
return boolType;
default: default:
throw 'Unknown WasmI64 member $name'; throw 'Unknown WasmI64 member $name';
} }

View file

@ -95,6 +95,23 @@ const jsRuntimeBlobPart3 = r'''
return wrapped; return wrapped;
} }
if (WebAssembly.String === undefined) {
console.log("WebAssembly.String is undefined, adding polyfill");
WebAssembly.String = {
"charCodeAt": (s, i) => s.charCodeAt(i),
"compare": (s1, s2) => {
if (s1 < s2) return -1;
if (s1 > s2) return 1;
return 0;
},
"concat": (s1, s2) => s1 + s2,
"equals": (s1, s2) => s1 === s2,
"fromCharCode": (i) => String.fromCharCode(i),
"length": (s) => s.length,
"substring": (s, a, b) => s.substring(a, b),
};
}
// Imports // Imports
const dart2wasm = { const dart2wasm = {
'''; ''';

View file

@ -409,6 +409,7 @@ class Chrome extends Browser {
"--no-first-run", "--no-first-run",
"--use-mock-keychain", "--use-mock-keychain",
"--user-data-dir=${userDir.path}", "--user-data-dir=${userDir.path}",
"--js-flags=--experimental-wasm-imported-strings",
url, url,
]; ];

View file

@ -604,6 +604,7 @@ class Dart2WasmCompilerConfiguration extends CompilerConfiguration {
final isD8 = runtimeConfiguration is D8RuntimeConfiguration; final isD8 = runtimeConfiguration is D8RuntimeConfiguration;
return [ return [
if (isD8) '--turboshaft-wasm', if (isD8) '--turboshaft-wasm',
if (isD8) '--experimental-wasm-imported-strings',
'pkg/dart2wasm/bin/run_wasm.js', 'pkg/dart2wasm/bin/run_wasm.js',
if (isD8) '--', if (isD8) '--',
'${filename.substring(0, filename.lastIndexOf('.'))}.mjs', '${filename.substring(0, filename.lastIndexOf('.'))}.mjs',

View file

@ -36,6 +36,6 @@ if [[ $D8_OPTIONS ]]; then
fi fi
# Find the JS runtime based on the input wasm file. # Find the JS runtime based on the input wasm file.
exec "$D8_EXEC" --turboshaft-wasm \ exec "$D8_EXEC" --turboshaft-wasm --experimental-wasm-imported-strings \
"${EXTRA_D8_OPTIONS[@]}" \ "${EXTRA_D8_OPTIONS[@]}" \
"$SDK_DIR/pkg/dart2wasm/bin/run_wasm.js" -- "$(realpath -- "${1%.*}.mjs")" "$@" "$SDK_DIR/pkg/dart2wasm/bin/run_wasm.js" -- "$(realpath -- "${1%.*}.mjs")" "$@"

View file

@ -0,0 +1,42 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:_wasm';
class IndexErrorUtils {
/// Same as [IndexError.check], but assumes that [length] is positive and
/// uses a single unsigned comparison. Always inlined.
@pragma("wasm:prefer-inline")
static void checkAssumePositiveLength(int index, int length) {
if (WasmI64.fromInt(length).leU(WasmI64.fromInt(index))) {
throw IndexError.withLength(index, length);
}
}
}
class RangeErrorUtils {
/// Same as `RangeError.checkValueInInterval(value, 0, maxValue)`, but
/// assumes that [maxValue] is positive and uses a single unsigned
/// comparison. Always inlined.
@pragma("wasm:prefer-inline")
static void checkValueBetweenZeroAndPositiveMax(int value, int maxValue) {
if (WasmI64.fromInt(maxValue).ltU(WasmI64.fromInt(value))) {
throw RangeError.range(value, 0, maxValue);
}
}
/// Same as [RangeError.checkValidRange], but assumes that [length] is
/// positive and does less checks. Error reporting is also slightly
/// different: when both [start] and [end] are negative, this reports [end]
/// instead of [start]. Always inlined.
@pragma("wasm:prefer-inline")
static void checkValidRangePositiveLength(int start, int end, int length) {
if (WasmI64.fromInt(length).ltU(WasmI64.fromInt(end))) {
throw RangeError.range(end, 0, length);
}
if (WasmI64.fromInt(end).ltU(WasmI64.fromInt(start))) {
throw RangeError.range(start, 0, end);
}
}
}

View file

@ -466,18 +466,30 @@ class JSArrayImpl implements List<JSAny?> {
'(a, l) => a.length = l', toExternRef, newLength.toJS.toExternRef); '(a, l) => a.length = l', toExternRef, newLength.toJS.toExternRef);
} }
@override @pragma("wasm:prefer-inline")
JSAny? operator [](int index) { JSAny? _getUnchecked(int index) =>
RangeError.checkValueInInterval(index, 0, length - 1); js.JSValue.boxT<JSAny?>(js.JS<WasmExternRef?>(
return js.JSValue.boxT<JSAny?>(js.JS<WasmExternRef?>( '(a, i) => a[i]', toExternRef, index.toJS.toExternRef));
'(a, i) => a[i]', toExternRef, index.toJS.toExternRef));
}
@override @override
@pragma("wasm:prefer-inline")
JSAny? operator [](int index) {
IndexErrorUtils.checkAssumePositiveLength(index, length);
return _getUnchecked(index);
}
@pragma("wasm:prefer-inline")
void _setUnchecked(int index, JSAny? value) => js.JS<void>(
'(a, i, v) => a[i] = v',
toExternRef,
index.toJS.toExternRef,
value.toExternRef);
@override
@pragma("wasm:prefer-inline")
void operator []=(int index, JSAny? value) { void operator []=(int index, JSAny? value) {
RangeError.checkValueInInterval(index, 0, length - 1); IndexErrorUtils.checkAssumePositiveLength(index, length);
js.JS<void>('(a, i, v) => a[i] = v', toExternRef, index.toJS.toExternRef, _setUnchecked(index, value);
value.toExternRef);
} }
@override @override

View file

@ -4,60 +4,71 @@
part of dart._js_types; part of dart._js_types;
String _matchString(Match match) => match[0]!;
String _stringIdentity(String string) => string;
/// The box class for JS' `string` object. [JSStringImpl] is heavily based off
/// of `sdk/lib/_internal/js_runtime/lib/js_string.dart`. TODO(joshualitt): Add
/// `JSString` fastpaths for cases where `String` arguments are really
/// `JSString`.
final class JSStringImpl implements String { final class JSStringImpl implements String {
final WasmExternRef? _ref; final WasmExternRef? _ref;
JSStringImpl(this._ref); JSStringImpl(this._ref);
@pragma("wasm:prefer-inline")
static String? box(WasmExternRef? ref) => static String? box(WasmExternRef? ref) =>
js.isDartNull(ref) ? null : JSStringImpl(ref); js.isDartNull(ref) ? null : JSStringImpl(ref);
@pragma("wasm:prefer-inline")
WasmExternRef? get toExternRef => _ref; WasmExternRef? get toExternRef => _ref;
@override
@pragma("wasm:prefer-inline")
int get length => _jsLength(toExternRef);
@override
@pragma("wasm:prefer-inline")
bool get isEmpty => length == 0;
@override
@pragma("wasm:prefer-inline")
bool get isNotEmpty => !isEmpty;
@pragma("wasm:entry-point") @pragma("wasm:entry-point")
static String interpolate(List<Object?> values) { static String interpolate(List<Object?> values) {
final array = JSArrayImpl.fromLength(values.length); final valuesLength = values.length;
for (int i = 0; i < values.length; i++) { final array = JSArrayImpl.fromLength(valuesLength);
for (int i = 0; i < valuesLength; i++) {
final o = values[i]; final o = values[i];
final s = o.toString(); final s = o.toString();
final jsString = final jsString =
s is JSStringImpl ? js.JSValue.boxT<JSAny?>(s.toExternRef) : s.toJS; s is JSStringImpl ? js.JSValue.boxT<JSAny?>(s.toExternRef) : s.toJS;
array[i] = jsString; array._setUnchecked(i, jsString);
} }
return JSStringImpl( return JSStringImpl(
js.JS<WasmExternRef?>("a => a.join('')", array.toExternRef)); js.JS<WasmExternRef?>("a => a.join('')", array.toExternRef));
} }
@override @override
@pragma("wasm:prefer-inline")
int codeUnitAt(int index) { int codeUnitAt(int index) {
RangeError.checkValueInInterval(index, 0, length - 1); final length = this.length;
return js IndexErrorUtils.checkAssumePositiveLength(index, length);
.JS<double>('(s, i) => s.charCodeAt(i)', toExternRef, return _codeUnitAtUnchecked(index);
index.toDouble().toExternRef) }
.toInt();
@pragma("wasm:prefer-inline")
int _codeUnitAtUnchecked(int index) {
return _jsCharCodeAt(toExternRef, index);
} }
@override @override
Iterable<Match> allMatches(String string, [int start = 0]) { Iterable<Match> allMatches(String string, [int start = 0]) {
if (0 > start || start > string.length) { final stringLength = string.length;
throw new RangeError.range(start, 0, string.length); RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(start, stringLength);
}
return StringAllMatchesIterable(string, this, start); return StringAllMatchesIterable(string, this, start);
} }
@override @override
Match? matchAsPrefix(String string, [int start = 0]) { Match? matchAsPrefix(String string, [int start = 0]) {
if (start < 0 || start > string.length) { final stringLength = string.length;
throw new RangeError.range(start, 0, string.length); RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(start, stringLength);
} final length = this.length;
if (start + length > string.length) return null; if (start + length > stringLength) return null;
// TODO(lrn): See if this can be optimized. // TODO(lrn): See if this can be optimized.
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
if (string.codeUnitAt(start + i) != codeUnitAt(i)) { if (string.codeUnitAt(start + i) != codeUnitAt(i)) {
@ -70,8 +81,7 @@ final class JSStringImpl implements String {
@override @override
String operator +(String other) { String operator +(String other) {
if (other is JSStringImpl) { if (other is JSStringImpl) {
return JSStringImpl(js.JS<WasmExternRef?>( return JSStringImpl(_jsConcat(toExternRef, other.toExternRef));
'(a, b) => a + b', toExternRef, other.toExternRef));
} }
// TODO(joshualitt): Refactor `string_patch.dart` so we can directly // TODO(joshualitt): Refactor `string_patch.dart` so we can directly
@ -81,7 +91,8 @@ final class JSStringImpl implements String {
@override @override
bool endsWith(String other) { bool endsWith(String other) {
int otherLength = other.length; final otherLength = other.length;
final length = this.length;
if (otherLength > length) return false; if (otherLength > length) return false;
return other == substring(length - otherLength); return other == substring(length - otherLength);
} }
@ -102,6 +113,7 @@ final class JSStringImpl implements String {
} else { } else {
StringBuffer result = StringBuffer(); StringBuffer result = StringBuffer();
result.write(to); result.write(to);
final length = this.length;
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
result.write(this[i]); result.write(this[i]);
result.write(to); result.write(to);
@ -143,12 +155,13 @@ final class JSStringImpl implements String {
if (onMatch == null) onMatch = _matchString; if (onMatch == null) onMatch = _matchString;
if (onNonMatch == null) onNonMatch = _stringIdentity; if (onNonMatch == null) onNonMatch = _stringIdentity;
if (from is String) { if (from is String) {
int patternLength = from.length; final patternLength = from.length;
if (patternLength == 0) { if (patternLength == 0) {
// Pattern is the empty string. // Pattern is the empty string.
StringBuffer buffer = StringBuffer(); StringBuffer buffer = StringBuffer();
int i = 0; int i = 0;
buffer.write(onNonMatch("")); buffer.write(onNonMatch(""));
final length = this.length;
while (i < length) { while (i < length) {
buffer.write(onMatch(StringMatch(i, this, ""))); buffer.write(onMatch(StringMatch(i, this, "")));
// Special case to avoid splitting a surrogate pair. // Special case to avoid splitting a surrogate pair.
@ -172,6 +185,7 @@ final class JSStringImpl implements String {
} }
StringBuffer buffer = StringBuffer(); StringBuffer buffer = StringBuffer();
int startIndex = 0; int startIndex = 0;
final length = this.length;
while (startIndex < length) { while (startIndex < length) {
int position = indexOf(from, startIndex); int position = indexOf(from, startIndex);
if (position == -1) { if (position == -1) {
@ -212,7 +226,7 @@ final class JSStringImpl implements String {
@override @override
String replaceFirst(Pattern from, String to, [int startIndex = 0]) { String replaceFirst(Pattern from, String to, [int startIndex = 0]) {
RangeError.checkValueInInterval(startIndex, 0, length, "startIndex"); RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(startIndex, length);
if (from is String) { if (from is String) {
int index = indexOf(from, startIndex); int index = indexOf(from, startIndex);
if (index < 0) return this; if (index < 0) return this;
@ -233,7 +247,7 @@ final class JSStringImpl implements String {
@override @override
String replaceFirstMapped(Pattern from, String replace(Match match), String replaceFirstMapped(Pattern from, String replace(Match match),
[int startIndex = 0]) { [int startIndex = 0]) {
RangeError.checkValueInInterval(startIndex, 0, this.length, "startIndex"); RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(startIndex, length);
Iterator<Match> matches = from.allMatches(this, startIndex).iterator; Iterator<Match> matches = from.allMatches(this, startIndex).iterator;
if (!matches.moveNext()) return this; if (!matches.moveNext()) return this;
Match match = matches.current; Match match = matches.current;
@ -292,15 +306,14 @@ final class JSStringImpl implements String {
@override @override
String replaceRange(int start, int? end, String replacement) { String replaceRange(int start, int? end, String replacement) {
final e = RangeError.checkValidRange(start, end, length); end ??= length;
return _replaceRange(start, e, replacement); RangeErrorUtils.checkValidRangePositiveLength(start, end, length);
return _replaceRange(start, end, replacement);
} }
@override @override
bool startsWith(Pattern pattern, [int index = 0]) { bool startsWith(Pattern pattern, [int index = 0]) {
if (index < 0 || index > length) { RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(index, length);
throw RangeError.range(index, 0, length);
}
if (pattern is String) { if (pattern is String) {
final patternLength = pattern.length; final patternLength = pattern.length;
final endIndex = index + patternLength; final endIndex = index + patternLength;
@ -312,9 +325,9 @@ final class JSStringImpl implements String {
@override @override
String substring(int start, [int? end]) { String substring(int start, [int? end]) {
end = RangeError.checkValidRange(start, end, this.length); end ??= length;
return JSStringImpl(js.JS<WasmExternRef?>('(o, s, i) => o.substring(s, i)', RangeErrorUtils.checkValidRangePositiveLength(start, end, length);
toExternRef, start.toDouble(), end.toDouble())); return JSStringImpl(_jsSubstring(toExternRef, start, end));
} }
@override @override
@ -398,9 +411,10 @@ final class JSStringImpl implements String {
/// Finds the index of the first non-whitespace character, or the /// Finds the index of the first non-whitespace character, or the
/// end of the string. Start looking at position [index]. /// end of the string. Start looking at position [index].
static int _skipLeadingWhitespace(String string, int index) { static int _skipLeadingWhitespace(JSStringImpl string, int index) {
while (index < string.length) { final stringLength = string.length;
int codeUnit = string.codeUnitAt(index); while (index < stringLength) {
int codeUnit = string._codeUnitAtUnchecked(index);
if (codeUnit != spaceCodeUnit && if (codeUnit != spaceCodeUnit &&
codeUnit != carriageReturnCodeUnit && codeUnit != carriageReturnCodeUnit &&
!_isWhitespace(codeUnit)) { !_isWhitespace(codeUnit)) {
@ -413,9 +427,9 @@ final class JSStringImpl implements String {
/// Finds the index after the last non-whitespace character, or 0. /// Finds the index after the last non-whitespace character, or 0.
/// Start looking at position [index - 1]. /// Start looking at position [index - 1].
static int _skipTrailingWhitespace(String string, int index) { static int _skipTrailingWhitespace(JSStringImpl string, int index) {
while (index > 0) { while (index > 0) {
int codeUnit = string.codeUnitAt(index - 1); int codeUnit = string._codeUnitAtUnchecked(index - 1);
if (codeUnit != spaceCodeUnit && if (codeUnit != spaceCodeUnit &&
codeUnit != carriageReturnCodeUnit && codeUnit != carriageReturnCodeUnit &&
!_isWhitespace(codeUnit)) { !_isWhitespace(codeUnit)) {
@ -426,7 +440,7 @@ final class JSStringImpl implements String {
return index; return index;
} }
// Dart2Wasm can't use JavaScript trim directly, // dart2wasm can't use JavaScript trim directly,
// because JavaScript does not trim // because JavaScript does not trim
// the NEXT LINE (NEL) character (0x85). // the NEXT LINE (NEL) character (0x85).
@override @override
@ -435,26 +449,27 @@ final class JSStringImpl implements String {
// either end of the string. // either end of the string.
final result = final result =
JSStringImpl(js.JS<WasmExternRef?>('s => s.trim()', toExternRef)); JSStringImpl(js.JS<WasmExternRef?>('s => s.trim()', toExternRef));
if (result.length == 0) return result; final resultLength = result.length;
int firstCode = result.codeUnitAt(0); if (resultLength == 0) return result;
int firstCode = result._codeUnitAtUnchecked(0);
int startIndex = 0; int startIndex = 0;
if (firstCode == nelCodeUnit) { if (firstCode == nelCodeUnit) {
startIndex = _skipLeadingWhitespace(result, 1); startIndex = _skipLeadingWhitespace(result, 1);
if (startIndex == result.length) return ""; if (startIndex == resultLength) return "";
} }
int endIndex = result.length; int endIndex = resultLength;
// We know that there is at least one character that is non-whitespace. // We know that there is at least one character that is non-whitespace.
// Therefore we don't need to verify that endIndex > startIndex. // Therefore we don't need to verify that endIndex > startIndex.
int lastCode = result.codeUnitAt(endIndex - 1); int lastCode = result.codeUnitAt(endIndex - 1);
if (lastCode == nelCodeUnit) { if (lastCode == nelCodeUnit) {
endIndex = _skipTrailingWhitespace(result, endIndex - 1); endIndex = _skipTrailingWhitespace(result, endIndex - 1);
} }
if (startIndex == 0 && endIndex == result.length) return result; if (startIndex == 0 && endIndex == resultLength) return result;
return substring(startIndex, endIndex); return substring(startIndex, endIndex);
} }
// Dart2Wasm can't use JavaScript trimLeft directly because it does not trim // dart2wasm can't use JavaScript trimLeft directly because it does not trim
// the NEXT LINE character (0x85). // the NEXT LINE character (0x85).
@override @override
String trimLeft() { String trimLeft() {
@ -463,17 +478,18 @@ final class JSStringImpl implements String {
int startIndex = 0; int startIndex = 0;
final result = final result =
JSStringImpl(js.JS<WasmExternRef?>('s => s.trimLeft()', toExternRef)); JSStringImpl(js.JS<WasmExternRef?>('s => s.trimLeft()', toExternRef));
if (result.length == 0) return result; final resultLength = result.length;
int firstCode = result.codeUnitAt(0); if (resultLength == 0) return result;
int firstCode = result._codeUnitAtUnchecked(0);
if (firstCode == nelCodeUnit) { if (firstCode == nelCodeUnit) {
startIndex = _skipLeadingWhitespace(result, 1); startIndex = _skipLeadingWhitespace(result, 1);
} }
if (startIndex == 0) return result; if (startIndex == 0) return result;
if (startIndex == result.length) return ""; if (startIndex == resultLength) return "";
return result.substring(startIndex); return result.substring(startIndex);
} }
// Dart2Wasm can't use JavaScript trimRight directly because it does not trim // dart2wasm can't use JavaScript trimRight directly because it does not trim
// the NEXT LINE character (0x85). // the NEXT LINE character (0x85).
@override @override
String trimRight() { String trimRight() {
@ -481,14 +497,15 @@ final class JSStringImpl implements String {
// string. // string.
final result = final result =
JSStringImpl(js.JS<WasmExternRef?>('s => s.trimRight()', toExternRef)); JSStringImpl(js.JS<WasmExternRef?>('s => s.trimRight()', toExternRef));
int endIndex = result.length; final resultLength = result.length;
int endIndex = resultLength;
if (endIndex == 0) return result; if (endIndex == 0) return result;
int lastCode = result.codeUnitAt(endIndex - 1); int lastCode = result.codeUnitAt(endIndex - 1);
if (lastCode == nelCodeUnit) { if (lastCode == nelCodeUnit) {
endIndex = _skipTrailingWhitespace(result, endIndex - 1); endIndex = _skipTrailingWhitespace(result, endIndex - 1);
} }
if (endIndex == result.length) return result; if (endIndex == resultLength) return result;
if (endIndex == 0) return ""; if (endIndex == 0) return "";
return result.substring(0, endIndex); return result.substring(0, endIndex);
} }
@ -503,16 +520,16 @@ final class JSStringImpl implements String {
@override @override
String padLeft(int width, [String padding = ' ']) { String padLeft(int width, [String padding = ' ']) {
int delta = width - this.length; int delta = width - length;
if (delta <= 0) return this; if (delta <= 0) return this;
return padding * delta + this; return (padding * delta) + this;
} }
@override @override
String padRight(int width, [String padding = ' ']) { String padRight(int width, [String padding = ' ']) {
int delta = width - this.length; int delta = width - length;
if (delta <= 0) return this; if (delta <= 0) return this;
return this + padding * delta; return this + (padding * delta);
} }
@override @override
@ -528,9 +545,8 @@ final class JSStringImpl implements String {
@override @override
int indexOf(Pattern pattern, [int start = 0]) { int indexOf(Pattern pattern, [int start = 0]) {
if (start < 0 || start > this.length) { final length = this.length;
throw RangeError.range(start, 0, this.length); RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(start, length);
}
if (pattern is JSStringImpl) { if (pattern is JSStringImpl) {
return _jsIndexOf(pattern.toExternRef, start); return _jsIndexOf(pattern.toExternRef, start);
} else if (pattern is String) { } else if (pattern is String) {
@ -539,7 +555,7 @@ final class JSStringImpl implements String {
Match? match = js.firstMatchAfter(pattern, this, start); Match? match = js.firstMatchAfter(pattern, this, start);
return (match == null) ? -1 : match.start; return (match == null) ? -1 : match.start;
} else { } else {
for (int i = start; i <= this.length; i++) { for (int i = start; i <= length; i++) {
if (pattern.matchAsPrefix(this, i) != null) return i; if (pattern.matchAsPrefix(this, i) != null) return i;
} }
return -1; return -1;
@ -553,10 +569,11 @@ final class JSStringImpl implements String {
@override @override
int lastIndexOf(Pattern pattern, [int? start]) { int lastIndexOf(Pattern pattern, [int? start]) {
final length = this.length;
if (start == null) { if (start == null) {
start = length; start = length;
} else if (start < 0 || start > this.length) { } else {
throw RangeError.range(start, 0, this.length); RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(start, length);
} }
if (pattern is JSStringImpl) { if (pattern is JSStringImpl) {
if (start + pattern.length > length) { if (start + pattern.length > length) {
@ -577,9 +594,8 @@ final class JSStringImpl implements String {
@override @override
bool contains(Pattern other, [int startIndex = 0]) { bool contains(Pattern other, [int startIndex = 0]) {
if (startIndex < 0 || startIndex > this.length) { final length = this.length;
throw RangeError.range(startIndex, 0, this.length); RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(startIndex, length);
}
if (other is String) { if (other is String) {
return indexOf(other, startIndex) >= 0; return indexOf(other, startIndex) >= 0;
} else if (other is js.JSSyntaxRegExp) { } else if (other is js.JSSyntaxRegExp) {
@ -589,31 +605,24 @@ final class JSStringImpl implements String {
} }
} }
@override
bool get isEmpty => length == 0;
@override
bool get isNotEmpty => !isEmpty;
/// This must be kept in sync with `StringBase.hashCode` in string_patch.dart. /// This must be kept in sync with `StringBase.hashCode` in string_patch.dart.
/// TODO(joshualitt): Find some way to cache the hash code. /// TODO(joshualitt): Find some way to cache the hash code.
@override @override
int get hashCode { int get hashCode {
int hash = 0; int hash = 0;
final length = this.length;
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
hash = stringCombineHashes(hash, codeUnitAt(i)); hash = stringCombineHashes(hash, _codeUnitAtUnchecked(i));
} }
return stringFinalizeHash(hash); return stringFinalizeHash(hash);
} }
@override @override
int get length => js.JS<double>('s => s.length', toExternRef).toInt(); @pragma("wasm:prefer-inline")
@override
String operator [](int index) { String operator [](int index) {
RangeError.checkValueInInterval(index, 0, length - 1); final length = this.length;
return JSStringImpl(js.JS<WasmExternRef?>( IndexErrorUtils.checkAssumePositiveLength(index, length);
'(s, i) => s[i]', toExternRef, index.toDouble().toExternRef)); return JSStringImpl(_jsFromCharCode(_codeUnitAtUnchecked(index)));
} }
@override @override
@ -621,25 +630,34 @@ final class JSStringImpl implements String {
if (identical(this, other)) { if (identical(this, other)) {
return true; return true;
} }
if (other is JSStringImpl && length == other.length) {
return js.areEqualInJS(toExternRef, other.toExternRef); if (other is JSStringImpl) {
} else if (other is String && length == other.length) { return _jsEquals(toExternRef, other.toExternRef);
}
final length = this.length;
if (other is String && length == other.length) {
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
if (codeUnitAt(i) != other.codeUnitAt(i)) { if (_codeUnitAtUnchecked(i) != other.codeUnitAt(i)) {
return false; return false;
} }
} }
return true; return true;
} }
return false; return false;
} }
@override @override
int compareTo(String other) { int compareTo(String other) {
int otherLength = other.length; if (other is JSStringImpl) {
int len = (length < otherLength) ? length : otherLength; return _jsCompare(toExternRef, other.toExternRef);
}
final otherLength = other.length;
final length = this.length;
final len = (length < otherLength) ? length : otherLength;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
int thisCodeUnit = this.codeUnitAt(i); int thisCodeUnit = _codeUnitAtUnchecked(i);
int otherCodeUnit = other.codeUnitAt(i); int otherCodeUnit = other.codeUnitAt(i);
if (thisCodeUnit < otherCodeUnit) { if (thisCodeUnit < otherCodeUnit) {
return -1; return -1;
@ -657,10 +675,10 @@ final class JSStringImpl implements String {
String toString() => js.stringify(toExternRef); String toString() => js.stringify(toExternRef);
int firstNonWhitespace() { int firstNonWhitespace() {
final len = this.length; final length = this.length;
int first = 0; int first = 0;
for (; first < len; first++) { for (; first < length; first++) {
if (!_isWhitespace(this.codeUnitAt(first))) { if (!_isWhitespace(_codeUnitAtUnchecked(first))) {
break; break;
} }
} }
@ -668,9 +686,9 @@ final class JSStringImpl implements String {
} }
int lastNonWhitespace() { int lastNonWhitespace() {
int last = this.length - 1; int last = length - 1;
for (; last >= 0; last--) { for (; last >= 0; last--) {
if (!_isWhitespace(this.codeUnitAt(last))) { if (!_isWhitespace(_codeUnitAtUnchecked(last))) {
break; break;
} }
} }
@ -678,6 +696,10 @@ final class JSStringImpl implements String {
} }
} }
String _matchString(Match match) => match[0]!;
String _stringIdentity(String string) => string;
@pragma("wasm:export", "\$jsStringToJSStringImpl") @pragma("wasm:export", "\$jsStringToJSStringImpl")
JSStringImpl _jsStringToJSStringImpl(WasmExternRef? string) => JSStringImpl _jsStringToJSStringImpl(WasmExternRef? string) =>
JSStringImpl(string); JSStringImpl(string);
@ -688,3 +710,34 @@ WasmExternRef? _jsStringFromJSStringImpl(JSStringImpl string) =>
bool _jsIdentical(WasmExternRef? ref1, WasmExternRef? ref2) => bool _jsIdentical(WasmExternRef? ref1, WasmExternRef? ref2) =>
js.JS<bool>('Object.is', ref1, ref2); js.JS<bool>('Object.is', ref1, ref2);
@pragma("wasm:prefer-inline")
int _jsCharCodeAt(WasmExternRef? stringRef, int index) => js
.JS<WasmI32>(
'WebAssembly.String.charCodeAt', stringRef, WasmI32.fromInt(index))
.toIntUnsigned();
WasmExternRef _jsConcat(WasmExternRef? s1, WasmExternRef? s2) =>
js.JS<WasmExternRef>('WebAssembly.String.concat', s1, s2);
@pragma("wasm:prefer-inline")
WasmExternRef _jsSubstring(
WasmExternRef? stringRef, int startIndex, int endIndex) =>
js.JS<WasmExternRef>('WebAssembly.String.substring', stringRef,
WasmI32.fromInt(startIndex), WasmI32.fromInt(endIndex));
@pragma("wasm:prefer-inline")
int _jsLength(WasmExternRef? stringRef) =>
js.JS<WasmI32>('WebAssembly.String.length', stringRef).toIntUnsigned();
@pragma("wasm:prefer-inline")
bool _jsEquals(WasmExternRef? s1, WasmExternRef? s2) =>
js.JS<WasmI32>('WebAssembly.String.equals', s1, s2).toBool();
@pragma("wasm:prefer-inline")
int _jsCompare(WasmExternRef? s1, WasmExternRef? s2) =>
js.JS<WasmI32>('WebAssembly.String.compare', s1, s2).toIntSigned();
@pragma("wasm:prefer-inline")
WasmExternRef _jsFromCharCode(int charCode) => js.JS<WasmExternRef>(
'WebAssembly.String.fromCharCode', WasmI32.fromInt(charCode));

View file

@ -13,6 +13,7 @@
/// change in the future. /// change in the future.
library dart._js_types; library dart._js_types;
import 'dart:_error_utils';
import 'dart:_internal'; import 'dart:_internal';
import 'dart:_js_helper' as js; import 'dart:_js_helper' as js;
import 'dart:_string_helper'; import 'dart:_string_helper';

View file

@ -156,6 +156,9 @@ class WasmI64 extends _WasmInt {
/// `i64.le_u`. /// `i64.le_u`.
external bool leU(WasmI64 other); external bool leU(WasmI64 other);
/// `i64.lt_u`.
external bool ltU(WasmI64 other);
} }
/// The Wasm `f32` type. /// The Wasm `f32` type.

View file

@ -306,7 +306,6 @@ class RangeError extends ArgumentError {
/// name and message text of the thrown error. /// name and message text of the thrown error.
/// ///
/// Returns [value] if it is in the interval. /// Returns [value] if it is in the interval.
@pragma("wasm:entry-point")
static int checkValueInInterval(int value, int minValue, int maxValue, static int checkValueInInterval(int value, int minValue, int maxValue,
[String? name, String? message]) { [String? name, String? message]) {
if (value < minValue || value > maxValue) { if (value < minValue || value > maxValue) {

View file

@ -239,6 +239,9 @@
"_internal/wasm/lib/int_common_patch.dart" "_internal/wasm/lib/int_common_patch.dart"
] ]
}, },
"_error_utils": {
"uri": "_internal/wasm/lib/error_utils.dart"
},
"_http": { "_http": {
"uri": "_http/http.dart" "uri": "_http/http.dart"
}, },

View file

@ -202,6 +202,8 @@ wasm_common:
- _internal/wasm/lib/boxed_double.dart - _internal/wasm/lib/boxed_double.dart
- _internal/wasm/lib/boxed_int.dart - _internal/wasm/lib/boxed_int.dart
- _internal/wasm/lib/int_common_patch.dart - _internal/wasm/lib/int_common_patch.dart
_error_utils:
uri: _internal/wasm/lib/error_utils.dart
_http: _http:
uri: _http/http.dart uri: _http/http.dart
_internal: _internal: