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, u64JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range. string, bytes parametersTwo i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values. string, bytes resultsTwo extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order. + cstring parametersOne 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 resultsAn 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. voidNo 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" } + ] +}