Support static String.prototype.at and codePointAt (#454)

* Support static string at and codePointAt

* Update static at diagnostics and island contracts
This commit is contained in:
Chris Tate
2026-09-25 21:34:21 -05:00
committed by GitHub
parent 894d687037
commit c92fdceedd
21 changed files with 446 additions and 58 deletions
@@ -55,6 +55,7 @@ import {
STATIC_MATH_FNS,
STATIC_MATH_PROPS,
STATIC_NUMBER_METHODS,
STRING_INDEX_METHODS,
STR_METHODS,
UNSUPPORTED_EXPR,
UNSUPPORTED_STMT,
@@ -199,6 +200,9 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
note: arityNote(entry.minArgs, entry.maxArgs),
});
}
for (const name of STRING_INDEX_METHODS) {
add({ id: `stdlib.string.${name}`, kind: "stdlib", name: `string.prototype.${name}`, status: "static", note: arityNote(0, 1) });
}
for (const name of [...ARRAY_METHODS]) {
add({ id: `stdlib.array.${name}`, kind: "stdlib", name: `Array.prototype.${name}`, status: "static" });
}
@@ -283,6 +287,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
}
}
for (const name of Object.keys(ISLAND_SURFACE.string)) {
if (STRING_INDEX_METHODS.has(name)) continue;
add({
id: `stdlib.string.${name}`,
kind: "stdlib",
@@ -1,11 +1,11 @@
import * as ts from "../../ts7/adapter.js";
import { BOOL, DYN, F64, IrExpr, IrFunction, IrStmt, IrType, STRING, SrcLoc, arrayOf, isUnitType, typeEquals, typeKey } from "../../../ir/ir.js";
import { BOOL, DYN, F64, IrExpr, IrFunction, IrStmt, IrType, STRING, SrcLoc, UNDEFINED_T, arrayOf, isUnitType, typeEquals, typeKey } from "../../../ir/ir.js";
import { numLit, strLit, varRef } from "../../../ir/build.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
import { nodeThrowExpr, own } from "../lowerer.js";
import { isRequireMainFilename } from "../expressions/optional-chains.js";
import { STR_METHODS } from "../surfaces.js";
import { STRING_INDEX_METHODS, STR_METHODS } from "../surfaces.js";
import { coerceStringSearchValue, defaultAfterUndefined, lowerOptionalArgument, lowerPositionArgument, lowerStaticallyUndefinedArgument, lowerStringSearchArgument, positionNumber } from "../optional-arguments.js";
function lowerSplitLimitArg(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr {
@@ -143,6 +143,99 @@ function lowerMethodReceiver(
return lowerer.coerceInto(node, lowered, expected);
}
/** Relative string indexing and code-point decoding over the UTF-16 intrinsics. */
export function lowerStringIndexCall(
lowerer: Lowerer,
call: ts.CallExpression,
method: "at" | "codePointAt",
receiver: IrExpr,
receiverNode: ts.Node,
argumentNodes: readonly ts.Expression[],
): IrExpr {
const loc = locOf(call);
if (argumentNodes.length > 1 || argumentNodes.some(ts.isSpreadElement)) {
return lowerer.noLowering(`String.prototype.${method} with ${argumentNodes.length} arguments`, call);
}
const indexNode = argumentNodes[0];
let index = lowerPositionArgument(lowerer, indexNode, numLit(0, loc));
if (indexNode && (index.type.kind === "record" || index.type.kind === "array" || index.type.kind === "func")) {
index = lowerer.coerceInto(indexNode, index, DYN);
}
const valueType = method === "at" ? STRING : F64;
const resultType = lowerer.withUndefinedArmOf(valueType);
if (!resultType || resultType.kind !== "union") return lowerer.noLowering(`String.prototype.${method} result`, call);
const undefinedTag = lowerer.armTag(resultType.unionId, UNDEFINED_T);
const valueTag = lowerer.armTag(resultType.unionId, valueType);
const key = `str.index:${method}:${typeKey(receiver.type)}:${typeKey(index.type)}`;
let helper = lowerer.widthHelpers.get(key);
if (!helper) {
helper = `%str.index.${lowerer.widthHelpers.size}`;
lowerer.widthHelpers.set(key, helper);
const rawReceiver = varRef("arg.0", receiver.type, loc);
const rawIndex = varRef("arg.1", index.type, loc);
const stringReceiver = varRef("receiver.0", STRING, loc);
const length = varRef("length.0", F64, loc);
const position = varRef("position.0", F64, loc);
const first = varRef("first.0", F64, loc);
const second = varRef("second.0", F64, loc);
const bin = (op: "<" | ">=" | ">" | "+" | "-" | "*", left: IrExpr, right: IrExpr): IrExpr =>
({ kind: "bin", op, left, right, type: op === "<" || op === ">=" || op === ">" ? BOOL : F64, loc });
const either = (left: IrExpr, right: IrExpr): IrExpr => ({ kind: "logical", op: "||", left, right, type: BOOL, loc });
const wrap = (value: IrExpr): IrExpr => ({ kind: "unionWrap", unionId: resultType.unionId, tag: valueTag, value, type: resultType, loc });
const miss: IrExpr = {
kind: "unionWrap", unionId: resultType.unionId, tag: undefinedTag,
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, type: resultType, loc,
};
const numeric = positionNumber(lowerer, rawIndex, numLit(0, loc), indexNode ?? call, "string index");
const numericValue = varRef("numeric.0", F64, loc);
const body: IrStmt[] = [
{ kind: "varDecl", localId: "receiver.0", init: lowerer.ensureString(rawReceiver, receiverNode), loc },
{ kind: "varDecl", localId: "length.0", init: { kind: "strIntrinsic", method: "length", receiver: stringReceiver, args: [], type: F64, loc }, loc },
{ kind: "varDecl", localId: "numeric.0", init: numeric, loc },
{ kind: "varDecl", localId: "position.0", init: {
kind: "ternary", cond: { kind: "libCall", fn: "num.isNaN", args: [numericValue], type: BOOL, loc },
then: numLit(0, loc), else_: { kind: "libCall", fn: "math.trunc", args: [numericValue], type: F64, loc }, type: F64, loc,
}, loc },
];
if (method === "at") {
body.push({ kind: "if", cond: bin("<", position, numLit(0, loc)),
then: [{ kind: "assign", localId: "position.0", value: bin("+", position, length), loc }], else_: null, loc });
}
body.push({ kind: "if", cond: either(bin("<", position, numLit(0, loc)), bin(">=", position, length)),
then: [{ kind: "return", value: miss, loc }], else_: null, loc });
if (method === "at") {
body.push({ kind: "return", value: wrap({ kind: "strIntrinsic", method: "charAt", receiver: stringReceiver, args: [position], type: STRING, loc }), loc });
} else {
body.push({ kind: "varDecl", localId: "first.0", init: { kind: "strIntrinsic", method: "charCodeAt", receiver: stringReceiver, args: [position], type: F64, loc }, loc });
body.push({ kind: "if", cond: either(either(bin("<", first, numLit(0xd800, loc)), bin(">", first, numLit(0xdbff, loc))), bin(">=", bin("+", position, numLit(1, loc)), length)),
then: [{ kind: "return", value: wrap(first), loc }], else_: null, loc });
body.push({ kind: "varDecl", localId: "second.0", init: { kind: "strIntrinsic", method: "charCodeAt", receiver: stringReceiver, args: [bin("+", position, numLit(1, loc))], type: F64, loc }, loc });
body.push({ kind: "if", cond: either(bin("<", second, numLit(0xdc00, loc)), bin(">", second, numLit(0xdfff, loc))),
then: [{ kind: "return", value: wrap(first), loc }], else_: null, loc });
body.push({ kind: "return", value: wrap(bin("+", bin("+", bin("*", bin("-", first, numLit(0xd800, loc)), numLit(1024, loc)), bin("-", second, numLit(0xdc00, loc))), numLit(0x10000, loc))), loc });
}
lowerer.liftedFns.push({
name: helper,
params: [{ localId: "arg.0", name: "receiver", type: receiver.type }, { localId: "arg.1", name: "index", type: index.type }],
returnType: resultType,
locals: [
{ id: "arg.0", name: "receiver", type: receiver.type, mutable: false },
{ id: "arg.1", name: "index", type: index.type, mutable: false },
{ id: "receiver.0", name: "stringReceiver", type: STRING, mutable: false },
{ id: "length.0", name: "length", type: F64, mutable: false },
{ id: "numeric.0", name: "numeric", type: F64, mutable: false },
{ id: "position.0", name: "position", type: F64, mutable: method === "at" },
...(method === "codePointAt" ? [
{ id: "first.0", name: "first", type: F64, mutable: false },
{ id: "second.0", name: "second", type: F64, mutable: false },
] : []),
],
body, loc,
});
}
return { kind: "call", callee: helper, args: [receiver, index], type: resultType, loc };
}
function paddingFillString(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr {
const loc = value.loc;
if (value.type.kind === "undefinedT" || value.type.kind === "void") return strLit(" ", loc);
@@ -453,7 +546,8 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
if (lowerer.chainBlocked(access, call)) return null;
if (dynReceiver === undefined && access.name.text === "localeCompare") return lowerLocaleCompareCall(lowerer, call, access);
const entry = own(STR_METHODS, access.name.text);
if (!entry) return null;
const indexMethod = STRING_INDEX_METHODS.has(access.name.text) ? access.name.text as "at" | "codePointAt" : null;
if (!entry && !indexMethod) return null;
// A validated dyn receiver (`pkg.name.replace(...)` on a JSON.parse
// value) arrives pre-extracted through `dynReceiver`; its checker type
// is `any`, so the type/symbol gates don't apply — the dyn value's
@@ -473,7 +567,7 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
}
// The lib declares optional parameters beyond some lowered forms; fence
// those arities instead of passing arguments the runtime doesn't take.
if (argumentNodes.length < entry.minArgs || argumentNodes.length > entry.maxArgs) {
if (argumentNodes.length < (entry?.minArgs ?? 0) || argumentNodes.length > (entry?.maxArgs ?? 1)) {
lowerer.noLowering(
`.${access.name.text} with ${argumentNodes.length} argument${argumentNodes.length === 1 ? "" : "s"} on strings`,
call,
@@ -483,6 +577,8 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
? dynReceiver()
: lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
const loc = locOf(call);
if (indexMethod) return lowerStringIndexCall(lowerer, call, indexMethod, receiver, access.expression, argumentNodes);
if (!entry) return null;
if (entry.method === "split") return lowerStringSplitCall(lowerer, call, receiver, access.expression, argumentNodes);
if (entry.method === "padStart" || entry.method === "padEnd") {
return lowerStringPaddingCall(lowerer, call, entry.method, receiver, access.expression, argumentNodes);
@@ -12,13 +12,13 @@ import { isJsSourceFile, isNodeEsmFile, locOf } from "../program.js";
import { genResultRecord, isGenericCallableMemberType, typeKey } from "../type-mapper.js";
import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx, nodeThrowExpr, staticImportNamespaceType } from "./lowerer.js";
import { enforceLibBoundary } from "./lib-boundary.js";
import { NARROW_FIRST, STR_METHODS, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js";
import { NARROW_FIRST, STRING_INDEX_METHODS, STR_METHODS, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js";
import { ffiBindingDiag, ffiSignatureDiag, libCallbackDiag, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
import { mixinFnShapeOf } from "./lower-mixins.js";
import { dynStringReceiver, lowerArrayFromCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerTupleReadMethodCall } from "./lower-containers.js";
import { bufEncoding } from "./containers/bytes.js";
import { lowerRegexMethodCall, lowerStringMethodCall, lowerStringPaddingCall, lowerStringSplitCall } from "./containers/string-and-regexp.js";
import { lowerRegexMethodCall, lowerStringIndexCall, lowerStringMethodCall, lowerStringPaddingCall, lowerStringSplitCall } from "./containers/string-and-regexp.js";
import { lowerChildStreamMethodCall, lowerChildWriterMethodCall, lowerCreateRequireCall, lowerCryptoHashMethodCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerImportMetaResolveCall, lowerNodeModuleCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerRequireResolveCall, lowerWatcherMethodCall } from "./lower-builtins.js";
import { lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, stringWrapperToString, templateRawTextOf } from "./lower-exprs.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
@@ -5111,18 +5111,22 @@ function lowerStringPrototypeCall(
const prototypeAccess = methodAccess.expression;
if (!ts.isPropertyAccessExpression(prototypeAccess) || prototypeAccess.name.text !== "prototype" ||
!ts.isIdentifier(prototypeAccess.expression) || prototypeAccess.expression.text !== "String" ||
!Object.hasOwn(STR_METHODS, methodAccess.name.text) || !lowerer.isStdlibMember(methodAccess)) return null;
(!Object.hasOwn(STR_METHODS, methodAccess.name.text) && !STRING_INDEX_METHODS.has(methodAccess.name.text)) ||
!lowerer.isStdlibMember(methodAccess)) return null;
if (call.arguments.some(ts.isSpreadElement)) return null;
const entry = STR_METHODS[methodAccess.name.text]!;
const entry = STR_METHODS[methodAccess.name.text];
const indexMethod = STRING_INDEX_METHODS.has(methodAccess.name.text) ? methodAccess.name.text as "at" | "codePointAt" : null;
const resultType = indexMethod ? lowerer.withUndefinedArmOf(indexMethod === "at" ? STRING : F64) : entry?.result;
if (!resultType) return null;
const method = methodAccess.name.text === "trimStart" ? "trimLeft" :
methodAccess.name.text === "trimEnd" ? "trimRight" : methodAccess.name.text;
const loc = locOf(call);
const nullishError = nodeThrowExpr(1, "", `String.prototype.${method} called on null or undefined`, entry.result, loc);
const nullishError = nodeThrowExpr(1, "", `String.prototype.${method} called on null or undefined`, resultType, loc);
const receiverNode = call.arguments[0];
if (!receiverNode) return nullishError;
// Receiver coercion runs after .call has evaluated its arguments; keep
// contextual wrapper lowering on forms without later method arguments.
const immediateWrapper = entry.maxArgs === 0 && call.arguments.length === 1
const immediateWrapper = entry?.maxArgs === 0 && call.arguments.length === 1
? immediatePrimitiveWrapperToString(lowerer, receiverNode) : null;
const receiverValue = immediateWrapper ?? lowerer.lowerExpr(receiverNode);
const receiverType = receiverValue.type;
@@ -5135,24 +5139,26 @@ function lowerStringPrototypeCall(
kind: "seqExpr",
stmts: values.filter((value) => !isSafeToDiscard(value)).map((value) => ({ kind: "exprStmt", expr: value, loc: value.loc })),
result: nullishError,
type: entry.result,
type: resultType,
loc,
};
}
const padding = entry.method === "padStart" || entry.method === "padEnd";
const padding = entry?.method === "padStart" || entry?.method === "padEnd";
const scalar = receiverType.kind === "string" || receiverType.kind === "f64" ||
receiverType.kind === "bool" || receiverType.kind === "bigint" ||
(receiverType.kind === "union" && (lowerer.unions.get(receiverType.unionId)?.arms.every((arm) =>
arm.kind === "string" || arm.kind === "f64" || arm.kind === "bool" || arm.kind === "bigint") ?? false));
// Other methods convert object receivers before lowering their arguments;
// padding uses a helper that delays conversion until every argument is ready.
const objectWithoutMethodArgs = entry.maxArgs === 0 && call.arguments.length === 1 &&
const objectWithoutMethodArgs = entry?.maxArgs === 0 && call.arguments.length === 1 &&
(receiverType.kind === "record" || receiverType.kind === "array" || receiverType.kind === "object");
const dynObjectWithoutMethodArgs = entry.maxArgs === 0 && call.arguments.length === 1 &&
const dynObjectWithoutMethodArgs = entry?.maxArgs === 0 && call.arguments.length === 1 &&
receiverType.kind === "dyn" && plainObjectCoercionReceiver(lowerer, receiverNode);
if (!scalar && !objectWithoutMethodArgs && !dynObjectWithoutMethodArgs && !(padding && receiverType.kind === "dyn")) {
return lowerer.noLowering(`String.prototype.${methodAccess.name.text}.call with ${lowerer.fmt(receiverType)} receivers`, call);
}
if (indexMethod) return lowerStringIndexCall(lowerer, call, indexMethod, receiverValue, receiverNode, call.arguments.slice(1));
if (!entry) return null;
if (padding) {
return lowerStringPaddingCall(lowerer, call, entry.method as "padStart" | "padEnd", receiverValue, receiverNode, call.arguments.slice(1));
}
@@ -12,7 +12,7 @@ import type { Lowerer } from "./lowerer.js";
import { wasiGuestPath } from "../../wasi-paths.js";
import { BIGINT_T, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, IrExpr, IrFunction, IrJsOp, IrLocal, IrRecordShape, IrStmt, IrType, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isJsonSafeType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionFuncSetArmsOk } from "../../ir/ir.js";
import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, COMPOUND_ASSIGN_OPS, CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, COMPOUND_ASSIGN_OPS, CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STRING_INDEX_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall, tryLowerNumericIndexRead } from "./lower-containers.js";
@@ -615,6 +615,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
return { kind: "strLit", value: "object", type: STRING, loc };
}
if (ts.isPropertyAccessExpression(expr.expression)) {
const member = expr.expression;
const prototype = member.expression;
if (STRING_INDEX_METHODS.has(member.name.text) && ts.isPropertyAccessExpression(prototype) &&
prototype.name.text === "prototype" && ts.isIdentifier(prototype.expression) &&
lowerer.isStdlibGlobal(prototype.expression, "String") && lowerer.isStdlibMember(member)) {
return { kind: "strLit", value: "function", type: STRING, loc };
}
const presence = lowerPromiseThenPresence(
lowerer,
expr.expression,
@@ -3636,7 +3643,7 @@ function lowerPromiseThenPresence(
}
if (
kind === "string"
? own(STR_METHODS, name) !== undefined || own(ISLAND_SURFACE.string, name) !== undefined
? own(STR_METHODS, name) !== undefined || STRING_INDEX_METHODS.has(name) || own(ISLAND_SURFACE.string, name) !== undefined
: ARRAY_METHODS.has(name)
) {
lowerer.unsupported("SC1090", expr, `${kind} methods as values (call '${name}' directly)`);
@@ -424,6 +424,9 @@ export const STR_METHODS: Record<
toWellFormed: { method: "toWellFormed", result: STRING, minArgs: 0, maxArgs: 0 },
};
/** String index reads return an undefined arm and compose the existing UTF-16 intrinsics. */
export const STRING_INDEX_METHODS = new Set(["at", "codePointAt"]);
/** One member of the island-backed ambient surface: declared argument
* types (tsc enforces them at call sites; the arity double-checks the
* table against the ambient file) and the validated island-exit target the
+9 -2
View File
@@ -3489,8 +3489,8 @@
"id": "stdlib.string.at",
"kind": "stdlib",
"name": "string.prototype.at",
"status": "dynamic-only",
"code": "SC2012"
"status": "static",
"note": "the lowered call form takes 0 to 1 arguments"
},
{
"id": "stdlib.string.charAt",
@@ -3506,6 +3506,13 @@
"status": "static",
"note": "the lowered call form takes 0 to 1 arguments"
},
{
"id": "stdlib.string.codePointAt",
"kind": "stdlib",
"name": "string.prototype.codePointAt",
"status": "static",
"note": "the lowered call form takes 0 to 1 arguments"
},
{
"id": "stdlib.string.endsWith",
"kind": "stdlib",
@@ -3487,6 +3487,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2113-string-index-methods.js": {
"order": [
"<repo>/tests/corpus/2113-string-index-methods.js"
],
"diags": []
},
"<repo>/tests/corpus/212-string-aliasing-append.ts": {
"order": [
"<repo>/tests/corpus/212-string-aliasing-append.ts"
+23
View File
@@ -0,0 +1,23 @@
const word = "café";
console.log(word.at(0), word.at(-1), word.at(-5), word.at(4));
console.log("abcdef".at(-1.9), "abcdef".at(1.9), "abcdef".at(-Infinity), "abcdef".at(NaN));
console.log("abc".at("1"), "abc".at(false), "abc".at(null), "abc".at(undefined));
console.log("abc".at([1]), "abc".at([]), "abc".at(function () {}));
const astral = "A😀B";
console.log(astral.codePointAt(0), astral.codePointAt(1), astral.codePointAt(2), astral.codePointAt(3), astral.codePointAt(4));
console.log(astral.codePointAt(-1), astral.codePointAt(Infinity), astral.codePointAt(NaN));
console.log(astral.codePointAt("1.9"), astral.codePointAt([3]), astral.codePointAt([]));
console.log(String.prototype.at.call(12345, -1), String.prototype.codePointAt.call(true, 0));
let order = "";
function receiver() { order += "r"; return "abc"; }
function argument() { order += "a"; return { valueOf() { order += "v"; return 1.8; } }; }
console.log(receiver().at(argument()), order);
order = "";
console.log(String.prototype.codePointAt.call(receiver(), argument()), order);
order = "";
function touched() { order += "x"; return 0; }
try { String.prototype.at.call(undefined, touched()); } catch (error) { console.log(error.name, error.message, order); }
try { String.prototype.codePointAt.call(null, touched()); } catch (error) { console.log(error.name, error.message, order); }
+1 -2
View File
@@ -1,5 +1,5 @@
// The island-backed ambient surface (number methods, string-pattern
// replace/at, the Number statics, ...)
// replace, the Number statics, ...)
// typechecks against real static types but executes in the embedded
// engine: in a static build every use site is its own SC2012 naming the
// flag — never an ICE, never a link error. Static Math methods include
@@ -11,5 +11,4 @@ const up = Math.cbrt(2);
const tau = Math.PI * 2;
const price = (19.99).toPrecision(4);
const swapped = "banana".replace("an", "AN");
const ch = "hello".at(0);
const n = Number.parseFloat("3.14");
@@ -12,24 +12,15 @@ dynamic-surface.ts:13:17 - error SC2012: '.replace()' on strings runs in the emb
12 | const price = (19.99).toPrecision(4);
13 | const swapped = "banana".replace("an", "AN");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 | const ch = "hello".at(0);
14 | const n = Number.parseFloat("3.14");
hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it
dynamic-surface.ts:14:12 - error SC2012: '.at()' on strings runs in the embedded dynamic engine, which this build does not include
dynamic-surface.ts:14:11 - error SC2012: 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include
13 | const swapped = "banana".replace("an", "AN");
14 | const ch = "hello".at(0);
| ^~~~~~~~~~~~~
15 | const n = Number.parseFloat("3.14");
hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it
dynamic-surface.ts:15:11 - error SC2012: 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include
14 | const ch = "hello".at(0);
15 | const n = Number.parseFloat("3.14");
14 | const n = Number.parseFloat("3.14");
| ^~~~~~~~~~~~~~~~~~~~~~~~~
16 |
15 |
hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it
+14 -25
View File
@@ -463,32 +463,21 @@ console.log(recovered);
});
/* ── the island-backed ambient surface (Math, methods, globals) ───────
* Value results are differential (corpus 1110–1116); what lives here is
* behavior Node cannot oracle: the `.at()` undefined-refusal divergence
* and the fiber re-anchor for surface calls inside async bodies. */
* Value results are differential (corpus 1110–1116 and 2113); this
* checks that --dynamic leaves static .at() behavior unchanged and that
* surface calls re-anchor the engine on fibers inside async bodies. */
test("out-of-range .at() throws a catchable TypeError instead of returning undefined", async () => {
// `at` is declared returning `string` (undefined is unrepresentable);
// out of range the engine yields undefined and the validated exit
// refuses it — strictly, catchably — where Node would hand back
// undefined. The documented divergence, pinned.
const r = await compileAndRun(
"at-out-of-range",
`const s = "abc";
try {
console.log("in range", s.at(1), s.at(-1));
const c = s.at(99);
console.log("unreachable", c);
} catch {
console.log("caught");
}
const direct = s.at(-4);
console.log("unreachable", direct);
`,
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("in range b c\ncaught\n");
expect(r.stderr).toContain("TypeError: expected string, got undefined");
test("string .at() returns undefined out of range in both build modes", async () => {
const source = `const s = "abc";
console.log("in range", s.at(1), s.at(-1));
console.log("out of range", s.at(99), s.at(-4));
`;
for (const dynamic of [false, true]) {
const r = await compileAndRun("at-out-of-range", source, { dynamic });
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe("in range b c\nout of range undefined undefined\n");
expect(r.stderr).toBe("");
}
});
test("island-backed calls re-anchor the engine on fibers (Math after await)", async () => {
+18
View File
@@ -52,6 +52,11 @@
"test/built-ins/Number/S9.3_A4.2_T1.js",
"test/built-ins/Number/isNaN/nan.js",
"test/built-ins/RegExp/prototype/test/S15.10.6.3_A1_T16.js",
"test/built-ins/String/prototype/at/index-argument-tointeger.js",
"test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js",
"test/built-ins/String/prototype/at/returns-item-relative-index.js",
"test/built-ins/String/prototype/at/returns-item.js",
"test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js",
"test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js",
"test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js",
"test/built-ins/String/prototype/charAt/S9.4_A1.js",
@@ -61,6 +66,10 @@
"test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js",
"test/built-ins/String/prototype/charCodeAt/pos-coerce-string.js",
"test/built-ins/String/prototype/charCodeAt/pos-rounding.js",
"test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js",
"test/built-ins/String/prototype/codePointAt/return-utf16-decode.js",
"test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js",
"test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js",
"test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js",
"test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js",
"test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js",
@@ -192,6 +201,11 @@
"test/built-ins/Number/S9.3_A4.2_T1.js": "fdf435ae47263f0372cce90bf25eb8ddae8d0abdc9dce30d4a3b098727a9ce6b",
"test/built-ins/Number/isNaN/nan.js": "d513a909b333f40762bc08a9e2d1435d4ca097ed8b6da76497d0201a7264589e",
"test/built-ins/RegExp/prototype/test/S15.10.6.3_A1_T16.js": "c96f0402289d55e41a2e0ef322060c5a143b599f456b94dc5ebe0493bc689f42",
"test/built-ins/String/prototype/at/index-argument-tointeger.js": "563b86b614bb22f0bdc35ab51d67bf1ea68395e71bb22699f92ab48eed02337f",
"test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js": "37a09110b92218ff022e8e84abee3259c6083d880f193fc679f1bbfb803de03e",
"test/built-ins/String/prototype/at/returns-item-relative-index.js": "a4be170547d99a5eed0dd4e86710ed8b4ae2b22246212f98eacd44f357744bae",
"test/built-ins/String/prototype/at/returns-item.js": "c15c0d8635a9255f3ea7a82a57974a5973352be043c6834be296080e4324aa23",
"test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js": "ec8106380287ef3f7a15adfc6fd1a351191078bff47102574be2500d3c2ced8a",
"test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js": "5b69e2580d17931baf5c9780a1372f863d52077e7e75eae8d8f9f5e09e633a14",
"test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js": "9f9e11e908365fa55a64b9f8e3939a278a750e5a8c1afb665dcd875e0bdb5612",
"test/built-ins/String/prototype/charAt/S9.4_A1.js": "5df5b9b1420a1152c0a37f2a60d603e29655a91fe953a733450e024cd9ac3966",
@@ -201,6 +215,10 @@
"test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js": "db261622d446903e8675e41b10365e0ffd7310e5ff16836324b34f6e20346d2e",
"test/built-ins/String/prototype/charCodeAt/pos-coerce-string.js": "bbfab35f15f7b86d54c77259365199c92535df80dddd51c8c1cbee67bbdadac3",
"test/built-ins/String/prototype/charCodeAt/pos-rounding.js": "e1a10338edd67a2cdf64e9e8478a3c9a27556a28976df5933f5d49b2139fdaa5",
"test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js": "7240a5a919f9561de06b1a9f0d82585726435e5ed203e9c5201cd83f4ec968b6",
"test/built-ins/String/prototype/codePointAt/return-utf16-decode.js": "3ab42fb5ae478fb230f2d2197fa432aa8eaffe03f34865daf4a29d7cec226c41",
"test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js": "f1b02e76f433e28a6f6b2f1a299e15fb38fc50ff570a1c26917a171ab72b3310",
"test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js": "c66bcc050ac6c05bf020190fa70c7f371cfdae9bde36d77f51980e0cb40354c2",
"test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js": "fae2241ee1b7c8e85c8ac351710f109069f407041f851ab39ae3691e35e8b696",
"test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js": "708b505a6dc830c907bb483b99c124582de700a9f2c0251985ace72361afaa72",
"test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js": "532eb90be62d4faa2faa230ee91180ea260dc330b4427b9a08a696ac7417a3a4",
@@ -0,0 +1,27 @@
// Copyright (C) 2020 Rick Waldron. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-string.prototype.at
description: >
Property type and descriptor.
info: |
String.prototype.at( index )
Let relativeIndex be ? ToInteger(index).
features: [String.prototype.at]
---*/
assert.sameValue(typeof String.prototype.at, 'function');
let valueOfCallCount = 0;
let index = {
valueOf() {
valueOfCallCount++;
return 1;
}
};
let s = "01";
assert.sameValue(s.at(index), '1', 's.at({valueOf() {valueOfCallCount++; return 1;}}) must return 1');
assert.sameValue(valueOfCallCount, 1, 'The value of `valueOfCallCount` is 1');
@@ -0,0 +1,26 @@
// Copyright (C) 2020 Rick Waldron. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-string.prototype.at
description: >
Property type and descriptor.
info: |
String.prototype.at( index )
Let relativeIndex be ? ToInteger(index).
features: [String.prototype.at]
---*/
assert.sameValue(typeof String.prototype.at, 'function');
let s = "01";
assert.sameValue(s.at(false), '0', 's.at(false) must return 0');
assert.sameValue(s.at(null), '0', 's.at(null) must return 0');
assert.sameValue(s.at(undefined), '0', 's.at(undefined) must return 0');
assert.sameValue(s.at(""), '0', 's.at("") must return 0');
assert.sameValue(s.at(function() {}), '0', 's.at(function() {}) must return 0');
assert.sameValue(s.at([]), '0', 's.at([]) must return 0');
assert.sameValue(s.at(true), '1', 's.at(true) must return 1');
assert.sameValue(s.at("1"), '1', 's.at("1") must return 1');
@@ -0,0 +1,29 @@
// Copyright (C) 2020 Rick Waldron. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-string.prototype.at
description: >
Returns the item value at the specified relative index
info: |
String.prototype.at ( )
Let O be ? ToObject(this value).
Let len be ? LengthOfStringLike(O).
Let relativeIndex be ? ToInteger(index).
If relativeIndex ≥ 0, then
Let k be relativeIndex.
Else,
Let k be len + relativeIndex.
If k < 0 or k ≥ len, then return undefined.
Return ? Get(O, ! ToString(k)).
features: [String.prototype.at]
---*/
assert.sameValue(typeof String.prototype.at, 'function');
let s = "12345";
assert.sameValue(s.at(0), "1", 's.at(0) must return "1"');
assert.sameValue(s.at(-1), "5", 's.at(-1) must return "5"');
assert.sameValue(s.at(-3), "3", 's.at(-3) must return "3"');
assert.sameValue(s.at(-4), "2", 's.at(-4) must return "2"');
@@ -0,0 +1,30 @@
// Copyright (C) 2020 Rick Waldron. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-string.prototype.at
description: >
Returns the item value at the specified index
info: |
String.prototype.at ( )
Let O be ? ToObject(this value).
Let len be ? LengthOfStringLike(O).
Let relativeIndex be ? ToInteger(index).
If relativeIndex ≥ 0, then
Let k be relativeIndex.
Else,
Let k be len + relativeIndex.
If k < 0 or k ≥ len, then return undefined.
Return ? Get(O, ! ToString(k)).
features: [String.prototype.at]
---*/
assert.sameValue(typeof String.prototype.at, 'function');
let s = "12345";
assert.sameValue(s.at(0), "1", 's.at(0) must return "1"');
assert.sameValue(s.at(1), "2", 's.at(1) must return "2"');
assert.sameValue(s.at(2), "3", 's.at(2) must return "3"');
assert.sameValue(s.at(3), "4", 's.at(3) must return "4"');
assert.sameValue(s.at(4), "5", 's.at(4) must return "5"');
@@ -0,0 +1,20 @@
// Copyright (C) 2020 Rick Waldron. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-string.prototype.at
description: >
Creates an iterator from a custom object.
info: |
String.prototype.at( index )
If k < 0 or k ≥ len, then return undefined.
features: [String.prototype.at]
---*/
assert.sameValue(typeof String.prototype.at, 'function');
let s = "";
assert.sameValue(s.at(-2), undefined, 's.at(-2) must return undefined'); // wrap around the end
assert.sameValue(s.at(0), undefined, 's.at(0) must return undefined');
assert.sameValue(s.at(1), undefined, 's.at(1) must return undefined');
@@ -0,0 +1,26 @@
// Copyright (C) 2015 the V8 project authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
es6id: 21.1.3.3
description: >
Return value on coerced values on ToInteger(position).
info: |
21.1.3.3 String.prototype.codePointAt ( pos )
...
4. Let position be ToInteger(pos).
...
---*/
assert.sameValue('\uD800\uDC00'.codePointAt(''), 65536);
assert.sameValue('\uD800\uDC00'.codePointAt('0'), 65536);
assert.sameValue('\uD800\uDC00'.codePointAt(NaN), 65536);
assert.sameValue('\uD800\uDC00'.codePointAt(false), 65536);
assert.sameValue('\uD800\uDC00'.codePointAt(null), 65536);
assert.sameValue('\uD800\uDC00'.codePointAt(undefined), 65536);
assert.sameValue('\uD800\uDC00'.codePointAt([]), 65536);
assert.sameValue('\uD800\uDC00'.codePointAt('1'), 56320);
assert.sameValue('\uD800\uDC00'.codePointAt(true), 56320);
assert.sameValue('\uD800\uDC00'.codePointAt([1]), 56320);
@@ -0,0 +1,39 @@
// Copyright (C) 2015 the V8 project authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
es6id: 21.1.3.3
description: >
Return UTF16 Decode value of the lead and trail elements at index position.
info: |
21.1.3.3 String.prototype.codePointAt ( pos )
...
8. Let first be the code unit value of the element at index position in the
String S.
9. If first < 0xD800 or first > 0xDBFF or position+1 = size, return first.
10. Let second be the code unit value of the element at index position+1 in
the String S.
11. If second < 0xDC00 or second > 0xDFFF, return first.
12. Return UTF16Decode(first, second).
10.1.2 Static Semantics: UTF16Decode( lead, trail )
Two code units, lead and trail, that form a UTF-16 surrogate pair are
converted to a code point by performing the following steps:
1. Assert: 0xD800 ≤ lead ≤ 0xDBFF and 0xDC00 ≤ trail ≤ 0xDFFF.
2. Let cp be (lead – 0xD800) × 1024 + (trail – 0xDC00) + 0x10000.
3. Return the code point cp.
---*/
assert.sameValue('\uD800\uDC00'.codePointAt(0), 65536, 'U+10000');
assert.sameValue('\uD800\uDDD0'.codePointAt(0), 66000, 'U+101D0');
assert.sameValue('\uD800\uDFFF'.codePointAt(0), 66559, 'U+103FF');
assert.sameValue('\uDAAA\uDC00'.codePointAt(0), 763904, 'U+BA800');
assert.sameValue('\uDAAA\uDDD0'.codePointAt(0), 764368, 'U+BA9D0');
assert.sameValue('\uDAAA\uDFFF'.codePointAt(0), 764927, 'U+BABFF');
assert.sameValue('\uDBFF\uDC00'.codePointAt(0), 1113088, 'U+10FC00');
assert.sameValue('\uDBFF\uDDD0'.codePointAt(0), 1113552, 'U+10FDD0');
assert.sameValue('\uDBFF\uDFFF'.codePointAt(0), 1114111, 'U+10FFFF');
@@ -0,0 +1,21 @@
// Copyright (C) 2015 the V8 project authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
es6id: 21.1.3.3
description: >
If pos >= size, return undefined
info: |
21.1.3.3 String.prototype.codePointAt ( pos )
1. Let O be RequireObjectCoercible(this value).
2. Let S be ToString(O).
3. ReturnIfAbrupt(S).
4. Let position be ToInteger(pos).
5. ReturnIfAbrupt(position).
6. Let size be the number of elements in S.
7. If position < 0 or position ≥ size, return undefined.
---*/
assert.sameValue('abc'.codePointAt(3), undefined);
assert.sameValue('abc'.codePointAt(4), undefined);
assert.sameValue('abc'.codePointAt(Infinity), undefined);
@@ -0,0 +1,20 @@
// Copyright (C) 2015 the V8 project authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
es6id: 21.1.3.3
description: >
If pos < size, return undefined
info: |
21.1.3.3 String.prototype.codePointAt ( pos )
1. Let O be RequireObjectCoercible(this value).
2. Let S be ToString(O).
3. ReturnIfAbrupt(S).
4. Let position be ToInteger(pos).
5. ReturnIfAbrupt(position).
6. Let size be the number of elements in S.
7. If position < 0 or position ≥ size, return undefined.
---*/
assert.sameValue('abc'.codePointAt(-1), undefined);
assert.sameValue('abc'.codePointAt(-Infinity), undefined);