diff --git a/docs/content/docs/wasm.mdx b/docs/content/docs/wasm.mdx
index a39c31e34..b498eccf7 100644
--- a/docs/content/docs/wasm.mdx
+++ b/docs/content/docs/wasm.mdx
@@ -96,11 +96,13 @@ After initialization, counter_step(2) returns 2, and a
i64, u64 | JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range. |
string, bytes parameters | Two i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values. |
string, bytes results | Two extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order. |
+ cstring parameters | One i32 argument: the byte offset of NUL-terminated UTF-8 text. The text is copied into a runtime-owned string; malformed UTF-8 becomes U+FFFD. A zero pointer traps with SC4012. |
+ cstring results | An i32 result: the byte offset of NUL-terminated UTF-8 text in the result arena. A result containing U+0000 traps with SC4015 instead of being truncated. |
void | No result. |
-The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host.
+The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host. A cstring callback parameter is one i32 pointer to NUL-terminated text, borrowed the same way; a value containing U+0000 traps with SC4015. The cstring class requires "profile_format": 2. In native libraries it is a const char * with the same rules.
Allocate host input buffers and output slots with scriptc_alloc(size); a zero result indicates allocation failure. Release each allocation exactly once with scriptc_free(pointer). Only free pointers returned by scriptc_alloc. These allocations remain live across application calls, initialization, and collection until freed.
diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
index 93149c51c..c2a744c16 100644
--- a/docs/src/generated/node-v24-compatibility-meta.json
+++ b/docs/src/generated/node-v24-compatibility-meta.json
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
- "artifactVersion": "a1ff929d49885bd8c92e",
+ "artifactVersion": "d569e27ff34d1ebed70d",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index 352c211b5..64ffffd77 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -509,9 +509,9 @@
"apiEntries": 24,
"static": {
"supported": 0,
- "partial": 23,
+ "partial": 24,
"refused": 0,
- "not-implemented": 1,
+ "not-implemented": 0,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -779,9 +779,9 @@
"apiEntries": 62,
"static": {
"supported": 0,
- "partial": 39,
+ "partial": 42,
"refused": 0,
- "not-implemented": 23,
+ "not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -52503,9 +52503,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -71599,9 +71599,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -71714,9 +71714,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -71806,9 +71806,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index fff784adc..0046a5acc 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -7,8 +7,8 @@
"tierItemCount": 3456,
"tiers": {
"static": {
- "audit-partial": 1091,
- "verify-gap": 2238,
+ "audit-partial": 1095,
+ "verify-gap": 2234,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -51739,12 +51739,14 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:os",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.os.setPriority",
+ "tests": [
+ "tests/corpus/5883-os-set-priority.ts"
+ ]
}
}
},
@@ -65617,7 +65619,8 @@
"source": "compiler-dedicated:StringDecoder",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
}
}
@@ -65645,7 +65648,8 @@
"source": "compiler-dedicated:stringDecoder.end",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
}
}
@@ -65673,7 +65677,8 @@
"source": "compiler-dedicated:stringDecoder.write",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
}
}
@@ -73455,12 +73460,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:url",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:url.urlSearchParams.entries",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
}
}
},
@@ -73580,12 +73588,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:url",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:url.urlSearchParams.keys",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
}
}
},
@@ -73680,12 +73691,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:url",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:url.urlSearchParams.values",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index 559bd48ca..67d79376e 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -536,9 +536,9 @@
"apiEntries": 24,
"static": {
"supported": 0,
- "partial": 23,
+ "partial": 24,
"refused": 0,
- "not-implemented": 1,
+ "not-implemented": 0,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -836,9 +836,9 @@
"apiEntries": 62,
"static": {
"supported": 0,
- "partial": 39,
+ "partial": 42,
"refused": 0,
- "not-implemented": 23,
+ "not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -74097,7 +74097,8 @@
"tests/corpus/2917-builtin-callable-widths.ts",
"tests/corpus/4598-os-available-parallelism.ts",
"tests/corpus/5140-os-host-facts.ts",
- "tests/corpus/5141-os-constants.ts"
+ "tests/corpus/5141-os-constants.ts",
+ "tests/corpus/5883-os-set-priority.ts"
]
},
"anchorSource": "chapter"
@@ -74646,8 +74647,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:os"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.os.setPriority",
+ "tests": [
+ "tests/corpus/5883-os-set-priority.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -90813,6 +90817,7 @@
"evidence": "derived:descendants",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts",
"tests/corpus/node24-string-decoder.ts"
]
},
@@ -90840,7 +90845,8 @@
"evidence": "compiler-dedicated:StringDecoder",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
},
"anchorSource": "exact"
@@ -90867,7 +90873,8 @@
"evidence": "compiler-dedicated:stringDecoder.end",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
},
"anchorSource": "exact"
@@ -90894,7 +90901,8 @@
"evidence": "compiler-dedicated:stringDecoder.write",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
},
"anchorSource": "exact"
@@ -99004,6 +99012,8 @@
"tests/corpus/5227-url-legacy-dependency/main.ts",
"tests/corpus/5228-url-domain-conversion.ts",
"tests/corpus/5229-url-legacy-errors.cjs",
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs",
"tests/corpus/node24-file-url-buffer.ts",
"tests/corpus/node24-file-url-errors.js",
"tests/corpus/node24-file-url-options.ts",
@@ -99788,8 +99798,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:url"
+ "status": "partial",
+ "evidence": "compiler-feature:url.urlSearchParams.entries",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
},
"anchorSource": "exact"
},
@@ -99903,8 +99917,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:url"
+ "status": "partial",
+ "evidence": "compiler-feature:url.urlSearchParams.keys",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
},
"anchorSource": "exact"
},
@@ -99995,8 +100013,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:url"
+ "status": "partial",
+ "evidence": "compiler-feature:url.urlSearchParams.values",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
},
"anchorSource": "exact"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index d68011393..252c19d71 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -104,6 +104,7 @@
"node-builtin.os.endianness": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.freemem": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.getPriority": ["tests/corpus/5140-os-host-facts.ts"],
+ "node-builtin.os.setPriority": ["tests/corpus/5883-os-set-priority.ts"],
"node-builtin.os.hostname": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.loadavg": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.machine": ["tests/corpus/5140-os-host-facts.ts"],
@@ -337,6 +338,7 @@
{ "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] },
{ "chapter": "url", "symbols": ["url.fileURLToPath", "url.fileURLToPathBuffer", "url.pathToFileURL"], "status": "partial", "evidence": ["tests/corpus/node24-file-url-buffer.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js", "tests/fixtures/node-types/file-url-conversion.ts"] },
{ "chapter": "url", "symbols": ["URL.canParse", "URL.parse", "url.toString", "url.toJSON"], "status": "partial", "evidence": ["tests/corpus/node24-url-factories.ts", "tests/corpus/node24-url-factory-errors.ts", "tests/fixtures/node-types/url-factories.ts", "tests/harness/project-config.test.ts"] },
+ { "chapter": "url", "symbols": ["urlSearchParams.entries", "urlSearchParams.keys", "urlSearchParams.values"], "status": "partial", "evidence": ["tests/corpus/5833-url-search-params-iterators.ts", "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"] },
{ "chapter": "url", "signatures": ["Class: `URL`"], "status": "partial", "evidence": ["tests/corpus/url-runtime-base.ts", "tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`protocol` Type: {string}", "`username` Type: {string}", "`password` Type: {string}", "`host` Type: {string}", "`hostname` Type: {string}", "`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-authority.ts", "tests/corpus/node24-url-authority-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`href` Type: {string}", "`pathname` Type: {string}", "`search` Type: {string}", "`hash` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
@@ -377,7 +379,7 @@
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
{ "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts"] },
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] },
- { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts"] },
+ { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] },
{ "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] },
{ "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] },
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs"] },
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index d87172f61..ef082b4f7 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -2109,6 +2109,8 @@ declare module "os" {
export function version(): string;
export function endianness(): "BE" | "LE";
export function getPriority(pid?: number): number;
+ export function setPriority(priority: number): void;
+ export function setPriority(pid: number, priority: number): void;
/* The TARGET platform's null device ("\\\\.\\nul" under a win32 triple). */
export const devNull: string;
/* The platform's constants: null-prototype objects with read-only
@@ -3618,7 +3620,8 @@ declare module "querystring" {
| string
| number
| boolean
- | ReadonlyArray
+ | bigint
+ | ReadonlyArray
| null
| undefined;
}
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index e2b1102c1..03fa03230 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -2190,6 +2190,12 @@ export class LlEmitter {
if (this.mod.lib.exports.some((e) => e.returns === "bytes")) {
this.declare(`declare void @scr_library_bytes_out(ptr, ptr, ptr)`);
}
+ if (this.mod.lib.exports.some((e) => e.params.includes("cstring"))) {
+ this.declare(`declare ptr @scr_library_cstr_in(ptr, ptr)`);
+ }
+ if (this.mod.lib.exports.some((e) => e.returns === "cstring")) {
+ this.declare(`declare ptr @scr_library_cstr_out(ptr)`);
+ }
}
// Helpers assemble BEFORE the declaration table flushes (they add
// write/abort declarations).
@@ -2966,6 +2972,15 @@ export class LlEmitter {
``,
);
}
+ if (e.inboundCstringTrap !== undefined) {
+ // The cstring-in helper's host-contract trap message (a NULL
+ // `const char *`): same assembled structured form and SC4012 code.
+ const trapBytes = Buffer.byteLength(e.inboundCstringTrap, "utf8");
+ out.push(
+ `@sc_lib_cstr_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundCstringTrap)}"`,
+ ``,
+ );
+ }
e.params.forEach((cls, i) => {
switch (cls) {
case "f64":
@@ -3023,6 +3038,13 @@ export class LlEmitter {
);
args.push(`ptr %c${i}`);
break;
+ case "cstring":
+ params.push(`ptr %a${i}`);
+ body.push(
+ ` %c${i} = call ptr @scr_library_cstr_in(ptr %a${i}, ptr @sc_lib_cstr_trap_${e.symbol})`,
+ );
+ args.push(`ptr %c${i}`);
+ break;
}
});
if (e.returns === "string" || e.returns === "bytes") {
@@ -3087,6 +3109,16 @@ export class LlEmitter {
` ret void`,
);
break;
+ case "cstring":
+ // The result's arena copy, returned as `const char *`.
+ retType = "ptr";
+ body.push(
+ ` %r = call ptr ${target}(${callArgs})`,
+ ` call void @scr_library_check_exc()`,
+ ` %z = call ptr @scr_library_cstr_out(ptr %r)`,
+ ` ret ptr %z`,
+ );
+ break;
}
out.push(
`define ${retType} @${e.symbol}(${params.join(", ")}) ${FN_ATTRS} { ; library export ${e.fnName}`,
diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts
index 1a34f5e81..6e8b8caf4 100644
--- a/packages/compiler/src/backend/llvm/expr-calls.ts
+++ b/packages/compiler/src/backend/llvm/expr-calls.ts
@@ -333,6 +333,16 @@ export function emitCallExpr(
natArgs.push(`ptr ${data}`, `${host.sizeType} ${len}`);
break;
}
+ case "cstring": {
+ // The string's own NUL-terminated data, after the check that
+ // it holds no U+0000 (a C string cannot carry one).
+ host.declare(`declare ptr @scr_library_cstr_arg(ptr)`);
+ const data = B.tmp();
+ B.line(`${data} = call ptr @scr_library_cstr_arg(ptr ${arg.name})`);
+ natTypes.push("ptr");
+ natArgs.push(`ptr ${data}`);
+ break;
+ }
case "bytes": {
const lenPtr = B.tmp();
const len = B.tmp();
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 9232cbd03..a9d6fb7b9 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -188,6 +188,7 @@ export const LIB_FN_SYMS: Record = {
"os.uptime": "scr_os_uptime",
"os.loadavg": "scr_os_loadavg",
"os.getPriority": "scr_os_get_priority",
+ "os.setPriority": "scr_os_set_priority",
"os.cpusTake": "scr_os_cpus_take",
"os.cpusModel": "scr_os_cpus_model",
"os.cpusField": "scr_os_cpus_field",
@@ -477,6 +478,7 @@ export const LIB_FN_SYMS: Record = {
"strdec.write": "scr_strdec_write",
"strdec.next": "scr_strdec_next",
"strdec.end": "scr_strdec_end",
+ "strdec.encoding": "scr_strdec_encoding",
"stats.isFile": "scr_stats_is_file",
"stats.isDirectory": "scr_stats_is_dir",
"stats.isSymbolicLink": "scr_stats_is_symlink",
diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts
index 65d954fb5..365d5e913 100644
--- a/packages/compiler/src/backend/target-diagnostics.ts
+++ b/packages/compiler/src/backend/target-diagnostics.ts
@@ -129,6 +129,7 @@ const WASI_UNAVAILABLE_HOST_FACTS: ReadonlyMap = new Map([
["os.loadavg", "system load averages (WASI Preview 1 has no load API)"],
["os.uptime", "system uptime (WASI Preview 1 has no boot-time API)"],
["os.getPriority", "process scheduling priority (WASI Preview 1 has no priority API)"],
+ ["os.setPriority", "process scheduling priority (WASI Preview 1 has no priority API)"],
["process.memoryUsageTake", "process memory statistics (WASI Preview 1 has no memory API)"],
["process.rss", "process memory statistics (WASI Preview 1 has no memory API)"],
["process.ppid", "the parent process id (WASI Preview 1 has no process API)"],
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
index dc141045f..58ac85aba 100644
--- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
@@ -40,6 +40,7 @@ import { lowerForkCall } from "./child-process.js";
import {
lowerOsCpusCall,
lowerOsGetPriorityCall,
+ lowerOsSetPriorityCall,
lowerOsNetworkInterfacesCall,
lowerOsUserInfoCall,
lowerPathFormatCall,
@@ -163,6 +164,9 @@ export function lowerBuiltinModuleCall(
if (bi.module === "os" && bi.member === "cpus") {
return lowerOsCpusCall(lowerer, expr, loc);
}
+ if (bi.module === "os" && bi.member === "setPriority") {
+ return lowerOsSetPriorityCall(lowerer, expr, loc);
+ }
if (bi.module === "os" && bi.member === "getPriority") {
return lowerOsGetPriorityCall(lowerer, expr, loc);
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts
index 051981f09..b88b97eb6 100644
--- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts
@@ -10,6 +10,7 @@ import {
STRING,
type SrcLoc,
UNDEFINED_T,
+ VOID,
arrayOf,
typeKey,
} from "../../../ir/ir.js";
@@ -511,6 +512,29 @@ export function lowerPathFormatCall(
return { kind: "call", callee: helper, args: [value], type: STRING, loc };
}
+/** `os.setPriority([pid, ]priority)` — the one-argument form is pid 0
+ * (Node's `if (priority === undefined) { priority = pid; pid = 0; }`);
+ * the runtime validates both. */
+export function lowerOsSetPriorityCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ loc: SrcLoc,
+): IrExpr {
+ const args = call.arguments;
+ if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(
+ "setPriority with these arguments",
+ call,
+ "setPriority(priority) or setPriority(pid, priority) takes numbers",
+ );
+ }
+ const [pid, priority] =
+ args.length === 1
+ ? [numLit(0, loc), lowerer.lowerExprExpecting(args[0]!, F64)]
+ : [lowerer.lowerExprExpecting(args[0]!, F64), lowerer.lowerExprExpecting(args[1]!, F64)];
+ return { kind: "libCall", fn: "os.setPriority", args: [pid, priority], type: VOID, loc };
+}
+
/** `os.getPriority(pid?)` — an omitted or undefined pid is 0 (Node's
* `if (pid === undefined) pid = 0`); the runtime validates the rest. */
export function lowerOsGetPriorityCall(
diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts
index 6e3beb9c2..daf857650 100644
--- a/packages/compiler/src/frontend/lowering/builtins/url.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/url.ts
@@ -26,6 +26,7 @@ import {
} from "../../../ir/ir.js";
import { optionalStringTags } from "./arguments.js";
import { type BuiltinModuleFn } from "../surfaces.js";
+import { lowerDynDispatchMethodCall } from "../lower-calls.js";
/** URL component assignment captures its receiver and RHS once, and yields
* the original RHS after the setter performs string conversion. */
@@ -255,10 +256,12 @@ export function lowerSearchParamsNew(
return { kind: "libCall", fn: "sp.fromPairs", args: [init], type: SEARCH_PARAMS_T, loc };
}
if (init.type.kind === "array" && init.type.elem.kind === "record") {
+ const pairs = searchParamsFromTuples(lowerer, init, init.type.elem, loc);
+ if (pairs) return pairs;
lowerer.unsupported(
"SC1090",
arg,
- "URLSearchParams from tuple-typed pairs (type the pairs as string[][] — the tuple rows have a different layout)",
+ "URLSearchParams from tuple rows other than [string, string] (type the pairs as [string, string][] or string[][])",
);
}
lowerer.unsupported(
@@ -268,6 +271,97 @@ export function lowerSearchParamsNew(
);
}
+/** `new URLSearchParams(pairs)` over a `[string, string][]` (an
+ * Object.entries result, for example): a helper per tuple shape appends
+ * each pair's name and value in array order. Null for other row types. */
+function searchParamsFromTuples(
+ lowerer: Lowerer,
+ init: IrExpr,
+ row: IrType & { kind: "record" },
+ loc: SrcLoc,
+): IrExpr | null {
+ const shape = lowerer.shapes.get(row.shapeId);
+ if (
+ !shape?.tuple ||
+ shape.fields.length !== 2 ||
+ !shape.fields.every(
+ (field) => (field.name === "0" || field.name === "1") && field.type.kind === "string",
+ )
+ )
+ return null;
+ const key = `sp.fromTuples:${row.shapeId}`;
+ let helper = lowerer.valueHelpers.get(key);
+ if (!helper) {
+ helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`;
+ lowerer.valueHelpers.set(key, helper);
+ const pairsT = init.type;
+ const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc);
+ const pair = (): IrExpr => varRef("pair.0", row, loc);
+ const field = (name: "0" | "1"): IrExpr => ({
+ kind: "recordGet",
+ obj: pair(),
+ shapeId: row.shapeId,
+ field: name,
+ type: STRING,
+ loc,
+ });
+ const body: IrStmt[] = [
+ {
+ kind: "varDecl",
+ localId: "sp.0",
+ init: { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc },
+ loc,
+ },
+ countedFor(
+ loc,
+ {
+ kind: "arrIntrinsic",
+ method: "length",
+ receiver: varRef("pairs.0", pairsT, loc),
+ args: [],
+ type: F64,
+ loc,
+ },
+ (index) => [
+ {
+ kind: "varDecl",
+ localId: "pair.0",
+ init: { kind: "arrayGet", arr: varRef("pairs.0", pairsT, loc), index, type: row, loc },
+ loc,
+ },
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "sp.append",
+ args: [sp(), field("0"), field("1")],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ],
+ ),
+ { kind: "return", value: sp(), loc },
+ ];
+ const fn: IrFunction = {
+ name: helper,
+ params: [{ localId: "pairs.0", name: "pairs", type: pairsT }],
+ returnType: SEARCH_PARAMS_T,
+ locals: [
+ { id: "pairs.0", name: "pairs", type: pairsT, mutable: false },
+ { id: "sp.0", name: "sp", type: SEARCH_PARAMS_T, mutable: false },
+ { id: "i.0", name: "i", type: F64, mutable: true },
+ { id: "pair.0", name: "pair", type: row, mutable: false },
+ ],
+ body,
+ loc,
+ };
+ lowerer.liftedFns.push(fn);
+ }
+ return { kind: "call", callee: helper, args: [init], type: SEARCH_PARAMS_T, loc };
+}
+
function lowerOptionalStringSearchParams(
lowerer: Lowerer,
init: IrExpr,
@@ -534,12 +628,14 @@ export function lowerSearchParamsMethodCall(
if (name === "forEach" && args.length === 1) {
return lowerSpForEachCall(lowerer, call, access);
}
- if (name === "keys" || name === "values" || name === "entries") {
- lowerer.unsupported(
- "SC1090",
- call,
- `URLSearchParams iterator objects outside a for-of head (write \`for (const x of sp.${name}())\` directly)`,
- );
+ if ((name === "keys" || name === "values" || name === "entries") && args.length === 0) {
+ // Outside a for-of head the iterator is a live object: the checked-
+ // dynamic view's iterator walks the pair list by index
+ // (scr_native_sp_step), so entries appended after it was created are
+ // visited, as in Node.
+ const boxed = lowerer.coerceInto(access.expression, lowerer.lowerExpr(access.expression), DYN);
+ const iterator = lowerDynDispatchMethodCall(lowerer, call, access, boxed, false);
+ if (iterator) return iterator;
}
lowerer.noLowering(
`URLSearchParams.${name}`,
@@ -751,3 +847,21 @@ export function lowerLegacyUrlCall(
}
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
}
+
+/** A URLSearchParams value, or a keys()/values()/entries() call on one: an
+ * iterable whose checked-dynamic view walks the live pair list. */
+export function isSearchParamsIterable(lowerer: Lowerer, node: ts.Expression): boolean {
+ while (ts.isParenthesizedExpression(node)) node = node.expression;
+ if (lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "searchParams") return true;
+ if (
+ !ts.isCallExpression(node) ||
+ node.arguments.length !== 0 ||
+ !ts.isPropertyAccessExpression(node.expression)
+ )
+ return false;
+ const name = node.expression.name.text;
+ return (
+ (name === "keys" || name === "values" || name === "entries") &&
+ lowerer.mapTypeOf(lowerer.typeOf(node.expression.expression))?.kind === "searchParams"
+ );
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
index 88c542ec0..015cb2123 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
@@ -1004,6 +1004,10 @@ export function lowerArrayFromCall(
if (src.type.kind === "dyn" && args.length === 1) {
return lowerCheckedArrayFrom(lowerer, src, loc);
}
+ // Array.from(sp): the checked-dynamic view's live pair iteration.
+ if (src.type.kind === "searchParams" && args.length === 1) {
+ return lowerCheckedArrayFrom(lowerer, lowerer.coerceInto(args[0]!, src, DYN), loc);
+ }
// A generator (or an iterator adapted into one) drains in order.
if (src.type.kind === "generator" && args.length === 1) {
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
index d9bf8bcd4..8c8349c0b 100644
--- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
+++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
@@ -41,6 +41,7 @@ import {
} from "../collection-ingestion.js";
import { strCharsCall } from "./array-construction.js";
import { arrayElementRead } from "./array-iteration.js";
+import { isSearchParamsIterable } from "../builtins/url.js";
/** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to
* mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop
@@ -1173,6 +1174,11 @@ export function lowerMapSeedNew(
mapT,
);
}
+ // new Map(sp) / new Map(sp.entries()): the checked-dynamic view's live
+ // pairs seed the map in order.
+ if (isSearchParamsIterable(lowerer, argNode)) {
+ return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT);
+ }
const input = collectionInput(lowerer, argNode);
if (input) {
if (!collectionDestinationMatches(lowerer, input, mapT)) return null;
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 17a4833cb..b0203af0d 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -79,7 +79,7 @@ import { isAssertClassNew } from "./assert-instances.js";
import { lowerArrayConstructor } from "./containers/array-construction.js";
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
-import { bufEncoding } from "./containers/bytes.js";
+import { knownBufEncoding } from "./containers/bytes.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { lowerSearchParamsNew } from "./builtins/url.js";
import { lowerTextCodecNew } from "./builtins/text-codecs.js";
@@ -8972,24 +8972,41 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
);
}
// `new StringDecoder(encoding?)` (node:string_decoder): the decoder
- // is a two-field record — the CANONICAL encoding name (aliases fold
- // at compile time, exactly what `.encoding` answers in Node) and
- // the packed-f64 pending state starting at 0 (nothing buffered).
- // The encoding must be a literal (Node's alias set); omitted means
- // utf8, Node's default.
+ // is a two-field record — the CANONICAL encoding name (exactly what
+ // `.encoding` answers in Node) and the packed-f64 pending state
+ // starting at 0 (nothing buffered). A literal Buffer encoding folds
+ // its alias at compile time; any other encoding argument is
+ // normalized at run time (Node's normalizeEncoding, which throws
+ // ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8,
+ // Node's default.
if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) {
const args = expr.arguments ?? [];
if (args.length > 1) {
lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol);
}
- const encName =
- args.length === 1 ? bufEncoding(lowerer, "new StringDecoder", args[0]!) : "utf8";
+ const encNode = args[0];
+ const encT = encNode ? lowerer.typeOf(encNode) : undefined;
+ const encName = !encNode
+ ? "utf8"
+ : encT!.isStringLiteralType()
+ ? knownBufEncoding(encT!.value)
+ : undefined;
+ const encoding: IrExpr =
+ encName !== undefined
+ ? { kind: "strLit", value: encName, type: STRING, loc }
+ : {
+ kind: "libCall",
+ fn: "strdec.encoding",
+ args: [lowerer.lowerExprExpecting(encNode!, DYN)],
+ type: STRING,
+ loc,
+ };
const decT = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (decT?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr));
return {
kind: "recordLit",
fields: [
- { name: "%enc", value: { kind: "strLit", value: encName, type: STRING, loc } },
+ { name: "%enc", value: encoding },
{ name: "%pending", value: { kind: "numLit", value: 0, type: F64, loc } },
],
type: decT,
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 08e12518b..77676f8f2 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -225,7 +225,7 @@ import {
} from "./lower-builtin-values.js";
import { lowerArrayFromValue } from "./containers/array-construction.js";
import { lowerPerfHooksTypeof } from "./builtins/performance.js";
-import { lowerUrlAssignment } from "./builtins/url.js";
+import { isSearchParamsIterable, lowerUrlAssignment } from "./builtins/url.js";
import { jsBindingHasOpenWrites } from "./lower-stmts.js";
import { checkedClassInstanceOf } from "./class-construction.js";
import { lowerModuleNamespaceElement } from "./module-namespace-elements.js";
@@ -5889,6 +5889,11 @@ export function lowerArrayLiteral(
const drained = generatorDrain(lowerer, src, type.elem, locOf(el));
if (drained) src = drained;
}
+ // `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its
+ // checked-dynamic view iterates the live pair list, and the drained
+ // [name, value] pairs convert to the literal's tuple elements.
+ const searchParamsSource = isSearchParamsIterable(lowerer, el.expression);
+ if (src.type.kind === "searchParams") src = lowerer.coerceInto(el.expression, src, DYN);
// A native checked iterable can supply scalar elements even when
// the checker inferred a typed array from its producer. Drain once
// and validate the elements before the ordinary spread copy.
@@ -5897,7 +5902,8 @@ export function lowerArrayLiteral(
(type.elem.kind === "dyn" ||
type.elem.kind === "f64" ||
type.elem.kind === "string" ||
- type.elem.kind === "bool")
+ type.elem.kind === "bool" ||
+ searchParamsSource)
) {
src = lowerer.coerceInto(el.expression, checkedIteratorPack(lowerer, src, locOf(el)), type);
}
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 367c147f8..e61bb6c11 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -5037,6 +5037,23 @@ function jsObjectBindingExpands(
) {
const name = property(node.left);
if (name === null || (name !== undefined && !names.has(name))) expands = true;
+ // `state = { _tag: "Done", right }` stores an object with properties
+ // the initializer's shape lacks; later reads of them must see them.
+ const target = unwrap(node.left);
+ const value = unwrap(node.right);
+ const targetSymbol = ts.isIdentifier(target) ? lowerer.resolveValueSymbol(target) : undefined;
+ if (
+ targetSymbol &&
+ aliases.has(targetSymbol) &&
+ ts.isObjectLiteralExpression(value) &&
+ value.properties.some(
+ (field) =>
+ !(ts.isPropertyAssignment(field) || ts.isShorthandPropertyAssignment(field)) ||
+ !(ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) ||
+ !names.has(field.name.text),
+ )
+ )
+ expands = true;
}
if (ts.isDeleteExpression(node) && property(node.expression) !== undefined) expands = true;
if (
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index 4c74e8b6a..df65c176a 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -1485,6 +1485,9 @@ export const BUILTIN_MODULE_FNS: Record<
result: F64,
valueParams: [{ mode: "optional", type: F64, defaultValue: 0 }],
},
+ // A one-argument call is setPriority(0, priority)
+ // (lowerOsSetPriorityCall completes it).
+ setPriority: { fn: "os.setPriority", params: [F64, F64], result: VOID },
// Special-cased (lowerOsCpusCall): the call site's CpuInfo[] assembles
// from one uv_cpu_info snapshot.
cpus: { fn: "os.cpusTake", params: [], result: VOID },
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 2381413f8..41387147c 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -34,6 +34,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"cp.spawnDynamic",
"child.onDyn",
"text.decoderEncoding",
+ "strdec.encoding",
"dyn.construct",
"dyn.reflectApply",
"dyn.reflectGet",
@@ -556,6 +557,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"dyn.arrayProtoCall",
"process.chdir",
"os.getPriority",
+ "os.setPriority",
// Pattern length TypeError, brace-sequence RangeError, and the
// SyntaxError of a segment regex invalid under the u flag.
"path.matchesGlob",
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 5e219ecf6..8b8e5b482 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -550,6 +550,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** os.getPriority(pid): validateInt32 (RangeError ERR_OUT_OF_RANGE),
* then the priority; a failure throws Node's SystemError. */
"os.getPriority": { argTypes: [F64], result: F64 },
+ /** os.setPriority(pid, priority): validateInt32 for both (priority in
+ * -20..19, RangeError ERR_OUT_OF_RANGE), then the priority; a failure
+ * throws Node's SystemError. */
+ "os.setPriority": { argTypes: [F64, F64], result: VOID },
/** os.cpus(): cpusTake snapshots every CPU and answers the count;
* cpusModel(i) and cpusField(i, k) (k: 0 speed, 1 user, 2 nice, 3 sys,
* 4 idle, 5 irq) read the snapshot. Never throw. */
@@ -3785,6 +3789,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"strdec.write": { argTypes: [STRING, F64, BYTES_U8], result: STRING },
"strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 },
"strdec.end": { argTypes: [STRING, F64], result: STRING },
+ /** A runtime encoding argument's canonical name (Node's normalizeEncoding:
+ * nullish and "" are utf8); an unknown name throws ERR_UNKNOWN_ENCODING. */
+ "strdec.encoding": { argTypes: [DYN], result: STRING },
// node:readline: the interface handle is f64; the callbacks' func types
// are program-dependent (the emitter picks the adapter by parameter
// count), so those slots are null like child.onExit's.
diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts
index 32284c820..0b1f9fb49 100644
--- a/packages/compiler/src/ir/function-references.test.ts
+++ b/packages/compiler/src/ir/function-references.test.ts
@@ -193,6 +193,7 @@ const MODULE_FIELDS: Record>, Role> = {
"lib.exports[].fnName": "reference",
"lib.exports[].inboundBytesTrap": "other",
"lib.exports[].inboundIntTrap": "other",
+ "lib.exports[].inboundCstringTrap": "other",
"lib.trapOverlays[].code": "other",
"lib.trapOverlays[].teaching": "other",
"lib.trapOverlays[].remediation": "other",
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index bfaa13c0e..eaee503fa 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -1176,8 +1176,19 @@ export interface IrLibExport {
/** i64/u64 (ask 4): int64_t/uint64_t at the C edge — inbound values
* range-check in the wrapper (`inboundIntTrap`), internal call sites
* and returns are compile-time proven before this lands on the IR. */
- params: ("f64" | "bool" | "string" | "bytes" | "u8" | "u32" | "i32" | "i64" | "u64")[];
- returns: "f64" | "bool" | "string" | "bytes" | "void" | "i64" | "u64";
+ params: (
+ | "f64"
+ | "bool"
+ | "string"
+ | "cstring"
+ | "bytes"
+ | "u8"
+ | "u32"
+ | "i32"
+ | "i64"
+ | "u64"
+ )[];
+ returns: "f64" | "bool" | "string" | "cstring" | "bytes" | "void" | "i64" | "u64";
/** The exact sink-message bytes this wrapper passes to the inbound-bytes
* marshalling helper's trap — present exactly when a parameter is
* bytes-classed. Already the assembled structured trap-teaching form
@@ -1192,6 +1203,10 @@ export interface IrLibExport {
* value cannot ride f64 exactly (|v| past 2^53−1). Same assembled
* structured form and the same SC4012 code — one host-contract story. */
inboundIntTrap?: string;
+ /** The sibling message for a NULL cstring parameter: present exactly
+ * when a parameter is cstring-classed, passed to scr_library_cstr_in.
+ * Same assembled structured form and the same SC4012 code. */
+ inboundCstringTrap?: string;
}
/** One runtime-trap overlay row: the profile's teaching (replaces the
@@ -1218,7 +1233,7 @@ export interface IrLibCallback {
name: string;
/** The runtime channel slot (profile declaration order). */
slot: number;
- params: ("f64" | "bool" | "string" | "bytes" | "u8" | "u32" | "i32")[];
+ params: ("f64" | "bool" | "string" | "cstring" | "bytes" | "u8" | "u32" | "i32")[];
returns: "f64" | "bool" | "u8" | "u32" | "i32" | "i64" | "u64" | "pointer" | "void";
/** The unregistered-call trap text (a DETECTED trap: plain bytes the
* library funnel classifies SC4025 and assembles with the current
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index d526e7cb8..8278fe356 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -357,7 +357,7 @@ export function validateModule(mod: IrModule): IrValidationError[] {
const fits = (cls: string, t: IrType): boolean =>
cls === "bool"
? t.kind === "bool"
- : cls === "string"
+ : cls === "string" || cls === "cstring"
? t.kind === "string"
: cls === "bytes"
? t.kind === "bytes" && t.elem === "u8"
diff --git a/packages/compiler/src/library/library-profile.ts b/packages/compiler/src/library/library-profile.ts
index f0126c25f..019dbb7e1 100644
--- a/packages/compiler/src/library/library-profile.ts
+++ b/packages/compiler/src/library/library-profile.ts
@@ -10,7 +10,7 @@
*
* Ratified shape (the ratified library-mode design §5.1 + the ABI ratification session):
* {
- * "profile_format": 1,
+ * "profile_format": 1 | 2, // 2 adds the cstring class
* "name": "",
* "entry": "src/lib.ts", // ONE module, profile-relative
* "emission": "llvm", // pins the emission; no fallback
@@ -268,6 +268,7 @@ export const LIB_PARAM_CLASSES = [
"f64",
"bool",
"string",
+ "cstring",
"bytes",
"u8",
"u32",
@@ -282,7 +283,16 @@ export const LIB_PARAM_CLASSES = [
* u8/u32/i32 plumbing classes stay param-only: they were ratified as
* inbound plumbing, and outbound integers are the proven classes'
* business. */
-export const LIB_RETURN_CLASSES = ["f64", "bool", "string", "bytes", "void", "i64", "u64"] as const;
+export const LIB_RETURN_CLASSES = [
+ "f64",
+ "bool",
+ "string",
+ "cstring",
+ "bytes",
+ "void",
+ "i64",
+ "u64",
+] as const;
/** Ask 4's declared integer classes (the prove-or-refuse pair). */
export type LibIntClass = "i64" | "u64";
@@ -297,7 +307,16 @@ export type LibReturnClass = (typeof LIB_RETURN_CLASSES)[number];
* declared integer classes stay export-map surface: an outbound i64/u64
* demands the prove-or-refuse machinery, which is scoped to export
* returns today. */
-const LIB_CALLBACK_PARAM_CLASSES = ["f64", "bool", "string", "bytes", "u8", "u32", "i32"] as const;
+const LIB_CALLBACK_PARAM_CLASSES = [
+ "f64",
+ "bool",
+ "string",
+ "cstring",
+ "bytes",
+ "u8",
+ "u32",
+ "i32",
+] as const;
/** Marshalling classes legal in a CALLBACK RETURN position (inbound: host
* → compiled code). Scalars only — every conversion to f64 is exact by
* construction; a buffer return would need an ownership contract the mode
@@ -377,7 +396,7 @@ export interface LibrarySidecarConfig {
}
export interface LibraryProfile {
- profileFormat: 1;
+ profileFormat: 1 | 2;
name: string;
/** The profile file that produced this configuration. Semantic
* designation errors anchor here, where the offending name is written. */
@@ -516,8 +535,16 @@ export function loadLibraryProfile(
}
const p = raw as Record;
const format = req(p["profile_format"], "profile_format", "number");
- if (format !== 1)
- throw new ProfileError(`unsupported profile_format ${format} (this scriptc reads format 1)`);
+ if (format !== 1 && format !== 2)
+ throw new ProfileError(
+ `unsupported profile_format ${format} (this scriptc reads formats 1 and 2)`,
+ );
+ // The cstring class (a NUL-terminated `const char *`) is format 2.
+ const cstringClass = (cls: string, path: string): void => {
+ if (cls === "cstring" && format < 2) {
+ throw new ProfileError(`'${path}' class 'cstring' requires profile_format 2`);
+ }
+ };
// Root keys are strict (checked AFTER the format gate, so a future
// format's profile refuses as the wrong format, not as a typo). The
// ask-5 key names get the pointed message: a `fences` array pasted at
@@ -656,6 +683,7 @@ export function loadLibraryProfile(
`'${path}.params[${j}]' must be one of ${LIB_PARAM_CLASSES.join("/")}, got ${JSON.stringify(c)}`,
);
}
+ cstringClass(c, `${path}.params[${j}]`);
return c as LibParamClass;
});
const returns = req(ee["returns"], `${path}.returns`, "string");
@@ -666,6 +694,7 @@ export function loadLibraryProfile(
: `'${path}.returns' must be one of ${LIB_RETURN_CLASSES.join("/")}, got '${returns}'`;
throw new ProfileError(detail);
}
+ cstringClass(returns, `${path}.returns`);
entries.push({ export: exportName, symbol, params, returns: returns as LibReturnClass });
});
@@ -709,12 +738,13 @@ export function loadLibraryProfile(
`'${path}.params[${j}]' must be one of ${LIB_CALLBACK_PARAM_CLASSES.join("/")}, got ${JSON.stringify(cls)}`,
);
}
+ cstringClass(cls, `${path}.params[${j}]`);
return cls as LibCallbackParamClass;
});
const cbReturns = req(cc["returns"], `${path}.returns`, "string");
if (!(LIB_CALLBACK_RETURN_CLASSES as readonly string[]).includes(cbReturns)) {
const detail =
- cbReturns === "string" || cbReturns === "bytes"
+ cbReturns === "string" || cbReturns === "cstring" || cbReturns === "bytes"
? `'${path}.returns': a callback return stays scalar — a buffer flowing host → library needs an ownership contract the mode does not define; pass buffers as callback parameters (library → host, borrowed for the call) or entry parameters instead`
: `'${path}.returns' must be one of ${LIB_CALLBACK_RETURN_CLASSES.join("/")}, got '${cbReturns}'`;
throw new ProfileError(detail);
@@ -1010,7 +1040,7 @@ export function loadLibraryProfile(
return {
ok: true,
profile: {
- profileFormat: 1,
+ profileFormat: format as 1 | 2,
name,
profilePath,
entry,
diff --git a/packages/compiler/src/library/prepare.ts b/packages/compiler/src/library/prepare.ts
index 9c6fe03fc..94f20198a 100644
--- a/packages/compiler/src/library/prepare.ts
+++ b/packages/compiler/src/library/prepare.ts
@@ -65,6 +65,7 @@ function libClassFits(cls: string, t: IrType): boolean {
case "bool":
return t.kind === "bool";
case "string":
+ case "cstring":
return t.kind === "string";
case "bytes":
return t.kind === "bytes" && t.elem === "u8";
@@ -178,6 +179,18 @@ function resolveLibrarySection(
profileRemediation(profile, LIB_INBOUND_BYTES_TRAP_CODE),
);
}
+ if (e.params.includes("cstring")) {
+ // The host-contract trap for a NULL cstring parameter: a C string
+ // argument is a pointer to NUL-terminated text, never NULL. Same
+ // code and assembly-once discipline as the bytes and integer traps.
+ resolvedExport.inboundCstringTrap = assembleTrapTeaching(
+ profileTeaching(profile, LIB_INBOUND_BYTES_TRAP_CODE) ??
+ "scriptc: library inbound cstring parameter is NULL\n",
+ LIB_INBOUND_BYTES_TRAP_CODE,
+ e.symbol,
+ profileRemediation(profile, LIB_INBOUND_BYTES_TRAP_CODE),
+ );
+ }
if (e.params.includes("i64") || e.params.includes("u64")) {
// The sibling host-contract trap for inbound declared-integer
// parameters (ask 4): a value past ±(2^53−1) cannot ride f64
@@ -524,10 +537,12 @@ export function prepareLibrary(
// SC4024 diagnostics, unused channels legal, undeclared references
// refused with the callback teaching. The library lane never loads a
// native-FFI manifest, so the channel set owns the surface outright.
+ // A cstring channel parameter is a string on the TypeScript side; the
+ // channel's own classes (lib.callbacks) drive the marshalling.
const cbImports: IrFfiImport[] = profile.callbacks.map((cb) => ({
name: cb.name,
symbol: cb.name,
- params: [...cb.params],
+ params: cb.params.map((cls) => (cls === "cstring" ? "string" : cls)),
returns: cb.returns,
}));
const integerSlotRoots: string[] = [];
diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json
index 63efde378..d7e360d82 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -1512,6 +1512,12 @@
"name": "os.release",
"status": "static"
},
+ {
+ "id": "node-builtin.os.setPriority",
+ "kind": "node-builtin",
+ "name": "os.setPriority",
+ "status": "static"
+ },
{
"id": "node-builtin.os.tmpdir",
"kind": "node-builtin",
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5833-url-search-params-iterators.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5833-url-search-params-iterators.ts.json
new file mode 100644
index 000000000..b1626d547
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5833-url-search-params-iterators.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5833-url-search-params-iterators.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5834-url-search-params-iterators-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5834-url-search-params-iterators-dynamic.mjs.json
new file mode 100644
index 000000000..3a60dda60
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5834-url-search-params-iterators-dynamic.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5836-url-search-params-tuple-pairs.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5836-url-search-params-tuple-pairs.ts.json
new file mode 100644
index 000000000..20a6b62bf
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5836-url-search-params-tuple-pairs.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5836-url-search-params-tuple-pairs.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5838-qs-stringify-bigint.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5838-qs-stringify-bigint.ts.json
new file mode 100644
index 000000000..0c1b09788
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5838-qs-stringify-bigint.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5838-qs-stringify-bigint.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5882-string-decoder-runtime-encoding.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5882-string-decoder-runtime-encoding.ts.json
new file mode 100644
index 000000000..29b8b2759
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5882-string-decoder-runtime-encoding.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5882-string-decoder-runtime-encoding.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5883-os-set-priority.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5883-os-set-priority.ts.json
new file mode 100644
index 000000000..5be1d2b3c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5883-os-set-priority.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5883-os-set-priority.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5939-js-let-object-reassigned-wider.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5939-js-let-object-reassigned-wider.js.json
new file mode 100644
index 000000000..b783afe3f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5939-js-let-object-reassigned-wider.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5939-js-let-object-reassigned-wider.js"
+ ],
+ "diags": []
+}
diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c
index df5b8a3c0..e9514531b 100644
--- a/packages/runtime/src/scr_bytes.c
+++ b/packages/runtime/src/scr_bytes.c
@@ -2462,6 +2462,22 @@ static ScrStr *scr_bytes_normalize_encoding(const ScrStr *raw) {
return NULL;
}
+/* new StringDecoder(encoding) with an encoding known only at run time:
+ * Node's normalizeEncoding. undefined, null and "" are utf8; a Buffer
+ * encoding name in any ASCII case answers its canonical name (+1); any
+ * other string throws ERR_UNKNOWN_ENCODING (NULL). Encodings that are not
+ * strings have no lowering. */
+ScrStr *scr_strdec_encoding(const ScrDyn *enc) {
+ if (enc->kind == SCR_DYN_UNDEF || enc->kind == SCR_DYN_NULL) return scr_str_new("utf8", 4);
+ if (enc->kind != SCR_DYN_STR) {
+ static const char message[] = "new StringDecoder with an encoding that is not a string has no lowering";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
+ return NULL;
+ }
+ if (enc->v.str->len == 0) return scr_str_new("utf8", 4);
+ return scr_bytes_normalize_encoding(enc->v.str);
+}
+
ScrStr *scr_bytes_to_str_checked(const ScrBytes *b, const ScrStr *enc) {
/* Node returns before resolving the encoding when there are no bytes
* to decode, so even an unknown runtime name answers the empty string. */
diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h
index b56f9477d..766c24514 100644
--- a/packages/runtime/src/scr_bytes.h
+++ b/packages/runtime/src/scr_bytes.h
@@ -119,6 +119,7 @@ double scr_bytes_to_u8_clamp(double value);
ScrStr *scr_strdec_write(const ScrStr *enc, double pending, const ScrBytes *chunk);
double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk);
ScrStr *scr_strdec_end(const ScrStr *enc, double pending);
+ScrStr *scr_strdec_encoding(const ScrDyn *enc); /* +1, or NULL with ERR_UNKNOWN_ENCODING pending */
/* `new Uint8Array(n)` / Buffer.alloc(n): zero-filled. n goes through
* ToIndex like JS (NaN → 0, truncate toward zero; negative or > 2^53-1
diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h
index a640169fa..c791d5a12 100644
--- a/packages/runtime/src/scr_core.h
+++ b/packages/runtime/src/scr_core.h
@@ -294,6 +294,15 @@ double scr_library_i64_in(int64_t v, const char *trap_msg);
double scr_library_u64_in(uint64_t v, const char *trap_msg);
void scr_library_str_out(ScrStr *s, const uint8_t **out, size_t *out_len);
void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len);
+/* The cstring class (a NUL-terminated `const char *`). Inbound: a copy
+ * decoded as UTF-8 with U+FFFD replacement; NULL delivers trap_msg (the
+ * assembled SC4012 message). Outbound results move into the arena and
+ * return their data; callback arguments return the string's own data,
+ * borrowed for the call. A string containing U+0000 cannot be a C string:
+ * both outbound forms trap instead of truncating. */
+ScrStr *scr_library_cstr_in(const char *p, const char *trap_msg); /* +1 */
+const char *scr_library_cstr_out(ScrStr *s);
+const char *scr_library_cstr_arg(const ScrStr *s);
#define scr_atexit(fn) scr_library_register_reset(fn)
#elif defined(SCR_WORKERS)
diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c
index 958112b66..49aba3acb 100644
--- a/packages/runtime/src/scr_json.c
+++ b/packages/runtime/src/scr_json.c
@@ -1449,6 +1449,8 @@ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) {
return scr_dyn_not_iterable(src, msg);
}
+static ScrDyn *scr_native_iterator_drain(const ScrDyn *src);
+
/* The for-of-over-dyn pack accessors: the emitted index loop drives them
* over a scr_dyn_iter_pack result (ARR by construction — the defensive
* arms cover nothing reachable from that lowering). Never throw. */
@@ -1463,6 +1465,13 @@ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src) {
scr_dyn_release(view);
if (scr_exc_pending()) return NULL;
}
+ /* A native iterator (sp.entries(), a Map's entries()) or a handle that
+ * steps through live entries (URLSearchParams): producing its entries
+ * runs no program code, so draining them first is what the consumer
+ * observes. */
+ if (src->kind == SCR_DYN_HANDLE &&
+ (src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step))
+ return scr_native_iterator_drain(src);
if (src->kind == SCR_DYN_HANDLE || src->kind == SCR_DYN_TYPED_REF) {
static const char message[] = "new Map(entries) over native non-array iterables has no lowering";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
@@ -4576,6 +4585,33 @@ ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection) {
return scr_native_collection_iterator(source, selection);
}
+/* The values a native iterator (or a live-stepping handle's entries
+ * iterator) produces from its current position, into a fresh array; NULL
+ * with the exception pending. */
+static ScrDyn *scr_native_iterator_drain(const ScrDyn *src) {
+ ScrDyn *iterator = src->v.handle.tag == SCR_DYNH_ITERATOR
+ ? scr_dyn_retain((ScrDyn *)src)
+ : scr_dyn_native_handle_iterator((ScrDyn *)src, 2);
+ ScrDyn *out = scr_dyn_new_arr();
+ for (;;) {
+ ScrDyn *step = scr_native_iterator_next(iterator->v.handle.ptr);
+ if (!step) {
+ scr_dyn_release(out);
+ out = NULL;
+ break;
+ }
+ bool done = scr_dyn_truthy(scr_dyn_obj_get(step, "done", 4));
+ if (!done) {
+ const ScrDyn *value = scr_dyn_obj_get(step, "value", 5);
+ scr_dyn_arr_push(out, scr_dyn_retain((ScrDyn *)(value ? value : scr_dyn_undefined())));
+ }
+ scr_dyn_release(step);
+ if (done) break;
+ }
+ scr_dyn_release(iterator);
+ return out;
+}
+
static ScrDyn *scr_builtin_iterator_checked(int kind) {
ScrDyn *self = scr_dyn_this_get();
ScrDyn *result = NULL;
diff --git a/packages/runtime/src/scr_library.c b/packages/runtime/src/scr_library.c
index 60e11ae0c..b2a5d4c75 100644
--- a/packages/runtime/src/scr_library.c
+++ b/packages/runtime/src/scr_library.c
@@ -371,6 +371,30 @@ void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len) {
*out_len = b->len;
}
+ScrStr *scr_library_cstr_in(const char *p, const char *trap_msg) {
+ if (p == NULL) scr_trap(trap_msg);
+ return scr_str_from_utf8_lossy((const uint8_t *)p, strlen(p));
+}
+
+/* A C string ends at its first NUL: text holding U+0000 cannot cross. */
+static void scr_library_cstr_check(const ScrStr *s) {
+ if (memchr(s->data, 0, s->len) != NULL) {
+ scr_trap("scriptc: TypeError: a cstring value contains U+0000, which a C string cannot "
+ "hold\n");
+ }
+}
+
+const char *scr_library_cstr_out(ScrStr *s) {
+ scr_library_arena_keep(s, true);
+ scr_library_cstr_check(s);
+ return s->data; /* NUL-terminated after len (ScrStr layout) */
+}
+
+const char *scr_library_cstr_arg(const ScrStr *s) {
+ scr_library_cstr_check(s);
+ return s->data;
+}
+
/* ── the reset registry + session reset ─────────────────────────────────
* Where an executable's always-linked units atexit() their lazy teardowns,
* library builds register here (scr_atexit in scr_runtime.h): registered
diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c
index 0bc50fb3f..76f69588e 100644
--- a/packages/runtime/src/scr_os.c
+++ b/packages/runtime/src/scr_os.c
@@ -296,23 +296,27 @@ static void scr_os_system_error(const char *syscall, int uv_code) {
if (cell->kind == SCR_EXC_OBJ) scr_error_set_code((ScrError *)cell->payload, "ERR_SYSTEM_ERROR");
}
-static bool scr_os_validate_int32(const char *name, double value) {
+static bool scr_os_validate_int32_in(const char *name, double value, int min, int max) {
if (isfinite(value) && trunc(value) == value) {
- if (value >= -2147483648.0 && value <= 2147483647.0) return true;
+ if (value >= (double)min && value <= (double)max) return true;
}
char received[48];
scr_num_received(value, received);
char text[192];
int n = isfinite(value) && trunc(value) == value
? snprintf(text, sizeof text,
- "The value of \"%s\" is out of range. It must be >= -2147483648 && <= 2147483647. Received %s",
- name, received)
+ "The value of \"%s\" is out of range. It must be >= %d && <= %d. Received %s",
+ name, min, max, received)
: snprintf(text, sizeof text, "The value of \"%s\" is out of range. It must be an integer. Received %s",
name, received);
scr_throw_error_msg_code(SCR_ERR_RANGE, text, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE");
return false;
}
+static bool scr_os_validate_int32(const char *name, double value) {
+ return scr_os_validate_int32_in(name, value, INT32_MIN, INT32_MAX);
+}
+
double scr_os_get_priority(double pid) {
if (!scr_os_validate_int32("pid", pid)) return 0;
#ifdef _WIN32
@@ -351,6 +355,48 @@ double scr_os_get_priority(double pid) {
#endif
}
+/* ── setPriority ──────────────────────────────────────────────────────
+ * validateInt32(pid, 'pid') and validateInt32(priority, 'priority', -20,
+ * 19) (a one-argument call passes pid 0), then uv_os_setpriority; a
+ * failure is Node's ERR_SYSTEM_ERROR. */
+
+void scr_os_set_priority(double pid, double priority) {
+ if (!scr_os_validate_int32("pid", pid)) return;
+ if (!scr_os_validate_int32_in("priority", priority, SCR_OS_PRIORITY_HIGHEST, SCR_OS_PRIORITY_LOW)) return;
+ int value = (int)priority;
+#ifdef _WIN32
+ /* libuv's mapping of nice values to Windows priority classes. */
+ DWORD klass = value < SCR_OS_PRIORITY_HIGH ? REALTIME_PRIORITY_CLASS
+ : value < SCR_OS_PRIORITY_ABOVE_NORMAL ? HIGH_PRIORITY_CLASS
+ : value < SCR_OS_PRIORITY_NORMAL ? ABOVE_NORMAL_PRIORITY_CLASS
+ : value < SCR_OS_PRIORITY_BELOW_NORMAL ? NORMAL_PRIORITY_CLASS
+ : value < SCR_OS_PRIORITY_LOW ? BELOW_NORMAL_PRIORITY_CLASS
+ : IDLE_PRIORITY_CLASS;
+ HANDLE handle = pid == 0 ? GetCurrentProcess()
+ : OpenProcess(PROCESS_SET_INFORMATION, FALSE, (DWORD)(int)pid);
+ if (handle == NULL) {
+ DWORD err = GetLastError();
+ scr_os_system_error("uv_os_setpriority", err == ERROR_ACCESS_DENIED ? SCR_NODE_UV__EPERM
+ : err == ERROR_INVALID_PARAMETER ? SCR_NODE_UV__EINVAL
+ : SCR_NODE_UV__UNKNOWN);
+ return;
+ }
+ BOOL ok = SetPriorityClass(handle, klass);
+ DWORD err = ok ? 0 : GetLastError();
+ if (pid != 0) CloseHandle(handle);
+ if (!ok) {
+ scr_os_system_error("uv_os_setpriority", err == ERROR_ACCESS_DENIED ? SCR_NODE_UV__EPERM
+ : SCR_NODE_UV__UNKNOWN);
+ }
+#elif defined(__wasi__)
+ (void)value;
+ scr_os_system_error("uv_os_setpriority", SCR_NODE_UV__ENOSYS);
+#else
+ if (setpriority(PRIO_PROCESS, (id_t)(int)pid, value) != 0)
+ scr_os_system_error("uv_os_setpriority", SCR_NODE_UV__ERR(errno));
+#endif
+}
+
/* ── cpus ─────────────────────────────────────────────────────────────
* uv_cpu_info, snapshotted once per os.cpus() call: scr_os_cpus_take
* reads every CPU and answers the count; the accessors read that
diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h
index b40a8027a..3c5009fc5 100644
--- a/packages/runtime/src/scr_os.h
+++ b/packages/runtime/src/scr_os.h
@@ -27,6 +27,10 @@ ScrArr *scr_os_loadavg(void);
/* os.getPriority(pid): validateInt32, then the scheduling priority;
* failures throw Node's RangeError or SystemError (ERR_SYSTEM_ERROR). */
double scr_os_get_priority(double pid);
+/* os.setPriority(pid, priority): validateInt32 for both (priority within
+ * -20..19), then the scheduling priority; failures throw Node's RangeError
+ * or SystemError (ERR_SYSTEM_ERROR). */
+void scr_os_set_priority(double pid, double priority);
/* os.cpus(): take answers the CPU count of a fresh snapshot; model and
* field (0 speed, 1 user, 2 nice, 3 sys, 4 idle, 5 irq) read it. */
double scr_os_cpus_take(void);
diff --git a/packages/runtime/src/scr_qs.c b/packages/runtime/src/scr_qs.c
index a61f95d94..1b110ba0d 100644
--- a/packages/runtime/src/scr_qs.c
+++ b/packages/runtime/src/scr_qs.c
@@ -478,8 +478,8 @@ ScrDyn *scr_qs_parse_dyn(const ScrStr *qs) {
/* ── stringify ───────────────────────────────────────────────────────── */
/* Node's encodeStringified over one dyn value: strings escape, finite
- * numbers render then escape, booleans are bare, everything else is the
- * empty value. Appends to b. */
+ * numbers render then escape, bigints and booleans are bare, everything
+ * else is the empty value. Appends to b. */
static void qs_stringify_value(QsBuf *b, const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_STR: {
@@ -497,12 +497,18 @@ static void qs_stringify_value(QsBuf *b, const ScrDyn *v) {
scr_str_release(num);
return;
}
+ case SCR_DYN_BIGINT: {
+ ScrStr *text = scr_bigint_to_string(v->v.bigint, 10);
+ qb_append_str(b, text);
+ scr_str_release(text);
+ return;
+ }
case SCR_DYN_BOOL:
if (v->v.b) qb_append(b, "true", 4);
else qb_append(b, "false", 5);
return;
default:
- return; /* null/undefined/objects/arrays/functions → '' */
+ return; /* null/undefined/symbols/objects/arrays/functions → '' */
}
}
diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c
index b6fdb9cd6..eeadba98f 100644
--- a/packages/runtime/src/scr_url.c
+++ b/packages/runtime/src/scr_url.c
@@ -1686,9 +1686,10 @@ static ScrDyn *scr_native_sp_step(void *ptr, double *index, unsigned selection)
} else value = scr_dyn_new_str(selection == 1 ? key : text);
scr_str_release(key); scr_str_release(text);
}
+ /* { value, done }, in the iterator result's own key order. */
ScrDyn *result = scr_dyn_new_obj();
- scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done));
scr_dyn_obj_set(result, "value", 5, value);
+ scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done));
return result;
}
diff --git a/tests/corpus/5833-url-search-params-iterators.ts b/tests/corpus/5833-url-search-params-iterators.ts
new file mode 100644
index 000000000..eb0b99d33
--- /dev/null
+++ b/tests/corpus/5833-url-search-params-iterators.ts
@@ -0,0 +1,40 @@
+// URLSearchParams keys(), values() and entries() outside a for-of head:
+// a stored iterator steps with next() ({ value, done }), spreads from its
+// current position, and walks the live pair list by index, so pairs
+// appended after it was created are visited. Spreads, Array.from, Object.fromEntries and
+// new Map over the params or their iterators read the same pairs.
+const sp = new URLSearchParams("b=2&a=1&a=3&c=%20x");
+
+const keys = sp.keys();
+console.log(JSON.stringify(keys.next()), JSON.stringify(keys.next()));
+sp.append("d", "4");
+console.log(JSON.stringify([...keys]));
+console.log(JSON.stringify(keys.next()));
+
+console.log(JSON.stringify([...sp]));
+console.log(JSON.stringify([...sp.keys()]));
+console.log(JSON.stringify([...sp.values()]));
+console.log(JSON.stringify([...sp.entries()]));
+console.log(JSON.stringify(Array.from(sp)));
+console.log(JSON.stringify(Array.from(sp.entries())));
+console.log(JSON.stringify(Array.from(sp.values())));
+console.log(JSON.stringify(Object.fromEntries(sp)));
+console.log(JSON.stringify(Object.fromEntries(sp.entries())));
+console.log(JSON.stringify([...new Map(sp)]));
+console.log(JSON.stringify([...new Map(sp.entries())]));
+
+const values = sp.values();
+const seen: string[] = [];
+for (const v of values) {
+ seen.push(v);
+ if (v === "1") sp.delete("b");
+}
+console.log(JSON.stringify(seen), sp.toString());
+
+const entries = new URL("https://h.example/p?x=1&y=2&x=3").searchParams.entries();
+const pairs: string[] = [];
+for (const [k, v] of entries) pairs.push(`${k}:${v}`);
+console.log(pairs.join(" "));
+
+const empty = new URLSearchParams().entries();
+console.log(JSON.stringify(empty.next()), JSON.stringify([...empty]));
diff --git a/tests/corpus/5834-url-search-params-iterators-dynamic.mjs b/tests/corpus/5834-url-search-params-iterators-dynamic.mjs
new file mode 100644
index 000000000..82b0c6d28
--- /dev/null
+++ b/tests/corpus/5834-url-search-params-iterators-dynamic.mjs
@@ -0,0 +1,22 @@
+// URLSearchParams iterators in a checked-dynamic program: next() results,
+// live iteration after append and delete, and the consumers (spread,
+// Array.from with and without a mapper, Object.fromEntries, new Map) over
+// the params and each projection.
+const sp = new URLSearchParams("b=2&a=1&a=3");
+const it = sp.keys();
+console.log(it.next());
+sp.append("c", "4");
+console.log(JSON.stringify([...it]), it.next());
+console.log(JSON.stringify([...sp.values()]), JSON.stringify(Array.from(sp.entries())));
+console.log(JSON.stringify(Array.from(sp.keys(), (k) => k.toUpperCase())));
+console.log(JSON.stringify(Object.fromEntries(sp.entries())), JSON.stringify(Object.fromEntries(sp)));
+console.log(JSON.stringify([...new Map(sp.entries())]), new Map(sp).size);
+const values = sp.values();
+const seen = [];
+let step = values.next();
+while (!step.done) {
+ seen.push(step.value);
+ if (step.value === "2") sp.delete("a");
+ step = values.next();
+}
+console.log(JSON.stringify(seen), sp.toString());
diff --git a/tests/corpus/5836-url-search-params-tuple-pairs.ts b/tests/corpus/5836-url-search-params-tuple-pairs.ts
new file mode 100644
index 000000000..d6fe1a86a
--- /dev/null
+++ b/tests/corpus/5836-url-search-params-tuple-pairs.ts
@@ -0,0 +1,34 @@
+// new URLSearchParams(pairs) over [string, string] tuple rows, such as an
+// Object.entries result: each pair appends its name and value in array
+// order, so repeated names keep every value and an empty array gives empty
+// params. The params are then as mutable as any other.
+const record: Record = { a: "1", "b c": "x&y", é: "ü" };
+const fromEntries = new URLSearchParams(Object.entries(record));
+console.log(fromEntries.toString(), fromEntries.size);
+
+const pairs: [string, string][] = [
+ ["k", "v"],
+ ["k", "w"],
+ ["", ""],
+ ["plus", "a+b c"],
+];
+const repeated = new URLSearchParams(pairs);
+console.log(repeated.toString(), JSON.stringify(repeated.getAll("k")));
+
+const none: [string, string][] = [];
+const empty = new URLSearchParams(none);
+console.log(JSON.stringify(empty.toString()), empty.size);
+
+const edited = new URLSearchParams(pairs);
+edited.append("y", "2");
+edited.delete("k");
+edited.sort();
+console.log(edited.toString(), pairs.length);
+
+const lone: [string, string][] = [["z", "\uD800x"]];
+console.log(new URLSearchParams(lone).toString());
+
+const url = new URL("https://example.test/?q=1");
+for (const [name, value] of new URLSearchParams(Object.entries({ q: "2", r: "3" })))
+ url.searchParams.set(name, value);
+console.log(url.href);
diff --git a/tests/corpus/5838-qs-stringify-bigint.ts b/tests/corpus/5838-qs-stringify-bigint.ts
new file mode 100644
index 000000000..cdc668a55
--- /dev/null
+++ b/tests/corpus/5838-qs-stringify-bigint.ts
@@ -0,0 +1,19 @@
+// querystring.stringify writes a bigint value as its decimal digits, bare
+// like a boolean (Node's encodeStringified), in plain and array-valued
+// fields and under custom separators. url.format's query object follows
+// the same rule.
+import { encode, stringify } from "node:querystring";
+import { format } from "node:url";
+
+console.log(stringify({ a: 1n, b: -2n, c: 0n }));
+console.log(stringify({ big: 12345678901234567890n, neg: -(2n ** 70n) }));
+console.log(stringify({ list: [1n, "two", 3, true, -4n] }));
+console.log(stringify({ a: 5n, b: [6n, 7n] }, ";", ":"));
+console.log(encode({ id: 42n, name: "x y" }));
+
+const counts: Record = { first: 10n, second: 20n };
+console.log(stringify(counts));
+const mixed: Record = { n: 1n, s: "v", z: null };
+console.log(stringify(mixed));
+
+console.log(format({ protocol: "https", host: "example.test", pathname: "/p", query: { id: 99n, tag: ["a", 2n] } }));
diff --git a/tests/corpus/5882-string-decoder-runtime-encoding.ts b/tests/corpus/5882-string-decoder-runtime-encoding.ts
new file mode 100644
index 000000000..059d2144f
--- /dev/null
+++ b/tests/corpus/5882-string-decoder-runtime-encoding.ts
@@ -0,0 +1,39 @@
+// new StringDecoder(encoding) with an encoding known only at run time
+// normalizes it as Node does: any ASCII case of a Buffer encoding name
+// selects that decoder and `.encoding` answers the canonical name
+// (ucs2 and utf-16le are utf16le, binary is latin1), undefined and ""
+// mean utf8, and an unknown name throws a TypeError with code
+// ERR_UNKNOWN_ENCODING. Literal names keep their compile-time form.
+import { StringDecoder } from "node:string_decoder";
+
+const bytes = Buffer.from([0xe2, 0x82, 0xac, 0x41, 0xc3, 0xa9, 0xff, 0x00]);
+
+function decodeAll(encoding: string | undefined): string {
+ const decoder = new StringDecoder(encoding as BufferEncoding | undefined);
+ const parts: string[] = [];
+ for (let i = 0; i < bytes.length; i += 3) parts.push(decoder.write(bytes.subarray(i, i + 3)));
+ parts.push(decoder.end());
+ return `${decoder.encoding} ${JSON.stringify(parts)}`;
+}
+
+const names = [
+ "utf8", "UTF-8", "Utf8", "hex", "HEX", "base64", "Base64Url", "latin1", "BINARY",
+ "ascii", "utf16le", "UTF-16LE", "ucs2", "Ucs-2", "", undefined,
+];
+for (const name of names) console.log(JSON.stringify(name), decodeAll(name));
+
+for (const bad of ["utf-16", "buffer", "utf 8", "latin-1"]) {
+ try {
+ new StringDecoder(bad as BufferEncoding);
+ console.log("no throw", bad);
+ } catch (error) {
+ const e = error as Error & { code?: string };
+ console.log(e.name, e.code, e.message);
+ }
+}
+
+const fromOptions: { encoding?: BufferEncoding } = { encoding: "hex" };
+const viaOption = new StringDecoder(fromOptions.encoding);
+console.log(viaOption.encoding, viaOption.write(Buffer.from("hi")), viaOption.end(Buffer.from("!")));
+const literal = new StringDecoder("ucs2");
+console.log(literal.encoding, JSON.stringify(literal.write(Buffer.from([0x3d, 0xd8, 0x00, 0xde]))));
diff --git a/tests/corpus/5883-os-set-priority.ts b/tests/corpus/5883-os-set-priority.ts
new file mode 100644
index 000000000..d87100183
--- /dev/null
+++ b/tests/corpus/5883-os-set-priority.ts
@@ -0,0 +1,33 @@
+// os.setPriority([pid, ]priority): the one-argument form sets the current
+// process, both arguments go through validateInt32 (the priority within
+// -20..19, a RangeError with code ERR_OUT_OF_RANGE otherwise), and a
+// refused change is a SystemError with code ERR_SYSTEM_ERROR naming
+// uv_os_setpriority. Raising the nice value is always allowed; lowering it
+// again depends on the privileges of the user running the program.
+import * as os from "node:os";
+import { getPriority, setPriority } from "node:os";
+
+function attempt(label: string, run: () => void): void {
+ try {
+ run();
+ console.log(label, "ok", getPriority());
+ } catch (error) {
+ const e = error as Error & { code?: string };
+ console.log(label, e.name, e.code, e.message);
+ }
+}
+
+const start = getPriority();
+attempt("same value", () => setPriority(start));
+attempt("same value with pid 0", () => os.setPriority(0, start));
+attempt("same value with own pid", () => setPriority(process.pid, start));
+attempt("raise", () => setPriority(Math.min(start + 5, 19)));
+console.log("raised by 5 or to 19:", getPriority() === Math.min(start + 5, 19));
+attempt("lower again", () => setPriority(start));
+attempt("priority 20", () => setPriority(20));
+attempt("priority -21", () => setPriority(0, -21));
+attempt("priority 1.5", () => setPriority(1.5));
+attempt("priority NaN", () => setPriority(Number.NaN));
+attempt("pid 2 ** 31", () => setPriority(2 ** 31, 0));
+attempt("pid -1.5", () => setPriority(-1.5, 0));
+attempt("missing process", () => setPriority(2147483646, 19));
diff --git a/tests/corpus/5939-js-let-object-reassigned-wider.js b/tests/corpus/5939-js-let-object-reassigned-wider.js
new file mode 100644
index 000000000..f23a6a059
--- /dev/null
+++ b/tests/corpus/5939-js-let-object-reassigned-wider.js
@@ -0,0 +1,29 @@
+// An untyped `let` that starts as one object literal and is later
+// assigned object literals with more properties holds each object as
+// written: the added properties read back, and properties an object lacks
+// read as undefined.
+function zip(left, right) {
+ let state = { _tag: "PullBoth" };
+ const out = [];
+ for (let i = 0; i < left.length; i++) {
+ if (state._tag === "PullBoth") state = { _tag: "PullLeft", right: right[i] };
+ else if (state._tag === "PullLeft") {
+ out.push([left[i], state.right]);
+ state = { _tag: "PullBoth" };
+ }
+ }
+ return { out, last: state._tag, pending: state.right };
+}
+console.log(zip([1, 2, 3, 4], ["a", "b", "c", "d"]));
+
+let point = { x: 1 };
+const update = () => {
+ point = { x: 2, y: 3 };
+};
+console.log(point.y, Object.keys(point));
+update();
+console.log(point.x, point.y, Object.keys(point), JSON.stringify(point));
+
+let config = { name: "base" };
+config = { ...config, debug: true };
+console.log(config.debug, config);
diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts
index 3ad6839c8..b884de3ce 100644
--- a/tests/fixtures/node-types/fenced.ts
+++ b/tests/fixtures/node-types/fenced.ts
@@ -33,10 +33,10 @@ ReadableStream.from(new Set([1, 2]));
* typecheck under @types/node and fence with the module-qualified name —
* calls and value reads alike. */
import { watchFile } from "fs";
-import { setPriority } from "node:os";
+import { EventEmitter, on } from "node:events";
import { win32 } from "path";
watchFile("x", () => {});
-setPriority(0);
+on(new EventEmitter(), "tick");
console.log(win32.sep);
/* URL getters and serialization lower under @types/node, but object-URL
* registry operations still fence by member name with the supported
diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt
index dca88df39..29e4a2ee9 100644
--- a/tests/harness/__snapshots__/node-types-fenced.txt
+++ b/tests/harness/__snapshots__/node-types-fenced.txt
@@ -48,22 +48,22 @@ fenced.ts:38:1 - error SC2020: 'fs.watchFile' is typed by @types/node but has no
37 | import { win32 } from "path";
38 | watchFile("x", () => {});
| ^~~~~~~~~~~~~~~~~~~~~~~~
- 39 | setPriority(0);
+ 39 | on(new EventEmitter(), "tick");
hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations)
-fenced.ts:39:1 - error SC2020: 'os.setPriority' is typed by @types/node but has no scriptc lowering yet
+fenced.ts:39:1 - error SC2020: 'events.on' is typed by @types/node but has no scriptc lowering yet
38 | watchFile("x", () => {});
- 39 | setPriority(0);
- | ^~~~~~~~~~~~~~
+ 39 | on(new EventEmitter(), "tick");
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 | console.log(win32.sep);
hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations)
fenced.ts:40:13 - error SC2020: 'PlatformPath.sep' is typed by @types/node but has no scriptc lowering yet
- 39 | setPriority(0);
+ 39 | on(new EventEmitter(), "tick");
40 | console.log(win32.sep);
| ^~~~~~~~~
41 | /* URL getters and serialization lower under @types/node, but object-URL
diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json
index 67bc151c4..96e54b1fe 100644
--- a/tests/harness/corpus-costs.json
+++ b/tests/harness/corpus-costs.json
@@ -1841,12 +1841,19 @@
"5648-dns-lookup-forms.ts": 2992,
"5649-dns-service-servers.ts": 2712,
"5650-dns-lookup-errors-untyped.cjs": 2822,
+ "5833-url-search-params-iterators.ts": 2678,
+ "5834-url-search-params-iterators-dynamic.mjs": 2696,
+ "5836-url-search-params-tuple-pairs.ts": 2118,
+ "5838-qs-stringify-bigint.ts": 2207,
+ "5882-string-decoder-runtime-encoding.ts": 2541,
+ "5883-os-set-priority.ts": 2380,
"5904-async-generator-completion.ts": 2950,
"5905-array-from-async-generator.ts": 2750,
"5906-array-from-async-generator-js.js": 2950,
"5910-set-prototype-of-primitive-object-kinds.ts": 2719,
"5911-dyn-class-to-primitive.ts": 2850,
"5913-prototype-type-error-messages.ts": 2484,
+ "5939-js-let-object-reassigned-wider.js": 1945,
"5950-tuple-predicate-methods.ts": 5990,
"5951-result-never-widening.ts": 4710,
"5955-any-promise-methods.js": 3940,
@@ -4478,12 +4485,19 @@
"5648-dns-lookup-forms.ts": 3416,
"5649-dns-service-servers.ts": 4916,
"5650-dns-lookup-errors-untyped.cjs": 3889,
+ "5833-url-search-params-iterators.ts": 5339,
+ "5834-url-search-params-iterators-dynamic.mjs": 3797,
+ "5836-url-search-params-tuple-pairs.ts": 3193,
+ "5838-qs-stringify-bigint.ts": 3519,
+ "5882-string-decoder-runtime-encoding.ts": 3854,
+ "5883-os-set-priority.ts": 3417,
"5904-async-generator-completion.ts": 3800,
"5905-array-from-async-generator.ts": 3500,
"5906-array-from-async-generator-js.js": 3750,
"5910-set-prototype-of-primitive-object-kinds.ts": 5033,
"5911-dyn-class-to-primitive.ts": 4732,
"5913-prototype-type-error-messages.ts": 4068,
+ "5939-js-let-object-reassigned-wider.js": 4436,
"5950-tuple-predicate-methods.ts": 7790,
"5951-result-never-widening.ts": 6120,
"5955-any-promise-methods.js": 5120,
diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts
index 3825cb3f4..0eb0cf2c3 100644
--- a/tests/harness/library-mode.test.ts
+++ b/tests/harness/library-mode.test.ts
@@ -299,6 +299,51 @@ wrap empty: len 2 bytes 60 62
expect(run.stdout).toBe(BUFFERS_EXPECTED);
});
+ /* ── cstring: NUL-terminated text in, out, and through callbacks ─────── */
+
+ test("cstring parameters, results, and callback arguments", async () => {
+ const { archive, outDir } = await buildLibrary("cstrings", emission);
+ const probe = buildProbe("cstrings", archive, outDir);
+ const run = runProbe(probe, ["ok"]);
+ expect(run.signal).toBeNull();
+ expect(run.status).toBe(0);
+ expect(run.stdout).toBe(`register: 0
+greet: hello, w\u00f6rld! (strlen 14)
+length ascii: 3
+length utf8: 4
+length malformed: 3
+length empty: 0
+log: QUIET (strlen 5, n 5)
+echo: 1
+done
+`);
+ });
+
+ test.each([
+ ["nullarg", "scriptc: library inbound cstring parameter is NULL\n", "SC4012", "kc_length"],
+ [
+ "nulresult",
+ "scriptc: TypeError: a cstring value contains U+0000, which a C string cannot hold\n",
+ "SC4015",
+ "kc_with_nul",
+ ],
+ [
+ "nularg",
+ "scriptc: TypeError: a cstring value contains U+0000, which a C string cannot hold\n",
+ "SC4015",
+ "kc_echo_nul",
+ ],
+ ])("cstring %s traps through the sink", async (mode, text, code, symbol) => {
+ const { archive, outDir } = await buildLibrary("cstrings", emission);
+ const probe = buildProbe("cstrings", archive, outDir);
+ const run = runProbe(probe, [mode]);
+ expect(run.signal).toBeNull();
+ expect(run.status).toBe(0);
+ expect(run.stdout).toBe(
+ `register: 0\ntext=[${text}]\ncode=[${code}]\nsymbol=[${symbol}]\ntrapped\n`,
+ );
+ });
+
/* ── K4: init re-run determinism ─────────────────────────────────────── */
const SESSION = `session start counter=0
diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts
index fffa88cc3..fc243ccb5 100644
--- a/tests/harness/library-profile.test.ts
+++ b/tests/harness/library-profile.test.ts
@@ -160,7 +160,56 @@ describe("library profile validation", () => {
});
test("parse error", () => expectSc4001("{ not json", "not valid JSON"));
- test("wrong format", () => expectSc4001({ ...good, profile_format: 2 }, "profile_format"));
+ test("wrong format", () => expectSc4001({ ...good, profile_format: 3 }, "profile_format"));
+ test("the cstring class requires profile_format 2", () => {
+ const cstringExport = {
+ export: "greet",
+ symbol: "kx_greet",
+ params: ["cstring"],
+ returns: "cstring",
+ };
+ const v2 = loadLibraryProfile(
+ writeProfile({ ...good, profile_format: 2, exports: [...good.exports, cstringExport] }),
+ );
+ expect(v2.ok).toBe(true);
+ if (v2.ok) {
+ expect(v2.profile.profileFormat).toBe(2);
+ expect(v2.profile.exports.at(-1)).toMatchObject({ params: ["cstring"], returns: "cstring" });
+ }
+ expectSc4001(
+ { ...good, exports: [...good.exports, cstringExport] },
+ "'exports[2].params[0]' class 'cstring' requires profile_format 2",
+ );
+ expectSc4001(
+ { ...good, exports: [...good.exports, { ...cstringExport, params: [] }] },
+ "'exports[2].returns' class 'cstring' requires profile_format 2",
+ );
+ const callbackAbi = { ...good.abi, callback_register_symbol: "kx_set_callback" };
+ const cstringCallback = { name: "log", params: ["cstring"], returns: "void" };
+ expect(
+ loadLibraryProfile(
+ writeProfile({
+ ...good,
+ profile_format: 2,
+ abi: callbackAbi,
+ callbacks: [cstringCallback],
+ }),
+ ).ok,
+ ).toBe(true);
+ expectSc4001(
+ { ...good, abi: callbackAbi, callbacks: [cstringCallback] },
+ "'callbacks[0].params[0]' class 'cstring' requires profile_format 2",
+ );
+ expectSc4001(
+ {
+ ...good,
+ profile_format: 2,
+ abi: callbackAbi,
+ callbacks: [{ name: "log", params: [], returns: "cstring" }],
+ },
+ "a callback return stays scalar",
+ );
+ });
test("missing name", () => {
const { name: _drop, ...rest } = good;
expectSc4001(rest, "'name'");
diff --git a/tests/harness/wasm-library.test.ts b/tests/harness/wasm-library.test.ts
index 8659c03d9..d0bccdc3d 100644
--- a/tests/harness/wasm-library.test.ts
+++ b/tests/harness/wasm-library.test.ts
@@ -272,6 +272,85 @@ describe.skipIf(!hasZig).each(["node", "native"] as const)(
},
);
+ test("cstring parameters, results, and callback arguments use NUL-terminated pointers", async () => {
+ const entry = join(directory, "cstring-lib.ts");
+ await writeFile(
+ entry,
+ [
+ "declare function log(text: string): void;",
+ "export function greet(name: string): string {",
+ " log(name.toUpperCase());",
+ " return `hello, ${name}!`;",
+ "}",
+ "export function withNul(): string {",
+ ' return "a\\u0000b";',
+ "}",
+ "",
+ ].join("\n"),
+ );
+ const profilePath = join(directory, "cstring.json");
+ await writeFile(
+ profilePath,
+ JSON.stringify({
+ profile_format: 2,
+ name: "wasm-cstring",
+ entry,
+ emission: "llvm",
+ abi: {
+ prefix: "cs_",
+ init_symbol: "cs_init",
+ sink_register_symbol: "cs_sink",
+ callback_register_symbol: "cs_callback",
+ collect_symbol: null,
+ result_reset_symbol: null,
+ },
+ callbacks: [{ name: "log", params: ["cstring"], returns: "void" }],
+ exports: [
+ { export: "greet", symbol: "cs_greet", params: ["cstring"], returns: "cstring" },
+ { export: "withNul", symbol: "cs_with_nul", params: [], returns: "cstring" },
+ ],
+ }),
+ );
+ const result = await compileLibrary({ profilePath, outDir: directory });
+ if (!result.ok) throw new Error(JSON.stringify(result.diagnostics));
+ const wasi = new WASI({ version: "preview1" });
+ let api: Api;
+ const logs: string[] = [];
+ const panics: string[] = [];
+ const cString = (ptr: number): string => {
+ const memory = new Uint8Array(api.memory.buffer);
+ let end = ptr;
+ while (memory[end] !== 0) end++;
+ return new TextDecoder().decode(memory.subarray(ptr, end));
+ };
+ const instance = new WebAssembly.Instance(
+ new WebAssembly.Module(await readFile(result.archivePath)),
+ {
+ ...wasi.getImportObject(),
+ scriptc: {
+ log: (ptr: number) => logs.push(cString(ptr)),
+ panic: (ptr: number, len: number) => {
+ const message = new TextDecoder().decode(new Uint8Array(api.memory.buffer, ptr, len));
+ panics.push(message);
+ throw new Error(message);
+ },
+ },
+ },
+ );
+ api = instance.exports as Api;
+ wasi.initialize(instance);
+ api.cs_init();
+ const input = new TextEncoder().encode("w\u00f6rld\0");
+ const ptr = api.scriptc_alloc(input.length);
+ new Uint8Array(api.memory.buffer, ptr, input.length).set(input);
+ expect(cString(api.cs_greet(ptr))).toBe("hello, w\u00f6rld!");
+ expect(logs).toEqual(["W\u00d6RLD"]);
+ api.scriptc_free(ptr);
+ expect(() => api.cs_with_nul()).toThrow();
+ expect(panics).toHaveLength(1);
+ expect(panics[0]).toContain("a cstring value contains U+0000");
+ });
+
test("refuses collisions with built-in Wasm exports and imports", async () => {
for (const change of [
{
diff --git a/tests/library-mode/cstrings/lib.ts b/tests/library-mode/cstrings/lib.ts
new file mode 100644
index 000000000..1016beb90
--- /dev/null
+++ b/tests/library-mode/cstrings/lib.ts
@@ -0,0 +1,26 @@
+// cstring conformance fixture: NUL-terminated text in both directions and
+// through a callback channel. `withNul` and `echoNul` produce text holding
+// U+0000, which a C string cannot carry.
+declare function log(text: string, n: number): void;
+
+export function greet(name: string): string {
+ return `hello, ${name}!`;
+}
+
+export function length(text: string): number {
+ return text.length;
+}
+
+export function echo(text: string): number {
+ log(text.toUpperCase(), text.length);
+ return 1;
+}
+
+export function withNul(): string {
+ return "a\u0000b";
+}
+
+export function echoNul(): number {
+ log("x\u0000y", 3);
+ return 1;
+}
diff --git a/tests/library-mode/cstrings/probe.c b/tests/library-mode/cstrings/probe.c
new file mode 100644
index 000000000..ba802f84a
--- /dev/null
+++ b/tests/library-mode/cstrings/probe.c
@@ -0,0 +1,76 @@
+/* cstring probe, mode-selected by argv[1]:
+ * ok — cstring parameters copy in (UTF-8, U+FFFD for malformed
+ * bytes), cstring results are NUL-terminated arena copies,
+ * and a cstring callback argument arrives NUL-terminated
+ * nullarg — a NULL cstring parameter is the SC4012 host-contract trap
+ * nulresult — a result holding U+0000 traps instead of truncating
+ * nularg — a callback argument holding U+0000 traps the same way
+ */
+#include
+#include
+#include
+#include
+
+extern void kc_init(void);
+extern void kc_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
+extern void kc_collect(void);
+extern int32_t kc_set_callback(const char *name, void (*fn)(void), void *ctx);
+extern const char *kc_greet(const char *name);
+extern double kc_length(const char *text);
+extern double kc_echo(const char *text);
+extern const char *kc_with_nul(void);
+extern double kc_echo_nul(void);
+
+static jmp_buf trap_jmp;
+
+static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
+ (void)ctx; (void)addr;
+ /* structured: 0x01 text 0x1F code 0x1F symbol */
+ const uint8_t *p = msg + 1, *end = msg + len;
+ const char *names[3] = {"text", "code", "symbol"};
+ for (int field = 0; field < 3 && p <= end; field++) {
+ const uint8_t *sep = memchr(p, 0x1f, (size_t)(end - p));
+ const uint8_t *stop = sep != NULL ? sep : end;
+ printf("%s=[%.*s]\n", names[field], (int)(stop - p), (const char *)p);
+ if (sep == NULL) break;
+ p = sep + 1;
+ }
+ longjmp(trap_jmp, 1);
+}
+
+static void on_log(void *ctx, const char *text, double n) {
+ (void)ctx;
+ printf("log: %s (strlen %zu, n %.0f)\n", text, strlen(text), n);
+}
+
+int main(int argc, char **argv) {
+ const char *mode = argc > 1 ? argv[1] : "ok";
+ kc_set_panic_sink(sink, NULL);
+ kc_init();
+ printf("register: %d\n", (int)kc_set_callback("log", (void (*)(void))on_log, NULL));
+ if (setjmp(trap_jmp) != 0) {
+ printf("trapped\n");
+ return 0;
+ }
+ if (strcmp(mode, "ok") == 0) {
+ const char *g = kc_greet("w\xC3\xB6rld");
+ printf("greet: %s (strlen %zu)\n", g, strlen(g));
+ printf("length ascii: %.0f\n", kc_length("abc"));
+ printf("length utf8: %.0f\n", kc_length("caf\xC3\xA9"));
+ printf("length malformed: %.0f\n", kc_length("a\xFF" "b"));
+ printf("length empty: %.0f\n", kc_length(""));
+ printf("echo: %.0f\n", kc_echo("quiet"));
+ kc_collect();
+ printf("done\n");
+ } else if (strcmp(mode, "nullarg") == 0) {
+ kc_length(NULL);
+ printf("UNREACHABLE\n");
+ } else if (strcmp(mode, "nulresult") == 0) {
+ kc_with_nul();
+ printf("UNREACHABLE\n");
+ } else if (strcmp(mode, "nularg") == 0) {
+ kc_echo_nul();
+ printf("UNREACHABLE\n");
+ }
+ return 0;
+}
diff --git a/tests/library-mode/cstrings/profile.json b/tests/library-mode/cstrings/profile.json
new file mode 100644
index 000000000..c5a36e424
--- /dev/null
+++ b/tests/library-mode/cstrings/profile.json
@@ -0,0 +1,22 @@
+{
+ "profile_format": 2,
+ "name": "conformance-cstrings",
+ "entry": "lib.ts",
+ "emission": "llvm",
+ "abi": {
+ "prefix": "kc_",
+ "init_symbol": "kc_init",
+ "sink_register_symbol": "kc_set_panic_sink",
+ "collect_symbol": "kc_collect",
+ "result_reset_symbol": null,
+ "callback_register_symbol": "kc_set_callback"
+ },
+ "callbacks": [{ "name": "log", "params": ["cstring", "f64"], "returns": "void" }],
+ "exports": [
+ { "export": "greet", "symbol": "kc_greet", "params": ["cstring"], "returns": "cstring" },
+ { "export": "length", "symbol": "kc_length", "params": ["cstring"], "returns": "f64" },
+ { "export": "echo", "symbol": "kc_echo", "params": ["cstring"], "returns": "f64" },
+ { "export": "withNul", "symbol": "kc_with_nul", "params": [], "returns": "cstring" },
+ { "export": "echoNul", "symbol": "kc_echo_nul", "params": [], "returns": "f64" }
+ ]
+}