Merge current main into Effect static compatibility branch

- Reconcile string search lowering with the latest compiler behavior.

- Keep corpus numbering unique and regenerate the order baseline.
This commit is contained in:
Chris Tate
2026-09-25 12:26:45 -05:00
44 changed files with 771 additions and 140 deletions
+3
View File
@@ -116,6 +116,9 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
<dt><code>--backend &lt;c|llvm&gt;</code></dt>
<dd>Code generator: <code>llvm</code> (default) or <code>c</code> (the readable debugging backend). On supported targets, LLVM-tier executable code generation uses the bundled helper and precompiled runtime pack before the platform link. Unset, a native build can fall back to C when the program is outside the LLVM tier. The production <code>wasm32-wasi</code> target never falls back: a missing LLVM lowering is <code>SC3001</code>.</dd>
<dt><code>--strip</code></dt>
<dd>Remove symbol and debug payload when linking an executable, including cross-compiled targets. This opt-in flag works with either backend and <code>--from-c</code> without changing code optimization.</dd>
<dt><code>--windows-subsystem &lt;console|gui&gt;</code></dt>
<dd>Select the subsystem of a Windows executable. <code>console</code> is the default; <code>gui</code> prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows, including <code>--from-c</code>.</dd>
+1
View File
@@ -128,6 +128,7 @@ async function tryFastPath(): Promise<number | null> {
dynamic: values.dynamic,
backend: backend ?? "auto",
...(optimization === "dev" ? { optimization: "dev" as const } : {}),
...(values.strip ? { strip: true as const } : {}),
npmStatic,
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${
+9 -3
View File
@@ -71,7 +71,7 @@ async function main(): Promise<number> {
const [command, inputArg] = positionals;
if (command === "cache") {
if (inputArg !== "warm") fail(`unknown cache command "${inputArg ?? ""}" (supported: warm)\n\n${USAGE}`);
if (values.lib || values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values["from-c"] || values.ffi !== undefined || values.profile !== undefined || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || values["provenance-sources"] || externalTypeArgs.length > 0 || values.out !== undefined || values["emit-ir"] || !values["keep-c"]) {
if (values.lib || values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values["from-c"] || values.ffi !== undefined || values.profile !== undefined || values.strip || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || values["provenance-sources"] || externalTypeArgs.length > 0 || values.out !== undefined || values["emit-ir"] || !values["keep-c"]) {
fail(`scriptc cache warm takes only native optimization/sanitizer options and profile names\n\n${USAGE}`);
}
const optimization = values.optimization;
@@ -119,9 +119,9 @@ async function main(): Promise<number> {
if (inputArg) {
fail("scriptc build --lib takes no input positional: the profile names the entry module");
}
if (values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values.optimization !== undefined || values.ffi !== undefined || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || externalTypeArgs.length > 0) {
if (values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values.optimization !== undefined || values.strip || values.ffi !== undefined || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || externalTypeArgs.length > 0) {
fail(
"scriptc build --lib takes no --dynamic/--backend/--emit/--print/--optimization/--windows-subsystem/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
"scriptc build --lib takes no --dynamic/--backend/--emit/--print/--optimization/--strip/--windows-subsystem/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
);
}
const profilePath = resolve(profileArg);
@@ -155,6 +155,9 @@ async function main(): Promise<number> {
if (command === "coverage" && values.emit !== undefined) {
fail(`--emit is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values.strip) {
fail(`--strip is a build/run option\n\n${USAGE}`);
}
if (values.print !== undefined && values.print !== "native-link-info") {
fail(`unknown print kind "${values.print}" (supported: native-link-info)\n\n${USAGE}`);
}
@@ -220,6 +223,7 @@ async function main(): Promise<number> {
keepC: values["keep-c"],
sanitize: values.sanitize,
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
strip: values.strip,
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
...(values.ffi === undefined ? {} : { ffi: values.ffi }),
});
@@ -295,6 +299,7 @@ async function main(): Promise<number> {
sanitize: values.sanitize,
dynamic: values.dynamic,
...(optimization !== undefined ? { optimization } : {}),
...(values.strip ? { strip: true } : {}),
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
});
return outPath;
@@ -308,6 +313,7 @@ async function main(): Promise<number> {
dynamic: values.dynamic,
...(backend !== undefined ? { backend } : {}),
...(optimization !== undefined ? { optimization } : {}),
...(values.strip ? { strip: true } : {}),
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
+4
View File
@@ -9,6 +9,7 @@ export interface OutputOptionValues {
keepC: boolean;
sanitize: boolean;
optimization?: string;
strip?: boolean;
windowsSubsystem?: string;
ffi?: string;
}
@@ -62,6 +63,9 @@ export function resolveOutputOptions(
if (values.windowsSubsystem !== undefined && emit !== "exe") {
return { ok: false, message: `--windows-subsystem is only supported with --emit=exe` };
}
if (values.strip && emit !== "exe") {
return { ok: false, message: `--strip is only supported with --emit=exe` };
}
if (emit === "ir" && backend !== undefined) {
return { ok: false, message: `--emit=ir cannot be combined with --backend; IR is emitted before backend selection` };
}
+3
View File
@@ -35,6 +35,8 @@ Options:
native optimization posture (default: release/-O2). dev
uses -O0 and stable cached LLVM object shards for faster
edits of large programs
--strip remove symbol/debug payload from the linked executable
for smaller builds (opt in; --emit=exe only)
--windows-subsystem <console|gui>
Windows executable subsystem (default: console). gui
prevents Windows from opening a console window
@@ -77,6 +79,7 @@ export const CLI_OPTIONS = {
print: { type: "string" },
backend: { type: "string" },
optimization: { type: "string" },
strip: { type: "boolean", default: false },
"windows-subsystem": { type: "string" },
"from-c": { type: "boolean", default: false },
"keep-c": { type: "boolean", default: true },
+9
View File
@@ -48,6 +48,8 @@ describe("output option compatibility", () => {
[{ backend: "wat" }, /unknown backend/],
[{ emit: "obj", windowsSubsystem: "gui" }, /windows-subsystem/],
[{ emit: "llvm", windowsSubsystem: "gui" }, /windows-subsystem/],
[{ emit: "obj", strip: true }, /strip/],
[{ emit: "llvm", strip: true }, /strip/],
] as const)("rejects %j", (override, message) => {
const result = resolveOutputOptions("build", { ...BASE, ...override });
expect(result).toEqual({ ok: false, message: expect.stringMatching(message) });
@@ -98,4 +100,11 @@ describe("output option compatibility", () => {
outputKind: "exe",
});
});
test("executable outputs accept stripping", () => {
expect(resolveOutputOptions("build", { ...BASE, strip: true })).toMatchObject({
ok: true,
outputKind: "exe",
});
});
});
+49
View File
@@ -0,0 +1,49 @@
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const repoRoot = join(import.meta.dirname, "../../..");
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js");
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
test.each(["auto", "c", "from-c"] as const)(
"--strip shrinks and runs %s executables without mixing cache variants",
async (route) => {
const dir = await mkdtemp(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-strip-"));
const entry = join(dir, route === "from-c" ? "main.c" : "main.ts");
const outPath = join(dir, process.platform === "win32" ? "program.exe" : "program");
const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") };
const routeArgs = route === "auto" ? [] : route === "c" ? ["--backend=c"] : ["--from-c"];
const build = async (strip: boolean, bootstrap = false): Promise<number> => {
await execFileAsync(process.execPath, [
...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]),
"build", entry, "-o", outPath, ...routeArgs, ...(strip ? ["--strip"] : []),
], { env, maxBuffer: 4 * 1024 * 1024 });
return (await stat(outPath)).size;
};
try {
await writeFile(entry, route === "from-c"
? '#include <stdio.h>\nint main(void) { puts("strip-ok"); return 0; }\n'
: 'console.log("strip-ok");\n');
const originalSize = await build(false);
const strippedSize = await build(true);
expect(strippedSize).toBeLessThan(originalSize);
expect((await execFileAsync(outPath)).stdout).toBe("strip-ok\n");
if (route === "auto") {
expect(await build(true, true)).toBe(strippedSize);
expect(await build(false, true)).toBe(originalSize);
expect((await execFileAsync(outPath)).stdout).toBe("strip-ok\n");
}
} finally {
await rm(dir, { recursive: true, force: true });
}
},
180_000,
);
@@ -12,6 +12,7 @@ import type { NativeArtifactDependency } from "./native-toolchain.js";
import { loadRuntimePack, type RuntimePackSelection } from "./runtime-pack.js";
import {
executableOptimizationLinkerArgs,
executableStripLinkerArgs,
windowsSubsystemLinkerArgs,
type NativeTargetSpec,
type WindowsSubsystem,
@@ -38,6 +39,7 @@ export async function createNativeLinkPlan(options: {
features: NativeLinkFeatures;
ffi: FfiProfile | null;
optimization: "release" | "dev";
strip?: boolean;
windowsSubsystem?: WindowsSubsystem;
programObjectDependencies?: readonly NativeArtifactDependency[];
env?: NodeJS.ProcessEnv;
@@ -62,6 +64,7 @@ export async function createNativeLinkPlan(options: {
index > 0 && args[index - 1] === "-target" ? options.target.linkerTargetTriple : arg
),
...executableOptimizationLinkerArgs(options.target.platform, options.optimization),
...executableStripLinkerArgs(options.target.platform, options.strip ?? false),
...windowsSubsystemLinkerArgs(options.target.platform, options.windowsSubsystem),
],
dependencyPaths: [
@@ -10,7 +10,7 @@ import { performance } from "node:perf_hooks";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { localizeElfObject, mergeAndLocalizeCoffObjects } from "./object-localize.js";
import { executableOptimizationLinkerArgs, windowsSubsystemLinkerArgs, type WindowsSubsystem } from "./targets.js";
import { executableOptimizationLinkerArgs, executableStripLinkerArgs, windowsSubsystemLinkerArgs, type WindowsSubsystem } from "./targets.js";
import {
createVendorArchives,
MBEDTLS_VERSION,
@@ -285,6 +285,8 @@ export interface CcOptions {
* executable lane; dev selects -O0 and may compile a caller-provided LLVM
* shard set into independently cached objects before the final link. */
optimization?: "release" | "dev";
/** Remove symbol/debug payload from the linked executable. */
strip?: boolean;
/** PE executable subsystem; omitted and console use the driver default. */
windowsSubsystem?: WindowsSubsystem;
/** Optional equivalent LLVM modules for dev compilation. Unsupported
@@ -4062,6 +4064,7 @@ async function compileCInternal(
targetPlatform(driver),
optimization,
),
...executableStripLinkerArgs(targetPlatform(driver), opts.strip ?? false),
...windowsSubsystemArgs,
];
// scr_async.c submits callback-style filesystem work to a native worker.
+13
View File
@@ -54,6 +54,19 @@ export function executableOptimizationLinkerArgs(
: [];
}
/** Remove symbol/debug payload at link time so the executable cache stores
* the same bytes the user receives, including for cross-compiled targets. */
export function executableStripLinkerArgs(platform: string, strip: boolean): string[] {
if (!strip) return [];
switch (platform) {
case "darwin": return ["-Wl,-S,-x"];
case "linux":
case "win32": return ["-Wl,-s"];
case "wasi": return ["-Wl,--strip-all"];
default: throw new Error(`--strip is not supported for the ${platform} executable target`);
}
}
export interface NativeTargetSpec {
/** Stable scriptc-facing identity used in cache keys and diagnostics. */
name: NativeTargetName;
@@ -85,6 +85,8 @@ export interface EarlyExecutableCacheOptions {
backend: "auto" | "c" | "llvm";
/** Omitted is the historical release posture and preserves v1 keys. */
optimization?: "dev";
/** Omitted retains the unstripped executable's historical cache key. */
strip?: true;
/** Omitted for the default Windows console subsystem. */
windowsSubsystem?: "gui";
npmStatic: readonly string[] | "auto" | null;
@@ -188,6 +190,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
options.dynamic ? "dynamic" : "static",
options.backend,
...(options.optimization === "dev" ? ["optimization-dev"] : []),
...(options.strip ? ["strip"] : []),
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
options.npmStatic === null
? "<npm-static-off>"
@@ -216,6 +219,7 @@ function routeKey(options: EarlyExecutableRouteOptions): string {
.update(options.dynamic ? "dynamic" : "static").update("\0")
.update(options.backend).update("\0");
if (options.optimization === "dev") hash.update("optimization-dev\0");
if (options.strip) hash.update("strip\0");
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
hash
.update(options.npmStatic === null
@@ -6,62 +6,13 @@ import type { Lowerer } from "../lowerer.js";
import { own } from "../lowerer.js";
import { isRequireMainFilename } from "../expressions/optional-chains.js";
import { STR_METHODS } from "../surfaces.js";
import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
import { defaultAfterUndefined, lowerOptionalArgument, lowerPositionArgument, lowerStaticallyUndefinedArgument, lowerStringSearchArgument, positionNumber } from "../optional-arguments.js";
function lowerSplitLimitArg(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr {
const defaultValue: IrExpr = { kind: "numLit", value: 4294967295, type: F64, loc };
return node ? lowerOptionalArgument(lowerer, node, F64, defaultValue) : defaultValue;
}
/** Complete unit-valued defaults while retaining argument side effects. */
function lowerStringPositionArgument(lowerer: Lowerer, node: ts.Expression | undefined, defaultValue: IrExpr): IrExpr {
if (!node) return defaultValue;
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node);
if (undefinedArg) return defaultAfterUndefined(undefinedArg, defaultValue);
const value = lowerer.lowerExpr(node);
if (isUnitType(value.type) || value.type.kind === "void") {
return defaultAfterUndefined(value, value.type.kind === "nullT" ? numLit(0, value.loc) : defaultValue);
}
return value;
}
/** Convert a helper parameter, after every argument expression has run.
* Only undefined selects a missing end; null still converts to zero. */
function stringPositionNumber(lowerer: Lowerer, value: IrExpr, defaultValue: IrExpr, node: ts.Expression): IrExpr {
const loc = value.loc;
switch (value.type.kind) {
case "f64": return value;
case "string": return { kind: "libCall", fn: "num.fromString", args: [value], type: F64, loc };
case "bool": return { kind: "ternary", cond: value, then: numLit(1, loc), else_: numLit(0, loc), type: F64, loc };
case "nullT": return numLit(0, loc);
case "undefinedT": return defaultValue;
case "jsval": return { kind: "jsExit", value, type: F64, loc };
case "union": {
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)!.arms;
let result: IrExpr = defaultValue;
for (let tag = arms.length - 1; tag >= 0; tag--) {
const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value, type: arms[tag]!, loc };
const converted = stringPositionNumber(lowerer, narrowed, defaultValue, node);
result = tag === arms.length - 1 ? converted : {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, value, negated: false, type: BOOL, loc },
then: converted, else_: result, type: F64, loc,
};
}
return result;
}
case "dyn": return {
kind: "ternary",
cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc },
then: defaultValue,
else_: { kind: "libCall", fn: "dyn.toNumberCoerce", args: [value], type: F64, loc },
type: F64, loc,
};
default: return lowerer.noLowering(`string position of '${lowerer.fmt(value.type)}' values`, node);
}
}
function lowerRegexSubject(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr {
const absent: IrExpr = { kind: "strLit", value: "undefined", type: STRING, loc };
if (!node) return absent;
@@ -320,24 +271,23 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
? dynReceiver()
: lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
const loc = locOf(call);
if ((entry.method === "indexOf" || entry.method === "includes" ||
entry.method === "startsWith" || entry.method === "endsWith") && call.arguments.length === 2) {
const rawNeedle = lowerer.lowerExpr(call.arguments[0]!);
// indexOf ToString-coerces the search value before converting the
// position. The other search methods must also reject RegExp values,
// so keep their non-string inputs fenced until IsRegExp is lowered.
const needle = rawNeedle.type.kind === "string"
? rawNeedle
: entry.method === "indexOf" && rawNeedle.type.kind === "dyn"
? { kind: "libCall", fn: "dyn.toStringCoerce", args: [rawNeedle], type: STRING, loc: rawNeedle.loc } satisfies IrExpr
: entry.method === "indexOf"
? lowerer.ensureString(rawNeedle, call.arguments[0]!)
: lowerer.noLowering(`.${entry.method} with a non-string search value`, call.arguments[0]!);
const searchMethod = entry.method === "indexOf" || entry.method === "includes" ||
entry.method === "startsWith" || entry.method === "endsWith";
if (searchMethod && call.arguments.length < 2) {
const needle = lowerStringSearchArgument(lowerer, call.arguments[0], loc);
return { kind: "strIntrinsic", method: entry.method, receiver, args: [needle], type: entry.result, loc };
}
if (searchMethod && call.arguments.length === 2) {
const needle = lowerer.lowerExpr(call.arguments[0]!);
const optionalNeedle = needle.type.kind === "union" && lowerer.runtimeOptionalWidening(needle.type, STRING) !== null;
if (needle.type.kind !== "string" && needle.type.kind !== "dyn" && !optionalNeedle) {
lowerer.noLowering(`.${entry.method} with '${lowerer.fmt(needle.type)}' search values`, call);
}
const defaultPosition: IrExpr = entry.method === "endsWith"
? { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc }
: numLit(0, loc);
const position = lowerStringPositionArgument(lowerer, call.arguments[1], defaultPosition);
if (position.type.kind === "f64" || position.type.kind === "jsval") {
const position = lowerPositionArgument(lowerer, call.arguments[1], defaultPosition);
if (needle.type.kind === "string" && (position.type.kind === "f64" || position.type.kind === "jsval")) {
return { kind: "strIntrinsic", method: entry.method, receiver, args: [needle, position], type: entry.result, loc };
}
const key = `str.positions:${entry.method}:${typeKey(needle.type)}:${typeKey(position.type)}`;
@@ -345,19 +295,31 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
if (!helper) {
helper = `%str.positions.${lowerer.widthHelpers.size}`;
const params = [receiver, needle, position].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, type: arg.type }));
const coerceNeedle = needle.type.kind !== "string";
const search: IrExpr = coerceNeedle ? varRef("search.0", STRING, loc) : varRef("arg.1", STRING, loc);
const result: IrExpr = {
kind: "strIntrinsic", method: entry.method, receiver: varRef("arg.0", STRING, loc),
args: [
varRef("arg.1", needle.type, loc),
stringPositionNumber(lowerer, varRef("arg.2", position.type, loc), defaultPosition, call.arguments[1]!),
search,
positionNumber(lowerer, varRef("arg.2", position.type, loc), defaultPosition, call.arguments[1]!, "string position"),
],
type: entry.result, loc,
};
const locals = params.map(param => ({ id: param.localId, name: param.name, type: param.type, mutable: false }));
const body: IrStmt[] = [];
if (coerceNeedle) {
const value = varRef("arg.1", needle.type, loc);
const init: IrExpr = needle.type.kind === "dyn"
? { kind: "libCall", fn: "dyn.toStringCoerce", args: [value], type: STRING, loc }
: lowerer.ensureString(value, call.arguments[0]!);
locals.push({ id: "search.0", name: "search", type: STRING, mutable: false });
body.push({ kind: "varDecl", localId: "search.0", init, loc });
}
body.push({ kind: "return", value: result, loc });
lowerer.widthHelpers.set(key, helper);
lowerer.liftedFns.push({
name: helper, params, returnType: entry.result,
locals: params.map(param => ({ id: param.localId, name: param.name, type: param.type, mutable: false })),
body: [{ kind: "return", value: result, loc }], loc,
locals, body, loc,
});
}
return { kind: "call", callee: helper, args: [receiver, needle, position], type: entry.result, loc };
@@ -367,7 +329,7 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
if (entry.method === "slice" || entry.method === "substring") {
defaults.push({ kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc });
}
const args = defaults.map((value, index) => lowerStringPositionArgument(lowerer, call.arguments[index], value));
const args = defaults.map((value, index) => lowerPositionArgument(lowerer, call.arguments[index], value));
// Keep ordinary numeric calls on the direct intrinsic path, including the
// existing boundary validation for island values in numeric slots.
if (args.every(arg => arg.type.kind === "f64" || arg.type.kind === "jsval")) {
@@ -383,7 +345,7 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
const params = [receiver, ...args].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, type: arg.type }));
const result: IrExpr = {
kind: "strIntrinsic", method: entry.method, receiver: varRef("arg.0", STRING, loc),
args: args.map((arg, index) => stringPositionNumber(lowerer, varRef(`arg.${index + 1}`, arg.type, loc), defaults[index]!, call.arguments[index] ?? call)),
args: args.map((arg, index) => positionNumber(lowerer, varRef(`arg.${index + 1}`, arg.type, loc), defaults[index]!, call.arguments[index] ?? call, "string position")),
type: entry.result, loc,
};
lowerer.widthHelpers.set(key, helper);
@@ -32,7 +32,7 @@ import {
import { lowerAbsenceProbe } from "./lower-exprs.js";
import { conditionalSpreadOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument, lowerStringSearchArgument } from "./optional-arguments.js";
import { HTTP2_CONSTANTS } from "./http2-constants.js";
import { CRYPTO_CIPHERS, CRYPTO_CONSTANTS, CRYPTO_CURVES, CRYPTO_HASHES } from "./crypto-tables.js";
import { generatorMeta, timerStyleCallback, type ParamShape } from "./lower-calls.js";
@@ -8925,8 +8925,8 @@ function staticTextDecoderEncoding(label: string): StaticTextDecoderEncoding | n
return { kind: "libCall", fn: "string.fromCharCode", args: [packed], type: STRING, loc };
}
/** `s.lastIndexOf(needle, position?)` on string receivers, using UTF-16
* indices. Omitted or undefined positions clamp to the string's end;
/** `s.lastIndexOf(searchValue?, position?)` on string receivers, using UTF-16
* indices. An omitted search value searches for "undefined". Omitted or undefined positions clamp to the string's end;
* MAX_SAFE_INTEGER has the same effect for every representable string.
* Null for non-string receivers and other members. */
export function lowerStringLastIndexOfCall(lowerer: Lowerer, call: ts.CallExpression,
@@ -8936,19 +8936,20 @@ function staticTextDecoderEncoding(label: string): StaticTextDecoderEncoding | n
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "string") return null;
if (!lowerer.isStdlibMember(access)) return null;
const loc = locOf(call);
if (call.arguments.length < 1 || call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) {
if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) {
lowerer.noLowering(
"lastIndexOf with this argument shape",
call,
"pass a string needle and an optional numeric position",
"pass no arguments, or a search value with an optional numeric position",
);
}
const receiver = lowerer.lowerExprExpecting(access.expression, STRING);
const needle = lowerer.lowerExprExpecting(call.arguments[0]!, STRING);
const positionNode = call.arguments[1];
if (!positionNode) {
const needle = lowerStringSearchArgument(lowerer, call.arguments[0], loc);
return { kind: "libCall", fn: "string.lastIndexOf", args: [receiver, needle], type: F64, loc };
}
const needle = lowerer.lowerExprExpecting(call.arguments[0]!, STRING);
const position = lowerOptionalArgument(lowerer, positionNode, F64, numLit(Number.MAX_SAFE_INTEGER, loc));
return { kind: "libCall", fn: "string.lastIndexOfFrom", args: [receiver, needle, position], type: F64, loc };
}
@@ -17,7 +17,7 @@ import { arrayIndexPresent, arrayValueRead, arrayValueStore, arrayValueType, cur
import { typeKey } from "../type-mapper.js";
import { WidthLift } from "./lowerer.js";
import { boolLit, countedFor, numLit, varRef } from "../../ir/build.js";
import { lowerOptionalArgument } from "./optional-arguments.js";
import { lowerOptionalArgument, lowerPositionArgument, positionNumber } from "./optional-arguments.js";
/** Build the argument array for a mutating/copying operation while keeping
* the receiver's scalar payload ABI. A missing read is represented as the
@@ -325,7 +325,7 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
// included — Node returns the unchanged length); reduce/reduceRight
// lower both declared forms (with and without an initial value).
const arity = {
push: [0, Number.MAX_SAFE_INTEGER], unshift: [0, Number.MAX_SAFE_INTEGER], pop: [0, 0], indexOf: [1, 2], includes: [1, 2], join: [1, 1],
push: [0, Number.MAX_SAFE_INTEGER], unshift: [0, Number.MAX_SAFE_INTEGER], pop: [0, 0], indexOf: [1, 2], lastIndexOf: [1, 2], includes: [0, 2], join: [1, 1],
concat: [0, Number.MAX_SAFE_INTEGER],
slice: [0, 2], shift: [0, 0], splice: [1, 2], at: [1, 1],
map: [1, 1], filter: [1, 1], forEach: [1, 1], find: [1, 1], findIndex: [1, 1], some: [1, 1],
@@ -333,7 +333,7 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
every: [1, 1], flatMap: [1, 1], reduce: [1, 2], reduceRight: [1, 2],
}[
name as
| "push" | "unshift" | "pop" | "concat" | "indexOf" | "includes" | "join" | "slice" | "shift" | "splice" | "at" | "map" | "filter"
| "push" | "unshift" | "pop" | "concat" | "indexOf" | "lastIndexOf" | "includes" | "join" | "slice" | "shift" | "splice" | "at" | "map" | "filter"
| "forEach" | "find" | "findIndex" | "findLast" | "findLastIndex" | "some" | "every" | "flatMap" | "reduce" | "reduceRight"
];
if (call.arguments.length < arity[0]! || call.arguments.length > arity[1]!) {
@@ -448,7 +448,7 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
loc,
};
}
if (name === "indexOf" || name === "includes") {
if (name === "indexOf" || name === "lastIndexOf" || name === "includes") {
return lowerArraySearchCall(lowerer, call, access, name, elem);
}
if (name === "concat") {
@@ -599,24 +599,28 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
/** `indexOf` and `includes` normally use the runtime's typed search helpers,
* but an indexed array read is a `T | undefined` value. Keep that union
* through a small lowered loop so the needle's missing state is observable:
* indexOf skips holes, while includes treats holes as undefined. The same
* loop also gives union-element arrays their value equality rather than
* comparing the compiler's union boxes by pointer. */
* indexOf and lastIndexOf skip holes, while includes treats holes as
* undefined. The same loop also gives union-element arrays their value
* equality rather than comparing the compiler's union boxes by pointer. */
function lowerArraySearchCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
method: "indexOf" | "includes",
method: "indexOf" | "lastIndexOf" | "includes",
elem: IrType,
): IrExpr {
const loc = locOf(call);
const receiver = lowerer.lowerExpr(access.expression);
const argNode = call.arguments[0]!;
const argNode = call.arguments[0] ?? call;
if (call.arguments.some(ts.isSpreadElement)) lowerer.noLowering(`.${method} with spread arguments`, call);
let needle = lowerer.lowerExpr(argNode);
const fromIndex = call.arguments[1]
? lowerOptionalArgument(lowerer, call.arguments[1], F64, numLit(0, loc))
: numLit(0, loc);
let needle: IrExpr = call.arguments[0]
? lowerer.lowerExpr(call.arguments[0])
: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc };
const fromIndex: IrExpr = call.arguments[1]
? lowerPositionArgument(lowerer, call.arguments[1], numLit(0, loc))
: method === "lastIndexOf"
? { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc }
: numLit(0, loc);
// A derived CLASS VALUE against a base-classval element widens (the same
// pointer — identity search is exact); the coercion path owns the ABI gate.
if (needle.type.kind === "classval" && elem.kind === "classval" && !typeEquals(needle.type, elem)) {
@@ -625,13 +629,13 @@ function lowerArraySearchCall(
// Preserve the existing fast path for an ordinary, non-union needle. Its
// runtime implementation already has exact primitive/reference equality.
if (call.arguments.length === 1 && elem.kind !== "union" && elem.kind !== "bigint" && typeEquals(needle.type, elem)) {
if (method !== "lastIndexOf" && call.arguments.length === 1 && elem.kind !== "union" && elem.kind !== "bigint" && typeEquals(needle.type, elem)) {
return {
kind: "arrIntrinsic",
method,
receiver,
args: [needle],
type: method === "indexOf" ? F64 : BOOL,
type: method === "includes" ? BOOL : F64,
loc,
};
}
@@ -641,6 +645,7 @@ function lowerArraySearchCall(
// static undefined arm. Keep their existing runtime path and its explicit
// type fence; typed static arrays take the state-aware helper below.
if (valueT.kind !== "union") {
if (method === "lastIndexOf") lowerer.noLowering(".lastIndexOf on an array of engine values", call);
if (call.arguments.length > 1) lowerer.noLowering(`.${method} with fromIndex on '${lowerer.fmt(elem)}' elements`, call);
if (!typeEquals(needle.type, elem)) lowerer.badType(argNode, lowerer.typeOf(argNode));
return {
@@ -648,7 +653,7 @@ function lowerArraySearchCall(
method,
receiver,
args: [needle],
type: method === "indexOf" ? F64 : BOOL,
type: method === "includes" ? BOOL : F64,
loc,
};
}
@@ -668,10 +673,16 @@ function lowerArraySearchCall(
for (const arm of needleArms) if (!arms.some((existing) => typeEquals(existing, arm))) arms.push(arm);
if (arms.length !== valueArms.length) valueT = { kind: "union", unionId: lowerer.unions.intern(arms) };
}
const searchNeedle = lowerer.coerceInto(argNode, needle, valueT);
if (!typeEquals(searchNeedle.type, valueT)) lowerer.badType(argNode, lowerer.typeOf(argNode));
const helper = arraySearchHelper(lowerer, method, elem, valueT, loc);
return { kind: "call", callee: helper, args: [receiver, searchNeedle, fromIndex], type: method === "indexOf" ? F64 : BOOL, loc };
const directNeedle = method === "lastIndexOf" && elem.kind !== "union" && typeEquals(needle.type, elem);
const searchNeedle = directNeedle ? needle : lowerer.coerceInto(argNode, needle, valueT);
// Checked dynamic extraction can copy a composite. Its new identity cannot
// be compared to an array element as though it were the original value.
if (method === "lastIndexOf" && searchNeedle.kind === "dynCheck" && (elem.kind === "record" || elem.kind === "array")) {
lowerer.noLowering(".lastIndexOf with a checked dynamic reference needle", call);
}
if (!directNeedle && !typeEquals(searchNeedle.type, valueT)) lowerer.badType(argNode, lowerer.typeOf(argNode));
const helper = arraySearchHelper(lowerer, method, elem, valueT, searchNeedle.type, directNeedle, fromIndex.type, call.arguments[1] ?? call, loc);
return { kind: "call", callee: helper, args: [receiver, searchNeedle, fromIndex], type: method === "includes" ? BOOL : F64, loc };
}
/** SameValueZero over a static value union. `unionEq` supplies strict
@@ -723,36 +734,41 @@ function arraySearchEquality(
function arraySearchHelper(
lowerer: Lowerer,
method: "indexOf" | "includes",
method: "indexOf" | "lastIndexOf" | "includes",
elem: IrType,
valueT: IrType & { kind: "union" },
needleT: IrType,
directNeedle: boolean,
fromT: IrType,
fromNode: ts.Expression,
loc: SrcLoc,
): string {
const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}`;
const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
const arrT = arrayOf(elem);
const resultT = method === "indexOf" ? F64 : BOOL;
const resultT = method === "includes" ? BOOL : F64;
const arrRef = varRef("a.0", arrT, loc);
const indexRef = varRef("i.0", F64, loc);
const needleRef = varRef("needle.0", valueT, loc);
const needleRef = varRef("needle.0", needleT, loc);
const stateRef = varRef("state.0", F64, loc);
const nextRef = varRef("next.0", F64, loc);
const fromRef = varRef("from.0", F64, loc);
const fromRef = varRef("from.0", fromT, loc);
const numberRef = varRef("number.0", F64, loc);
const integerRef = varRef("integer.0", F64, loc);
// ToIntegerOrInfinity, followed by the relative start index. Keeping the
// length read inside the helper preserves argument effects before search.
// Snapshot length before coercing fromIndex, then apply ToIntegerOrInfinity.
const number = positionNumber(lowerer, fromRef, numLit(0, loc), fromNode, "array search position");
const integer: IrExpr = {
kind: "ternary",
cond: { kind: "libCall", fn: "num.isNaN", args: [fromRef], type: BOOL, loc },
cond: { kind: "libCall", fn: "num.isNaN", args: [numberRef], type: BOOL, loc },
then: numLit(0, loc),
else_: { kind: "libCall", fn: "math.trunc", args: [fromRef], type: F64, loc },
else_: { kind: "libCall", fn: "math.trunc", args: [numberRef], type: F64, loc },
type: F64,
loc,
};
const start: IrExpr = {
const forwardStart: IrExpr = {
kind: "ternary",
cond: { kind: "bin", op: "<", left: integerRef, right: numLit(0, loc), type: BOOL, loc },
then: {
@@ -765,10 +781,24 @@ function arraySearchHelper(
type: F64,
loc,
};
const start: IrExpr = method === "lastIndexOf" ? {
kind: "ternary",
cond: { kind: "bin", op: "<", left: integerRef, right: numLit(0, loc), type: BOOL, loc },
then: { kind: "bin", op: "+", left: varRef("n.0", F64, loc), right: integerRef, type: F64, loc },
else_: {
kind: "libCall", fn: "math.min",
args: [
{ kind: "bin", op: "+", left: integerRef, right: numLit(0, loc), type: F64, loc },
{ kind: "bin", op: "-", left: varRef("n.0", F64, loc), right: numLit(1, loc), type: F64, loc },
],
type: F64, loc,
},
type: F64, loc,
} : forwardStart;
const rawRead: IrExpr = { kind: "arrayGet", arr: arrRef, index: indexRef, type: elem, loc };
const missing = lowerer.wrappedUndefined(valueT, loc);
if (!missing) throw new InternalCompilerError("array search helper needs an undefined arm");
const value: IrExpr = {
const optionalValue: IrExpr = {
kind: "ternary",
cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc },
then: typeEquals(elem, valueT) ? rawRead : lowerer.coerceToExpected(rawRead, valueT),
@@ -776,12 +806,14 @@ function arraySearchHelper(
type: valueT,
loc,
};
const equal = arraySearchEquality(lowerer, varRef("value.0", valueT, loc), needleRef, method === "includes", loc);
const match = method === "indexOf"
const value = directNeedle ? rawRead : optionalValue;
const readT = directNeedle ? elem : valueT;
const equal = arraySearchEquality(lowerer, varRef("value.0", readT, loc), needleRef, method === "includes", loc);
const match = method !== "includes"
? {
kind: "logical" as const,
op: "&&" as const,
left: { kind: "bin" as const, op: "!==" as const, left: stateRef, right: numLit(0, loc), type: BOOL, loc },
left: { kind: "bin" as const, op: directNeedle ? "===" as const : "!==" as const, left: stateRef, right: numLit(directNeedle ? 1 : 0, loc), type: BOOL, loc },
right: equal,
type: BOOL,
loc,
@@ -789,7 +821,7 @@ function arraySearchHelper(
: equal;
const undefinedTag = lowerer.armTag(valueT.unionId, UNDEFINED_T);
if (undefinedTag < 0) throw new InternalCompilerError("array search helper needs an undefined tag");
const needleIsUndefined: IrExpr = {
const needleIsUndefined: IrExpr = directNeedle ? boolLit(false, loc) : {
kind: "unionIsTag",
unionId: valueT.unionId,
tag: undefinedTag,
@@ -801,7 +833,7 @@ function arraySearchHelper(
const advance: IrStmt = {
kind: "assign",
localId: "i.0",
value: { kind: "bin", op: "+", left: indexRef, right: numLit(1, loc), type: F64, loc },
value: { kind: "bin", op: method === "lastIndexOf" ? "-" : "+", left: indexRef, right: numLit(1, loc), type: F64, loc },
loc,
};
const visitPresent: IrStmt[] = [
@@ -809,13 +841,13 @@ function arraySearchHelper(
{
kind: "if",
cond: match,
then: [{ kind: "return", value: method === "indexOf" ? indexRef : boolLit(true, loc), loc }],
then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }],
else_: null,
loc,
},
advance,
];
const skipHole: IrStmt[] = [
const skipHole: IrStmt[] = method === "lastIndexOf" ? [advance] : [
{
kind: "varDecl",
localId: "next.0",
@@ -841,32 +873,36 @@ function arraySearchHelper(
name,
params: [
{ localId: "a.0", name: "a", type: arrT },
{ localId: "needle.0", name: "needle", type: valueT },
{ localId: "from.0", name: "from", type: F64 },
{ localId: "needle.0", name: "needle", type: needleT },
{ localId: "from.0", name: "from", type: fromT },
],
returnType: resultT,
locals: [
{ id: "a.0", name: "a", type: arrT, mutable: true },
{ id: "needle.0", name: "needle", type: valueT, mutable: true },
{ id: "from.0", name: "from", type: F64, mutable: false },
{ id: "needle.0", name: "needle", type: needleT, mutable: true },
{ id: "from.0", name: "from", type: fromT, mutable: false },
{ id: "number.0", name: "number", type: F64, mutable: false },
{ id: "integer.0", name: "integer", type: F64, mutable: false },
{ id: "n.0", name: "n", type: F64, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: true },
{ id: "state.0", name: "state", type: F64, mutable: false },
{ id: "next.0", name: "next", type: F64, mutable: false },
{ id: "value.0", name: "value", type: valueT, mutable: false },
{ id: "value.0", name: "value", type: readT, mutable: false },
],
body: [
readLenStmt(arrT, loc),
{ kind: "varDecl", localId: "number.0", init: number, loc },
{ kind: "varDecl", localId: "integer.0", init: integer, loc },
{ kind: "varDecl", localId: "i.0", init: start, loc },
{
kind: "while",
cond: { kind: "bin", op: "<", left: indexRef, right: varRef("n.0", F64, loc), type: BOOL, loc },
cond: method === "lastIndexOf"
? { kind: "bin", op: ">=", left: indexRef, right: numLit(0, loc), type: BOOL, loc }
: { kind: "bin", op: "<", left: indexRef, right: varRef("n.0", F64, loc), type: BOOL, loc },
body: loopBody,
loc,
},
{ kind: "return", value: method === "indexOf" ? numLit(-1, loc) : boolLit(false, loc), loc },
{ kind: "return", value: method === "includes" ? boolLit(false, loc) : numLit(-1, loc), loc },
],
loc,
});
@@ -1,6 +1,7 @@
import * as ts from "../ts7/adapter.js";
import type { IrExpr, IrType } from "../../ir/ir.js";
import { typeEquals } from "../../ir/ir.js";
import type { IrExpr, IrType, SrcLoc } from "../../ir/ir.js";
import { BOOL, F64, isUnitType, typeEquals } from "../../ir/ir.js";
import { numLit, strLit } from "../../ir/build.js";
import type { Lowerer } from "./lowerer.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
@@ -42,6 +43,19 @@ export function defaultAfterUndefined(value: IrExpr, defaultValue: IrExpr): IrEx
};
}
/** Convert an omitted or supplied string-search value after preserving its effects. */
export function lowerStringSearchArgument(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr {
const absent = strLit("undefined", loc);
if (!node) return absent;
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node);
if (undefinedArg) return defaultAfterUndefined(undefinedArg, absent);
const value = lowerer.lowerExpr(node);
if (isUnitType(value.type)) {
return defaultAfterUndefined(value, strLit(value.type.kind === "nullT" ? "null" : "undefined", loc));
}
return lowerer.ensureString(value, node);
}
/** Lower an optional argument, applying its default only to the undefined arm. */
export function lowerOptionalArgument(
lowerer: Lowerer,
@@ -64,3 +78,57 @@ export function lowerOptionalArgument(
}
return lowerer.coerceInto(node, value, expected);
}
/** Complete an omitted or statically undefined position without dropping argument effects. */
export function lowerPositionArgument(lowerer: Lowerer, node: ts.Expression | undefined, defaultValue: IrExpr): IrExpr {
if (!node) return defaultValue;
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node);
if (undefinedArg) return defaultAfterUndefined(undefinedArg, defaultValue);
const value = lowerer.lowerExpr(node);
if (isUnitType(value.type) || value.type.kind === "void") {
return defaultAfterUndefined(value, value.type.kind === "nullT" ? numLit(0, value.loc) : defaultValue);
}
return value;
}
/** Convert a stabilized position after the method has evaluated its arguments. */
export function positionNumber(
lowerer: Lowerer,
value: IrExpr,
defaultValue: IrExpr,
node: ts.Expression,
subject: string,
): IrExpr {
const loc = value.loc;
switch (value.type.kind) {
case "f64": return value;
case "string": return { kind: "libCall", fn: "num.fromString", args: [value], type: F64, loc };
case "bool": return { kind: "ternary", cond: value, then: numLit(1, loc), else_: numLit(0, loc), type: F64, loc };
case "nullT": return numLit(0, loc);
case "undefinedT": return defaultValue;
case "jsval": return { kind: "jsExit", value, type: F64, loc };
case "union": {
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)!.arms;
let result: IrExpr = defaultValue;
for (let tag = arms.length - 1; tag >= 0; tag--) {
const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value, type: arms[tag]!, loc };
const converted = positionNumber(lowerer, narrowed, defaultValue, node, subject);
result = tag === arms.length - 1 ? converted : {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, value, negated: false, type: BOOL, loc },
then: converted, else_: result, type: F64, loc,
};
}
return result;
}
case "dyn": return {
kind: "ternary",
cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc },
then: defaultValue,
else_: { kind: "libCall", fn: "dyn.toNumberCoerce", args: [value], type: F64, loc },
type: F64, loc,
};
default: return lowerer.noLowering(`${subject} of '${lowerer.fmt(value.type)}' values`, node);
}
}
@@ -315,6 +315,7 @@ export const ARRAY_METHODS = new Set([
"reduce",
"reduceRight",
"indexOf",
"lastIndexOf",
"includes",
"join",
"slice",
@@ -381,13 +382,13 @@ export const STR_METHODS: Record<
> = {
charCodeAt: { method: "charCodeAt", result: F64, minArgs: 0, maxArgs: 1 },
charAt: { method: "charAt", result: STRING, minArgs: 0, maxArgs: 1 },
indexOf: { method: "indexOf", result: F64, minArgs: 1, maxArgs: 2 },
indexOf: { method: "indexOf", result: F64, minArgs: 0, maxArgs: 2 },
// includes with a position argument is indexOf's clamp exactly (the
// spec routes both through StringIndexOf) — the emitter composes
// scr_str_index_of(...) != -1 for the two-argument form.
includes: { method: "includes", result: BOOL, minArgs: 1, maxArgs: 2 },
startsWith: { method: "startsWith", result: BOOL, minArgs: 1, maxArgs: 2 },
endsWith: { method: "endsWith", result: BOOL, minArgs: 1, maxArgs: 2 },
includes: { method: "includes", result: BOOL, minArgs: 0, maxArgs: 2 },
startsWith: { method: "startsWith", result: BOOL, minArgs: 0, maxArgs: 2 },
endsWith: { method: "endsWith", result: BOOL, minArgs: 0, maxArgs: 2 },
slice: { method: "slice", result: STRING, minArgs: 0, maxArgs: 2 },
// substring: slice's clamp-and-swap sibling (negatives clamp to 0
// instead of counting from the end; start > end swaps).
+15
View File
@@ -223,6 +223,8 @@ export interface CompileBaseOptions {
/** Native optimization posture. Release is the shipped -O2 default; dev
* uses -O0 and stable multi-TU object caching for large LLVM programs. */
optimization?: "release" | "dev";
/** Remove symbol/debug payload from an executable at link time. */
strip?: boolean;
/** Windows PE executable subsystem. Console is the default; GUI suppresses
* automatic console-window creation. Only valid for Windows executables. */
windowsSubsystem?: WindowsSubsystem;
@@ -1077,6 +1079,7 @@ async function compileExecutableNative(
sanitize: boolean,
ffi: FfiProfile | null,
windowsSubsystem?: WindowsSubsystem,
strip?: boolean,
programSplit: ReturnType<typeof splitLlvmProgram> = null,
programObjectDependencies: readonly NativeArtifactDependency[] = [],
onArtifactReady?: NonNullable<Parameters<typeof compileExternalC>[0]["onArtifactReady"]>,
@@ -1093,6 +1096,7 @@ async function compileExecutableNative(
features,
ffi,
optimization: features.optimization ?? "release",
...(strip ? { strip: true } : {}),
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
programObjectDependencies,
});
@@ -1130,6 +1134,7 @@ async function compileExecutableNative(
outPath,
cacheIdentity: "scriptc-generated-v1",
...(features.optimization === "dev" ? { optimization: "dev" as const } : {}),
...(strip ? { strip: true } : {}),
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
...(effectiveProgramSplit === null
? {}
@@ -1243,6 +1248,13 @@ async function compileTracked(
sourceTexts: new Map(),
};
}
if (opts.strip === true && outputKind !== "exe") {
return {
ok: false,
diagnostics: [nativeCodegenDiag("SC3002", "--strip is only supported for executable output", entryPath)],
sourceTexts: new Map(),
};
}
if (opts.nativeLinkInfo === true && outputKind !== "obj") {
return {
ok: false,
@@ -1387,6 +1399,7 @@ async function compileTracked(
dynamic: opts.dynamic ?? false,
backend: opts.backend ?? "auto",
...(opts.optimization === "dev" ? { optimization: "dev" as const } : {}),
...(opts.strip ? { strip: true as const } : {}),
...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}),
npmStatic: opts.npmStatic ?? null,
ffiProfile:
@@ -1486,6 +1499,7 @@ async function compileTracked(
opts.sanitize ?? false,
ffi,
opts.windowsSubsystem,
opts.strip,
null,
nativeProgramObject?.dependencies,
opts.nativeProgramObject === true ? undefined : async ({ dependencies }) => {
@@ -1775,6 +1789,7 @@ async function compileTracked(
opts.sanitize ?? false,
ffi,
opts.windowsSubsystem,
opts.strip,
programSplit,
nativeProgramObject?.dependencies,
opts.nativeProgramObject === true ? undefined : async ({ dependencies }) => {
+10 -4
View File
@@ -2182,6 +2182,12 @@
"name": "Array.prototype.join",
"status": "static"
},
{
"id": "stdlib.array.lastIndexOf",
"kind": "stdlib",
"name": "Array.prototype.lastIndexOf",
"status": "static"
},
{
"id": "stdlib.array.map",
"kind": "stdlib",
@@ -3505,21 +3511,21 @@
"kind": "stdlib",
"name": "string.prototype.endsWith",
"status": "static",
"note": "the lowered call form takes 1 to 2 arguments"
"note": "the lowered call form takes 0 to 2 arguments"
},
{
"id": "stdlib.string.includes",
"kind": "stdlib",
"name": "string.prototype.includes",
"status": "static",
"note": "the lowered call form takes 1 to 2 arguments"
"note": "the lowered call form takes 0 to 2 arguments"
},
{
"id": "stdlib.string.indexOf",
"kind": "stdlib",
"name": "string.prototype.indexOf",
"status": "static",
"note": "the lowered call form takes 1 to 2 arguments"
"note": "the lowered call form takes 0 to 2 arguments"
},
{
"id": "stdlib.string.isWellFormed",
@@ -3582,7 +3588,7 @@
"kind": "stdlib",
"name": "string.prototype.startsWith",
"status": "static",
"note": "the lowered call form takes 1 to 2 arguments"
"note": "the lowered call form takes 0 to 2 arguments"
},
{
"id": "stdlib.string.substring",
@@ -6925,15 +6925,27 @@
],
"diags": []
},
"<repo>/tests/corpus/3026-string-search-needle-coercion.js": {
"<repo>/tests/corpus/3026-array-last-index-of.js": {
"order": [
"<repo>/tests/corpus/3026-string-search-needle-coercion.js"
"<repo>/tests/corpus/3026-array-last-index-of.js"
],
"diags": []
},
"<repo>/tests/corpus/3027-arguments-with-parameters.js": {
"<repo>/tests/corpus/3027-array-last-index-of-records.ts": {
"order": [
"<repo>/tests/corpus/3027-arguments-with-parameters.js"
"<repo>/tests/corpus/3027-array-last-index-of-records.ts"
],
"diags": []
},
"<repo>/tests/corpus/3028-string-search-dynamic-needle.js": {
"order": [
"<repo>/tests/corpus/3028-string-search-dynamic-needle.js"
],
"diags": []
},
"<repo>/tests/corpus/3029-string-search-union-needle.js": {
"order": [
"<repo>/tests/corpus/3029-string-search-union-needle.js"
],
"diags": []
},
@@ -6943,6 +6955,36 @@
],
"diags": []
},
"<repo>/tests/corpus/3030-array-search-no-arg.js": {
"order": [
"<repo>/tests/corpus/3030-array-search-no-arg.js"
],
"diags": []
},
"<repo>/tests/corpus/3031-string-last-index-search-value.js": {
"order": [
"<repo>/tests/corpus/3031-string-last-index-search-value.js"
],
"diags": []
},
"<repo>/tests/corpus/3032-string-search-value.js": {
"order": [
"<repo>/tests/corpus/3032-string-search-value.js"
],
"diags": []
},
"<repo>/tests/corpus/3033-string-search-needle-coercion.js": {
"order": [
"<repo>/tests/corpus/3033-string-search-needle-coercion.js"
],
"diags": []
},
"<repo>/tests/corpus/3034-arguments-with-parameters.js": {
"order": [
"<repo>/tests/corpus/3034-arguments-with-parameters.js"
],
"diags": []
},
"<repo>/tests/corpus/304-compound-incdec.ts": {
"order": [
"<repo>/tests/corpus/304-compound-incdec.ts"
+20
View File
@@ -0,0 +1,20 @@
let trace = "";
function receiver() { trace += "r"; return [0, 1, 0]; }
function needle() { trace += "n"; return 0; }
function position() { trace += "p"; return "1.9"; }
console.log(receiver().lastIndexOf(needle(), position()), trace);
trace = "";
console.log(receiver().lastIndexOf(needle(), void position()), trace);
const numbers = [0, 1, 0, 1, 0];
/** @type {(number | string | boolean | null | undefined)[]} */
const positions = [undefined, null, false, true, "", "2.9", "bad", NaN, -Infinity, -3, -1, -0, 0, 1, 2, 4, Infinity, 99];
for (const value of positions) {
console.log(numbers.lastIndexOf(0, value), numbers.lastIndexOf(1, value));
}
console.log(numbers.lastIndexOf(0), numbers.lastIndexOf(0, undefined));
console.log([NaN, 0, -0, NaN].lastIndexOf(NaN), [NaN, 0, -0].lastIndexOf(0));
console.log([undefined].lastIndexOf(undefined), [null, undefined].lastIndexOf(null));
const sparse = [1, , undefined, 1];
console.log(sparse.lastIndexOf(undefined), sparse.lastIndexOf(undefined, 1), sparse.lastIndexOf(1, 2));
@@ -0,0 +1,7 @@
interface Item { id: number }
const first: Item = { id: 1 };
const second: Item = { id: 2 };
const refs: Item[] = [first, second, first];
console.log(refs.lastIndexOf(first), refs.lastIndexOf(second), refs.lastIndexOf({ id: 1 }));
console.log(refs.lastIndexOf(first, 1), refs.lastIndexOf(second, 0));
console.log(refs.lastIndexOf(refs[0]), refs.lastIndexOf(refs[1]));
@@ -0,0 +1,13 @@
function probe(needle, position) {
const value = "ababa";
console.log(
value.indexOf(needle, position),
value.includes(needle, position),
value.startsWith(needle, position),
value.endsWith(needle, position),
);
}
probe("ba", 1);
probe(1, 0);
probe(undefined, 0);
@@ -0,0 +1,17 @@
/** @param {string | undefined} needle */
function probe(needle) {
const value = "ababa";
console.log(
value.indexOf(needle),
value.indexOf(needle, 1),
value.includes(needle),
value.includes(needle, 1),
value.startsWith(needle),
value.startsWith(needle, 1),
value.endsWith(needle),
value.endsWith(needle, 2),
);
}
probe("ba");
probe(undefined);
+18
View File
@@ -0,0 +1,18 @@
console.log([0].includes(), [undefined].includes(), [null].includes(), [].includes());
console.log([undefined, 0, undefined].includes());
const holes = [2];
holes[2] = 2;
console.log(holes.includes());
const untyped = [];
untyped[2] = 2;
console.log(untyped.includes());
const present = [undefined, 1];
present[3] = undefined;
console.log(present.includes());
let evaluations = 0;
function receiver() { evaluations++; return [undefined, 1]; }
console.log(receiver().includes(), evaluations);
@@ -0,0 +1,13 @@
console.log("undefined".lastIndexOf(), "".lastIndexOf(), "undefinedundefined".lastIndexOf());
console.log("gnullunazzgnull".lastIndexOf(null));
console.log("undefined".lastIndexOf(undefined), "undefined".lastIndexOf(void 0));
console.log("truefalse".lastIndexOf(true), "42".lastIndexOf(42), "1".lastIndexOf(1n));
const missing = undefined;
const nil = null;
console.log("undefined".lastIndexOf(missing), "nullnull".lastIndexOf(nil));
let trace = "";
function receiver() { trace += "r"; return "xundefined"; }
function needleEffect() { trace += "n"; return 1; }
console.log(receiver().lastIndexOf(void needleEffect()), trace);
+36
View File
@@ -0,0 +1,36 @@
console.log("undefined".indexOf(), "x".indexOf(), "undefined".includes(), "x".includes());
console.log("undefined".startsWith(), "x".startsWith(), "xundefined".endsWith(), "x".endsWith());
console.log("undefined".indexOf(undefined), "undefined".includes(void 0));
console.log("undefined".startsWith(undefined), "undefined".endsWith(undefined));
console.log("anullb".indexOf(null), "anullb".includes(null));
console.log("nulla".startsWith(null), "anull".endsWith(null));
console.log("truefalse".indexOf(true), "truefalse".includes(false));
console.log("truefalse".startsWith(true), "truefalse".endsWith(false));
console.log("42".indexOf(42), "42".includes(42), "42".startsWith(42), "42".endsWith(42));
console.log("1".indexOf(1n), "1".includes(1n), "1".startsWith(1n), "1".endsWith(1n));
/** @param {string | number | boolean | null | undefined} value */
function search(value) {
const text = "undefined-null-1-true";
console.log(text.indexOf(value), text.includes(value), text.startsWith(value), text.endsWith(value));
}
search(undefined);
search(null);
search(1);
search(true);
search("true");
let trace = "";
function receiver() { trace += "r"; return "undefined"; }
function needleEffect() { trace += "n"; return 1; }
console.log(receiver().indexOf(void needleEffect()), trace);
trace = "";
console.log(receiver().includes(void needleEffect()), trace);
trace = "";
console.log(receiver().startsWith(void needleEffect()), trace);
trace = "";
console.log(receiver().endsWith(void needleEffect()), trace);
function nullEffect() { trace += "n"; return null; }
trace = "";
console.log(receiver().indexOf(nullEffect()), trace);
+30
View File
@@ -6,6 +6,7 @@
"snapshotSha256": "584d260eaa9aef0c42d18b6b03e8b3755ecd54bdf43e01701f5b3eca3043c4c3",
"testFiles": 53597,
"tests": [
"test/built-ins/Array/prototype/includes/no-arg.js",
"test/built-ins/Array/prototype/indexOf/15.4.4.14-4-1.js",
"test/built-ins/Array/prototype/indexOf/15.4.4.14-5-13.js",
"test/built-ins/Array/prototype/indexOf/15.4.4.14-5-14.js",
@@ -29,6 +30,14 @@
"test/built-ins/Array/prototype/indexOf/15.4.4.14-9-b-ii-1.js",
"test/built-ins/Array/prototype/indexOf/15.4.4.14-9-b-ii-4.js",
"test/built-ins/Array/prototype/indexOf/fromindex-zero-conversion.js",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-1.js",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-2.js",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-3.js",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-4.js",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-5.js",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-6.js",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-7.js",
"test/built-ins/Array/prototype/lastIndexOf/fromindex-zero-conversion.js",
"test/built-ins/BigInt/prototype/toString/default-radix.js",
"test/built-ins/Math/abs/S15.8.2.1_A1.js",
"test/built-ins/Math/abs/S15.8.2.1_A2.js",
@@ -54,6 +63,12 @@
"test/built-ins/String/prototype/includes/coerced-values-of-position.js",
"test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js",
"test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T4.js",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T5.js",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T7.js",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T4.js",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T5.js",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T7.js",
"test/built-ins/String/prototype/lastIndexOf/not-a-substring.js",
"test/built-ins/String/prototype/slice/S15.5.4.13_A1_T4.js",
"test/built-ins/String/prototype/startsWith/out-of-bounds-position.js",
@@ -87,6 +102,7 @@
"LICENSE": "4dd9244dfe8197c75348c4b24ab53d29d3b1cfad143ac76b5a3d8942aa354ce0",
"harness/assert.js": "206e274ca325eb8a652e3911c3fbd090e2480d11ed7579dc17a5d17a2360ed48",
"harness/sta.js": "1930c54af79455c484799f43e9a28e2b2f15c40d0917c9941ca54e26db243f35",
"test/built-ins/Array/prototype/includes/no-arg.js": "e38e7db529094b08ac13cc8a53cdbf59b5b2537711922af9bf8b1178f615e511",
"test/built-ins/Array/prototype/indexOf/15.4.4.14-4-1.js": "ef762d69cbdf878821a6f998064590c2c19aa9e3db297968a5db02a6f5cd455a",
"test/built-ins/Array/prototype/indexOf/15.4.4.14-5-13.js": "3c1ebd36e0e4657c42abbb76a707546a7b46ad954d3e569a84b796695d10da02",
"test/built-ins/Array/prototype/indexOf/15.4.4.14-5-14.js": "7bb87d45a34bffcc61b39131c30289d6da7b51cd6b2a3db4af38674864fbd15e",
@@ -110,6 +126,14 @@
"test/built-ins/Array/prototype/indexOf/15.4.4.14-9-b-ii-1.js": "db248a7b40b4285a0331fa7eaee0a78e6a9af4e57294c9ce9e89fe392845ca2e",
"test/built-ins/Array/prototype/indexOf/15.4.4.14-9-b-ii-4.js": "07b115c3b90dfb5bcb9e38990fd1db57720c4ac423e052082828ed8ccb6b4214",
"test/built-ins/Array/prototype/indexOf/fromindex-zero-conversion.js": "ababfd2ad12b4099f9d2108ba6e58ff33feb358522f3bbd4ddde5144cbf0bc7f",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-1.js": "58e752c5eda47f23bdc281f13ac7f3d3df37a0051b0ccd194198987796c8a841",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-2.js": "70cf040ba435efad144dcdbab8dc29547174ea3af9a7a5750eb0b0939110f497",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-3.js": "448016548d98ed3c4cefb81f0d995cd12e3d9d13fd90a5d119dd072108979419",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-4.js": "888ef295cf236543663ac873c2ca3c9c43e9b9c9a15cc35e4757d34b19356ca9",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-5.js": "599f01a3f1d5bfa8a070fc7839620c0b694e18730ba13c013ce9235554fa9ba2",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-6.js": "d389d03867aef471519b23d75bf6b24e68038a91de9f4489ac0d08da9f393bbb",
"test/built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-7.js": "5afb53c527ddb49da7a505af67cd59ad49d8b47d5af9280ed0e5578672a94c40",
"test/built-ins/Array/prototype/lastIndexOf/fromindex-zero-conversion.js": "8231b818adf4123a4e5c9b65206b0b72984894b825319da669b99f820c75f963",
"test/built-ins/BigInt/prototype/toString/default-radix.js": "d901ef7e3fab5b8c528cb60a12c80145a6c5b7fbd02f829fe657259eddbe711a",
"test/built-ins/Math/abs/S15.8.2.1_A1.js": "c5b508cdc6130c8670e8ac6d822ca93e5dbead7566543cb65b793602b7203d7c",
"test/built-ins/Math/abs/S15.8.2.1_A2.js": "a625e117f7858319c2ff25b959539266a6f29473925bb47ad5d8bbfe88e7b31e",
@@ -135,6 +159,12 @@
"test/built-ins/String/prototype/includes/coerced-values-of-position.js": "292b6f29f5f223027deb54dafe62726790e2a61ddf2eb5e5036688b7088f105f",
"test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js": "4e65326e88ae9b915771fcc70c52fd7e4aa1f7a6aa8ca047010806555b57d668",
"test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js": "c5552ee08afae1dd6147d388e987a1083571d2faf18ace56b0324204d1ea50b2",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T4.js": "dee63261be6e97a1a42a9267c98b21144489caf800d6dcc3e93bde61c2cf5103",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T5.js": "b3e357871ed5fee11206e1c7ecef7c7cc094a3a2a37eacbcfbdaec600ccca754",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T7.js": "31ee4085c1ab60c5ace3f4c4adfca8afdbc6b35d029f843fc8041d38f31a9435",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T4.js": "db9a202295d7f2f48cb023cf6774e3e23084e05dbb9f1071f4aa7e3bd93710b3",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T5.js": "fc3de1c70005ef91a3ab573220e0660255a33cf1bcf8d169d7845622747f02fe",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T7.js": "d9392644874a8a2ffb6549f52aa2447d6d03c3e43762008fc6a32c18d35dea6a",
"test/built-ins/String/prototype/lastIndexOf/not-a-substring.js": "1fa0e88ffe6a342e6204c925918ae6fc29b003d47007210fc377c24bc56bd41d",
"test/built-ins/String/prototype/slice/S15.5.4.13_A1_T4.js": "1f67b7089a1e53f850b6ceb962193f49d8a2c114fc37a0377ac6553108c83ac0",
"test/built-ins/String/prototype/startsWith/out-of-bounds-position.js": "2c27bbb2505d166a6a0fbab388ca9660780c6b396e0634e87a3e855c11865cb3",
@@ -0,0 +1,20 @@
// Copyright (C) 2016 the V8 project authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-array.prototype.includes
description: no argument searches for a undefined value
info: |
22.1.3.11 Array.prototype.includes ( searchElement [ , fromIndex ] )
...
7. Repeat, while k < len
a. Let elementK be the result of ? Get(O, ! ToString(k)).
b. If SameValueZero(searchElement, elementK) is true, return true.
c. Increase k by 1.
...
features: [Array.prototype.includes]
---*/
assert.sameValue([0].includes(), false, "[0].includes()");
assert.sameValue([undefined].includes(), true, "[undefined].includes()");
@@ -0,0 +1,12 @@
// Copyright (c) 2012 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-array.prototype.lastindexof
description: Array.prototype.lastIndexOf when fromIndex is string
---*/
var a = new Array(0, 1, 1);
assert.sameValue(a.lastIndexOf(1, "1"), 1, '"1" resolves to 1');
assert.sameValue(a.lastIndexOf(1, "one"), -1, 'NaN string resolves to 01');
@@ -0,0 +1,13 @@
// Copyright (c) 2012 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-array.prototype.lastindexof
description: Array.prototype.lastIndexOf when fromIndex is floating point number
---*/
var a = new Array(1, 2, 1);
assert.sameValue(a.lastIndexOf(2, 1.49), 1, '1.49 resolves to 1');
assert.sameValue(a.lastIndexOf(2, 0.51), -1, '0.51 resolves to 0');
assert.sameValue(a.lastIndexOf(1, 0.51), 0, '0.51 resolves to 0');
@@ -0,0 +1,12 @@
// Copyright (c) 2012 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-array.prototype.lastindexof
description: Array.prototype.lastIndexOf when fromIndex is boolean
---*/
var a = new Array(1, 2, 1);
assert.sameValue(a.lastIndexOf(2, true), 1, 'true resolves to 1');
assert.sameValue(a.lastIndexOf(2, false), -1, 'false resolves to 0');
@@ -0,0 +1,14 @@
// Copyright (c) 2012 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-array.prototype.lastindexof
description: Array.prototype.lastIndexOf when fromIndex is undefined
---*/
var a = new Array(1, 2, 1);
// undefined resolves to 0, no second argument resolves to len
assert.sameValue(a.lastIndexOf(2, undefined), -1, 'a.lastIndexOf(2,undefined)');
assert.sameValue(a.lastIndexOf(1, undefined), 0, 'a.lastIndexOf(1,undefined)');
assert.sameValue(a.lastIndexOf(1), 2, 'a.lastIndexOf(1)');
@@ -0,0 +1,13 @@
// Copyright (c) 2012 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-array.prototype.lastindexof
description: Array.prototype.lastIndexOf when fromIndex is null
---*/
var a = new Array(1, 2, 1);
// null resolves to 0
assert.sameValue(a.lastIndexOf(2, null), -1, 'a.lastIndexOf(2,null)');
assert.sameValue(a.lastIndexOf(1, null), 0, 'a.lastIndexOf(1,null)');
@@ -0,0 +1,14 @@
// Copyright (c) 2012 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-array.prototype.lastindexof
description: Array.prototype.lastIndexOf when 'fromIndex' isn't passed
---*/
var arr = [0, 1, 2, 3, 4];
//'fromIndex' will be set as 4 if not passed by default
assert.sameValue(arr.lastIndexOf(0), arr.lastIndexOf(0, 4), 'arr.lastIndexOf(0)');
assert.sameValue(arr.lastIndexOf(2), arr.lastIndexOf(2, 4), 'arr.lastIndexOf(2)');
assert.sameValue(arr.lastIndexOf(4), arr.lastIndexOf(4, 4), 'arr.lastIndexOf(4)');
@@ -0,0 +1,12 @@
// Copyright (c) 2012 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-array.prototype.lastindexof
description: >
Array.prototype.lastIndexOf - value of 'fromIndex' is a number
(value is 0)
---*/
assert.sameValue([0, 100].lastIndexOf(100, 0), -1, 'verify fromIndex is not more than 0');
assert.sameValue([200, 0].lastIndexOf(200, 0), 0, 'verify fromIndex is not less than 0');
@@ -0,0 +1,16 @@
// Copyright 2016 The V8 project authors. All rights reserved.
// This code is governed by the license found in the LICENSE file.
/*---
esid: sec-array.prototype.lastindexof
description: >
Return +0 when fromIndex is -0 and return index refers to the first position
info: |
22.1.3.15 Array.prototype.lastIndexOf ( searchElement [ , fromIndex ] )
...
5. If n ≥ 0, then
a. If n is -0, let k be +0; else let k be min(n, len - 1).
...
---*/
assert.sameValue(1 / [true].lastIndexOf(true, -0), +Infinity);
@@ -0,0 +1,19 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.indexOf(searchString, position)
es5id: 15.5.4.7_A1_T4
description: >
Call indexOf(searchString, position) function without arguments of
string
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString() evaluates to "" indexOf() evaluates to indexOf("",0)
if ("".indexOf() !== -1) {
throw new Test262Error('#1: "".indexOf() === -1. Actual: ' + ("".indexOf()));
}
//
//////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,23 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.indexOf(searchString, position)
es5id: 15.5.4.7_A1_T5
description: >
Call indexOf(searchString, position) function with null argument
of function object
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString(null) evaluates to "null" indexOf(null) evaluates to indexOf("",0)
if (function() {
return "gnulluna"
}().indexOf(null) !== 1) {
throw new Test262Error('#1: function(){return "gnulluna"}().indexOf(null) === 1. Actual: ' + function() {
return "gnulluna"
}().indexOf(null));
}
//
//////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,19 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.indexOf(searchString, position)
es5id: 15.5.4.7_A1_T7
description: >
Call indexOf(searchString, position) function with undefined
argument of string object
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString(undefined) evaluates to "undefined" indexOf(undefined) evaluates to indexOf("undefined",0)
if (String("undefined").indexOf(undefined) !== 0) {
throw new Test262Error('#1: String("undefined").indexOf(undefined) === 0. Actual: ' + String("undefined").indexOf(undefined));
}
//
//////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,19 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.lastIndexOf(searchString, position)
es5id: 15.5.4.8_A1_T4
description: >
Call lastIndexOf(searchString, position) function without
arguments of string
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString() evaluates to "" lastIndexOf() evaluates to lastIndexOf("",0)
if ("".lastIndexOf() !== -1) {
throw new Test262Error('#1: "".lastIndexOf() === -1. Actual: ' + ("".lastIndexOf()));
}
//
//////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,23 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.lastIndexOf(searchString, position)
es5id: 15.5.4.8_A1_T5
description: >
Call lastIndexOf(searchString, position) function with null
argument of function object
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString(null) evaluates to "null" lastIndexOf(null) evaluates to lastIndexOf("",0)
if (function() {
return "gnullunazzgnull"
}().lastIndexOf(null) !== 11) {
throw new Test262Error('#1: function(){return "gnullunazzgnull"}().lastIndexOf(null) === 11. Actual: ' + function() {
return "gnullunazzgnull"
}().lastIndexOf(null));
}
//
//////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,19 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.lastIndexOf(searchString, position)
es5id: 15.5.4.8_A1_T7
description: >
Call lastIndexOf(searchString, position) function with undefined
argument of string object
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString(undefined) evaluates to "undefined" lastIndexOf(undefined) evaluates to lastIndexOf("undefined",0)
if (String("undefined").lastIndexOf(undefined) !== 0) {
throw new Test262Error('#1: String("undefined").lastIndexOf(undefined) === 0. Actual: ' + String("undefined").lastIndexOf(undefined));
}
//
//////////////////////////////////////////////////////////////////////////////