diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2de42af9a..01df362ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,7 @@ jobs: run: >- pnpm test --shard=${{ matrix.shard }}/10 --exclude=tests/harness/effect.test.ts + --exclude=tests/harness/effect4.test.ts --exclude=tests/harness/differential.test.ts --exclude=tests/harness/llvm-differential.test.ts --exclude=tests/harness/npm.test.ts @@ -154,6 +155,7 @@ jobs: run: >- pnpm test tests/harness/effect.test.ts + tests/harness/effect4.test.ts tests/harness/differential.test.ts tests/harness/llvm-differential.test.ts tests/harness/npm.test.ts diff --git a/docs/src/app/cli/page.mdx b/docs/src/app/cli/page.mdx index 9324be2f3..1e57aa27d 100644 --- a/docs/src/app/cli/page.mdx +++ b/docs/src/app/cli/page.mdx @@ -52,7 +52,7 @@ fib.s Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file. -The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined scr_* runtime symbols and a required scr_runtime_abi_v5 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime. +The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined scr_* runtime symbols and a required scr_runtime_abi_v6 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime. ## scriptc run diff --git a/docs/src/app/how-it-works/page.mdx b/docs/src/app/how-it-works/page.mdx index 2a2dc6bfc..6bdf750e6 100644 --- a/docs/src/app/how-it-works/page.mdx +++ b/docs/src/app/how-it-works/page.mdx @@ -13,7 +13,7 @@ TypeScript ──tsc: parse + typecheck──▶ lowering ──▶ typed IR ─ 3. **LLVM backend** — `--emit=llvm` writes textual LLVM IR and stops. On supported macOS, Linux, and Windows hosts and for WASI, `--emit=asm|obj` sends LLVM IR to the matching LLVM 22 helper. Executable and library builds use the same LLVM backend. 4. **Link** — runtime packs contain precompiled C runtime objects plus the required vendor archives. A hashed manifest selects objects from the program's runtime features, optimization mode, and library state model. The target platform linker and SDK/sysroot produce the executable. Runtime development with AddressSanitizer uses an instrumented C runtime. -Program objects define main and leave their selected scr_* runtime functions undefined. The scr_runtime_abi_v5 reference is a strong link-time compatibility check. --print=native-link-info exposes the exact precompiled runtime pack and link ordering for external builds; that object ABI is currently experimental and exact-runtime-version compatible, not semver-stable. +Program objects define main and leave their selected scr_* runtime functions undefined. The scr_runtime_abi_v6 reference is a strong link-time compatibility check. --print=native-link-info exposes the exact precompiled runtime pack and link ordering for external builds; that object ABI is currently experimental and exact-runtime-version compatible, not semver-stable. Inspect any stage yourself: diff --git a/docs/src/app/limitations/page.mdx b/docs/src/app/limitations/page.mdx index 8364202ff..9ac018384 100644 --- a/docs/src/app/limitations/page.mdx +++ b/docs/src/app/limitations/page.mdx @@ -19,7 +19,7 @@ These are rejected at compile time with an `SC` code, a code frame, and usually - Block- and function-scoped `using` and `await using` compile for statically represented classes with zero-parameter disposal methods and for the supported FileHandle, timer, immediate, child-process, and readline handles. Array `for (using ... of ...)` loops compile. Top-level and switch-clause declarations, disposal through an engine-held package value, and the `DisposableStack` constructors remain fenced; a caught `SuppressedError` exposes its Node-compatible `name` and `message`, while its `error` and `suppressed` payload properties remain outside the static catch surface. - Typed async generator functions and methods compile with lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also consumes Node `Readable` streams, Web `ReadableStream` values, and statically represented class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record; abrupt completion runs the iterator's cleanup, including literal `destroyOnReturn` and `preventCancel` options. Async `yield*` delegation, stored Web/Node iterator handles, and other structural async-iterator objects remain fenced. The `node:timers/promises` `setInterval(delay, value)` async iterator compiles, while its AbortSignal options remain fenced. - Generics monomorphize when the target resolves statically. The remaining edges include generic functions declared inside another function, generic class expressions, generic classes whose base depends on their own type parameters, and generic methods that would require dynamic dispatch. -- Generic function values must be pinned at use to a concrete signature and come from a never-reassigned binding; unpinned or rebound values remain fenced. Immutable aliases of overloaded program functions retain per-call overload resolution and function identity. Typed rest-parameter functions are first-class values: indirect calls pack their surplus arguments into the function's typed array slot. Lowered `node:path` functions, including optional `basename` and variadic `join`/`resolve`, are first-class static values across the bare, POSIX, and win32 modules; the exact zero-argument `node:os` functions and `querystring.escape`/`unescape` are first-class too. The fixed-signature filesystem functions `existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, `closeSync`, and the `node:fs/promises` `unlink`, `chmod`, and `rename` functions are first-class as well. Filesystem APIs with behavior-bearing trailing arguments—including writers, open, directory creation/removal, metadata readers, and directory readers—remain call-only so function-width adaptation cannot discard modes, flags, encodings, or options. Other immutable aliases of table-backed Node builtins can be called directly, while escaping them remains fenced; `util.promisify` has compile-time projections for `child_process.execFile` and utf8 `fs.readFile`. +- Generic function values must be pinned at use to a concrete signature and come from a never-reassigned binding; unpinned or rebound values remain fenced. Immutable aliases of overloaded program functions retain per-call overload resolution and function identity. Typed rest-parameter functions are first-class values: indirect calls pack their surplus arguments into the function's typed array slot. Lowered `node:path` functions, including optional `basename` and variadic `join`/`resolve`, are first-class static values across the bare, POSIX, and win32 modules; the exact zero-argument `node:os` functions and `querystring.escape`/`unescape` are first-class too. The fixed-signature filesystem functions `existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, `closeSync`, and the `node:fs/promises` `unlink`, `chmod`, and `rename` functions are first-class as well. Synchronous and promise filesystem APIs with behavior-bearing trailing arguments—including writers, open, directory creation/removal, metadata readers, and directory readers—remain call-only so function-width adaptation cannot discard modes, flags, encodings, or options. Native error-first filesystem callbacks can be stored and passed to platform adapters. Callback reads support Buffer and UTF-8 results; writes preserve string flags and modes. Callback copy supports regular files and recursive directories. Callback reads and writes validate AbortSignals and reject signals already aborted; file operations complete synchronously before their deferred callback, so cancellation cannot interrupt native I/O already in progress. Symbolic-link copy, copy filters and clone modes, recursive glob patterns, glob exclusions, recursive directory reads, and Dirent results remain unsupported and report an explicit native limitation. Other immutable aliases of table-backed Node builtins can be called directly, while escaping them remains fenced; `util.promisify` has compile-time projections for `child_process.execFile` and utf8 `fs.readFile`. - Spread arguments compile when their arity is static: non-empty fixed tuples flatten into fixed signatures with left-to-right, evaluate-once semantics, while arrays, Sets, and statically represented class iterables spread into typed rest parameters. Runtime-length spreads into fixed signatures remain on the checked-dynamic or island paths in JavaScript and otherwise receive a compile-time diagnostic. - Literal `import()` of compiled ESM/TypeScript modules and supported Node builtins compiles without the dynamic engine. Evaluation remains lazy and microtask-delayed, top-level `await` is honored, repeated imports share one namespace identity, exported mutable bindings stay live, and `Object.keys` on compiled-module namespaces uses Node's sorted key order. Computed specifiers, import attributes, CommonJS namespace synthesis, and shipped-JavaScript package imports remain explicitly fenced or require `--dynamic`. @@ -45,7 +45,7 @@ const results = await Promise.all(jobs); // number[] — compiles **Standard library** -- `TextEncoder` and `TextDecoder` instances compile as native values in fields, arguments, return values, arrays, and closures. `encode(string)` and `encode()` produce UTF-8 bytes; `decode(Uint8Array)` and `decode()` support recognized literal WHATWG labels with default options. Runtime labels, constructor options, streaming, `encodeInto`, codec property getters, inspection, JSON/dynamic conversion, and module-scope codec declarations using `var` or lacking an initializer remain fenced. Use `let` or `const` with an initializer for stored codecs. +- `TextEncoder` and `TextDecoder` instances compile as native values in fields, arguments, return values, arrays, and closures. `encode(string)` and `encode()` produce UTF-8 bytes; `decode(Uint8Array)` and `decode()` accept recognized WHATWG labels, including runtime labels and an omitted or undefined encoding. Constructor object literals support `fatal` and `ignoreBOM`; decode option literals support streaming UTF-8, UTF-16, and single-byte encodings. Streaming other legacy encodings, runtime options objects, `encodeInto`, codec property getters, inspection, JSON/dynamic conversion, and module-scope codec declarations using `var` or lacking an initializer remain fenced. Use `let` or `const` with an initializer for stored codecs. - `EventEmitter` uses a fixed argument tuple for literal event names. Computed string names compile when a constant prefix or suffix proves they cannot name `error`, the meta events, or stream-internal events; those calls pass exact-arity checked-dynamic arguments. Unrestricted names, symbols, computed names that may reach an internal event, and typed `listeners()`/`rawListeners()` results for names that may have computed registrations remain fenced. - CommonJS module metadata (`module.id`, `filename`, `path`, `paths`, `loaded`, `isPreloading`, `parent`, `children`, `require.main`, and read-only `require.cache` lookup/enumeration) compiles natively. A compiled binary has a fixed module graph, so cache deletion/reloading, metadata writes, `module.paths` mutation, and `require.extensions` remain explicit refusals. - `child_process.fork(modulePath, args?, options?)` compiles when `modulePath` resolves at build time from `new URL("./worker.ts", import.meta.url)` or `fileURLToPath(new URL("./worker.ts", import.meta.url))`, including a never-reassigned `const` alias and statically resolvable template parts. The worker and its imports are embedded in the executable; the native binary re-executes itself and starts only that worker module. Arguments begin at `process.argv[2]` in the worker, matching Node's positions under the documented process-shape divergence below. The default inherited stdio and an inline options object with `cwd`, `env`, `silent`, `windowsHide`, or `[stdin, stdout, stderr, "ipc"]` compile. `execArgv` is accepted but has no native effect because no Node executable or source loader starts at runtime. Parent `ChildProcess` and child `process` channels support `send(message, callback?)`, `connected`, `disconnect()`, and `on`/`once` for `"message"` and `"disconnect"`; message listeners may return `void` or `Promise`. Messages use Node's default newline-delimited JSON serialization, so their static types must be JSON-stringifiable or `unknown` backed by the checked JSON tree. `send()` returns the native channel's backpressure state, its callback runs later with `null` or an `Error`, and `disconnect()` changes `connected` synchronously before either side's later `"disconnect"` event. Runtime-valued worker paths, custom `execPath`, advanced serialization, transferred sockets/servers, uid/gid, timeout/signal options, and other stdio arrays remain explicit refusals. @@ -64,7 +64,7 @@ const results = await Promise.all(jobs); // number[] — compiles - Synchronous `for...of` over checked iterables follows their live iterator protocol, including compiled class iterator methods and cleanup on abrupt completion. Checked, mapper-less `Array.from` also consumes those iterator methods and supports plain array-like objects. Sparse checked arrays, general borrowed iterator factories, and native Map/Set iterator objects remain unsupported; direct statically typed class loops retain their existing restriction on iterator `return` and `throw` methods. - `Set` preserves its backing collection through native checked storage. Checked calls support `add`, `has`, `delete`, `clear`, `size`, and `forEach`, including mutations during callbacks. Detached method values and iterator objects remain unsupported. -- Regular expressions can pass through native checked storage, preserving identity and exposing `source`, `flags`, flag properties, and `test`. Runtime constructors accept checked strings and native regex copies; string replacement accepts checked regex search values with string templates. Stateful `test` with `g` or `y`, `lastIndex`, detached methods, and `exec` through an untyped receiver remain unsupported. +- Regular expressions can pass through native checked storage, preserving identity and exposing `source`, `flags`, flag properties, and stateful `test`. Static `exec`, global `match`, and numeric `lastIndex` reads and writes use the native matcher. Runtime constructors accept checked strings and native regex copies; string replacement accepts checked regex search values with string templates. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver remain unsupported. - `Object.freeze` and `Object.isFrozen` support plain checked dictionaries and primitives. Freezing is shallow and preserves accessors. Freezing aliased typed records, arrays, proxies, and other native objects remains unsupported. **The `any`/`unknown` boundary** @@ -93,11 +93,13 @@ A static-tier program otherwise produces byte-identical stdout and the same exit **Untyped Buffer construction accepts native data.** `Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects. Encoding arguments must be supported literals. Custom `valueOf` hooks on the input object and opaque class or native-handle inputs throw an explicit unsupported-operation error. -**JSON callbacks have a native subset.** Function replacers and two-argument revivers run statically, including nested replacements, object-property deletion, and thrown exceptions. A replacer that omits the root returns `undefined`. Stored `JSON.stringify` functions support numeric and string indentation; the direct-call form retains its literal-indentation restriction. Replacer property lists and reviver source contexts remain unsupported; a reviver that deletes an array element throws because checked-dynamic arrays cannot represent holes. Callback values use the checked-dynamic boundary: typed records and arrays become snapshots, so callback mutations do not update the original typed containers, record fields retain declaration order, and typed-array views retain their backing storage and brands. `Buffer.isBuffer` preserves the Buffer brand across `unknown` storage. JavaScript callbacks receive the holder as `this`; TypeScript callbacks that access a dynamic `this` remain unsupported. +**JSON callbacks have a native subset.** Function replacers and two-argument revivers run statically, including nested replacements, object-property deletion, and thrown exceptions. A replacer that omits the root returns `undefined`. Stored `JSON.stringify` functions support numeric and string indentation; the direct-call form retains its literal-indentation restriction. Replacer property lists and reviver source contexts remain unsupported; a reviver that deletes an array element throws because checked-dynamic arrays cannot represent holes. Callback values use the checked-dynamic boundary: typed records and arrays retain their native identity and mutations, record fields retain declaration order, and typed-array views retain their backing storage and brands. `Buffer.isBuffer` preserves the Buffer brand across `unknown` storage. JavaScript callbacks receive the holder as `this`; TypeScript callbacks that access a dynamic `this` remain unsupported. + +**Typed Promise exits check fulfilled values.** A Promise crossing from `unknown` into a typed result checks its fulfillment payload, preserves rejection reasons, and retains reference equality across conversions. A mismatched payload rejects with a `TypeError`. **A lying cast on dynamic data throws instead of corrupting memory** — the headline divergence, and the point. `JSON.parse(s) as Config` with mismatched data throws a catchable error naming the offending path (`expected number at $.port, got string`) where JS would silently hand you garbage. -**Structural width subtyping copies.** A record flowing into a strict field-subset shape is copied, not aliased: mutations through the narrower reference are invisible to the original. Typed records and arrays generally cross the checked-value boundary as copies. JavaScript argument and return conversions preserve native array references, and class instances retain identity at their exact class type or a base class. Fresh dense array literals created directly in checked storage preserve their own identity, and supported native handles cross by reference. +**Structural width subtyping copies.** A record flowing into a strict field-subset shape is copied, not aliased: mutations through the narrower reference are invisible to the original. Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. Empty record layouts remain snapshots because they carry no property storage. Class instances retain identity at their exact class type or a base class. Fresh dense array literals created directly in checked storage preserve their own identity, and supported native handles cross by reference. **`Object.keys`/`values`/`entries` and `JSON.stringify` report a record's declaration order**, not per-object insertion order. Identical to Node whenever objects are built in declaration order (the overwhelmingly common case). @@ -105,7 +107,7 @@ A static-tier program otherwise produces byte-identical stdout and the same exit **Date parsing and timezone data are bounded.** The one-string constructor and `Date.parse(dateString)` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and the GMT certificate-validity strings returned by the supported X509 surface; other V8-specific forms and offset-less local date-times return an Invalid Date or `NaN`, respectively. Local calendar getters use the operating system's timezone database, so historical results can differ when it and Node's timezone data disagree. When a platform's calendar API cannot represent an otherwise-valid extreme year, scriptc queries its zone rule at a calendar-equivalent year in the 400-year Gregorian cycle. -**Weak collections have a bounded native key surface.** `WeakMap` and `WeakSet` support checked native objects and arrays, functions, numeric typed arrays, ArrayBuffers, and other native weak collections. They do not retain their keys. Typed records and arrays that cross into untyped code as copies, class instances, promises, other native handles, and symbol keys remain unsupported. Constructor seeds support arrays; custom iterator protocols and detached method values remain unsupported. Values that refer back to a weak key can keep that key alive until the entry is deleted or the collection is released. +**Weak collections have a bounded native key surface.** `WeakMap` and `WeakSet` support checked native objects and arrays, functions, numeric typed arrays, ArrayBuffers, native weak collections, and typed records, arrays, tuples, and class instances that preserve their native identity through checked storage. They do not retain their keys. Copied values, other native handles, and symbol keys remain unsupported. Constructor seeds support arrays; custom iterator protocols and detached method values remain unsupported. Values that refer back to a weak key can keep that key alive until the entry is deleted or the collection is released. **Memory is reference-counted.** Acyclic values free deterministically; reference cycles are collected at deterministic collection points, not by a concurrent GC. Native checked objects, arrays, Sets, and captured closures participate in collection. Cycles through opaque native capsules or across the static/island boundary can remain alive. diff --git a/docs/src/app/native-objects/page.mdx b/docs/src/app/native-objects/page.mdx index 727fceb91..d23d04894 100644 --- a/docs/src/app/native-objects/page.mdx +++ b/docs/src/app/native-objects/page.mdx @@ -6,7 +6,7 @@ The external object ABI is **experimental**. Its `scr_*` function and data surface may change before 1.0, so consumers must use the exact runtime-pack version reported by the same compiler installation. This is stricter than semver compatibility. -The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v5`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility. +The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v6`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility. ## Machine-readable link information diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 761215849..a40284893 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -5749,7 +5749,11 @@ "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -5854,7 +5858,11 @@ "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -5893,7 +5901,11 @@ "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -5932,7 +5944,11 @@ "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -6714,7 +6730,8 @@ "confidence": "high", "source": "compiler-feature:child_process.'close'", "tests": [ - "tests/corpus/2939-child-shell-text.ts" + "tests/corpus/2939-child-shell-text.ts", + "tests/corpus/3131-child-rest-listeners.js" ] }, "dynamic": { @@ -44968,7 +44985,8 @@ "source": "compiler-feature:globals.TextDecoder", "tests": [ "tests/corpus/1423-text-codec.ts", - "tests/corpus/text-codec-values/main.ts" + "tests/corpus/text-codec-values/main.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -45041,7 +45059,8 @@ "source": "compiler-feature:globals.TextEncoder", "tests": [ "tests/corpus/1423-text-codec.ts", - "tests/corpus/text-codec-values/main.ts" + "tests/corpus/text-codec-values/main.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -93873,7 +93892,8 @@ "tests": [ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", - "tests/harness/project-config.test.ts" + "tests/harness/project-config.test.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -93945,7 +93965,8 @@ "tests": [ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", - "tests/harness/project-config.test.ts" + "tests/harness/project-config.test.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -94081,7 +94102,8 @@ "tests": [ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", - "tests/harness/project-config.test.ts" + "tests/harness/project-config.test.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -94120,7 +94142,8 @@ "tests": [ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", - "tests/harness/project-config.test.ts" + "tests/harness/project-config.test.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 6e558ee16..ca3a0363c 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -14062,7 +14062,11 @@ "tests/corpus/2898-child-stdin-spawn-failure.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -14237,7 +14241,11 @@ "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -14352,7 +14360,11 @@ "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -14386,7 +14398,11 @@ "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -14420,7 +14436,11 @@ "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/3127-spawn-runtime-options.ts", + "tests/corpus/3130-child-generic-streams.ts", + "tests/corpus/3131-child-rest-listeners.js", + "tests/corpus/3132-child-generic-stream-completion.ts" ] }, "dynamic": { @@ -15153,7 +15173,8 @@ "status": "partial", "evidence": "compiler-feature:child_process.'close'", "tests": [ - "tests/corpus/2939-child-shell-text.ts" + "tests/corpus/2939-child-shell-text.ts", + "tests/corpus/3131-child-rest-listeners.js" ] }, "dynamic": { @@ -59460,6 +59481,7 @@ "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", + "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/text-codec-values/main.ts", "tests/harness/fetch-conformance.test.ts" ] @@ -60857,7 +60879,8 @@ "evidence": "compiler-feature:globals.TextDecoder", "tests": [ "tests/corpus/1423-text-codec.ts", - "tests/corpus/text-codec-values/main.ts" + "tests/corpus/text-codec-values/main.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -60921,7 +60944,8 @@ "evidence": "compiler-feature:globals.TextEncoder", "tests": [ "tests/corpus/1423-text-codec.ts", - "tests/corpus/text-codec-values/main.ts" + "tests/corpus/text-codec-values/main.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -110429,6 +110453,7 @@ "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts", + "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts" @@ -110985,7 +111010,8 @@ "tests": [ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", - "tests/harness/project-config.test.ts" + "tests/harness/project-config.test.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -111128,7 +111154,8 @@ "tests": [ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", - "tests/harness/project-config.test.ts" + "tests/harness/project-config.test.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -111241,7 +111268,8 @@ "tests": [ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", - "tests/harness/project-config.test.ts" + "tests/harness/project-config.test.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { @@ -111276,7 +111304,8 @@ "tests": [ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", - "tests/harness/project-config.test.ts" + "tests/harness/project-config.test.ts", + "tests/corpus/3133-text-decoder-streaming.ts" ] }, "dynamic": { diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index cca76bd20..61cd87fd7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -37,7 +37,7 @@ "node-builtin.assert": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1609-assert-async.ts"], "node-builtin.async_hooks": ["tests/corpus/2213-async-local-storage.cjs"], "node-builtin.buffer": ["tests/corpus/1402-buffer-encodings.ts", "tests/corpus/1660-buffer-read-write-num.ts", "tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"], - "node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts"], + "node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"], "node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"], "node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"], "node-builtin.crypto": ["tests/corpus/1358-crypto-random.ts", "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", "tests/harness/npm-static.test.ts"], @@ -128,8 +128,8 @@ { "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] }, { "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" }, { "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] }, - { "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts"] }, - { "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts"] }, + { "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts"] }, + { "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts"] }, { "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2880-cjs-module-graph/main.cjs", "tests/corpus/2923-cjs-factory-export/main.cjs"] }, { "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs"] }, { "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] }, @@ -147,7 +147,7 @@ { "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, { "chapter": "child_process", "signatures": ["`child_process.execFile(file[, args][, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/2911-execfile-callback.ts"] }, { "chapter": "child_process", "signatures": ["`child_process.fork(modulePath[, args][, options])`", "`subprocess.send(message[, sendHandle[, options]][, callback])`", "`subprocess.disconnect()`", "`connected` Type: {boolean} Set to `false` after `subprocess.disconnect()` is called.", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] }, - { "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts"] }, + { "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts", "tests/corpus/3131-child-rest-listeners.js"] }, { "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] }, { "chapter": "child_process", "signatures": ["`stdin` Type: {stream.Writable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/2893-child-stdin-duplex.ts", "tests/corpus/2894-child-stdin-backpressure.ts", "tests/corpus/2895-child-stdin-write-after-end.ts", "tests/corpus/2896-child-stdin-write-after-finish.ts", "tests/corpus/2897-child-stdin-destroy.ts", "tests/corpus/2898-child-stdin-spawn-failure.ts"] }, { "chapter": "fs", "signatures": ["`fsPromises.lstat(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] }, diff --git a/packages/cli/README.md b/packages/cli/README.md index 9aeeb89d5..f41f220d0 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -34,7 +34,7 @@ Builds use a bounded persistent cache. Unchanged source can reuse the validated - `scriptc run ` — compile and run - `scriptc coverage ` — what compiles statically, and why the rest doesn't -`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v5` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available. +`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v6` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available. For embedder-hosted modules that are not installed npm packages, coverage can map an exact bare specifier to a local declaration with repeatable `--external-types ` options. This is analysis-only: the types unblock application measurement, while runtime module uses remain reported as blockers. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 5d77606b4..4b90e7779 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -63,7 +63,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 5, marker: "scr_runtime_abi_v5" }, + runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -142,6 +142,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { const error = await execFileAsync("clang", [ "-target", "arm64-apple-macosx14.0.0", object, stub, "-o", join(dir, "bad"), ]).then(() => null, (failure: { stderr?: string }) => failure); - expect(error?.stderr).toContain("scr_runtime_abi_v5"); + expect(error?.stderr).toContain("scr_runtime_abi_v6"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 838bf934f..9bc2bdcb6 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -751,8 +751,7 @@ declare var ReadableStream: { * unknown — the same dynamic boundary as JSON.parse: cast and validate. */ /* Headers — a response's native header map (lowercase names, * combine-on-append, sorted iteration). The value is a checked-dynamic - * native handle like Response itself; constructing one statically - * (`new Headers()`) keeps the constructor fence. */ + * native handle like Response itself. */ interface Headers { append(name: string, value: string): void; delete(name: string): void; @@ -767,7 +766,7 @@ interface Headers { [Symbol.iterator](): IterableIterator<[string, string]>; } declare var Headers: { - new (init?: Record | readonly (readonly [string, string])[]): Headers; + new (init?: Headers | Record | readonly (readonly [string, string])[]): Headers; readonly prototype: Headers; }; interface Response { @@ -810,6 +809,18 @@ interface Request { readonly method: string; readonly url: string; readonly headers: Headers; + readonly destination: string; + readonly referrer: string; + readonly referrerPolicy: string; + readonly mode: string; + readonly credentials: string; + readonly cache: string; + readonly redirect: "follow" | "error" | "manual"; + readonly integrity: string; + readonly keepalive: boolean; + readonly isReloadNavigation: boolean; + readonly isHistoryNavigation: boolean; + readonly duplex: "half"; readonly signal: AbortSignal; readonly body: ReadableStream | null; readonly bodyUsed: boolean; @@ -1059,14 +1070,12 @@ declare module "node:fs" { export function readFileSync(fd: number): Buffer; /* The options-object spelling of the utf8 form. */ export function readFileSync(path: string, options: { encoding: "utf8" | "utf-8" }): string; - /* Error-first callback target used by the static util.promisify - * projection. Its options stay unknown because direct calls remain - * outside the typed fs slice; this also lets checked-JS validation - * ladders observe invalid runtime encodings without contextual narrowing. */ + /* Error-first callbacks select string or Buffer data from their encoding. + * Unknown options retain that union until the caller validates the value. */ export function readFile( path: string, options: unknown, - callback: (error: Error | null, data: string) => void, + callback: (error: Error | null, data: string | Buffer) => void, ): void; /* The options form carries the mode (applied at CREATION only, like * Node — an existing file keeps its permissions) and/or the utf8 @@ -1678,6 +1687,9 @@ declare module "child_process" { } export interface ChildProcess extends Disposable { + ref(): void; + on(event: "spawn", listener: () => void): void; + once(event: "spawn", listener: () => void): void; /* The exit listener may also take Node's second parameter — the * terminating signal's name, null for a normal exit. */ on(event: "exit", listener: (code: number | null, signal: string | null) => void): void; @@ -1715,16 +1727,13 @@ declare module "child_process" { readonly stdout: NodeJS.ReadableStream | null; readonly stderr: NodeJS.ReadableStream | null; } - export function spawn( - command: string, - args?: string[], - options?: { + export interface SpawnOptions { /* The 3-tuple form admits number fds in the stdout/stderr slots — * openSync results dup2'd into the child (the daemon-log idiom * ["ignore", logFd, logFd]) — and "pipe" there too (child.stdout/ * child.stderr streams); "pipe" in the stdin slot exposes * child.stdin. */ - stdio: "ignore" | "inherit" | "pipe" | ("ignore" | "inherit" | "pipe" | number)[]; + stdio?: "ignore" | "inherit" | "pipe" | ("ignore" | "inherit" | "pipe" | number)[]; /* detached gives the child its own session and process group * (POSIX_SPAWN_SETSID); env REPLACES the child environment; cwd * sets its working directory; windowsHide is a POSIX no-op; shell @@ -1734,8 +1743,9 @@ declare module "child_process" { cwd?: string; windowsHide?: boolean; shell?: boolean; - }, - ): ChildProcess; + } + export function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess; + export function spawn(command: string, options?: SpawnOptions): ChildProcess; export function fork( modulePath: string | URL, diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 3bded59e6..ba7e9ea40 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -347,7 +347,8 @@ export function emitClassShapes( lines.push(` call void @scr_cyc_free(ptr %o)`); } else { host.declare(`declare void @free(ptr)`); - lines.push(` call void @free(ptr %o)`); + host.declare(`declare void @scr_weak_dispose(ptr)`); + lines.push(` call void @scr_weak_dispose(ptr %o)`, ` call void @free(ptr %o)`); } }; diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 52d205c5d..4788294f5 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -71,6 +71,7 @@ export const DYN_KIND = { export interface DynHost extends WalkerHost { unitInstanceRef(unionId: string, tag: number): string; liveDynRefAdapter(t: IrType): { snapshot: string; commit: string }; + dynPromiseAdapter(t: IrType): string; isErrorClass(className: string): boolean; classSubtypes(className: string): readonly string[]; } @@ -439,10 +440,12 @@ export class LlDyn { B.terminate(`ret i1 ${r}`); break; } - case "url": { - this.host.declare(`declare zeroext i1 @scr_dyn_native_url_is(ptr)`); + case "url": + case "searchParams": { + const native = t.kind === "url" ? "url" : "search_params"; + this.host.declare(`declare zeroext i1 @scr_dyn_native_${native}_is(ptr)`); const r = B.tmp(); - B.line(`${r} = call zeroext i1 @scr_dyn_native_url_is(ptr %d)`); + B.line(`${r} = call zeroext i1 @scr_dyn_native_${native}_is(ptr %d)`); B.terminate(`ret i1 ${r}`); break; } @@ -753,7 +756,7 @@ export class LlDyn { if (t.kind !== "union") { host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`); host.declare(`declare ptr @scr_dyn_typed_ref_cached_cast(ptr, ptr, ${host.sizeType})`); - host.declare(`declare void @scr_dyn_typed_ref_cache_cast(ptr, ptr, ${host.sizeType}, ptr, ptr, ptr)`); + host.declare(`declare void @scr_dyn_typed_ref_cache_cast(ptr, ptr, ${host.sizeType}, ptr, ptr, ptr, i1)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); const rc = vAdapters(host, t); const kind = this.kindOf(B, "%d"); @@ -853,14 +856,14 @@ export class LlDyn { B.line(`call void ${releaseSym(host, t)}(ptr ${checked})`); B.terminate(`ret ptr ${cached}`); B.startBlock(lCache); - B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release})`); + B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release}, i1 ${traceAdapter(host, t) !== null ? "true" : "false"})`); B.terminate(`ret ptr ${checked}`); } else { const lCache = B.newLabel("dc.tr.put"); const lReturn = B.newLabel("dc.tr.ret"); B.condBr(ok, lCache, lReturn); B.startBlock(lCache); - B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release})`); + B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release}, i1 ${traceAdapter(host, t) !== null ? "true" : "false"})`); B.br(lReturn); B.startBlock(lReturn); B.terminate(`ret ptr ${checked}`); @@ -889,10 +892,12 @@ export class LlDyn { B.terminate(`ret double ${r}`); break; } - case "url": { - host.declare(`declare ptr @scr_dyn_native_url_check(ptr, ptr)`); + case "url": + case "searchParams": { + const native = t.kind === "url" ? "url" : "search_params"; + host.declare(`declare ptr @scr_dyn_native_${native}_check(ptr, ptr)`); const r = B.tmp(); - B.line(`${r} = call ptr @scr_dyn_native_url_check(ptr %d, ptr %path)`); + B.line(`${r} = call ptr @scr_dyn_native_${native}_check(ptr %d, ptr %path)`); B.terminate(`ret ptr ${r}`); break; } @@ -984,6 +989,16 @@ export class LlDyn { requireKind(DYN_KIND.PROMISE, "dc"); host.declare(`declare ptr @scr_promise_retain(ptr)`); const promise = this.payloadOf(B, "%d", "ptr"), retained = B.tmp(); + if (t.inner.kind !== "dyn") { + host.declare(`declare ptr @scr_promise_new()`); + host.declare(`declare void @scr_promise_race_add(ptr, ptr, ptr)`); + B.line(`${retained} = call ptr @scr_promise_new()`); + host.declare(`declare void @scr_promise_share_identity(ptr, ptr)`); + B.line(`call void @scr_promise_share_identity(ptr ${retained}, ptr ${promise})`); + B.line(`call void @scr_promise_race_add(ptr ${retained}, ptr ${promise}, ptr @${host.dynPromiseAdapter(t.inner)})`); + B.terminate(`ret ptr ${retained}`); + break; + } B.line(`${retained} = call ptr @scr_promise_retain(ptr ${promise})`); B.terminate(`ret ptr ${retained}`); break; @@ -1288,7 +1303,7 @@ export class LlDyn { // live narrowed alias). host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`); host.declare(`declare ptr @scr_dyn_typed_ref_cached_cast(ptr, ptr, ${host.sizeType})`); - host.declare(`declare void @scr_dyn_typed_ref_cache_cast(ptr, ptr, ${host.sizeType}, ptr, ptr, ptr)`); + host.declare(`declare void @scr_dyn_typed_ref_cache_cast(ptr, ptr, ${host.sizeType}, ptr, ptr, ptr, i1)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); host.declare(`declare void @scr_union_release(ptr)`); const rc = vAdapters(host, t); @@ -1349,7 +1364,7 @@ export class LlDyn { B.line(`call void @scr_union_release(ptr ${checked})`); B.terminate(`ret ptr ${cached}`); B.startBlock(lCache); - B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release})`); + B.line(`call void @scr_dyn_typed_ref_cache_cast(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${checked}, ptr ${rc.retain}, ptr ${rc.release}, i1 ${traceAdapter(host, t) !== null ? "true" : "false"})`); B.terminate(`ret ptr ${checked}`); B.startBlock(lFail); B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`); @@ -1451,10 +1466,12 @@ export class LlDyn { const B = new BlockBuilder(); let sourceAccessor: { name: string; release: string } | null = null; switch (t.kind) { - case "url": { - host.declare(`declare ptr @scr_dyn_native_url(ptr)`); + case "url": + case "searchParams": { + const native = t.kind === "url" ? "url" : "search_params"; + host.declare(`declare ptr @scr_dyn_native_${native}(ptr)`); const r = B.tmp(); - B.line(`${r} = call ptr @scr_dyn_native_url(ptr %v)`); + B.line(`${r} = call ptr @scr_dyn_native_${native}(ptr %v)`); B.terminate(`ret ptr ${r}`); break; } @@ -1781,6 +1798,8 @@ export class LlDyn { B.terminate(`br label %${doneLabel}`); B.startBlock(doneLabel); }); + host.declare(`declare void @scr_arr_copy_metadata(ptr, ptr)`); + B.line(`call void @scr_arr_copy_metadata(ptr %v, ptr ${d})`); if (cyclicArr) B.line(`call void @scr_dyn_from_leave()`); host.declare(`declare ptr @scr_dyn_mark_snapshot(ptr)`); B.line(`call ptr @scr_dyn_mark_snapshot(ptr ${d})`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index cc8a83b3f..b2a80197b 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -181,8 +181,8 @@ export class LlEmitter { * immortal (rc == SIZE_MAX) static per (union, unit tag). * RC entry points and the collector skip immortals. */ private readonly unitInstances = new Map(); - /** Interned regex literals: "/" → { symbol, interned - * source/flags literal refs } — one immortal ScrRegex per distinct + /** Regex literal templates: "/" → { symbol, interned + * source/flags literal refs } — one immortal ScrRegex template per distinct * (pattern, flags) pair; the bytecode slot starts null and the runtime * compiles it lazily on first use. The source/flags strings intern at * REGISTRATION (bodies emit before the literal table flushes). */ @@ -400,6 +400,7 @@ export class LlEmitter { cstr: (text) => this.cstr(text), unitInstanceRef: (unionId, tag) => this.unitInstanceRef(unionId, tag), liveDynRefAdapter: (type) => this.liveDynRefAdapter(type), + dynPromiseAdapter: (type) => this.dynPromiseAdapter(type), isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error", classSubtypes: (name) => { const target = this.classMetaOf(name); @@ -948,6 +949,15 @@ export class LlEmitter { // Declared NOW — the extern block flushes before main assembles. if (usesEvents) this.declare(`declare void @scr_events_install()`); if (usesChildProcess) this.declare(`declare void @scr_child_dyn_install()`); + const childStreamBoxes = usesChildProcess && usesStream && + this.classMeta.has("%Readable") && this.classMeta.has("%Writable") + ? [this.dyn.toDynHelper({ kind: "object", className: "%Readable" }), + this.dyn.toDynHelper({ kind: "object", className: "%Writable" })] + : null; + if (childStreamBoxes) { + this.declare(`@scr_stream_child_ops = external constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr }`); + this.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`); + } if (usesFsWatch) this.declare(`declare void @scr_watch_install()`); if (this.ffiHasForeignCallback) this.declare(`declare void @scr_ffi_install()`); if (usesStream) this.declare(`declare void @scr_stream_install()`); @@ -1137,11 +1147,11 @@ export class LlEmitter { `%ScrUnion = type { ${this.sizeType}, i32, ptr, ptr, ptr, i64 }`, `%ScrClosure = type { ${this.sizeType}, ptr, ${this.sizeType}, ptr, i32 }`, `%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`, - `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr }`, + `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr }`, // ScrArr mirrors scr_runtime.h field-for-field. Live dynamic stream // commits swap its mutable dense, sparse, presence, and property // storage while preserving the target object's identity. - `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, ptr, ptr, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ${this.sizeType} }`, + `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, ptr, ptr, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ${this.sizeType}, ptr }`, // The runtime error prefix { rc, vt, name, message, code, cause } and the // class-object shape { rc, pre, post, ctor, name } — field reads on // builtin errors and classval loads GEP through these. @@ -1249,7 +1259,7 @@ export class LlEmitter { // the interned source/flags strings. The bc slot starts null (lazy // compile, cached by the runtime) — a mutable global, not constant. out.push( - `@${re.sym} = internal ${tl}global %ScrRegex { ${this.sizeType} -1, ptr ${re.src}, ptr ${re.fl}, ptr null } ; ${key.replace(/\n/g, "\\n")}`, + `@${re.sym} = internal ${tl}global %ScrRegex { ${this.sizeType} -1, ptr ${re.src}, ptr ${re.fl}, ptr null, double 0.0, ptr null } ; ${key.replace(/\n/g, "\\n")}`, ); } if (this.regexInstances.size > 0) out.push(``); @@ -1266,7 +1276,7 @@ export class LlEmitter { out.push( `@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, - `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, ptr null, ptr null, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null, ${this.sizeType} 0, ${this.sizeType} 0, ptr null, ${this.sizeType} 0, ${this.sizeType} 0 }`, + `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, ptr null, ptr null, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null, ${this.sizeType} 0, ${this.sizeType} 0, ptr null, ${this.sizeType} 0, ${this.sizeType} 0, ptr null }`, ); } if (this.templateStringsInstances.size > 0) out.push(``); @@ -1414,6 +1424,7 @@ export class LlEmitter { // this line is emitted (native-toolchain.ts gates on the same predicate). ...(usesEvents ? [` call void @scr_events_install()`] : []), ...(usesChildProcess ? [` call void @scr_child_dyn_install()`] : []), + ...(childStreamBoxes ? [` call void @scr_child_dyn_streams_install(ptr @scr_stream_child_ops, ptr @${childStreamBoxes[0]}, ptr @${childStreamBoxes[1]})`] : []), // fs.watch programs fill the loop's watch hooks the same way — // scr_watch.c links only when this line is emitted. ...(usesFsWatch ? [` call void @scr_watch_install()`] : []), diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 27bfd29a1..f514116de 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -157,6 +157,11 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" | // BORROWED by scr_dyn_invoke; the result is owned and may ride a // pending exception. const recv = host.emitExpr(e.recv); + host.declare(`declare ptr @scr_dyn_prepare_method(ptr, ptr)`); + const prepared = B.tmp(); + B.line(`${prepared} = call ptr @scr_dyn_prepare_method(ptr ${recv.name}, ptr ${host.cstr(e.method)})`); + host.own({ name: prepared, type: { kind: "dyn" } }); + host.emitPendingCheck(); const args = e.args.map((a) => host.emitExpr(a)); let argsPtr = "null"; if (args.length > 0) { @@ -169,10 +174,10 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" | }); argsPtr = arr; } - host.declare(`declare ptr @scr_dyn_invoke(ptr, ptr, ptr, ${host.sizeType}, ptr)`); + host.declare(`declare ptr @scr_dyn_invoke_prepared(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); const t = B.tmp(); B.line( - `${t} = call ptr @scr_dyn_invoke(ptr ${recv.name}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${host.cstr(e.calleeName)})`, + `${t} = call ptr @scr_dyn_invoke_prepared(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${host.cstr(e.calleeName)})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); @@ -490,9 +495,9 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" | const d = host.emitExpr(e.value); let test: string; if (e.test === "bytes") { - host.declare(`declare zeroext i1 @scr_dyn_bytes_is(ptr, i32)`); + host.declare(`declare zeroext i1 @scr_dyn_typed_array_is(ptr, i32)`); test = B.tmp(); - B.line(`${test} = call zeroext i1 @scr_dyn_bytes_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`); + B.line(`${test} = call zeroext i1 @scr_dyn_typed_array_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`); } else if (e.test === "truthy") { host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`); test = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index c443a4204..4efbcc1ff 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -93,6 +93,11 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un }; if ((e.op === "===" || e.op === "!==") && e.left.type.kind === "bool") { B.line(`${t} = icmp ${e.op === "===" ? "eq" : "ne"} i1 ${l.name}, ${r.name}`); + } else if ((e.op === "===" || e.op === "!==") && e.left.type.kind === "promise" && e.right.type.kind === "promise") { + host.declare(`declare zeroext i1 @scr_promise_identity_equal(ptr, ptr)`); + const equal = e.op === "===" ? t : B.tmp(); + B.line(`${equal} = call zeroext i1 @scr_promise_identity_equal(ptr ${l.name}, ptr ${r.name})`); + if (e.op === "!==") B.line(`${t} = xor i1 ${equal}, true`); } else if ((e.op === "===" || e.op === "!==") && host.llType(e.left.type) === "ptr") { // Reference identity (JS object equality) — closures, arrays, // records compared as pointers, exactly the C `==`. @@ -406,7 +411,10 @@ export function emitStringExpr(host: LlvmEmitterContext, e: ExprOf<"strConcat" | }; host.regexInstances.set(key, re); } - return host.own({ name: host.retainValue(`@${re.sym}`, e.type), type: e.type }); + host.declare("declare ptr @scr_regex_literal(ptr)"); + const value = B.tmp(); + B.line(`${value} = call ptr @scr_regex_literal(ptr @${re.sym})`); + return host.own({ name: value, type: e.type }); } case "templateStrings": { // One immortal static string array per template SITE (the key); diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 0d282369b..639f82893 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -83,7 +83,8 @@ export function emitRegexIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind: B.line(`${t} = call zeroext i1 @scr_regex_test(ptr ${r.name}, ptr ${args[0]!.name})`); return { name: t, type: e.type }; } - case "match": { + case "match": + case "exec": { // +1 string[] or NULL from the runtime; the `string[] | null` // union wraps type-directedly, the envGet convention. if (e.type.kind !== "union") throw new InternalCompilerError("llvm emitter bug: match result not a union"); @@ -91,9 +92,10 @@ export function emitRegexIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind: const arrTag = def ? def.arms.findIndex((a) => a.kind === "array") : -1; const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; if (arrTag < 0 || nullTag < 0 || !def) throw new InternalCompilerError("llvm emitter bug: match union lacks its arms"); - host.declare(`declare ptr @scr_regex_match(ptr, ptr)`); + const symbol = e.method === "exec" ? "scr_regex_exec" : "scr_regex_match"; + host.declare(`declare ptr @${symbol}(ptr, ptr)`); const raw = B.tmp(); - B.line(`${raw} = call ptr @scr_regex_match(ptr ${r.name}, ptr ${args[0]!.name})`); + B.line(`${raw} = call ptr @${symbol}(ptr ${r.name}, ptr ${args[0]!.name})`); return host.wrapNullable(raw, raw, def.arms[arrTag]!, arrTag, e.type, nullTag); } case "search": { @@ -108,6 +110,12 @@ export function emitRegexIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind: B.line(`${t} = call ptr @scr_regex_source(ptr ${r.name})`); return host.own({ name: t, type: e.type }); } + case "lastIndex": { + host.declare("declare double @scr_regex_last_index(ptr)"); + const t = B.tmp(); + B.line(`${t} = call double @scr_regex_last_index(ptr ${r.name})`); + return { name: t, type: e.type }; + } case "flags": { host.declare(`declare ptr @scr_regex_flags(ptr)`); const t = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index eb4e7a8e7..78655c7a5 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -2,7 +2,7 @@ import { typedRefConstructor } from "./shapes.js"; /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { streamTypedRefEligible } from "../../ir/analysis.js"; -import { type IrType, DYN_CLASS_PROPERTIES, SYMBOL_T, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js"; +import { type IrType, DYN_CLASS_PROPERTIES, RUNTIME_STREAM_CLASSES, SYMBOL_T, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js"; import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; import { classFieldIndex, classStructSym } from "./classes.js"; @@ -11,7 +11,7 @@ import { FN_ATTRS, llFieldType, releaseSym, traceArg, vAdapters } from "./shapes import type { LlvmEmitterContext, LlStreamTypedRefAdapter } from "./expr-context.js"; export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): string { - if (!isRefCounted(inner) || inner.kind === "dyn") { + if (inner.kind === "dyn") { throw new InternalCompilerError( `dynamic promise adapter requires a concrete reference type, got ${typeKey(inner)}`, ); @@ -31,7 +31,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri const pending = B.tmp(); B.line(`${dyn} = call ptr @scr_promise_payload_ref(ptr %src)`); B.line( - `${value} = call ptr @${host.dyn.dynCheckHelper(inner)}(ptr ${dyn}, ptr null)`, + inner.kind === "void" ? `; void fulfillment ignores the checked payload` : `${value} = call ${host.llType(inner)} @${host.dyn.dynCheckHelper(inner)}(ptr ${dyn}, ptr null)`, ); B.line(`call void @scr_dyn_release_v(ptr ${dyn})`); B.line(`${pending} = call zeroext i1 @scr_exc_pending()`); @@ -42,7 +42,14 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri B.line(`call void @scr_promise_reject_pending(ptr %dst)`); B.terminate(`ret void`); B.startBlock(ok); - if (inner.kind === "string") { + if (inner.kind === "void") { + host.declare(`declare void @scr_promise_fulfill_void(ptr)`); + B.line(`call void @scr_promise_fulfill_void(ptr %dst)`); + } else if (inner.kind === "f64" || inner.kind === "date" || inner.kind === "bool") { + const fn = inner.kind !== "bool" ? "scr_promise_fulfill_f64" : "scr_promise_fulfill_bool"; + host.declare(`declare void @${fn}(ptr, ${host.llType(inner)}${inner.kind === "bool" ? " zeroext" : ""})`); + B.line(`call void @${fn}(ptr %dst, ${host.llType(inner)}${inner.kind === "bool" ? " zeroext" : ""} ${value})`); + } else if (inner.kind === "string") { host.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`); B.line(`call void @scr_promise_fulfill_str(ptr %dst, ptr ${value})`); } else { @@ -111,6 +118,7 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext, { name: "props", index: 12, type: "ptr" }, { name: "prop_len", index: 13, type: host.sizeType }, { name: "prop_cap", index: 14, type: host.sizeType }, + { name: "metadata", index: 15, type: "ptr" }, ]; for (const member of storageMembers) { lines.push( @@ -460,7 +468,7 @@ export function streamTypedRefBoxValue(host: LlvmEmitterContext, B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${value})`); return boxed; } - if (!streamTypedRefEligible(t) && !isDynTypedRefType(t)) { + if (t.kind === "bytes" || !streamTypedRefEligible(t) && !isDynTypedRefType(t)) { const valueTy = t.kind === "f64" ? "double" : t.kind === "bool" @@ -513,12 +521,32 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext, `llvm emitter bug: typed-ref materialize of unknown class ${t.className}`, ); } + if (meta.hierarchy && meta.children.length) { + const vtSlot = B.tmp(), vt = B.tmp(), preSlot = B.tmp(), pre = B.tmp(); + B.line(`${vtSlot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 1`); + B.line(`${vt} = load ptr, ptr ${vtSlot}`); + B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`); + B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`); + const descendants = (current: typeof meta): (typeof meta)[] => current.children.flatMap((child) => [child, ...descendants(child)]); + for (const child of descendants(meta).reverse()) { + const childAdapter = host.liveDynRefAdapter({ kind: "object", className: child.def.name }); + const exact = B.tmp(), yes = B.newLabel("class.derived"), next = B.newLabel("class.next"); + B.line(`${exact} = icmp eq ${host.sizeType} ${pre}, ${child.pre}`); + B.condBr(exact, yes, next); B.startBlock(yes); + const result = B.tmp(); B.line(`${result} = call ptr @${childAdapter.snapshot}(ptr %p)`); + B.terminate(`ret ptr ${result}`); B.startBlock(next); + } + } host.declare(`declare ptr @scr_dyn_new_obj()`); host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`); const out = B.tmp(); if (meta.root.def.name === "%Error") { host.declare(`declare ptr @scr_error_dyn_fields(ptr)`); B.line(`${out} = call ptr @scr_error_dyn_fields(ptr %p)`); + } else if (meta.def.runtime && RUNTIME_STREAM_CLASSES.has(t.className) && + !meta.def.instancePrototypeHelper && !meta.def.prototypeDataHelper) { + host.declare(`declare ptr @scr_stream_dyn_view(ptr)`); + B.line(`${out} = call ptr @scr_stream_dyn_view(ptr %p)`); } else B.line(`${out} = call ptr @scr_dyn_new_obj()`); if (meta.def.instancePrototypeHelper || meta.def.prototypeDataHelper) { host.declare(`declare ptr @scr_dyn_set_prototype(ptr, ptr)`); @@ -717,6 +745,8 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext, B.br(doneLabel); B.startBlock(doneLabel); }); + host.declare(`declare void @scr_arr_copy_metadata(ptr, ptr)`); + B.line(`call void @scr_arr_copy_metadata(ptr %p, ptr ${out})`); B.terminate(`ret ptr ${out}`); } else { const out = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index c95d5c045..defe4f962 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -23,18 +23,37 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr B.line(`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter})`); return host.own({ name: out, type: e.type }); } - if (e.fn === "cp.spawn" || e.fn === "cp.spawnOpts" || e.fn === "cp.fork") { + if (e.fn === "cp.spawn" || e.fn === "cp.spawnOpts" || e.fn === "cp.spawnDynamic" || e.fn === "cp.fork") { // child_process.spawn: the child starts NOW (posix_spawnp); the // loop reaps it and fires its listeners. Never throws — spawn // failure defers to "error". host.usesTimers = true; - const sym = e.fn === "cp.spawn" ? "scr_spawn" : e.fn === "cp.fork" ? "scr_fork" : "scr_spawn_opts"; + const sym = e.fn === "cp.spawn" ? "scr_spawn" : e.fn === "cp.fork" ? "scr_fork" : e.fn === "cp.spawnDynamic" ? "scr_spawn_dynamic" : "scr_spawn_opts"; const args = e.args.map((a) => host.emitExpr(a)); const argDecl = args.map((a) => (host.llType(a.type) === "i1" ? "i1 zeroext" : host.llType(a.type))).join(", "); host.declare(`declare ptr @${sym}(${argDecl})`); const t = B.tmp(); B.line(`${t} = call ptr @${sym}(${args.map((a) => `${host.llType(a.type)} ${a.name}`).join(", ")})`); - return host.own({ name: t, type: e.type }); + const out = host.own({ name: t, type: e.type }); + if (e.fn === "cp.spawnDynamic") host.emitPendingCheck(); + return out; + } + if (e.fn === "child.onDyn") { + const child = host.emitExpr(e.args[0]!); + const event = host.emitExpr(e.args[1]!); + const cb = host.emitExpr(e.args[2]!); + host.declare("declare void @scr_child_on_dyn(ptr, ptr, ptr)"); + B.line(`call void @scr_child_on_dyn(ptr ${child.name}, ptr ${event.name}, ptr ${cb.name})`); + host.emitPendingCheck(); + return { name: "", type: e.type }; + } + if (e.fn === "child.onSpawn") { + const child = host.emitExpr(e.args[0]!); + const cb = host.emitExpr(e.args[1]!); + host.moveTemp(cb); + host.declare("declare void @scr_child_on_spawn(ptr, ptr)"); + B.line(`call void @scr_child_on_spawn(ptr ${child.name}, ptr ${cb.name})`); + return { name: "", type: e.type }; } if (e.fn === "child.onExit" || e.fn === "child.onClose") { // The callback MOVES into the child's registry; the third diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9a512db81..b02cdd7ba 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -35,6 +35,8 @@ export const LIB_FN_SYMS: Record = { "dyn.fromEntries": "scr_dyn_from_entries", "bytes.constructor": "scr_bytes_constructor", "bytes.construct": "scr_bytes_construct", + "bytes.instanceOf": "scr_bytes_instanceof", + "arrayBuffer.constructor": "scr_array_buffer_constructor", "arrayBuffer.new": "scr_array_buffer_new", "ffi.argument": "scr_ffi_argument", "ffi.memoryModule": "scr_ffi_memory_module", @@ -91,6 +93,7 @@ export const LIB_FN_SYMS: Record = { "str.encodeUriComponent": "scr_str_encode_uri_component", "error.cause": "scr_error_cause", "error.setCause": "scr_error_set_cause", + "error.defineCause": "scr_error_define_cause", "error.deleteCause": "scr_error_delete_cause", "error.hasCause": "scr_error_has_cause", // DOMException: construction and the read surface never throw; the @@ -104,6 +107,7 @@ export const LIB_FN_SYMS: Record = { "num.toExponential": "scr_num_to_exponential", "num.toFixed0": "scr_num_to_fixed0", "num.toFixed": "scr_num_to_fixed", + "num.toStringRadix": "scr_num_to_string_radix", "num.sameValue": "scr_num_same_value", "intl.numFormatEnUs": "scr_intl_num_format_en_us", "number.isFinite": "scr_num_is_finite", @@ -154,6 +158,8 @@ export const LIB_FN_SYMS: Record = { "global.native": "scr_global_native", "process.builtinModule": "scr_process_builtin_module", "process.builtinUnsupported": "scr_process_builtin_unsupported", + "fs.callbackValue": "scr_fs_callback_value", + "fs.callbackCall": "scr_fs_callback_call", "process.hrtimeValue": "scr_process_hrtime_value", "process.versionsOpenssl": "scr_process_versions_openssl", "process.umask": "scr_process_umask", @@ -288,6 +294,7 @@ export const LIB_FN_SYMS: Record = { "buffer.fromDyn": "scr_buffer_from_dyn", "buffer.concat": "scr_bytes_concat", "buffer.concatLen": "scr_bytes_concat_len", + "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", "buffer.byteLenStr": "scr_bytes_byte_length_str", "buffer.isEncoding": "scr_bytes_is_encoding", // The checked-dynamic compare/equals validators (scr_bytes_io.c): @@ -298,9 +305,11 @@ export const LIB_FN_SYMS: Record = { "dyn.toNumeric": "scr_dyn_to_numeric", "dyn.increment": "scr_dyn_increment", "dyn.numberConstructor": "scr_dyn_number_constructor", + "dyn.bigintConstructor": "scr_dyn_bigint_constructor", "dyn.toNumberCoerce": "scr_dyn_number_coerce", "dyn.add": "scr_dyn_add", "dyn.arithmetic": "scr_dyn_arithmetic", + "dyn.compare": "scr_dyn_compare", "dyn.bitwise": "scr_dyn_bitwise", "dyn.proxyNew": "scr_dyn_proxy_new", "buffer.compareChk": "scr_buffer_compare_chk", @@ -349,7 +358,13 @@ export const LIB_FN_SYMS: Record = { "stats.mtimeMs": "scr_stats_mtime_ms", "stats.ctimeMs": "scr_stats_ctime_ms", "text.decode": "scr_text_decode", + "bytes.bufferSource": "scr_bytes_buffer_source", + "text.decodeBufferSource": "scr_text_decode_buffer_source", + "text.decodeOptions": "scr_text_decode_options", + "text.decodeStream": "scr_text_decode_stream", + "text.decodeLegacyOptions": "scr_text_decode_legacy_options", "text.decodeLegacy": "scr_text_decode_legacy", + "text.decoderEncoding": "scr_text_decoder_encoding", "zlib.deflateSync": "scr_zlib_deflate", "zlib.inflateSync": "scr_zlib_inflate", "zlib.crc32": "scr_zlib_crc32", @@ -363,6 +378,7 @@ export const LIB_FN_SYMS: Record = { "crypto.randomUUID": "scr_crypto_random_uuid", "crypto.hashDigestStr": "scr_crypto_hash_digest_str", "crypto.hashDigestBytes": "scr_crypto_hash_digest_bytes", + "crypto.native": "scr_crypto_native", "crypto.hashNew": "scr_crypto_hash_new", "crypto.hmacNewStr": "scr_crypto_hmac_new_str", "crypto.hmacNewBytes": "scr_crypto_hmac_new_bytes", @@ -426,6 +442,7 @@ export const LIB_FN_SYMS: Record = { "url.pathToFileURLPlatform": "scr_url_from_path_platform", "url.pathToFileURLWin32": "scr_url_from_path", "sp.new": "scr_sp_new", + "sp.newChecked": "scr_sp_new_checked", "sp.parse": "scr_sp_parse", "sp.copy": "scr_sp_copy", "sp.fromPairs": "scr_sp_from_pairs", @@ -474,6 +491,7 @@ export const LIB_FN_SYMS: Record = { "child.kill": "scr_child_kill", "child.killNum": "scr_child_kill_num", "child.unref": "scr_child_unref", + "child.ref": "scr_child_ref", "procStream.write": "scr_proc_stream_write", "process.kill": "scr_process_kill_named", "process.killNum": "scr_process_kill", @@ -550,6 +568,10 @@ export const LIB_FN_SYMS: Record = { // ambient-this read never throw. The fs dyn read is the sync-fs story. "fetch.start": "scr_fetch_static", "fetch.responseNew": "scr_fetch_response_new", + "fetch.input": "scr_fetch_input", + "fetch.function": "scr_fetch_function", + "fetch.requestNew": "scr_fetch_request_new", + "fetch.headersNew": "scr_fetch_headers_new", "fetch.responseArrayBuffer": "scr_fetch_response_array_buffer", "fetch.responseJson": "scr_fetch_response_json", "fetch.responseText": "scr_fetch_response_text", @@ -558,6 +580,8 @@ export const LIB_FN_SYMS: Record = { "fetch.abortTimeout": "scr_fetch_abort_timeout", "fetch.abortNow": "scr_fetch_abort_now", "fetch.abortAny": "scr_fetch_abort_any", + "fetch.webIs": "scr_fetch_web_is", + "fetch.streamIs": "scr_fetch_stream_is", "fetch.streamNew": "scr_fetch_stream_new", "fetch.streamFrom": "scr_fetch_stream_from", "fetch.readerRead": "scr_fetch_reader_read", @@ -604,6 +628,8 @@ export const LIB_FN_SYMS: Record = { "date.checkedValue": "scr_dyn_native_date_value", "dyn.nativeRegexIs": "scr_dyn_native_regex_is", "dyn.toString": "scr_dyn_to_string_argument", + "dyn.dataViewIs": "scr_dyn_data_view_is", + "dyn.construct": "scr_dyn_construct", "dyn.this": "scr_dyn_this_get", "dyn.generatorThis": "scr_gen_receiver", "insp.dyn": "scr_insp_dyn", @@ -677,6 +703,8 @@ export const LIB_FN_SYMS: Record = { "writable.uncork": "scr_stream_uncork", "stream.destroyErr": "scr_stream_destroy", "stream.iteratorClose": "scr_stream_iterator_close", + "stream.onDyn": "scr_stream_on_dyn", + "readable.readDyn": "scr_stream_read_dyn", "readable.newDyn": "scr_stream_new_readable_dyn", "writable.newDyn": "scr_stream_new_writable_dyn", "duplex.newDyn": "scr_stream_new_duplex_dyn", @@ -849,6 +877,7 @@ export const LIB_FN_SYMS: Record = { "dc.chanHasSubscribers": "scr_dc_chan_has_subscribers", "dc.chanName": "scr_dc_chan_name", "str.decodeUriComponent": "scr_str_decode_uri_component", + "str.decodeUri": "scr_str_decode_uri", // TracingChannel (scr_dc.c): registry handles are plain doubles; the // subscribe/trace forms ride the may-throw pending check, and // tcTracePromise marks the loop live (USES_TIMERS_LIB_FNS — the @@ -908,12 +937,14 @@ export const LIB_FN_SYMS: Record = { "dyn.propertyIsEnumerable": "scr_dyn_property_is_enumerable", "dyn.assign": "scr_dyn_assign", "dyn.copyDataProperties": "scr_dyn_copy_data_properties", + "dyn.objectRest": "scr_dyn_object_rest", // variadic Object.assign: the source pack (push never throws; the // spread flatten throws V8's spread-call TypeErrors) and the final // left-to-right copy (ToObject TypeError on a nullish target). "dyn.packPush": "scr_dyn_pack_push", "dyn.packPushSpread": "scr_dyn_pack_push_spread", "dyn.packPushSpreadIter": "scr_dyn_pack_push_spread_iter", + "dyn.reflectApply": "scr_dyn_reflect_apply", "dyn.assignAll": "scr_dyn_assign_all", "dyn.objCreateNullProto": "scr_dyn_new_obj_null_proto", "dyn.arrayPrototype": "scr_dyn_array_prototype", @@ -943,6 +974,7 @@ export const LIB_FN_SYMS: Record = { "dyn.cloneMissing": "scr_structured_clone_missing", "dyn.cloneTransferFail": "scr_structured_clone_transfer_fail", "regex.new": "scr_regex_new", + "regex.resetLastIndex": "scr_regex_reset_last_index", "regex.newChecked": "scr_regex_new_checked", // queueMicrotask's checked-dynamic form (borrowed dyn; a non-function // throws synchronously), the minted setImmediate value, and the @@ -954,7 +986,7 @@ export const LIB_FN_SYMS: Record = { export const USES_TIMERS_LIB_FNS = new Set([ "process.stdio", - "fetch.start", + "fetch.start", "fetch.input", "fetch.function", "fetch.requestNew", "fetch.abortTimeout", "fetch.streamNew", "fetch.streamFrom", "readable.new", "writable.new", "duplex.new", "transform.new", "passthrough.new", "readable.init", "writable.init", "duplex.init", "transform.init", "passthrough.init", @@ -962,7 +994,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "readable.initDyn", "writable.initDyn", "duplex.initDyn", "transform.initDyn", "passthrough.initDyn", "readable.push", "readable.pushStr", "readable.pushStrEnc", "readable.pushEncoding", "readable.pushNull", "readable.pushU", "readable.pushDyn", - "readable.unshift", "readable.unshiftStr", "readable.read", "readable.setEncoding", + "readable.unshift", "readable.unshiftStr", "readable.read", "readable.readDyn", "readable.setEncoding", "readable.nextChunk", "readable.nextChunkDyn", "readable.fromArr", "readable.resume", "readable.pipe", "readable.unpipe", "writable.write", "writable.writeStr", "writable.writeU", "writable.writeDyn", @@ -979,6 +1011,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "net.listenOptsReusePort", "net.listenOptsReusePortCb", "net.connect", "net.connectCb", "net.connectLookup", "net.connectAttempt", "fs.existsChk", + "fs.callbackValue", "fs.callbackCall", "http.createServer", "http.createServerEmpty", "http.request", "http.requestCb", "http.requestUrl", "http.requestUrlCb", "https.request", "https.requestCb", "https.requestUrl", "https.requestUrlCb", diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 7cddfd26d..5496425ab 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -171,7 +171,9 @@ export function traceArg(host: ShapeHost, t: IrType): string { /** Keep native capsule edges visible when the referent is cycle-capable. */ export function typedRefConstructor(host: ShapeHost, t: IrType): string { - const name = traceAdapter(host, t) === null ? "scr_dyn_new_typed_ref" : "scr_dyn_new_typed_ref_traced"; + const observed = t.kind === "record" || t.kind === "array" || t.kind === "map" || t.kind === "set" || + t.kind === "object" && !RUNTIME_ERROR_CLASSES.has(t.className) && t.className !== RUNTIME_EMITTER_CLASS && !RUNTIME_STREAM_CLASSES.has(t.className); + const name = traceAdapter(host, t) !== null ? "scr_dyn_new_typed_ref_traced" : observed ? "scr_dyn_new_typed_ref_observed" : "scr_dyn_new_typed_ref"; host.declare(`declare ptr @${name}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`); return `@${name}`; } @@ -438,7 +440,8 @@ export function emitRecordShapes(host: ShapeHost, mod: IrModule): { typeDefs: st freeBody.push(` call void @scr_cyc_free(ptr %o)`); } else { host.declare(`declare void @free(ptr)`); - freeBody.push(` call void @free(ptr %o)`); + host.declare(`declare void @scr_weak_dispose(ptr)`); + freeBody.push(` call void @scr_weak_dispose(ptr %o)`, ` call void @free(ptr %o)`); } defs.push(...releaseBody(host, mangleRecordRelease(shape.id), traced, freeBody), ``); diff --git a/packages/compiler/src/backend/native-toolchain.ts b/packages/compiler/src/backend/native-toolchain.ts index 5cbccb01e..5e7aa4211 100644 --- a/packages/compiler/src/backend/native-toolchain.ts +++ b/packages/compiler/src/backend/native-toolchain.ts @@ -86,7 +86,7 @@ function stableTestMemo( return pending; } -export const EXECUTABLE_RUNTIME_SOURCES = ["scr_number.c", "scr_bigint.c", "scr_string.c", "scr_grapheme.c", "scr_array.c", "scr_bytes.c", "scr_bytes_io.c", "scr_map.c", "scr_closure.c", "scr_ffi.c", "scr_object.c", "scr_union.c", "scr_exception.c", "scr_error.c", "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_json.c", "scr_node_builtin.c", "scr_async.c", "scr_crypto_async.c", "scr_child.c", "scr_cycle.c"] as const; +export const EXECUTABLE_RUNTIME_SOURCES = ["scr_number.c", "scr_bigint.c", "scr_string.c", "scr_grapheme.c", "scr_array.c", "scr_bytes.c", "scr_bytes_io.c", "scr_map.c", "scr_closure.c", "scr_ffi.c", "scr_object.c", "scr_union.c", "scr_exception.c", "scr_error.c", "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_url_params.c", "scr_json.c", "scr_node_builtin.c", "scr_async.c", "scr_crypto_async.c", "scr_child.c", "scr_cycle.c"] as const; /** * Per-target section-elimination recipe. This belongs beside the native @@ -311,11 +311,8 @@ export interface CcOptions { * Symbol-free binaries keep their exact link line. */ symbol?: boolean; bigint?: boolean; - /** The program uses the URLSearchParams surface (moduleUsesSearchParams - * on the IR): compiles scr_url_params.c into the binary — the symbol - * gating precedent: pure data structure (no loop hooks, no install), - * cross-compiles everywhere. sp-free binaries keep their exact link - * line (scr_url.c never references the unit). */ + /** Records URLSearchParams use in the build identity. Its runtime unit is + * part of the base sources because checked URL handles expose searchParams. */ searchParams?: boolean; /** The program uses the node:querystring surface (moduleUsesQs on the * IR): compiles scr_qs.c into the binary — the searchParams gating @@ -992,7 +989,6 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise ...(opts.inspect ? ["scr_inspect.c", "scr_console_native.c"] : []), ...(opts.symbol ? ["scr_symbol.c"] : []), ...(opts.assert && opts.bigint ? ["scr_bigint_assert.c"] : []), - ...(opts.searchParams ? ["scr_url_params.c"] : []), ...(opts.emitter ? ["scr_events_emitter.c", "scr_dyn_handle.c"] : []), ...(opts.dynInvoke ? ["scr_dyn_invoke.c"] : []), ...(opts.zlib ? ["scr_zlib.c"] : []), @@ -4310,7 +4306,6 @@ async function compileCInternal( ...(opts.emitter || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []), ...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []), ...(opts.assert && opts.bigint ? [rt(join(rtDir, "scr_bigint_assert.c"))] : []), - ...(opts.searchParams ? [rt(join(rtDir, "scr_url_params.c"))] : []), ...(opts.qs ? [rt(join(rtDir, "scr_qs.c"))] : []), ...(opts.parseArgs ? [rt(join(rtDir, "scr_util.c"))] : []), ...(opts.stream ? [rt(join(rtDir, "scr_stream.c"))] : []), diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 2da115602..02dbd2c2d 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 5 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v5" as const; +export const RUNTIME_ABI_VERSION = 6 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v6" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index cd3a4a9dd..502cc779f 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -34,7 +34,7 @@ async function fixture(target: NativeTargetSpec) { const manifest: RuntimePackManifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: target.runtimePackPackage, version: compilerReleaseVersion(), target: { name: target.name, llvm_triple: target.llvmTriple, architecture: target.architecture, object_format: target.objectFormat, minimum_os: target.minimumOs }, - runtime_abi: { version: 5, marker: "scr_runtime_abi_v5" }, + runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index bc4becd4b..2d11fdc0c 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -102,7 +102,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 5, marker: "scr_runtime_abi_v5" }, + runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/compat/fetch-profile.ts b/packages/compiler/src/compat/fetch-profile.ts index 24073e03a..1958f3c8d 100644 --- a/packages/compiler/src/compat/fetch-profile.ts +++ b/packages/compiler/src/compat/fetch-profile.ts @@ -170,8 +170,6 @@ const outOfScopeEntry = ( reason, }); -const constructorUnsupported = - "the interface constructor has no compiler bridge in either tier"; const widerMemberFence = "the member is outside the native static handle projection"; const typedInterfaceUnsupported = @@ -259,6 +257,35 @@ export const NODE24_FETCH_COMPAT_PROFILE = { readableStreamCalls: ["cancel", "getReader"], }, operations: [ + { + id: "stdlib.headers.constructor", + name: "Headers constructor", + kind: "constructor", + facets: ["webidl-conversion", "mutation", "identity"], + scope: "Record and iterable header pairs, including copying native Headers", + evidence: [fixture("static-request")], + }, + { + id: "stdlib.request.constructor", + name: "Request constructor", + kind: "constructor", + facets: ["webidl-conversion", "body-consumption", "error-shape"], + scope: "Absolute URLs with method, headers, body, duplex, redirect, and signal; other RequestInit members retain explicit fences", + evidence: [fixture("static-request")], + }, + ...[ + "method", "url", "headers", "destination", "referrer", "referrerPolicy", "mode", "credentials", "cache", "redirect", "integrity", "keepalive", "isReloadNavigation", "isHistoryNavigation", "signal", "body", "bodyUsed", "duplex", + ].map((member): FetchCompatOperation => ({ + id: `stdlib.request.${member}`, name: `Request.${member}`, kind: "property", + facets: ["property-read", "state-machine"], + scope: "Reads on native Requests within the constructor's supported RequestInit subset", + evidence: [fixture("static-request")], + })), + ...["arrayBuffer", "text", "json", "bytes"].map((member): FetchCompatOperation => ({ + id: `stdlib.request.${member}`, name: `Request.${member}`, kind: "method", + facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], + evidence: [fixture("static-request")], + })), { id: "stdlib.response.constructor", name: "Response constructor", @@ -593,13 +620,7 @@ export const NODE24_FETCH_COMPAT_PROFILE = { metadataExclusion, ), - unsupportedEntry( - "stdlib.headers.constructor", - "Headers", - "constructor", - "constructor", - constructorUnsupported, - ), + staticEntry("stdlib.headers.constructor", "Headers", "constructor", "constructor"), ...[ "append", "delete", @@ -635,13 +656,7 @@ export const NODE24_FETCH_COMPAT_PROFILE = { metadataExclusion, ), - unsupportedEntry( - "stdlib.request.constructor", - "Request", - "constructor", - "constructor", - typedInterfaceUnsupported, - ), + staticEntry("stdlib.request.constructor", "Request", "constructor", "constructor"), ...[ "method", "url", @@ -670,13 +685,9 @@ export const NODE24_FETCH_COMPAT_PROFILE = { "bytes", "attribute", ].map((member) => - unsupportedEntry( - `stdlib.request.${member}`, - "Request", - member, - "prototype", - typedInterfaceUnsupported, - ) + ["clone", "blob", "formData", "attribute"].includes(member) + ? unsupportedEntry(`stdlib.request.${member}`, "Request", member, "prototype", typedInterfaceUnsupported) + : staticEntry(`stdlib.request.${member}`, "Request", member, "prototype") ), outOfScopeEntry( "stdlib.request.symbol.toStringTag", diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index e4617e25b..832ef29d8 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -286,18 +286,13 @@ export const UNSUPPORTED: Record = { hint: "numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet", }, // Regex fences. SC1120 is the shared code for regex features outside the - // supported slice (the rejection site names the construct); SC1121 is - // the statefulness fence, with its own hint. + // supported slice (the rejection site names the construct). SC1120: { feature: "this regex feature", milestone: "later", hint: "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates", }, - SC1121: { - feature: "'.test()' on a regex with the 'g' or 'y' flag", - milestone: "later", - hint: "g/y regexes carry mutable lastIndex state between calls, which is not modeled; drop the flag for a plain match test, or use replace/replaceAll/split (their iteration is internal)", - }, + // SC1121 (stateful g/y regex tests) shipped — code retired, do not reuse. }; /** The construct-fence codes MINTED BY FACTORY FUNCTIONS below (never keys diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index e09cdfd22..08b919e65 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -4,8 +4,19 @@ import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traver import { dynUndefinedExpr, type Lowerer } from "./lowerer.js"; import type { ParamShape } from "./lower-calls.js"; import { classPrototypeData } from "./class-prototypes.js"; +import { classInstanceOf } from "./class-dynamic-dispatch.js"; const PREFIX = "%class.construct:"; +const INSTANCEOF = "%class.instanceof:dynamic"; + +export function checkedClassInstanceOf(lowerer: Lowerer, value: IrExpr, callee: IrExpr, loc: SrcLoc): IrExpr { + if (!lowerer.liftedFns.some((fn) => fn.name === INSTANCEOF)) { + const params = [{ localId: "value", name: "value", type: DYN }, { localId: "callee", name: "callee", type: DYN }]; + lowerer.liftedFns.push({ name: INSTANCEOF, params, locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + returnType: BOOL, body: [{ kind: "return", value: { kind: "libCall", fn: "bytes.instanceOf", args: [varRef("value", DYN, loc), varRef("callee", DYN, loc)], type: BOOL, loc }, loc }], loc }); + } + return { kind: "call", callee: INSTANCEOF, args: [value, callee], type: BOOL, loc }; +} /** Checked construction dispatches to emitted native constructor thunks. */ export function checkedClassConstruction(lowerer: Lowerer, callee: IrExpr, args: IrExpr[], loc: SrcLoc): IrExpr { @@ -13,7 +24,7 @@ export function checkedClassConstruction(lowerer: Lowerer, callee: IrExpr, args: if (!lowerer.liftedFns.some((fn) => fn.name === name)) { const params = [callee, ...args].map((_, index) => ({ localId: `p.${index}`, name: `p${index}`, type: DYN })); lowerer.liftedFns.push({ name, params, locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), - returnType: DYN, body: [{ kind: "return", value: { kind: "libCall", fn: "bytes.construct", args: [ + returnType: DYN, body: [{ kind: "return", value: { kind: "libCall", fn: "dyn.construct", args: [ varRef("p.0", DYN, loc), { kind: "dynArrLit", elems: params.slice(1).map((param) => varRef(param.localId, DYN, loc)), type: DYN, loc }, { kind: "strLit", value: "constructor", type: STRING, loc }, ], type: DYN, loc }, loc }], loc }); @@ -96,7 +107,7 @@ export class ClassConstructionDispatch { changed = true; } for (const fn of functions) { - if (!fn.name.startsWith(PREFIX)) continue; + if (!fn.name.startsWith(PREFIX) && fn.name !== INSTANCEOF) continue; let completed = this.completed.get(fn.name); if (!completed) this.completed.set(fn.name, completed = new Set()); const loc = fn.loc; @@ -105,6 +116,15 @@ export class ClassConstructionDispatch { completed.add(name); const info = lowerer.classes.get(name); if (!info || info.generic || info.def.runtime) continue; + if (fn.name === INSTANCEOF) { + const type: IrType = { kind: "classval", className: name }; + const callee = varRef("callee", DYN, loc); + const result = classInstanceOf(lowerer, varRef("value", DYN, loc), info, loc, { kind: "dynCheck", value: callee, type, loc }); + fn.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.classIs", args: [callee, + { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc }, then: [{ kind: "return", value: result, loc }], else_: null, loc }); + changed = true; + continue; + } const args = constructorArguments(lowerer, info.ctorParams, fn.params.slice(1).map((param) => varRef(param.localId, DYN, loc)), loc); if (!args) continue; const type: IrType = { kind: "classval", className: name }; diff --git a/packages/compiler/src/frontend/lowering/class-descriptors.ts b/packages/compiler/src/frontend/lowering/class-descriptors.ts index 4ed75c855..5ccf6c76c 100644 --- a/packages/compiler/src/frontend/lowering/class-descriptors.ts +++ b/packages/compiler/src/frontend/lowering/class-descriptors.ts @@ -1,10 +1,9 @@ import * as ts from "../ts7/adapter.js"; -import { DYN, type IrExpr, type IrStmt, isDynTypedRefType } from "../../ir/ir.js"; +import { BOOL, DYN, STRING, VOID, type SrcLoc, type IrExpr, type IrStmt, isDynTypedRefType } from "../../ir/ir.js"; import { varRef } from "../../ir/build.js"; import { locOf } from "../program.js"; import type { Lowerer } from "./lowerer.js"; import type { ClassInfo } from "./lower-classes.js"; -import { isCompiledPrototypeMember } from "./class-prototypes.js"; import { classPropertiesHelper } from "./class-dynamic-dispatch.js"; function literalName(name: ts.PropertyName): string | null { @@ -12,7 +11,7 @@ function literalName(name: ts.PropertyName): string | null { } function bagOnlyProperty(lowerer: Lowerer, owner: ClassInfo, name: string): boolean { - return !owner.fields.has(name) && !isCompiledPrototypeMember(lowerer, owner, name) && + return !owner.fields.has(name) && owner.subclasses.every((child) => bagOnlyProperty(lowerer, child, name)); } @@ -43,7 +42,7 @@ export function lowerClassDataDescriptor(lowerer: Lowerer, call: ts.CallExpressi const descriptors = call.arguments[member === "defineProperty" ? 2 : 1]!; if (member === "defineProperty") { const key = call.arguments[1]!; - if (!ts.isStringLiteral(key) || !safeName(info, key.text) || !descriptor(descriptors)) return null; + if (ts.isStringLiteral(key) && !safeName(info, key.text) || !descriptor(descriptors)) return null; } else { if (!ts.isObjectLiteralExpression(descriptors) || !descriptors.properties.every((p) => { if (!ts.isPropertyAssignment(p)) return false; @@ -54,17 +53,53 @@ export function lowerClassDataDescriptor(lowerer: Lowerer, call: ts.CallExpressi const loc = locOf(call); const receiver = lowerer.declareHiddenLocal("%descriptorReceiver", target.type); const desc = lowerer.declareHiddenLocal("%descriptors", DYN); + const key = member === "defineProperty" ? lowerer.declareHiddenLocal("%descriptorKey", DYN) : null; + if (key) key.mutable = true; const value = varRef(receiver.id, target.type, loc); const boxed = lowerer.coerceToExpected(value, DYN); const helper = classPropertiesHelper(lowerer, loc); const bag: IrExpr = { kind: "call", callee: helper.name, args: [boxed], type: DYN, loc }; const args = member === "defineProperty" - ? [bag, lowerer.lowerExprExpecting(call.arguments[1]!, DYN), varRef(desc.id, DYN, loc)] + ? [bag, varRef(key!.id, DYN, loc), varRef(desc.id, DYN, loc)] : [bag, varRef(desc.id, DYN, loc)]; const stmts: IrStmt[] = [ { kind: "varDecl", localId: receiver.id, init: target, loc }, + ...(key ? [{ kind: "varDecl" as const, localId: key.id, init: lowerer.lowerExprExpecting(call.arguments[1]!, DYN), loc }] : []), { kind: "varDecl", localId: desc.id, init: lowerer.lowerExprExpecting(descriptors, DYN), loc }, + ...(key && !ts.isStringLiteral(call.arguments[1]!) ? [{ kind: "assign" as const, localId: key.id, + value: { kind: "libCall" as const, fn: "dyn.propertyKey" as const, args: [varRef(key.id, DYN, loc)], type: DYN, loc }, loc }, { kind: "exprStmt" as const, + expr: { kind: "call" as const, callee: descriptorGuard(lowerer, info, loc), args: [varRef(key.id, DYN, loc)], type: VOID, loc }, loc }] : []), { kind: "exprStmt", expr: { kind: "libCall", fn: member === "defineProperty" ? "dyn.defineProperty" : "dyn.defineProps", args, type: DYN, loc }, loc }, ]; return { kind: "seqExpr", stmts, result: value, type: value.type, loc }; } + +function descriptorGuard(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): string { + const name = `%descriptorGuard:${info.def.name}`; + const existing = lowerer.liftedFns.find((fn) => fn.name === name); + const key = varRef("key", DYN, loc); + const fields = new Set(); + const collect = (owner: ClassInfo): void => { + for (const field of owner.fields.keys()) fields.add(field); + for (const child of owner.subclasses) collect(child); + }; + collect(info); + const body: IrStmt[] = [...fields].filter((field) => !field.startsWith("%") && !field.startsWith("#")).map((field): IrStmt => ({ + kind: "if", cond: { kind: "dynScalarEq", left: key, right: lowerer.coerceToExpected({ kind: "strLit", value: field, type: STRING, loc }, DYN), type: BOOL, loc }, + then: [{ kind: "runtimeFence", code: "SC2020", message: `changing the descriptor of native field '${field}' is not supported yet`, loc }], else_: null, loc, + })); + if (existing) { existing.body = body; return name; } + lowerer.liftedFns.push({ name, params: [{ localId: "key", name: "key", type: DYN }], + locals: [{ id: "key", name: "key", type: DYN, mutable: false }], returnType: VOID, loc, + body, + }); + return name; +} + +export function refreshDescriptorGuards(lowerer: Lowerer): void { + for (const fn of lowerer.liftedFns) { + if (!fn.name.startsWith("%descriptorGuard:")) continue; + const info = lowerer.classes.get(fn.name.slice("%descriptorGuard:".length)); + if (info) descriptorGuard(lowerer, info, fn.loc); + } +} diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 0dfac99b2..93dd1acd1 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1,10 +1,10 @@ -import { BOOL, DYN, DYN_CLASS_PROPERTIES as PROPERTY_BAG, STRING, VOID, canConvertToDyn, canDynCheckTo, isClassOwnEnumerableFieldName, isDynTypedRefType, isUnitType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js"; +import { BOOL, DYN, F64, DYN_CLASS_PROPERTIES as PROPERTY_BAG, STRING, VOID, canConvertToDyn, canDynCheckTo, isClassOwnEnumerableFieldName, isDynTypedRefType, isUnitType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js"; import { streamTypedRefEligible } from "../../ir/analysis.js"; import { varRef } from "../../ir/build.js"; import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traverse.js"; import { dynUndefinedExpr, PoisonError, type Lowerer } from "./lowerer.js"; import { implicitDefaultInstance, type ParamShape } from "./lower-calls.js"; -import { accessorCall, classValueRef, findGenericMethodOn, findMethodOn, genericOverrideBelow, upcastTo, type ClassInfo } from "./lower-classes.js"; +import { accessorCall, classValueRef, findGenericMethodOn, findMethodOn, findStaticOn, genericOverrideBelow, upcastTo, type ClassInfo } from "./lower-classes.js"; import { classPrototypeData, hasClassPrototypeData, reflectClassPrototype } from "./class-prototypes.js"; import { errorPropertyRead, errorPropertyWrite, errorToStringCall, errorToStringMethod, refreshErrorPropertyDispatch, refreshErrorMethodDispatch } from "./error-methods.js"; import { GeneratorDynamicDispatch } from "./generator-dynamic-dispatch.js"; @@ -12,8 +12,14 @@ import { classMethodValue } from "./class-method-values.js"; import * as ts from "../ts7/adapter.js"; import { SYMBOL_T } from "../../ir/ir.js"; import { isClassCallback } from "./class-callbacks.js"; +import { refreshDescriptorGuards } from "./class-descriptors.js"; import { ClassConstructionDispatch } from "./class-construction.js"; +const STREAM_METHODS = new Set(["on", "addListener", "once", "prependListener", "prependOnceListener", "off", "removeListener", "read", "pause", "resume", "isPaused", "destroy"]); +const STREAM_BOOL_PROPERTIES = new Set(["readable", "readableEnded", "writable", "writableEnded", "writableFinished", "writableNeedDrain", "destroyed", "closed", "readableObjectMode", "writableObjectMode", "allowHalfOpen"]); +const STREAM_NUM_PROPERTIES = new Set(["readableLength", "readableHighWaterMark", "writableLength", "writableHighWaterMark", "writableCorked"]); +const streamProperty = (name: string): boolean => STREAM_BOOL_PROPERTIES.has(name) || STREAM_NUM_PROPERTIES.has(name); + type Invoke = Extract; interface Dispatch { source: Invoke; @@ -85,6 +91,7 @@ export class ClassDynamicDispatch { private readonly generatorDispatch = new GeneratorDynamicDispatch(); private readonly constructionDispatch = new ClassConstructionDispatch(); private readonly boxed = new Set(); + private readonly boxedConstructors = new Set(); private readonly dispatches = new Map(); private readonly properties = new Map(); private readonly computed = new Map & { keyLocal: string; branchIndex: number; dynamicKey: boolean; probe: "in" | "own" | "enumerable" | undefined }>(); @@ -115,7 +122,10 @@ export class ClassDynamicDispatch { const key = typeKey(type); if (seenTypes.has(key)) return; seenTypes.add(key); - if (isDynTypedRefType(type)) this.boxed.add(type.className); + if (type.kind === "object") { + if (isDynTypedRefType(type)) this.boxed.add(type.className); + } + else if (type.kind === "classval") this.boxedConstructors.add(type.className); else if (type.kind === "array") discover(type.elem); else if (type.kind === "record") { const shape = lowerer.shapes.get(type.shapeId); @@ -295,13 +305,15 @@ export class ClassDynamicDispatch { }); changed = true; } + refreshDescriptorGuards(lowerer); const byMethod = new Map(); const candidates = (method: string): ClassInfo[] => { const found = byMethod.get(method); if (found) return found; const matching = [...this.boxed].flatMap((name) => { const info = lowerer.classes.get(name); - if (!info || info.fields.has(method) || info.builtinEmitter || info.builtinStream || info.builtinError) return []; + if (!info || info.fields.has(method) || info.builtinEmitter || info.builtinError) return []; + if (info.builtinStream) return STREAM_METHODS.has(method) ? [info] : []; return findMethodOn(lowerer, info, method) || findGenericMethodOn(lowerer, info, method) ? [info] : []; }); byMethod.set(method, matching); @@ -575,10 +587,25 @@ export class ClassDynamicDispatch { } } for (const dispatch of this.properties.values()) { - for (const plan of this.propertyReceivers(lowerer)) { + if (!dispatch.write) for (const name of this.boxedConstructors) { + const info = lowerer.classes.get(name); + const field = info && findStaticOn(lowerer, info, dispatch.name)?.field; + const key = `constructor:${name}`; + if (!field || dispatch.name.startsWith("#") || dispatch.classes.has(key)) continue; + dispatch.classes.add(key); + const loc = dispatch.fn.loc; + const value = varRef("p.0", DYN, loc); + const type: IrType = { kind: "classval", className: name }; + dispatch.fn.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.classIs", args: [value, + { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc }, then: [ + { kind: "return", value: lowerer.coerceToExpected(varRef(field.globalId, field.type, loc), DYN), loc }, + ], else_: null, loc }); + changed = true; + } + for (const plan of this.propertyReceivers(lowerer, true)) { const { info } = plan; if (!isClassOwnEnumerableFieldName(dispatch.name) || - !findMethodOn(lowerer, info, dispatch.name) && !findGenericMethodOn(lowerer, info, dispatch.name) && dispatch.name !== "constructor" && !info.fields.has(dispatch.name) && !findMethodOn(lowerer, info, `get:${dispatch.name}`) && !findMethodOn(lowerer, info, `set:${dispatch.name}`)) continue; + !(info.builtinStream !== undefined && streamProperty(dispatch.name)) && !findMethodOn(lowerer, info, dispatch.name) && !findGenericMethodOn(lowerer, info, dispatch.name) && dispatch.name !== "constructor" && !info.fields.has(dispatch.name) && !findMethodOn(lowerer, info, `get:${dispatch.name}`) && !findMethodOn(lowerer, info, `set:${dispatch.name}`)) continue; if (dispatch.classes.has(plan.key)) continue; dispatch.classes.add(plan.key); const loc = dispatch.fn.loc; @@ -600,7 +627,7 @@ export class ClassDynamicDispatch { } } for (const dispatch of this.computed.values()) { - for (const plan of this.propertyReceivers(lowerer)) { + for (const plan of this.propertyReceivers(lowerer, true)) { if (dispatch.classes.has(plan.key)) continue; const { info } = plan; dispatch.classes.add(plan.key); @@ -612,6 +639,7 @@ export class ClassDynamicDispatch { ? { kind: "fieldGet", obj: lowerer.upcastTo(receiver, "%Error"), className: "%Error", field: `%${name}Present`, type: BOOL, loc } : { kind: "fieldGet", obj: lowerer.upcastTo(receiver, "%Error"), className: "%Error", field: `%${name}Enumerable`, type: BOOL, loc }, loc, }]; + if (info.builtinStream !== undefined && streamProperty(name) && dispatch.probe) return [{ kind: "return", value: { kind: "boolLit", value: dispatch.probe === "in", type: BOOL, loc }, loc }]; if (!dispatch.probe) return this.propertyBody(lowerer, { ...dispatch, name }, info, receiver); const prototypeMethod = lowerer.prototypeMethodAccesses.has(name); if (info.fields.has(name) || dispatch.probe === "in" && !prototypeMethod) { @@ -675,6 +703,7 @@ export class ClassDynamicDispatch { } } const names = new Set([...info.fields.keys()].filter(isClassOwnEnumerableFieldName)); + if (info.builtinStream) for (const name of [...STREAM_BOOL_PROPERTIES, ...STREAM_NUM_PROPERTIES]) names.add(name); for (let owner: ClassInfo | null = info; owner; owner = owner.base) { const methodNames = [...owner.methods.keys()]; @@ -800,11 +829,11 @@ export class ClassDynamicDispatch { /** A capsule retains its static type, while its object may be a subclass. * Preorder insertion lets later branches test the most derived layout first. */ - private propertyReceivers(lowerer: Lowerer): PropertyReceiver[] { + private propertyReceivers(lowerer: Lowerer, streams = false): PropertyReceiver[] { const plans: PropertyReceiver[] = []; for (const name of this.boxed) { const capsule = lowerer.classes.get(name); - if (!capsule || capsule.builtinEmitter || capsule.builtinStream || capsule.builtinError) continue; + if (!capsule || capsule.builtinEmitter || capsule.builtinStream && !streams || capsule.builtinError) continue; const visit = (info: ClassInfo): void => { plans.push({ info, capsule, key: JSON.stringify([name, info.def.name]) }); for (const child of info.subclasses) visit(child); @@ -831,6 +860,10 @@ export class ClassDynamicDispatch { private propertyBody(lowerer: Lowerer, dispatch: PropertyDispatch, info: ClassInfo, receiver: IrExpr): IrStmt[] { const { name, write } = dispatch; const loc = dispatch.fn.loc; + if (info.builtinStream !== undefined && streamProperty(name)) { + if (write) return [{ kind: "runtimeFence", code: "SC2020", message: `writing stream property '${name}' through an untyped value is not supported`, loc }]; + return [{ kind: "return", value: lowerer.coerceToExpected({ kind: "libCall", fn: "stream.prop", args: [receiver, { kind: "strLit", value: name, type: STRING, loc }], type: STREAM_BOOL_PROPERTIES.has(name) ? BOOL : F64, loc }, DYN), loc }]; + } if ((name === "message" || name === "name") && lowerer.isSubclassOf(info.def.name, "%Error")) return write ? [errorPropertyWrite(lowerer, receiver, lowerer.coerceToExpected(varRef("p.1", DYN, loc), STRING), name), { kind: "return", value: null, loc }] : [{ kind: "return", value: lowerer.coerceToExpected(errorPropertyRead(lowerer, receiver, name), DYN), loc }]; @@ -911,6 +944,14 @@ export class ClassDynamicDispatch { const mutable = (t: IrType): boolean => streamTypedRefEligible(t) || (t.kind === "union" && (getUnion(t.unionId)?.arms.some(mutable) ?? false)); if (boxed.kind === "dynFrom" && mutable(boxed.value.type)) boxed.liveRef = true; + if (accessor) { + const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name, + args: [lowerer.coerceToExpected(receiver, DYN)], type: DYN, loc }; + const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; + return [{ kind: "if", cond: { kind: "libCall", fn: "dyn.hasOwn", args: [bag, key], type: BOOL, loc }, + then: [{ kind: "return", value: { kind: "dynKeyGet", value: bag, key, type: DYN, loc }, loc }], else_: null, loc }, + { kind: "return", value: boxed, loc }]; + } return [{ kind: "return", value: boxed, loc }]; } if (!field && !accessor && isClassOwnEnumerableFieldName(name) && !name.startsWith("sym:") && !findMethodOn(lowerer, info, `get:${name}`)) { @@ -940,6 +981,25 @@ export class ClassDynamicDispatch { private methodBody(lowerer: Lowerer, dispatch: Dispatch, info: ClassInfo, receiver: IrExpr): IrStmt[] { const { method, loc } = dispatch.source; const fence = (): IrStmt[] => [{ kind: "runtimeFence", code: "SC2020", message: `calling '${method}' on this native class through an untyped value is not supported yet`, loc }]; + if (info.builtinStream) { + const incoming = dispatch.source.args.map((_, i) => varRef(`p.${i + 1}`, DYN, loc)); + const registering = ["on", "addListener", "once", "prependListener", "prependOnceListener"].includes(method); + const removing = method === "off" || method === "removeListener"; + const ret = (value: IrExpr): IrStmt[] => [{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc }]; + if (registering || removing) { + if (incoming.length < 2) return fence(); + const event = lowerer.coerceToExpected(incoming[0]!, STRING); + const cb = incoming[1]!; + return [{ kind: "exprStmt", expr: { kind: "libCall", fn: "emitter.checkListener", args: [cb], type: VOID, loc }, loc }, ...ret({ kind: "libCall", fn: registering ? "stream.onDyn" : "emitter.offDyn", args: [receiver, event, cb, ...(registering ? [ + { kind: "boolLit" as const, value: method === "once" || method === "prependOnceListener", type: BOOL, loc }, + { kind: "boolLit" as const, value: method.startsWith("prepend"), type: BOOL, loc }, + ] : [])], type: receiver.type, loc })]; + } + if (method === "read") return ret({ kind: "libCall", fn: "readable.readDyn", args: [receiver, incoming[0] ?? dynUndefinedExpr(loc)], type: DYN, loc }); + if (method === "pause" || method === "resume" || method === "isPaused") return ret({ kind: "libCall", fn: method === "pause" ? "readable.pause" : method === "resume" ? "readable.resume" : "readable.isPaused", args: [receiver], type: method === "isPaused" ? BOOL : receiver.type, loc }); + if (method === "destroy" && (!incoming.length || incoming[0]?.kind === "unitLit")) return ret({ kind: "libCall", fn: "stream.destroy", args: [receiver], type: receiver.type, loc }); + return fence(); + } const methodInfo = findMethodOn(lowerer, info, method); if (this.asyncFree && (methodInfo?.sig.gen || methodInfo?.sig.async)) { return [{ kind: "runtimeFence", code: "SC4005", message: `calling async or generator method '${method}' through an untyped library value is not supported`, loc }]; diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index f34c274f2..6a019b60c 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -92,6 +92,9 @@ export function reflectClassPrototype(lowerer: Lowerer, info: ClassInfo, loc: Sr return { kind: "call", callee: name, args: [], type: DYN, loc }; }; if (info.prototypeReflectionRefusal) return { kind: "call", callee: info.prototypeReflectionRefusal, args: [], type: DYN, loc }; + // Local JavaScript factories initialize captured prototype members on + // their constructor value, so the shared runtime view is already complete. + if (info.localClass && info.runtimePrototypeMembers) return prototype; if (info.localClass) return refuse("prototype reflection on a function-local class is not supported yet"); if (info.base && !info.base.builtinError) { reflectClassPrototype(lowerer, info.base, loc, info.def.baseValueGlobal diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 4459d9faf..476a2eda5 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -3,11 +3,13 @@ import * as ts from "../../ts7/adapter.js"; import { BYTES_ELEMENT_SIZE, BIGINT_T, BOOL, BYTES_U8, DYN, F64, type IrBytesElem, type IrBytesIntrinsicMethod, type IrExpr, type IrType, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, bytesOf, typeEquals } from "../../../ir/ir.js"; import { locOf } from "../../program.js"; import type { Lowerer } from "../lowerer.js"; -import { dynUndefinedExpr, own } from "../lowerer.js"; +import { dynUndefinedExpr, newFnCtx, own } from "../lowerer.js"; import { buildBytesSortFn } from "../lower-array-sort.js"; import { isSafeToDiscard } from "../expressions/evaluation-safety.js"; import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument } from "../optional-arguments.js"; +import { lowerCheckedArrayFrom } from "../lower-containers.js"; +import { varRef } from "../../../ir/build.js"; /** Uint8Array.prototype.toSorted. The receiver/comparator expressions are * evaluated before entering the helper; the helper snapshots with @@ -124,7 +126,7 @@ const BYTES_CTORS: Record = { * Null when this isn't a stdlib * typed-array construction. */ export function lowerBytesNew(lowerer: Lowerer, expr: ts.NewExpression, symbol: ts.Symbol | null | undefined): IrExpr | null { - if (symbol?.name === "ArrayBuffer" && lowerer.isStdlibSymbol(symbol)) { + if (symbol != null && symbol.name === "ArrayBuffer" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering("resizable ArrayBuffer construction", expr, "fixed-length ArrayBuffers accept a byte length; resizable storage is not supported yet"); @@ -238,11 +240,36 @@ export function lowerBytesStaticCall(lowerer: Lowerer, call: ts.CallExpression, const symbol = lowerer.resolveValueSymbol(access.expression); const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined; if (!elem || !lowerer.isStdlibSymbol(symbol ?? undefined)) return null; - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering("TypedArray.from with a mapping callback or this argument count", call); + if (call.arguments.length < 1 || call.arguments.length > 3 || call.arguments.some(ts.isSpreadElement)) { + lowerer.noLowering("TypedArray.from with this argument count", call); } const node = call.arguments[0]!; const source = lowerer.lowerExpr(node); + if (call.arguments.length > 1) { + if (!["string", "array", "bytes"].includes(source.type.kind)) lowerer.noLowering("TypedArray.from mapper over this source representation", call); + const loc = locOf(call); + const key = `%builtin.${elem}.fromMapped`; + if (!lowerer.liftedFns.some((fn) => fn.name === key)) { + const context = newFnCtx(false, null, null, bytesOf(elem)); + lowerer.fnStack.push(context); + try { + const locals = ["source", "mapper", "receiver"].map((name) => lowerer.declareHiddenLocal(name, DYN)); + const [input, mapper, receiver] = locals.map((local) => varRef(local.id, DYN, loc)); + const collected = lowerCheckedArrayFrom(lowerer, input!, loc); + const mapped = lowerCheckedArrayFrom(lowerer, collected.result, loc, mapper!, receiver!, true); + // Consume the iterable before mapping, but validate the mapper first. + lowerer.liftedFns.push({ name: key, params: locals.map((local) => ({ localId: local.id, name: local.name, type: DYN })), + returnType: bytesOf(elem), locals: context.locals, loc, body: [ + mapped.stmts[0]!, ...collected.stmts, ...mapped.stmts.slice(1), + { kind: "return", value: { kind: "bytesNew", source: mapped.result, from: true, type: bytesOf(elem), loc }, loc }, + ] }); + } finally { lowerer.fnStack.pop(); } + } + return { kind: "call", callee: key, args: [lowerer.coerceInto(node, source, DYN), + lowerer.lowerExprExpecting(call.arguments[1]!, DYN), + call.arguments[2] ? lowerer.lowerExprExpecting(call.arguments[2], DYN) : dynUndefinedExpr(loc)], type: bytesOf(elem), loc }; + + } if (source.type.kind === "bytes" || (source.type.kind === "array" && source.type.elem.kind === "f64")) { return { kind: "bytesNew", source, type: bytesOf(elem), loc: locOf(call) }; } @@ -373,17 +400,22 @@ export function lowerBytesMethodCall(lowerer: Lowerer, call: ts.CallExpression, }); return { kind: "bytesIntrinsic", method: "copyWithin", receiver, args, type: receiverIr, loc }; } - if (name === "fill" && receiverIr.elem !== "u8") { - // TypedArray.prototype.fill on the non-u8 kinds: per-element value - // coercion, slice-clamped relative indices, never throws. u8 - // receivers keep the Buffer fill family (string patterns, throwing - // offset validation) — lowerBufferInstanceMethod's surface. - if (nArgs < 1 || nArgs > 3) { + if (name === "fill" && !(() => { + const symbol = lowerer.checker.getSymbolAtLocation(access.name); + return symbol !== undefined && lowerer.checker.declarationsOf(symbol).some( + (declaration) => ts.isInterfaceDeclaration(declaration.parent) && declaration.parent.name.text === "Buffer", + ); + })()) { + // Typed arrays use element coercion and relative, clamped indices. + // Buffer's same-named method keeps its pattern and offset validation. + if (nArgs > 3 || call.arguments.some(ts.isSpreadElement)) { lowerer.noLowering(`.fill with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const v = lowerer.lowerExprExpecting(call.arguments[0]!, F64); - const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); + const zero: IrExpr = { kind: "numLit", value: 0, type: F64, loc }; + const v = call.arguments[0] ? lowerOptionalArgument(lowerer, call.arguments[0], F64, zero) : zero; + const idx = call.arguments.slice(1).map((a, i) => lowerOptionalArgument(lowerer, a, F64, + { kind: "numLit", value: i === 0 ? 0 : Infinity, type: F64, loc })); return { kind: "bytesIntrinsic", method: "fillElem", receiver, args: [v, ...idx], type: receiverIr, loc }; } if (name === "set") { @@ -934,7 +966,7 @@ function lowerBufferStaticValue(lowerer: Lowerer, call: ts.CallExpression, const args = call.arguments; if (member === "from") { if (args.length >= 1 && args.length <= 3 && !args.some(ts.isSpreadElement) && - lowerer.typeOf(args[0]!).getSymbol()?.name === "ArrayBuffer") { + ((symbol) => symbol != null && symbol.name === "ArrayBuffer")(lowerer.typeOf(args[0]!).getSymbol())) { return lowerArrayBufferView(lowerer, args, "u8", loc); } // Buffer.from(x.buffer[, byteOffset[, length]]): a u8 VIEW sharing @@ -1154,6 +1186,10 @@ function lowerBufferStaticValue(lowerer: Lowerer, call: ts.CallExpression, const enc: IrExpr = { kind: "strLit", value: encName, type: STRING, loc }; return { kind: "libCall", fn: "buffer.byteLenStr", args: [s, enc], type: F64, loc }; } + if ((srcIr === null || srcIr.kind === "dyn") && (!args[1] || ts.isStringLiteralLike(args[1]))) { + const encoding: IrExpr = { kind: "strLit", value: args[1] ? bufEncoding(lowerer, "Buffer.byteLength", args[1]) : "utf8", type: STRING, loc }; + return { kind: "libCall", fn: "buffer.byteLenDyn", args: [lowerer.lowerExprExpecting(args[0]!, DYN), encoding], type: F64, loc }; + } if (srcIr?.kind === "bytes" && args.length === 1) { const receiver = lowerer.lowerExpr(args[0]!); return { kind: "bytesIntrinsic", method: "byteLength", receiver, args: [], type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 8c55542b7..9fe2275a5 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -297,14 +297,18 @@ export function lowerStringPaddingCall( lowerer.noLowering(`.${method} with ${argumentNodes.length} arguments`, call); } const loc = locOf(call); - const maxLength = lowerPositionArgument(lowerer, argumentNodes[0], numLit(0, loc)); + const rawLength = lowerPositionArgument(lowerer, argumentNodes[0], numLit(0, loc)); + const maxLength = (rawLength.type.kind === "record" || rawLength.type.kind === "object") && lowerer.dynConvertible(rawLength.type) + ? lowerer.coerceToExpected(rawLength, DYN) : rawLength; const fillNode = argumentNodes[1]; const undefinedFill = fillNode ? lowerStaticallyUndefinedArgument(lowerer, fillNode) : null; const rawFill = !fillNode ? strLit(" ", loc) : undefinedFill ? defaultAfterUndefined(undefinedFill, strLit(" ", loc)) : lowerer.lowerExpr(fillNode); const fill = rawFill.type.kind === "nullT" - ? defaultAfterUndefined(rawFill, strLit("null", loc)) : rawFill; + ? defaultAfterUndefined(rawFill, strLit("null", loc)) + : (rawFill.type.kind === "record" || rawFill.type.kind === "object") && lowerer.dynConvertible(rawFill.type) + ? lowerer.coerceToExpected(rawFill, DYN) : rawFill; const values = [receiver, maxLength, fill]; const key = `str.pad:${method}:${values.map(value => typeKey(value.type)).join(":")}`; let helper = lowerer.widthHelpers.get(key); @@ -387,54 +391,27 @@ export function lowerRegexMethodCall(lowerer: Lowerer, call: ts.CallExpression, if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { lowerer.noLowering("RegExp.prototype.test with surplus or spread arguments", call); } - // The statefulness fence, at compile time where the flags are - // visible: a literal receiver (possibly parenthesized). Values that - // flow through variables hit the same fence at runtime. - let recv: ts.Expression = access.expression; - while (ts.isParenthesizedExpression(recv)) recv = recv.expression; - if (ts.isRegularExpressionLiteral(recv)) { - const flags = recv.text.slice(recv.text.lastIndexOf("/") + 1); - if (flags.includes("g") || flags.includes("y")) { - lowerer.unsupported("SC1121", call); - } - } const receiver = lowerReceiver(); const args = [lowerRegexSubject(lowerer, call.arguments[0], loc)]; return { kind: "regexIntrinsic", method: "test", receiver, args, type: BOOL, loc }; } - // `re.exec(s)` for non-g/y regexes: spec-identical to `s.match(re)` - // (Symbol.match delegates to exec when lastIndex is out of play), so - // it lowers to the SAME match intrinsic with the operands swapped — - // the honest `string[] | null` slice, nonparticipating captures "" - // (match's documented rule). The g/y statefulness fence applies at - // compile time on literal receivers, exactly test()'s stance; values - // reaching the runtime with those flags abort there. + // `re.exec(s)` returns the exec-shaped string[] | null slice, with + // numeric lastIndex state for global/sticky regexes. Nonparticipating + // captures retain match's documented empty-string representation. if (receiverKind === "regex" && name === "exec") { if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) return null; - let recv: ts.Expression = access.expression; - while (ts.isParenthesizedExpression(recv)) recv = recv.expression; - if (ts.isRegularExpressionLiteral(recv)) { - const flags = recv.text.slice(recv.text.lastIndexOf("/") + 1); - if (flags.includes("g") || flags.includes("y")) { - lowerer.unsupported("SC1121", call); - } - } const re = lowerReceiver(); const subject = lowerRegexSubject(lowerer, call.arguments[0], loc); const resultT: IrType = { kind: "union", unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]) }; // The shared match intrinsic takes the string first; preserve exec's // receiver-before-subject evaluation order before swapping operands. const saved = lowerer.declareHiddenLocal("%execReceiver", re.type); - const result: IrExpr = { kind: "regexIntrinsic", method: "match", receiver: subject, args: [varRef(saved.id, re.type, loc)], type: resultT, loc }; + const result: IrExpr = { kind: "regexIntrinsic", method: "exec", receiver: subject, args: [varRef(saved.id, re.type, loc)], type: resultT, loc }; return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: saved.id, init: re, loc }], result, type: resultT, loc }; } - // `s.match(re)` for non-g/y regexes: Node's exec-shaped result reduced - // to the honest slice — the `string[] | null` union holding - // [whole match, ...captures] or the null arm. The g-flag match returns - // EVERY match (a different shape) and /y is stateful — both fence at - // compile time on literal arguments (values reaching the runtime with - // those flags abort, the test() stance). `.index`/`.input` reads on - // the result fence per member (array-typed value); `.groups` reads + // `s.match(re)` returns all whole matches under /g, otherwise the + // exec-shaped slice [whole match, ...captures], or null. Non-global /y + // uses exec's lastIndex transitions. `.groups` reads // desugar at their access sites when the regex is statically known // (lowerMatchGroupsRead). // `s.match(re)` also claims a NULLABLE string receiver (string + unit @@ -456,18 +433,6 @@ export function lowerRegexMethodCall(lowerer: Lowerer, call: ts.CallExpression, const arg0 = call.arguments[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern match: the SC2020 fence if (call.arguments.length !== 1) return null; - let reNode: ts.Expression = arg0; - while (ts.isParenthesizedExpression(reNode)) reNode = reNode.expression; - if (ts.isRegularExpressionLiteral(reNode)) { - const flags = reNode.text.slice(reNode.text.lastIndexOf("/") + 1); - if (flags.includes("g") || flags.includes("y")) { - lowerer.unsupported( - "SC1120", - call, - "'.match()' with the 'g' or 'y' flag (an every-match array is a different shape — use replaceAll/split, or test() per position)", - ); - } - } const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); diff --git a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts index 581eee8b4..db25c67f6 100644 --- a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts +++ b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts @@ -34,8 +34,9 @@ export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrEx } } const inputs = args.map((arg) => { - const value = ts.isObjectLiteralExpression(arg) + let value = ts.isObjectLiteralExpression(arg) ? lowerer.lowerExprExpecting(arg, DYN) : lowerer.lowerExpr(arg); + if (value.type.kind !== "dyn" && lowerer.dynConvertible(value.type)) value = lowerer.coerceToExpected(value, DYN); if (value.type.kind !== "dyn") { lowerer.unsupported("SC1090", arg, "Proxy targets and handlers outside checked-native object storage"); } diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts index eecede61d..0b1ccfae5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts +++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts @@ -52,24 +52,30 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp write = (value) => ({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [receiver, key, value], type: VOID, loc }, loc }); } else { const field = lowerer.fieldTarget(target); - if (!field) lowerer.unsupported("SC1090", target, "short-circuit assignment to an unsupported property"); - field.obj = save(field.obj, "%shortCircuitReceiver"); - read = lowerer.fieldGetExpr(field, locOf(target), target); - storage = field.fieldType; - const blame = target; - write = (value) => lowerer.fieldSetStmt(field, value, loc, blame); + if (field) { + field.obj = save(field.obj, "%shortCircuitReceiver"); + read = lowerer.fieldGetExpr(field, locOf(target), target); + storage = field.fieldType; + const blame = target; + write = (value) => lowerer.fieldSetStmt(field, value, loc, blame); + } else if (probed?.type.kind === "object" && lowerer.dynConvertible(probed.type)) { + // Inferred JS fields may live in the instance's property bag. + // Its checked capsule writes through to the same native object. + const receiver = save(lowerer.coerceToExpected(probed, DYN), "%shortCircuitReceiver"); + const key: IrExpr = { kind: "strLit", value: target.name.text, type: STRING, loc }; + storage = DYN; + read = { kind: "dynKeyGet", key, value: receiver, type: DYN, loc }; + write = (value) => ({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [receiver, key, value], type: VOID, loc }, loc }); + } else lowerer.unsupported("SC1090", target, "short-circuit assignment to an unsupported property"); } } else { const receiver = save(lowerer.lowerExpr(target.expression), "%shortCircuitReceiver"); let key = lowerer.lowerExpr(target.argumentExpression); if (receiver.type.kind === "dyn") { - if (key.type.kind !== "string" && key.type.kind !== "f64" && key.type.kind !== "bool") { - lowerer.unsupported("SC1090", target.argumentExpression, "short-circuit assignment with non-scalar property keys"); - } - key = save(lowerer.ensureString(key, target.argumentExpression), "%shortCircuitKey"); + key = save({ kind: "libCall", fn: "dyn.propertyKey", args: [lowerer.coerceInto(target.argumentExpression, key, DYN)], type: DYN, loc }, "%shortCircuitKey"); storage = DYN; read = { kind: "dynKeyGet", key, value: receiver, type: DYN, loc }; - write = (value) => ({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [receiver, key, value], type: VOID, loc }, loc }); + write = (value) => ({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySetComputed", args: [receiver, key, value], type: VOID, loc }, loc }); } else if (receiver.type.kind === "array" && key.type.kind === "f64") { key = save(key, "%shortCircuitKey"); storage = receiver.type.elem; diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 13b3649fb..c41081193 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -1,3 +1,4 @@ +import { pathModuleValue } from "../lower-builtin-values.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; import { @@ -18,11 +19,11 @@ import type { IrExpr, IrLocal, IrRecordShape, IrStmt, IrType, SrcLoc } from "../ import { isCjsExportTableLiteral, isJsSourceFile, locOf } from "../../program.js"; import { NARROW_FIRST } from "../surfaces.js"; import { recordShapeMismatchDiag } from "../../../diagnostics/diagnostic.js"; -import { PoisonError, dynUndefinedExpr } from "../lowerer.js"; +import { PoisonError, dynUndefinedExpr, neverTaintedJsType } from "../lowerer.js"; import type { Lowerer } from "../lowerer.js"; import { lowerIndexMergeHelper } from "../lower-containers.js"; import type { IndexMergeContributor } from "../lower-containers.js"; -import { isGenericCallableMemberType } from "../../type-mapper.js"; +import { isGenericCallableMemberType, jsOpenObjectType } from "../../type-mapper.js"; import { numLit, strLit, varRef } from "../../../ir/build.js"; import { isSafeToRepeat } from "./evaluation-safety.js"; import { tryLowerExpression } from "./try-lower-expression.js"; @@ -229,7 +230,10 @@ export function lowerDynObjectLiteral( for (const prop of expr.properties) { if (ts.isSpreadAssignment(prop)) { flushFields(); - const raw = lowerer.lowerExpr(prop.expression); + const builtin = lowerer.builtinNamespaceModuleOf(prop.expression); + const raw = builtin === "path" || builtin === "path/posix" || builtin === "path/win32" + ? pathModuleValue(lowerer, builtin === "path" ? "path/posix" : builtin, locOf(prop)) + : lowerer.lowerExpr(prop.expression); fenceSymbolFieldCopy(lowerer, prop.expression, raw.type); const source = boxValue ? boxValue(prop.expression, raw) @@ -565,6 +569,24 @@ return prefix.length === 0 ? selected : { kind: "seqExpr", stmts: prefix, result export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpression, expected?: IrType & { kind: "record" },): IrExpr { + // Unannotated JavaScript accumulator fields have no element contract. + // Preserve their checked arrays instead of assigning never[] a numeric ABI. + if (!expected && isJsSourceFile(expr.getSourceFile()) && expr.properties.some((prop) => + ts.isPropertyAssignment(prop) && (ts.isArrayLiteralExpression(prop.initializer) && + prop.initializer.elements.length === 0 || neverTaintedJsType(lowerer, prop.initializer, lowerer.typeOf(prop.initializer))))) { + return lowerDynObjectLiteral(lowerer, expr); + } + if (!expected && isJsSourceFile(expr.getSourceFile()) && expr.properties.some((prop) => + ts.isPropertyAssignment(prop) && lowerer.mapTypeOf(lowerer.typeOf(prop.initializer))?.kind === "array" && + tryLowerExpression(lowerer, prop.initializer)?.type.kind === "dyn")) { + return lowerDynObjectLiteral(lowerer, expr); + } + // JavaScript object methods/accessors carry a live receiver. Keep the + // literal in checked native storage instead of erasing its prototype ABI. + if (isJsSourceFile(expr.getSourceFile()) && (expr.properties.length === 0 || expr.properties.some((prop) => + ts.isMethodDeclaration(prop) || ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)))) { + return lowerDynObjectLiteral(lowerer, expr); + } const loc = locOf(expr); // Module collection already chose checked storage for this JavaScript // binding. Construct in that representation: an intermediate record would @@ -577,7 +599,8 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre // A checked spread carries runtime symbol keys and receiver-sensitive // methods absent from its inferred record shape. Copy the real properties. if (!expected && isJsSourceFile(expr.getSourceFile()) && expr.properties.some((property) => - ts.isSpreadAssignment(property) && tryLowerExpression(lowerer, property.expression)?.type.kind === "dyn")) { + ts.isSpreadAssignment(property) && (lowerer.builtinNamespaceModuleOf(property.expression)?.startsWith("path") || + tryLowerExpression(lowerer, property.expression)?.type.kind === "dyn"))) { return lowerDynObjectLiteral(lowerer, expr); } const unionClone = lowerUnionRecordClone(lowerer, expr); @@ -594,7 +617,7 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre expr.properties.some( (p) => { const n = ts.isSpreadAssignment(p) ? undefined : p.name; - return n !== undefined && ts.isComputedPropertyName(n) && literalComputedKey(lowerer, n) === null; + return n !== undefined && ts.isComputedPropertyName(n); }, ) ) { @@ -879,7 +902,8 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre // descriptor records with `any` values) — builds as a dyn OBJECT: // each field converts through the usual dyn boundary, and dynamic // consumers ride the keyed-dyn paths. TypeScript keeps the fence. - if (mapped?.kind === "dyn" || (!mapped && isJsSourceFile(expr.getSourceFile()))) { + if (mapped?.kind === "dyn" || (!mapped && isJsSourceFile(expr.getSourceFile())) || + (!expected && mapped && jsOpenObjectType(expr, mapped, lowerer.shapes, lowerer.unions).kind === "dyn")) { return lowerDynObjectLiteral(lowerer, expr); } if (!mapped || mapped.kind !== "record") lowerer.badType(expr, tsType); @@ -2213,7 +2237,9 @@ export function lowerShorthandValue(lowerer: Lowerer, prop: ts.ShorthandProperty lowerer.requireExactArityValue(prop, propName, sig.params, funcType); return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc }; } - if (lowerer.genericFnsBySymbol.has(resolved)) { + const generic = lowerer.genericFnsBySymbol.get(resolved); + if (generic?.implicitParams) return lowerer.lowerGenericFnValue(propName, generic); + if (generic) { lowerer.unsupported( "SC1090", prop, diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index e39c381bc..d26da155d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -2,7 +2,7 @@ import * as ts from "../../ts7/adapter.js"; import { DYN, JSVAL, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../../ir/ir.js"; import type { IrExpr, IrStmt, SrcLoc } from "../../../ir/ir.js"; import { isNodeEsmFile, locOf } from "../../program.js"; -import type { Lowerer } from "../lowerer.js"; +import { dynUndefinedExpr, type Lowerer } from "../lowerer.js"; /** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on * the member access) and `f?.()` (the token on the call). The receiver @@ -87,14 +87,13 @@ export function isRequireMainFilename(lowerer: Lowerer, expr: ts.Expression): bo /** True when `expr` is the tail of an optional chain that must short-circuit * whole: an unhandled deeper `?.` guarded by a unit-armed union or an island - * value. An island tail needs the guard because an ordinary engine read of - * the short-circuited undefined would throw. Dyn ('unknown') tails use - * their optional keyed reads; never-nullish receivers fold at the token. */ + * value. Dynamic tails need the guard because an ordinary read of the + * short-circuited undefined would throw; never-nullish receivers fold at the token. */ export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): boolean { const tail = chainTailDot(lowerer, expr); if (!tail) return false; const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression)); - if (recvT?.kind === "jsval") return true; + if (recvT?.kind === "jsval" || recvT?.kind === "dyn") return true; if (recvT?.kind !== "union") return false; const def = lowerer.unions.get(recvT.unionId); return !!def && def.arms.some(isUnitType); @@ -135,7 +134,7 @@ export function lowerOptionalChain(lowerer: Lowerer, expr: ts.CallExpression | t recvNode = expr.expression; } else if ( ts.isCallExpression(expr) && - ts.isPropertyAccessExpression(expr.expression) && + (ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression)) && expr.expression.questionDotToken ) { dotNode = expr.expression; // a?.m() @@ -210,17 +209,16 @@ export function lowerOptionalChain(lowerer: Lowerer, expr: ts.CallExpression | t return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr); } if (dotNode === expr && ts.isElementAccessExpression(expr)) { - const key = lowerer.lowerExpr(expr.argumentExpression); - if (key.type.kind === "string") { - return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr); - } - // NUMBER-typed indices (`entries?.[0]`): the property key is - // ToString(i), exactly JS — the canonical number text answers - // array indices in the dyn helper, anything else (fractions, - // negatives, NaN) reads as an absent key. - if (key.type.kind === "f64") { - const skey: IrExpr = { kind: "toString", operand: key, type: STRING, loc: key.loc }; - return lowerer.maybeNarrow({ kind: "dynKeyGet", key: skey, optional: true, value: receiver, type: DYN, loc }, expr); + const id = `chain.${lowerer.chainCounter++}`; + const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; + lowerer.chainRecvByNode.set(recvNode, reference); + lowerer.chainHandled.add(dotNode); + try { + const body = lowerer.lowerExpr(expr); + return { kind: "optChain", id, receiver, body: lowerer.coerceToExpected(body, DYN), type: DYN, loc }; + } finally { + lowerer.chainRecvByNode.delete(recvNode); + lowerer.chainHandled.delete(dotNode); } } // The METHOD-call step (`rawName?.match(re)` on a dyn value): a @@ -264,7 +262,18 @@ export function lowerOptionalChain(lowerer: Lowerer, expr: ts.CallExpression | t } return { kind: "optChain", id, receiver, body, type: DYN, loc }; } - lowerer.unsupported("SC1100", expr, "optional chaining on 'unknown' values"); + const id = `chain.${lowerer.chainCounter++}`; + const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; + lowerer.chainRecvByNode.set(recvNode, reference); + lowerer.chainHandled.add(dotNode); + try { + const body = lowerer.lowerExpr(expr); + if (body.type.kind === "void") return { kind: "optChain", id, receiver, body, type: VOID, loc }; + return { kind: "optChain", id, receiver, body: lowerer.coerceToExpected(body, DYN), type: DYN, loc }; + } finally { + lowerer.chainRecvByNode.delete(recvNode); + lowerer.chainHandled.delete(dotNode); + } } // An 'any' (island-handle) receiver: the nullish test asks the ENGINE // value at runtime — null/undefined short-circuit to the engine's @@ -341,11 +350,8 @@ export function lowerOptionalChain(lowerer: Lowerer, expr: ts.CallExpression | t } const rest = def.arms.filter((a) => !isUnitType(a)); if (rest.length === 0) { - lowerer.unsupported( - "SC1090", - expr, - `'?.' on '${lowerer.fmt(receiver.type)}' without a non-nullish receiver`, - ); + const result = dynUndefinedExpr(loc); + return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: receiver, loc }], result, type: DYN, loc }; } const narrowed = rest.length === 1 ? rest[0]! diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts index f9e751930..e42f40654 100644 --- a/packages/compiler/src/frontend/lowering/function-context.ts +++ b/packages/compiler/src/frontend/lowering/function-context.ts @@ -12,6 +12,11 @@ export interface FnCtx { /** Lexical this has no checker symbol and one binding per function. * Direct parameters and threaded captures use this same slot. */ thisLocal: IrLocal | null; + /** Derived constructors initialize fields at the actual super() site, + * including sites nested in a try/finally block. Nested functions do not + * inherit this hook. */ + superInitialized?: IrLocal; + superCall?: (call: ts.CallExpression) => unknown; localCounters: Map; /** Bindings belong to this lowering of the body. Generic specializations * share checker symbols and AST declarations, but never local storage. */ diff --git a/packages/compiler/src/frontend/lowering/lib-boundary.ts b/packages/compiler/src/frontend/lowering/lib-boundary.ts index b76986c1d..c95c1b444 100644 --- a/packages/compiler/src/frontend/lowering/lib-boundary.ts +++ b/packages/compiler/src/frontend/lowering/lib-boundary.ts @@ -36,7 +36,7 @@ * The pass never rewrites a well-typed argument: typeEquals matches are * untouched, so byte-stability holds for every program that lowered * cleanly before. The validator stays the backstop for anything else. */ -import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; import { canAdaptDynFuncTo, canMarshalTypedFuncIntoIsland, DYN, DYN_HANDLE_KINDS, type IrExpr, type IrStmt, type IrType, JSVAL, type SrcLoc, STRING, isDynTypedRefType, isUnitType, typeEquals } from "../../ir/ir.js"; @@ -264,3 +264,51 @@ function enforceExprBoundary(lowerer: Lowerer, node: IrExpr): void { return; } } + +/** JavaScript evaluates surplus arguments before invoking a fixed native callback. */ +export function lowerSurplusCalls(lowerer: Lowerer, body: IrStmt[]): IrStmt[] { + const stmt = (value: IrStmt): IrStmt => { + // Only JavaScript permits this call shape. Preserve untouched IR + // identities, including the compiler's own recursive union layouts. + let needed = false; + const inspectStmt = (node: IrStmt): boolean => everyStmtChild(node, inspectExpr, inspectStmt); + const inspectExpr = (node: IrExpr): boolean => { + if (node.kind === "callValue" && node.callee.type.kind === "func" && + node.args.length > node.callee.type.params.length && /\.(?:[cm]?js)$/.test(node.loc.file)) needed = true; + return everyExprChild(node, inspectExpr, inspectStmt); + }; + inspectStmt(value); + if (!needed) return value; + const next = mapStmtChildren(value, expr, stmt); + if (next.kind === "return") for (const entry of lowerer.ctx.inferReturn?.entries ?? []) { + if (entry.stmt === value) entry.stmt = next; + } + return next; + }; + const expr = (value: IrExpr): IrExpr => { + const e = mapExprChildren(value, expr, stmt); + if (e.kind !== "callValue" || e.callee.type.kind !== "func") return e; + const params = e.callee.type.params; + if (e.args.length > params.length && /\.(?:[cm]?js)$/.test(e.loc.file)) { + const callee = lowerer.declareHiddenLocal("%surplusCallee", e.callee.type); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: callee.id, init: e.callee, loc: e.loc }]; + let receiver = e.receiver; + if (receiver) { + const local = lowerer.declareHiddenLocal("%surplusReceiver", receiver.type); + stmts.push({ kind: "varDecl", localId: local.id, init: receiver, loc: receiver.loc }); + receiver = { kind: "varRef", localId: local.id, type: receiver.type, loc: receiver.loc }; + } + const args = e.args.map((value) => { + const local = lowerer.declareHiddenLocal("%surplusArgument", value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + return { kind: "varRef" as const, localId: local.id, type: local.type, loc: value.loc }; + }); + const result: IrExpr = { ...e, callee: { kind: "varRef", localId: callee.id, type: e.callee.type, loc: e.loc }, args: args.slice(0, params.length) }; + if (receiver) result.receiver = receiver; + result.args.forEach((value, i) => { result.args[i] = coerceSlot(lowerer, value, params[i]!, `argument ${i + 1} of the call`); }); + return { kind: "seqExpr", stmts, result, type: e.type, loc: e.loc }; + } + return e; + }; + return body.map(stmt); +} diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index 887e4df95..a2593cd44 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -144,6 +144,22 @@ const MODULES = ["path/posix", "path/win32", "os", "worker_threads"] as const; const box = (value: IrExpr): IrExpr => value.type.kind === "dyn" ? value : { kind: "dynFrom", value, type: DYN, loc: value.loc }; +export function pathModuleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { + const fields: { key: IrExpr; value: IrExpr }[] = []; + const add = (member: string, value: IrExpr): void => { fields.push({ key: strLit(member, loc), value: box(value) }); }; + for (const member of Object.keys(BUILTIN_MODULE_FNS[module] ?? {})) { + const value = lowerer.lowerBuiltinCallableValue({ module, member }, loc); + if (value) add(member, value); + } + for (const member of Object.keys(BUILTIN_MODULE_CONSTS[module] ?? {})) { + const value = builtinModuleConstOf(lowerer, module, member); + if (value !== undefined) add(member, builtinConstLit(value, loc)); + } + add("posix", moduleValue(lowerer, "path/posix", loc)); + add("win32", moduleValue(lowerer, "path/win32", loc)); + return { kind: "dynObjLit", fields, type: DYN, loc }; +} + function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { const cacheKey = `%builtin.module.get.${module}`; let name = lowerer.builtinCallableValueFns.get(cacheKey); diff --git a/packages/compiler/src/frontend/lowering/lower-builtins.ts b/packages/compiler/src/frontend/lowering/lower-builtins.ts index 9d2ba13fa..2697da25a 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtins.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtins.ts @@ -1315,6 +1315,9 @@ function lowerFsSyncBufferWindow( export function lowerFsLadderCall(lowerer: Lowerer, expr: ts.CallExpression, bi: { module: string; member: string }, loc: SrcLoc,): IrExpr | null { + if (bi.module === "fs" && bi.member !== "rename" && builtinModuleFnOf(lowerer, bi.module, bi.member)?.fn === "fs.callbackCall") { + return lowerer.lowerBuiltinModuleCall(expr, bi, { fn: "fs.callbackCall", params: [], result: DYN }, loc); + } if (bi.module !== "fs" && bi.module !== "fs/promises") return null; if (!isJsSourceFile(expr.getSourceFile())) return null; const args = expr.arguments; @@ -1576,6 +1579,11 @@ function lowerFsSyncBufferWindow( fn: BuiltinModuleFn, loc: SrcLoc,): IrExpr { const name = expr.expression.getText(); + if (fn.fn === "fs.callbackCall" && bi.member !== "rename") { + if (expr.arguments.some(ts.isSpreadElement)) lowerer.noLowering("filesystem callback call with spread arguments", expr); + const args: IrExpr = { kind: "dynArrLit", elems: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }; + return { kind: "libCall", fn: "fs.callbackCall", args: [{ kind: "strLit", value: bi.member, type: STRING, loc }, args], type: DYN, loc }; + } // Numeric open flags are interpreted symbolically at the call site. The // O_* bit values differ between Darwin and Linux, so emit a stable mask // and let the target runtime select its own native constants. @@ -2638,11 +2646,10 @@ function lowerFsSyncBufferWindow( const bi = ts.isPropertyAccessExpression(targetNode) ? lowerer.builtinMemberOf(targetNode) : null; const fn = bi ? builtinModuleFnOf(lowerer, bi.module, bi.member) : null; if (!fn || fn.variadicPack !== true) { - lowerer.noLowering( - `Reflect.apply of '${targetNode.getText()}'`, - targetNode, - fenceHint, - ); + const target = lowerer.lowerExprExpecting(targetNode, DYN); + const receiver = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + const argumentsList = lowerer.lowerExprExpecting(call.arguments[2]!, DYN); + return { kind: "libCall", fn: "dyn.reflectApply", args: [target, receiver, argumentsList], type: DYN, loc }; } const thisNode = call.arguments[1]!; const effectFree = @@ -2867,8 +2874,8 @@ function lowerFsSyncBufferWindow( } /** `spawn(command, args?, options)` → one cp.spawn / cp.spawnOpts - * libCall. The options argument, when present, must be an object - * LITERAL. Stdio accepts "ignore", "inherit", or "pipe" as the scalar, + * libCall. Computed options use native runtime normalization. Inline + * stdio accepts "ignore", "inherit", or "pipe" as the scalar, * or the 3-tuple whose stdin pipe becomes child.stdin, stdout/stderr * pipes become child.stdout/child.stderr, and output slots may also be * number fds. Omitting stdio uses Node's default: all three piped. The @@ -2903,11 +2910,11 @@ function lowerFsSyncBufferWindow( else optsNode = second; } if (optsNode !== undefined && !ts.isObjectLiteralExpression(optsNode)) { - lowerer.noLowering( - "spawn with a non-literal options argument", - optsNode, - "pass stdio, detached, env, cwd, windowsHide, and shell in an inline object literal", - ); + return { + kind: "libCall", fn: "cp.spawnDynamic", + args: [cmd, lowerer.lowerChildArgsArg(argsNode, loc), lowerer.lowerExprExpecting(optsNode, DYN)], + type: CHILD_T, loc, + }; } const emptyStr: IrExpr = { kind: "strLit", value: "", type: STRING, loc }; @@ -5577,6 +5584,9 @@ function errorFirstBytesCallback(lowerer: Lowerer, node: ts.Expression, api: str return acc; } const init = lowerer.lowerExpr(arg); + if (init.type.kind === "dyn") { + return { kind: "libCall", fn: "sp.newChecked", args: [init], type: SEARCH_PARAMS_T, loc }; + } if (init.type.kind === "string") { return { kind: "libCall", fn: "sp.parse", args: [init], type: SEARCH_PARAMS_T, loc }; } @@ -6558,7 +6568,7 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr if ((name === "on" || name === "once") && call.arguments.length === 2) { const evT = lowerer.typeOf(call.arguments[0]!); const event = evT.isStringLiteralType() ? evT.value : null; - if (event !== "exit" && event !== "close" && event !== "error" && event !== "message" && event !== "disconnect") { + if (event !== "spawn" && event !== "exit" && event !== "close" && event !== "error" && event !== "message" && event !== "disconnect") { lowerer.noLowering( `child.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, call.arguments[0]!, @@ -6592,7 +6602,32 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr loc, }; } + const listenerNode = stripTypeCasts(call.arguments[1]!); + if (isJsSourceFile(listenerNode.getSourceFile()) && + (ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) && + listenerNode.parameters.some((param) => param.dotDotDotToken) && + listenerNode.parameters.every((param) => !param.type) && + !/@(?:param|type)\b/.test(listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()))) { + for (const param of listenerNode.parameters) lowerer.checkedCallbackParams.add(param); + } const cb = lowerer.lowerExpr(call.arguments[1]!); + if (cb.type.kind === "func" && cb.type.rest) { + if (!canBoxFuncIntoDyn(cb.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) { + lowerer.noLowering("child event rest listeners with non-representable parameters", call.arguments[1]!); + } + return { + kind: "libCall", fn: "child.onDyn", + args: [receiver, { kind: "strLit", value: event, type: STRING, loc }, + { kind: "dynFrom", value: cb, type: DYN, loc }], + type: VOID, loc, + }; + } + if (event === "spawn") { + if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") { + lowerer.noLowering("spawn event listeners with parameters or a returned value", call.arguments[1]!); + } + return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc }; + } if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) { lowerer.unsupported( "SC1090", @@ -6697,9 +6732,9 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr // child.unref(): drops the child from the event loop's keep-alive set // (the process may exit while the child runs — Node's semantics; the // child is still reaped while the loop runs for other reasons). - if (name === "unref" && call.arguments.length === 0) { + if ((name === "unref" || name === "ref") && call.arguments.length === 0) { const receiver = lowerer.lowerExpr(access.expression); - return { kind: "libCall", fn: "child.unref", args: [receiver], type: VOID, loc }; + return { kind: "libCall", fn: name === "ref" ? "child.ref" : "child.unref", args: [receiver], type: VOID, loc }; } lowerer.noLowering( `ChildProcess.${name}`, @@ -7132,13 +7167,13 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr if (!ts.isIdentifier(expr.expression)) return null; const bi = lowerer.builtinImportOf(expr.expression); if (!bi || bi.module !== "fs" || bi.member !== "constants") return null; - const MODES: Record = { F_OK: 0, X_OK: 1, W_OK: 2, R_OK: 4 }; + const MODES: Record = { F_OK: 0, X_OK: 1, W_OK: 2, R_OK: 4, COPYFILE_EXCL: 1, COPYFILE_FICLONE: 2, COPYFILE_FICLONE_FORCE: 4 }; const value = own(MODES, expr.name.text); if (value === undefined) { lowerer.noLowering( `fs.constants.${expr.name.text}`, expr, - "F_OK, R_OK, W_OK, and X_OK are the lowered constants", + "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants", ); } return { kind: "numLit", value, type: F64, loc: locOf(expr) }; @@ -8842,24 +8877,40 @@ type TextCodecCtor = { export function lowerTextCodecNew(lowerer: Lowerer, ctor: ts.NewExpression, cls: TextCodecCtor["cls"]): IrExpr { const args = ctor.arguments ?? []; const loc = locOf(ctor); - let encoding = -1; + let encoding: IrExpr = { kind: "numLit", value: -1, type: F64, loc }; let label: IrExpr | null = null; if (cls === "TextEncoder") { if (args.length !== 0) lowerer.noLowering("new TextEncoder with arguments", ctor); } else if (args.length !== 0) { const labelT = lowerer.typeOf(args[0]!); const parsed = labelT.isStringLiteralType() ? staticTextDecoderEncoding(labelT.value) : null; - if (args.length !== 1 || parsed === null) { - lowerer.noLowering("new TextDecoder with runtime-valued options or an unknown label", ctor, - "a recognized literal WHATWG label with default options compiles"); + if (args.length > 2) lowerer.noLowering("new TextDecoder with extra arguments", ctor); + if (parsed === null) { + encoding = { kind: "libCall", fn: "text.decoderEncoding", args: [lowerer.lowerExprExpecting(args[0]!, DYN)], type: F64, loc }; + } else { + encoding = { kind: "numLit", value: parsed.kind === "utf8" ? -1 : parsed.id, type: F64, loc }; + label = lowerer.lowerExprExpecting(args[0]!, STRING); + } + } + const options = args[1] ? stripTypeCasts(args[1]) : null; + const flags = new Map(); + for (const name of ["fatal", "ignoreBOM"]) flags.set(name, { kind: "boolLit", value: false, type: BOOL, loc }); + if (options) { + if (!ts.isObjectLiteralExpression(options)) lowerer.noLowering("TextDecoder options outside an object literal", options); + for (const property of options.properties) { + if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) || + !ts.isIdentifier(property.name) || !flags.has(property.name.text)) lowerer.noLowering("unknown TextDecoder option", property); + const value = ts.isPropertyAssignment(property) ? property.initializer : property.name; + flags.set(property.name.text, lowerer.lowerExprExpecting(value, BOOL)); } - encoding = parsed.kind === "utf8" ? -1 : parsed.id; - label = lowerer.lowerExprExpecting(args[0]!, STRING); } const type = lowerer.mapTypeOf(lowerer.typeOf(ctor)); if (type?.kind !== "record") lowerer.badType(ctor, lowerer.typeOf(ctor)); const result: IrExpr = { - kind: "recordLit", fields: [{ name: `%${cls}`, value: { kind: "numLit", value: encoding, type: F64, loc } }], type, loc, + kind: "recordLit", fields: [{ name: `%${cls}`, value: encoding }, ...(cls === "TextDecoder" ? [ + ...[...flags].map(([name, value]) => ({ name: `%${name}`, value })), + { name: "%state", value: { kind: "dynObjLit", fields: [], type: DYN, loc } as IrExpr }, + ] : [])], type, loc, }; return label === null || label.kind === "strLit" ? result : { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: label, loc }], result, type, loc, @@ -8878,16 +8929,29 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc const cls = storedTextCodecClassOf(lowerer, access.expression); if (cls === null || !lowerer.isStdlibMember(access)) return null; if (access.name.text !== (cls === "TextEncoder" ? "encode" : "decode")) return null; - if (call.arguments.length > 1) { - lowerer.noLowering(cls === "TextDecoder" ? "decode with a stream option" : "TextEncoder.encode with extra arguments", call); + if (call.arguments.length > (cls === "TextDecoder" ? 2 : 1)) { + lowerer.noLowering(`${cls}.${access.name.text} with extra arguments`, call); } const loc = locOf(call); + let stream: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + if (cls === "TextDecoder" && call.arguments[1]) { + const options = stripTypeCasts(call.arguments[1]); + if (!ts.isObjectLiteralExpression(options)) lowerer.noLowering("TextDecoder.decode options outside an object literal", options); + for (const property of options.properties) { + if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) || + !ts.isIdentifier(property.name) || property.name.text !== "stream") lowerer.noLowering("unknown TextDecoder.decode option", property); + stream = lowerer.lowerExprExpecting(ts.isPropertyAssignment(property) ? property.initializer : property.name, BOOL); + } + } const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "record") lowerer.badType(access.expression, lowerer.typeOf(access.expression)); - const arg: IrExpr = call.arguments.length === 0 + let arg: IrExpr = call.arguments.length === 0 ? cls === "TextEncoder" ? strLit("", loc) : { kind: "bytesNew", source: null, type: BYTES_U8, loc } : cls === "TextEncoder" ? lowerer.lowerExprExpecting(call.arguments[0]!, STRING) : lowerer.lowerExpr(call.arguments[0]!); + if (cls === "TextDecoder" && (arg.type.kind === "dyn" || arg.type.kind === "bytes" && arg.type.elem !== "u8")) { + arg = { kind: "libCall", fn: "bytes.bufferSource", args: [lowerer.coerceInto(call.arguments[0]!, arg, DYN)], type: BYTES_U8, loc }; + } if (cls === "TextDecoder" && !(arg.type.kind === "bytes" && arg.type.elem === "u8")) { lowerer.noLowering(`TextDecoder.decode of '${lowerer.fmt(arg.type)}' values`, call, "Uint8Array/Buffer input decodes (ArrayBuffer values have no representation)"); @@ -8901,22 +8965,22 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc const input = varRef("input.0", arg.type, loc); const result: IrExpr = cls === "TextEncoder" ? { kind: "libCall", fn: "buffer.fromStr", args: [input, { kind: "strLit", value: "utf8", type: STRING, loc }], type: BYTES_U8, loc } - : { - kind: "ternary", - cond: { kind: "bin", op: "<", left: { kind: "recordGet", obj: varRef("codec.0", recT, loc), shapeId: recT.shapeId, field: "%TextDecoder", type: F64, loc }, right: { kind: "numLit", value: 0, type: F64, loc }, type: BOOL, loc }, - then: { kind: "libCall", fn: "text.decode", args: [input], type: STRING, loc }, - else_: { kind: "libCall", fn: "text.decodeLegacy", args: [input, { kind: "recordGet", obj: varRef("codec.0", recT, loc), shapeId: recT.shapeId, field: "%TextDecoder", type: F64, loc }], type: STRING, loc }, - type: STRING, loc, - }; + : { kind: "libCall", fn: "text.decodeStream", args: [ + { kind: "recordGet", obj: varRef("codec.0", recT, loc), shapeId: recT.shapeId, field: "%state", type: DYN, loc }, + input, + { kind: "recordGet", obj: varRef("codec.0", recT, loc), shapeId: recT.shapeId, field: "%TextDecoder", type: F64, loc }, + ...["%fatal", "%ignoreBOM"].map((field): IrExpr => ({ kind: "recordGet", obj: varRef("codec.0", recT, loc), shapeId: recT.shapeId, field, type: BOOL, loc })), + varRef("stream.0", BOOL, loc), + ], type: STRING, loc }; lowerer.liftedFns.push({ name, - params: [{ localId: "codec.0", name: "codec", type: recT }, { localId: "input.0", name: "input", type: arg.type }], + params: [{ localId: "codec.0", name: "codec", type: recT }, { localId: "input.0", name: "input", type: arg.type }, { localId: "stream.0", name: "stream", type: BOOL }], returnType: result.type, - locals: [{ id: "codec.0", name: "codec", type: recT, mutable: false }, { id: "input.0", name: "input", type: arg.type, mutable: false }], + locals: [{ id: "codec.0", name: "codec", type: recT, mutable: false }, { id: "input.0", name: "input", type: arg.type, mutable: false }, { id: "stream.0", name: "stream", type: BOOL, mutable: false }], body: [{ kind: "return", value: result, loc }], loc, }); } - return { kind: "call", callee: name, args: [receiver, arg], type: cls === "TextEncoder" ? BYTES_U8 : STRING, loc }; + return { kind: "call", callee: name, args: [receiver, arg, stream], type: cls === "TextEncoder" ? BYTES_U8 : STRING, loc }; } /** A direct construction of THE stdlib TextEncoder/TextDecoder, through @@ -8951,6 +9015,7 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc if (info === null) return lowerStoredTextCodecCall(lowerer, call, access); if (!lowerer.isStdlibMember(access)) return null; const { cls, ctor: recv } = info; + if (cls === "TextDecoder" && ((recv.arguments?.length ?? 0) > 1 || call.arguments.length > 1)) return lowerStoredTextCodecCall(lowerer, call, access); if (!(cls === "TextDecoder" && member === "decode") && !(cls === "TextEncoder" && member === "encode")) { return null; } @@ -8967,11 +9032,7 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc ? staticTextDecoderEncoding(labelT.value) : null; if (ctorArgs.length > 1 || encoding === null) { - lowerer.noLowering( - "new TextDecoder with runtime-valued options or an unknown label", - recv, - "a recognized literal WHATWG label with default options compiles", - ); + return lowerStoredTextCodecCall(lowerer, call, access); } const labelEffect = ctorArgs.length === 1 ? lowerer.lowerExprExpecting(ctorArgs[0]!, STRING) @@ -8998,7 +9059,10 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc ); } const argNode = call.arguments[0]!; - const arg = lowerer.lowerExpr(argNode); + let arg = lowerer.lowerExpr(argNode); + if (arg.type.kind === "dyn" || arg.type.kind === "bytes" && arg.type.elem !== "u8") { + arg = { kind: "libCall", fn: "bytes.bufferSource", args: [lowerer.coerceInto(argNode, arg, DYN)], type: BYTES_U8, loc }; + } if (!(arg.type.kind === "bytes" && arg.type.elem === "u8")) { lowerer.noLowering( `TextDecoder.decode of '${lowerer.fmt(arg.type)}' values`, @@ -9273,7 +9337,7 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc // reads are the routed keyed ops). All-static tuples keep the // typed fence hint below. --dynamic only by construction: jsval // entries exist only there. - if (elems.some((e) => e.type.kind === "jsval")) { + if (lowerer.dynamic && elems.some((e) => e.type.kind === "jsval" || e.type.kind === "dyn")) { const diagsBefore = lowerer.diags.length; try { const marshaled = argNode.elements.map((el, i) => lowerer.jsvalIn(elems[i]!, el)); @@ -9309,7 +9373,7 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc // lifts per element by reference), the result staying an island // value the static side awaits through the island→static bridge. if ( - entries.type.kind === "jsval" || + entries.type.kind === "jsval" || lowerer.dynamic && entries.type.kind === "dyn" || (entries.type.kind === "array" && entries.type.elem.kind === "jsval") ) { const diagsBefore = lowerer.diags.length; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 4540f8828..4a2b539a9 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -17,12 +17,12 @@ import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiRel import { isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js"; import { genResultRecord, isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js"; import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, neverTaintedJsType, newFnCtx, nodeThrowExpr, staticImportNamespaceType } from "./lowerer.js"; -import { enforceLibBoundary } from "./lib-boundary.js"; -import { NARROW_FIRST, STRING_INDEX_METHODS, STR_METHODS, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js"; +import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js"; +import { NARROW_FIRST, STRING_INDEX_METHODS, STR_METHODS, builtinFenceHintOf, builtinModuleFnOf, stdlibGlobalNameOf } from "./surfaces.js"; import { ffiBindingDiag, ffiSignatureDiag, libCallbackDiag, requiresDynamicDiag } from "../../diagnostics/diagnostic.js"; import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { mixinFnShapeOf } from "./lower-mixins.js"; -import { dynStringReceiver, lowerArrayConstructor, lowerArrayFromCall, lowerArrayOfCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerTupleReadMethodCall } from "./lower-containers.js"; +import { dynStringReceiver, lowerArrayConstructor, lowerArrayFromCall, lowerArrayOfCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectConstructor, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerTupleReadMethodCall } from "./lower-containers.js"; import { lowerBytesStaticCall } from "./containers/bytes.js"; import { lowerRegexMethodCall, lowerStringIndexCall, lowerStringMethodCall, lowerStringPaddingCall, lowerStringSplitCall } from "./containers/string-and-regexp.js"; import { createRequireSpecOf, lowerChildStreamMethodCall, lowerChildWriterMethodCall, lowerCreateRequireCall, lowerCryptoHashMethodCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerImportMetaResolveCall, lowerNodeModuleCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectCall, lowerRequireResolveCall, lowerWatcherMethodCall } from "./lower-builtins.js"; @@ -31,7 +31,7 @@ import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { hasRuntimeStatics } from "./class-runtime-statics.js"; import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerCompatReqStreamOptionalCall, lowerHttpClientFnCall } from "./lower-server.js"; -import { EMITTER_API_MEMBERS, exactClassOfReceiver, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, returnsOnlyThis, storedClassValueType, type ClassInfo } from "./lower-classes.js"; +import { EMITTER_API_MEMBERS, exactClassOfReceiver, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, lowerStaticFieldRead, returnsOnlyThis, storedClassValueType, type ClassInfo } from "./lower-classes.js"; import { classCallbackCall, isClassCallback } from "./class-callbacks.js"; import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js"; import { lowerConsoleInspectArg, lowerFormatCall } from "./lower-inspect.js"; @@ -47,6 +47,7 @@ import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedA import { fenceSymbolFieldCopy } from "./symbol-fields.js"; import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js"; import { classStaticDataFor } from "./class-static-data.js"; +import { jsBindingHasOpenWrites } from "./lower-stmts.js"; export { bodyReadsArguments }; @@ -265,6 +266,10 @@ export interface GenericInstance { * - `...xs: T[]`: the ABI type is the array; call sites pack the surplus. */ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): ParamShape { + if (param.initializer && isJsSourceFile(param.getSourceFile()) && !param.type && + !/@(?:param|type)\b/.test(param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()))) { + return { type: DYN, mode: "omittable", bodyType: DYN }; + } // Inferred JS array patterns describe the bindings, not an exact tuple // contract. Consume the actual iterable, including surplus elements. if (ts.isArrayBindingPattern(param.name) && isJsSourceFile(param.getSourceFile()) && !param.type && @@ -273,6 +278,7 @@ export interface GenericInstance { return param.initializer ? { type: DYN, mode: "omittable", bodyType: DYN } : { type: DYN, mode: "required" }; } if (lowerer.checkedCallbackParams.has(param)) { + if (param.dotDotDotToken) return { type: DYN, mode: "dynRest" }; return param.initializer ? { type: DYN, mode: "omittable", bodyType: DYN } : { type: DYN, mode: "required" }; @@ -784,10 +790,7 @@ function defaultParameterShape(lowerer: Lowerer, param: ts.ParameterDeclaration, ); } const v = lowerer.lowerExpr(a); - if ( - v.kind !== "unitLit" && v.kind !== "numLit" && v.kind !== "strLit" && - v.kind !== "boolLit" && v.kind !== "varRef" && v.kind !== "closure" - ) { + if (!isSafeToDiscard(v)) { lowerer.unsupported( "SC1090", a, @@ -1049,7 +1052,7 @@ function completeFuncValueArgs( /** A declaration's checker-derived IR return type. The unmappable-type * diagnostic points at `blame` (the name for top-level declarations, the * whole node for lambda-likes — preserving each caller's historical loc). */ - function hasExplicitJsDocReturn(decl: ts.SignatureDeclaration): boolean { + export function hasExplicitJsDocReturn(decl: ts.SignatureDeclaration): boolean { const sourceFile = decl.getSourceFile(); return /@returns?\b/.test(sourceFile.text.slice(decl.pos, decl.getStart(sourceFile))); } @@ -1060,6 +1063,16 @@ function completeFuncValueArgs( (lowerer.unions.get(type.unionId)?.arms.some((arm) => promiseCarriesDyn(lowerer, arm)) ?? false); } + function producesConstructor(lowerer: Lowerer, type: ts.Type): boolean { + let result: ts.Type | undefined = type; + for (let depth = 0; result && depth < 8; depth++) { + if (lowerer.checker.getConstructSignatures(result).length > 0) return true; + const signatures = lowerer.checker.getCallSignatures(result); + result = signatures.length === 1 ? lowerer.checker.getReturnTypeOfSignature(signatures[0]!) : undefined; + } + return false; + } + export function declaredReturnType(lowerer: Lowerer, decl: ts.SignatureDeclaration, blame: ts.Node): IrType { const sig = lowerer.checker.getSignatureFromDeclaration(decl); if (!sig) lowerer.unsupported("SC1090", decl, "this function form"); @@ -2297,6 +2310,20 @@ function runtimeOptionalHofGenericBinding( if (elem && (elem.flags & ts.TypeFlags.Any) !== 0) return null; } const mapped = lowerer.mapTypeOf(retTs); + if (mapped?.kind === "symbol" && !hasExplicitJsDocReturn(info.decl)) return null; + if (mapped?.kind === "date" && !hasExplicitJsDocReturn(info.decl)) return DYN; + if (!hasExplicitJsDocReturn(info.decl) && info.decl.body) { + let openReturn = false; + ts.walkPreorder(info.decl.body, (node) => { + if (node !== info.decl.body && ts.isFunctionLike(node)) return "skip"; + if (!ts.isReturnStatement(node) || !node.expression || !ts.isIdentifier(node.expression)) return undefined; + const symbol = lowerer.resolveValueSymbol(node.expression); + if (symbol && lowerer.checker.declarationsOf(symbol).some((decl) => + ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl))) openReturn = true; + return undefined; + }); + if (openReturn) return null; + } // A JavaScript result inferred from an initial record can later // carry additional fields. Settle its representation from the // lowered returns instead of copying it back into that initial shape. @@ -2347,16 +2374,32 @@ export function implicitMethodCallInfersReturn(lowerer: Lowerer, call: ts.CallEx * checker's inferred shape. Preserve that boundary when specializing JS. */ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrType | null { while (ts.isParenthesizedExpression(arg)) arg = arg.expression; + if (isJsSourceFile(arg.getSourceFile()) && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))) { + const saved = lowerer.diagSink; + const diagnostics: typeof lowerer.diags = []; + lowerer.diagSink = diagnostics; + let value: IrExpr | null; + try { value = tryLowerExpression(lowerer, arg); } + finally { lowerer.diagSink = saved; } + // Direct inferred helpers can receive native callbacks even when their + // result is an opaque handle that cannot cross checked storage. + if (diagnostics.length === 0 && value?.type.kind === "func") return value.type; + } if (arg.kind === ts.SyntaxKind.ThisKeyword) return lowerer.resolveThis()?.type ?? null; if (ts.isAwaitExpression(arg)) { const awaited = storedImplicitArgumentType(lowerer, arg.expression); return awaited?.kind === "promise" ? awaited.inner : awaited; } - if (ts.isObjectLiteralExpression(arg) && isJsSourceFile(arg.getSourceFile()) && arg.properties.some((property) => - !ts.isSpreadAssignment(property) && property.name !== undefined && ts.isComputedPropertyName(property.name) && - lowerer.foldedStringKeyOf(property.name.expression) === null)) return DYN; + if (ts.isObjectLiteralExpression(arg) && isJsSourceFile(arg.getSourceFile()) && + (arg.properties.length === 0 || arg.properties.some((property) => + ts.isMethodDeclaration(property) || ts.isAccessor(property) || + !ts.isSpreadAssignment(property) && property.name !== undefined && ts.isComputedPropertyName(property.name)))) return DYN; if (ts.isIdentifier(arg)) return lowerer.peekLocal(arg)?.type ?? lowerer.globalOf(arg)?.type ?? null; if (ts.isCallExpression(arg) && ts.isIdentifier(arg.expression)) { + if (isJsSourceFile(arg.getSourceFile())) { + const represented = tryLowerExpression(lowerer, arg); + if (represented?.type.kind === "dyn" || represented?.type.kind === "func") return represented.type; + } const callee = storedImplicitArgumentType(lowerer, arg.expression); if (callee?.kind === "dyn") return DYN; if (callee?.kind === "func") return callee.ret; @@ -2401,12 +2444,29 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp shapes.push({ type: stored, mode: "required" }); return; } + if (stored?.kind === "func") { + // The lowered closure may have an inferred JS destructuring + // parameter widened from the checker's exact tuple to dyn. + // Preserve that signature throughout the specialization. + const checkerType = lowerer.mapTypeOf(lowerer.typeOf(arg)); + argTypes.set(sym, checkerType && typeEquals(checkerType, stored) + ? lowerer.typeOf(arg) : lowerer.checker.getUnknownType()); + shapes.push({ type: stored, mode: "required" }); + return; + } // The argument's own checker type, literal-widened ('add' binds // string) — typeOf consults the ACTIVE instance's bindings, so a // bound param forwarded into another implicit call transitively // instantiates it (this._initCommandGroup(command)). const t = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(arg)); const mapped = (neverTaintedJsType(lowerer, arg, t) ? null : lowerer.mapTypeOf(t)) ?? dynFallbackType(lowerer, arg, t); + if (isJsSourceFile(arg.getSourceFile()) && ts.isArrayLiteralExpression(arg) && mapped?.kind === "array" && + (mapped.elem.kind === "classval" || mapped.elem.kind === "union" && + lowerer.unions.get(mapped.elem.unionId)?.arms.every((arm) => arm.kind === "classval"))) { + argTypes.set(sym, lowerer.checker.getUnknownType()); + shapes.push({ type: DYN, mode: "required" }); + return; + } // JavaScript arguments-based callables travel as checked functions. // Specializing a parameter to the inferred rest signature would // require extracting a native variadic ABI that the value does not @@ -2420,6 +2480,11 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp // That checker supertype is not an empty-record runtime layout: // preserve represented arms through the checked boundary instead // of specializing a call to a union that would discard them. + if (isJsSourceFile(arg.getSourceFile()) && mapped?.kind === "func" && !lowerer.dynConvertible(mapped)) { + argTypes.set(sym, lowerer.checker.getUnknownType()); + shapes.push({ type: DYN, mode: "required" }); + return; + } const mappedArms = mapped?.kind === "union" ? lowerer.unions.get(mapped.unionId)?.arms ?? [] : mapped ? [mapped] : []; const hasTopObject = mappedArms.some((arm) => { const shape = arm.kind === "record" ? lowerer.shapes.get(arm.shapeId) : undefined; @@ -2962,7 +3027,15 @@ export function islandPrimitiveExit(lowerer: Lowerer, call: ts.CallExpression, r if (expr.arguments.length !== 1) { lowerer.noLowering(`clearTimeout with ${expr.arguments.length} arguments`, expr); } - const handle = lowerer.lowerExpr(expr.arguments[0]!); + let handle = lowerer.lowerExpr(expr.arguments[0]!); + if (handle.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + const stored = lowerer.declareHiddenLocal("%timer.clear", DYN); + const ref = varRef(stored.id, DYN, loc); + handle = { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: stored.id, init: handle, loc }], result: { + kind: "ternary", cond: { kind: "dynTest", test: "number", value: ref, type: BOOL, loc }, + then: lowerer.coerceInto(expr.arguments[0]!, ref, F64), else_: numLit(0, loc), type: F64, loc, + }, type: F64, loc }; + } if (handle.type.kind !== "f64") { lowerer.noLowering( `clearTimeout of '${lowerer.fmt(handle.type)}' handles`, @@ -3596,6 +3669,14 @@ function lowerProjectedBuiltinCall( export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); + // Object's checker return type is `any`. Claim known object inputs + // before generic checked-dynamic dispatch tries to load the constructor. + if (ts.isIdentifier(expr.expression) && expr.expression.text === "Object" && + lowerer.isStdlibGlobal(expr.expression, "Object")) { + const object = lowerObjectConstructor(lowerer, expr.arguments, loc); + if (object) return object; + } + // A call whose chain ROOTS at an ambient-undefined name (`declare // const value: Y | undefined; value?.foo("a")`, `declare function // chain...; chain(o).mapValues(f).value()`, a trap binding's read): @@ -3719,7 +3800,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // the chain lowering's re-entrant dispatch from looping. if ( (expr.questionDotToken && !lowerer.chainHandled.has(expr)) || - (ts.isPropertyAccessExpression(expr.expression) && + ((ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression)) && expr.expression.questionDotToken && !lowerer.chainHandled.has(expr.expression)) ) { @@ -3730,6 +3811,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // top-level statement (its field-initializer ordering lives there); // any other position would misorder initialization — rejected. if (expr.expression.kind === ts.SyntaxKind.SuperKeyword) { + const superCall = lowerer.ctx.superCall; + if (superCall) return superCall(expr) as IrExpr; lowerer.unsupported("SC1090", expr, "super() calls anywhere but as a top-level constructor statement"); } // super.method(...): a DIRECT (never virtual) call of the base chain's @@ -3860,6 +3943,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { }); } + if (ts.isPropertyAccessExpression(expr.expression) && + TIMER_MODULE_MEMBERS.has(expr.expression.name.text) && + lowerer.stdlibGlobalMember(expr.expression, "globalThis") === expr.expression.name.text) { + const served = lowerTimersMemberCall(lowerer, expr, expr.expression.name.text, loc); + if (served) return served; + } + // The timer globals — setTimeout/clearTimeout, setInterval/ // clearInterval, setImmediate/clearImmediate. Provenance-checked (a // user function shadowing the name has a different, non-ambient @@ -4025,9 +4115,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // constants ("", false, 0). Wrapper objects stay fenced except where // the receiver is immediately converted by a direct String method call. if ( - ts.isIdentifier(expr.expression) && - expr.expression.text === "BigInt" && - lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) + lowerer.isStdlibGlobal(expr.expression, "BigInt") ) { if (expr.arguments.length !== 1) { lowerer.noLowering(`BigInt with ${expr.arguments.length} arguments`, expr); @@ -4042,13 +4130,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } if ( - ts.isIdentifier(expr.expression) && - (expr.expression.text === "String" || - expr.expression.text === "Boolean" || - expr.expression.text === "Number") && - lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) + lowerer.isStdlibGlobal(expr.expression, "String") || + lowerer.isStdlibGlobal(expr.expression, "Boolean") || + lowerer.isStdlibGlobal(expr.expression, "Number") ) { - const name = expr.expression.text; + const name = lowerer.isStdlibGlobal(expr.expression, "String") ? "String" : + lowerer.isStdlibGlobal(expr.expression, "Boolean") ? "Boolean" : "Number"; if (expr.arguments.length > 1) { lowerer.noLowering(`${name} with ${expr.arguments.length} arguments`, expr); } @@ -4540,6 +4627,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if ( (expr.expression.text === "encodeURIComponent" || expr.expression.text === "encodeURI" || + expr.expression.text === "decodeURI" || expr.expression.text === "decodeURIComponent") && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { @@ -4552,10 +4640,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } const loc = locOf(expr); const argNode = expr.arguments[0]!; - if (name === "decodeURIComponent") { + if (name === "decodeURIComponent" || name === "decodeURI") { const d = optionalCallValue(lowerer, argNode) ?? lowerer.lowerExpr(argNode); const s = lowerer.ensureString(d, argNode); - return { kind: "libCall", fn: "str.decodeUriComponent", args: [s], type: STRING, loc }; + return { kind: "libCall", fn: name === "decodeURI" ? "str.decodeUri" : "str.decodeUriComponent", args: [s], type: STRING, loc }; } const value = optionalCallValue(lowerer, argNode) ?? lowerer.lowerExpr(argNode); const s = lowerer.ensureString(value, argNode); @@ -4841,6 +4929,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // Reflect.apply of a builtin rest-parameter fn (fixtures.js's // fixturesPath idiom) — before the stdlib member fence claims it. lowerReflectCall(lowerer, expr, expr.expression) ?? + lowerBigIntStaticCall(lowerer, expr, expr.expression) ?? lowerer.lowerNumberStaticCall(expr, expr.expression) ?? lowerer.lowerDateCall(expr, expr.expression) ?? lowerer.lowerTextCodecCall(expr, expr.expression) ?? @@ -4912,8 +5001,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // Readable.from — the stream classes' one static (before the // stdlib chokepoint claims the member). lowerStreamStaticCall(lowerer, expr, expr.expression) ?? - // Radix-free n.toString() is the STATIC number formatter (identical - // to `${n}` / String(n)); the explicit-radix form stays island. + // Primitive toString calls, including numeric radices, use the + // native formatters before the island method path. lowerNumberToStringCall(lowerer, expr, expr.expression) ?? // Union receivers whose every arm has a text — the ngrok // `(chunk: Buffer | string) => chunk.toString()` idiom. @@ -4954,7 +5043,6 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // by the receiver's own element. lowerer.lowerFilterNarrowCall(expr, expr.expression) ?? lowerArrayIsArrayCall(lowerer, expr, expr.expression) ?? - lowerBigIntStaticCall(lowerer, expr, expr.expression) ?? lowerBigIntMethodCall(lowerer, expr, expr.expression) ?? lowerSymbolStaticCall(lowerer, expr, expr.expression) ?? lowerSymbolMethodCall(lowerer, expr, expr.expression) ?? @@ -4988,6 +5076,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // monomorphized against the defining literal's declaration. lowerObjLitGenericMethodCall(lowerer, expr, expr.expression); if (intrinsic) return intrinsic; + if (isJsSourceFile(expr.getSourceFile())) { + const staticField = lowerStaticFieldRead(lowerer, expr.expression); + if (staticField?.type.kind === "dyn" && !expr.arguments.some(ts.isSpreadElement)) { + return { kind: "dynCall", callee: staticField, receiver: lowerer.lowerExprExpecting(expr.expression.expression, DYN), calleeName: expr.expression.getText(), + args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(expr) }; + } + } const accessorTarget = lowerer.fieldTarget(expr.expression); if (accessorTarget?.container === "accessor") { const callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression); @@ -5073,7 +5168,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { !symbolFieldInfo(lowerer, expr.expression)) { const access = expr.expression; const value = tryLowerExpression(lowerer, access.expression); - if (value?.type.kind === "dyn" || value?.type.kind === "object" && !lowerer.classes.get(value.type.className)?.def.runtime) { + if (value?.type.kind === "dyn" || value?.type.kind === "generator" || + value && ["array", "bytes", "map", "set"].includes(value.type.kind) && lowerer.dynConvertible(value.type) || + value?.type.kind === "string" && !ts.isStringLiteralLike(access.argumentExpression) && lowerer.dynConvertible(value.type) || + value?.type.kind === "object" && !lowerer.classes.get(value.type.className)?.def.runtime) { const local = lowerer.declareHiddenLocal("%computedReceiver", value.type); const reference: IrExpr = { kind: "varRef", localId: local.id, type: value.type, loc }; const receiver = lowerer.coerceToExpected(reference, DYN); @@ -5175,6 +5273,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { : "value"; return { kind: "dynCall", callee, calleeName, args, type: DYN, loc }; } + if (callee.type.kind === "union" && lowerer.dynConvertible(callee.type) && + lowerer.unions.get(callee.type.unionId)?.arms.some((arm) => arm.kind === "func")) { + return { kind: "dynCall", callee: lowerer.coerceToExpected(callee, DYN), calleeName: expr.expression.getText(), + args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }; + } if (callee.type.kind !== "func") { lowerer.badType(expr.expression, lowerer.typeOf(expr.expression)); } @@ -5430,6 +5533,9 @@ function lowerNumberConstructorValue(lowerer: Lowerer, argNode: ts.Expression, l } if (arg.type.kind === "string") return { kind: "libCall", fn: "num.fromString", args: [arg], type: F64, loc }; if (arg.type.kind === "dyn") return { kind: "libCall", fn: "dyn.numberConstructor", args: [arg], type: F64, loc }; + if (arg.type.kind === "union" && lowerer.dynConvertible(arg.type)) return { + kind: "libCall", fn: "dyn.numberConstructor", args: [lowerer.coerceToExpected(arg, DYN)], type: F64, loc, + }; const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc); if (optionalNumber) return optionalNumber; const scalarUnionNumber = lowerScalarUnionNumber(lowerer, arg, argNode, loc); @@ -5447,6 +5553,7 @@ function lowerNumberConstructorValue(lowerer: Lowerer, argNode: ts.Expression, l * checker guard. Dispatch BigInt by the actual value, including nullish * TypeErrors, rather than interpreting that storage as the narrowed arm. */ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc): IrExpr | null { + if (arg.type.kind === "dyn") return { kind: "libCall", fn: "dyn.bigintConstructor", args: [arg], type: BIGINT_T, loc }; if (arg.type.kind === "bigint") return arg; if (arg.type.kind === "string") return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc }; if (arg.type.kind === "f64") return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc }; @@ -5910,6 +6017,10 @@ function lowerStringPrototypeCall( // padding uses a helper that delays conversion until every argument is ready. const objectWithoutMethodArgs = entry?.maxArgs === 0 && call.arguments.length === 1 && (receiverType.kind === "record" || receiverType.kind === "array" || receiverType.kind === "object"); + if (padding && !scalar && lowerer.dynConvertible(receiverType)) { + return lowerStringPaddingCall(lowerer, call, entry.method as "padStart" | "padEnd", + lowerer.coerceToExpected(receiverValue, DYN), receiverNode, call.arguments.slice(1)); + } if (!scalar && !objectWithoutMethodArgs) { lowerer.noLowering(`String.prototype.${methodAccess.name.text}.call with ${lowerer.fmt(receiverType)} receivers`, call); } @@ -6166,7 +6277,7 @@ function lowerOptionalStringNumber( // tsc types the return by the evolved shape, the binding lowered // dyn): probe the lowering and claim exactly the dyn results. const probed = tryLowerExpression(lowerer, access.expression); - if (isJsSourceFile(call.getSourceFile()) && probed?.type.kind === "func" && + if (probed?.type.kind === "func" && stdlibGlobalNameOf(lowerer, access.expression) === null && !["call", "apply", "bind", "toString", "valueOf", "constructor"].includes(access.name.text) && canBoxFuncIntoDyn(probed.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) { const loc = locOf(call); @@ -6187,6 +6298,10 @@ function lowerOptionalStringNumber( args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }, type: DYN, loc, }; } + if (probed?.type.kind === "classval") { + const info = lowerer.classes.get(probed.type.className); + if (info && (info.callableBase || hasRuntimeStatics(info))) return lowerStaticMethodCall(lowerer, call, access); + } if (probed?.type.kind !== "dyn") return null; recv = probed; } @@ -6194,7 +6309,7 @@ function lowerOptionalStringNumber( // checker result. Runtime static members live on that constructor. if (recv.type.kind === "classval") { const info = lowerer.classes.get(recv.type.className); - if (info && (info.callableBase || hasRuntimeStatics(info))) recv = lowerer.coerceToExpected(recv, DYN); + if (info && (info.callableBase || hasRuntimeStatics(info))) return lowerStaticMethodCall(lowerer, call, access); } // A checker-`any` receiver that already lowered to a real STRING (the // chained form — `cfg.host.trim().toLowerCase()`, where the first step @@ -6291,9 +6406,6 @@ function lowerOptionalStringNumber( // machinery's short-circuit semantics — not modeled here yet. if (call.questionDotToken || access.questionDotToken) return null; const loc = locOf(call); - if (call.arguments.some((a) => ts.isSpreadElement(a))) { - lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); - } const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN); const receiver: IrExpr = { kind: "varRef", localId: receiverLocal.id, type: DYN, loc }; const member: IrExpr = { @@ -6303,6 +6415,13 @@ function lowerOptionalStringNumber( type: DYN, loc: locOf(access), }; + if (call.arguments.some(ts.isSpreadElement)) { + const spread = lowerSpreadArgsCall(lowerer, call, member, loc); + if (spread?.kind === "dynCall") return { kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc }], + result: { ...spread, receiver }, type: DYN, loc }; + lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); + } const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc }], @@ -6376,7 +6495,7 @@ const DYN_DISPATCH_METHODS = new Set([ "dispatchEvent", "preventDefault", "stopPropagation", "stopImmediatePropagation", "composedPath", - "json", "text", "bytes", + "json", "text", "bytes", "arrayBuffer", // Promise.prototype (SCR_DYN_PROMISE receivers): the reaction trio // rides the fiber machinery (scr_dyn_promise_then); on every other dyn // kind then/catch/finally answer the stored-member path (OBJ own @@ -6402,6 +6521,16 @@ export function lowerDynDispatchMethodCall( const method = access.name.text; if ((!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(call.getSourceFile())) || lowerer.chainBlocked(call, access)) return null; if (call.arguments.some((arg) => ts.isSpreadElement(arg))) { + const loc = locOf(call); + const local = lowerer.declareHiddenLocal("%spreadReceiver", DYN); + const receiver = varRef(local.id, DYN, loc); + const callee: IrExpr = { kind: "dynKeyGet", value: receiver, + key: { kind: "strLit", value: method, type: STRING, loc }, type: DYN, loc }; + const spread = lowerSpreadArgsCall(lowerer, call, callee, loc); + if (spread?.kind === "dynCall") return { kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: recv, loc }, + { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc }, loc }], + result: { ...spread, receiver }, type: DYN, loc }; lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } const predicate = method === "filter" && call.arguments[0] @@ -6621,17 +6750,32 @@ const inliningPredicates = new Set(); * and String(n) lower to (Number::toString with radix 10 IS that * conversion, per spec) — booleans the "true"/"false" texts, and strings * the identity read (String.prototype.toString returns `this`). The - * explicit-radix number form keeps its island lowering (ISLAND_SURFACE); + * explicit-radix number form uses the native radix formatter; * null for other receivers, argument shapes, or non-lib members (a * user's own `.toString` takes the ordinary paths). */ function lowerNumberToStringCall(lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression,): IrExpr | null { if (lowerer.chainBlocked(call, access)) return null; - if (access.name.text !== "toString" || call.arguments.length !== 0) return null; + if (access.name.text !== "toString" || call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) return null; const recvKind = lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind; if (recvKind !== "f64" && recvKind !== "bool" && recvKind !== "string") return null; if (!lowerer.isStdlibMember(access)) return null; const operand = lowerer.lowerExpr(access.expression); + // JavaScript array and typed-array reads can still be undefined even + // when the checker declares a number. Preserve that runtime value and + // let native method dispatch format numbers or throw for missing data. + if (operand.type.kind === "dyn") { + return { + kind: "libCall", fn: "dyn.toString", args: [operand, + call.arguments[0] ? lowerer.lowerExprExpecting(call.arguments[0], DYN) : dynUndefinedExpr(locOf(call)), + { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }, + ], type: STRING, loc: locOf(call), + }; + } + if (call.arguments.length === 1) { + if (operand.type.kind !== "f64") return null; + return { kind: "libCall", fn: "num.toStringRadix", args: [operand, lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], type: STRING, loc: locOf(call) }; + } if (operand.type.kind === "string") return operand; // identity, receiver evaluated if (operand.type.kind !== "f64" && operand.type.kind !== "bool") return null; return { kind: "toString", operand, type: STRING, loc: locOf(call) }; @@ -7079,15 +7223,14 @@ const inliningPredicates = new Set(); * maps to string[] (type-mapper.ts), so the array rides the ordinary * slot-directed coercion into whatever the tag's first parameter wants * — string[] exactly, an `any` slot through the dyn boundary, a rest - * pack's first element. `.raw` does not exist on the lowered object: - * reads fence per member, and String.raw itself lowered above (the raw - * spans splice directly, no array materializes). + * pack's first element. Checked tags receive frozen cooked and raw + * arrays with the standard nonenumerable `.raw` descriptor. Typed direct + * tags retain the string-array representation. * * Tag forms: a top-level declared function (the direct-call fast path — * overload sets reconcile through the resolved signature exactly like * plain calls), an island value under --dynamic (engine method/function - * call: the engine side sees a plain marshaled array — a tag reading - * `.raw` there answers undefined where Node carries the raw spans), and + * call with the engine's cooked/raw template object), and * a checked-dynamic value (the dynCall boundary — a non-function tag * throws Node's catchable TypeError). Everything else — generic tags, * method tags, function-value bindings — fences by name. */ @@ -7153,10 +7296,11 @@ const inliningPredicates = new Set(); if (ts.isIdentifier(expr.tag) && !lowerer.isSelfReference(expr.tag)) { if (lowerer.isTopLevelFnSymbol(expr.tag) && !lowerer.peekLocal(expr.tag)) { fenceEarlyAliasUse(lowerer, expr.tag, expr); - if (lowerer.genericFnOf(expr.tag)) { + const genericTag = lowerer.genericFnOf(expr.tag); + if (genericTag && !genericTag.implicitParams) { lowerer.unsupported("SC1090", expr, "tagged templates with generic tag functions"); } - const sig = lowerer.fnSigOf(expr.tag); + const sig = genericTag ? null : lowerer.fnSigOf(expr.tag); if (sig) { lowerer.noteEdge(sig.name); const args = completeArgs(lowerer, values, sig.params, loc, expr, [strings()]); @@ -7185,10 +7329,13 @@ const inliningPredicates = new Set(); // Checked-dynamic tags (`var f: any; f\`abc\``, dyn property chains): // the dynCall boundary — arguments convert into dyn, a non-function // tag throws Node's catchable " is not a function" TypeError. - const callee = lowerer.lowerExpr(expr.tag); + let callee = lowerer.lowerExpr(expr.tag); + if (callee.type.kind === "func" && isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(callee.type)) { + callee = { kind: "dynFrom", value: callee, type: DYN, loc }; + } if (callee.type.kind === "dyn") { const args = [ - lowerer.coerceInto(expr.template, strings(), DYN), + loweredTemplateStrings(lowerer, expr, pieces.map((piece) => piece.text), loc, pieces.map(templateRawTextOf)), ...values.map((a) => lowerer.lowerExprExpecting(a, DYN)), ]; const calleeName = ts.isPropertyAccessExpression(expr.tag) || ts.isElementAccessExpression(expr.tag) @@ -7210,12 +7357,16 @@ function loweredTemplateStrings( expr: ts.TaggedTemplateExpression, cooked: string[], loc: SrcLoc, + raw?: string[], ): IrExpr { // Normal runtime construction keeps this array aligned with the current // ScrArr layout in the backend. The old static-header IR node encoded the // pre-sparse layout in LLVM and crashed as soon as a tag read its strings. - const type = arrayOf(STRING); - const key = `templateStringsGlobal:${loc.file}:${expr.template.getStart()}`; + const type = raw ? DYN : arrayOf(STRING); + const array = (values: string[]): IrExpr => ({ kind: "dynArrLit", elems: values.map((value): IrExpr => ({ kind: "dynFrom", value: { kind: "strLit", value, type: STRING, loc }, type: DYN, loc })), type: DYN, loc }); + const freeze = (value: IrExpr): IrExpr => ({ kind: "libCall", fn: "dyn.freeze", args: [value], type: DYN, loc }); + const checked: IrExpr | null = raw ? freeze({ kind: "libCall", fn: "dyn.defineProperty", args: [array(cooked), { kind: "dynFrom", value: { kind: "strLit", value: "raw", type: STRING, loc }, type: DYN, loc }, { kind: "dynObjLit", fields: [{ key: { kind: "strLit", value: "value", type: STRING, loc }, value: freeze(array(raw)) }], type: DYN, loc }], type: DYN, loc }) : null; + const key = `templateStringsGlobal:${loc.file}:${expr.template.getStart()}:${raw ? "checked" : "typed"}`; let getter = lowerer.widthHelpers.get(key); if (!getter) { const site = lowerer.globalsList.length; @@ -7240,7 +7391,7 @@ function loweredTemplateStrings( { kind: "assign", localId: globalId, - value: { + value: checked ?? { kind: "arrayLit", elems: cooked.map((value): IrExpr => ({ kind: "strLit", value, type: STRING, loc })), type, @@ -7350,6 +7501,22 @@ function loweredTemplateStrings( ); } const shapes = lowerer.paramShapes(node.parameters); + // Error-first callbacks captured by native filesystem adapters receive + // the selected runtime overload. Ambient contextual inference can pick + // the string overload even when options request a Buffer. + if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && ts.isCallExpression(node.parent) && + node.parent.arguments.at(-1) === node) { + const builtin = ts.isPropertyAccessExpression(node.parent.expression) ? lowerer.builtinMemberOf(node.parent.expression) + : ts.isIdentifier(node.parent.expression) ? lowerer.builtinImportOf(node.parent.expression) : null; + if (builtin?.module === "fs" && builtin.member !== "rename" && builtinModuleFnOf(lowerer, builtin.module, builtin.member)?.fn === "fs.callbackCall") { + for (let index = 0; index < shapes.length; index++) { + if (!node.parameters[index]?.type) { + shapes[index]!.type = DYN; + if (shapes[index]!.bodyType) shapes[index]!.bodyType = DYN; + } + } + } + } // A concise arrow over an h2-only stream/session call (`() => // req.stream.destroy()`): the call ALWAYS throws on this lowering // (stream is undefined — the streamUndefCall precedent), so the body @@ -7513,9 +7680,10 @@ function loweredTemplateStrings( const fnCtx = newFnCtx(true, selfSymbol, funcType, bodyReturn); const inferredSignature = isJsSourceFile(node.getSourceFile()) && !node.type ? lowerer.checker.getSignatureFromDeclaration(node) : undefined; - const returnsConstructor = inferredSignature && lowerer.checker.getConstructSignatures(lowerer.checker.getReturnTypeOfSignature(inferredSignature)).length > 0; + const returnsConstructor = inferredSignature && producesConstructor(lowerer, lowerer.checker.getReturnTypeOfSignature(inferredSignature)); if (isJsSourceFile(node.getSourceFile()) && !node.type && !isAsync && !isGenerator && - (bodyReturn.kind === "classval" || bodyReturn.kind === "dyn" && returnsConstructor)) { + (bodyReturn.kind === "classval" || bodyReturn.kind === "date" || bodyReturn.kind === "symbol" || bodyReturn.kind === "record" || + (bodyReturn.kind === "dyn" || bodyReturn.kind === "func") && returnsConstructor)) { fnCtx.inferReturn = { entries: [] }; } fnCtx.inheritsArguments = ts.isArrowFunction(node); @@ -7542,6 +7710,15 @@ function loweredTemplateStrings( params.push({ localId: argsLocal.id, name: "%arguments", type: DYN }); fnCtx.argumentsLocal = argsLocal; } + if (isJsSourceFile(node.getSourceFile()) && !ts.isArrowFunction(node) && + shapes.length === 1 && shapes[0]?.mode === "dynRest" && bodyReadsArguments(node)) { + const argsLocal = lowerer.declareHiddenLocal("%arguments", DYN); + prologue.push({ kind: "varDecl", localId: argsLocal.id, init: { + kind: "dynInvoke", recv: varRef(params[0]!.localId, DYN, loc), method: "slice", + calleeName: "Array.prototype.slice", args: [], type: DYN, loc, + }, loc }); + fnCtx.argumentsLocal = argsLocal; + } let body: IrStmt[]; if (ts.isBlock(node.body!)) { @@ -7604,6 +7781,7 @@ function loweredTemplateStrings( // lib-boundary chokepoint runs here (idempotent for block bodies, // whose statements were already walked). A fence poisons the // enclosing statement — the lambda IS part of it. + body = lowerSurplusCalls(lowerer, body); enforceLibBoundary(lowerer, body); appendImplicitUndefinedReturn(lowerer, body, bodyReturn, loc); @@ -7748,7 +7926,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression if (call.questionDotToken || access.questionDotToken) return null; const member = access.name.text; if (member !== "then" && member !== "catch" && member !== "finally") return null; - let recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + let recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)) ?? dynFallbackType(lowerer, access.expression, lowerer.typeOf(access.expression)); // A dynamic-import promise under an unmappable checker type // (`Promise` — module-namespace types have no // static mapping): the BINDING holds the island promise @@ -7801,6 +7979,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression loc, }; } + if (receiver.type.kind === "dyn" && lowerer.dynConvertible(recvT)) receiver = lowerer.coerceInto(access.expression, receiver, recvT); if (receiver.type.kind !== "promise") { // mapTypeOf said promise but the value lowered as something else — // a lowering gap, named rather than ICEd on. @@ -9170,6 +9349,8 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression object = { kind: "dynFrom", value: object, type: DYN, loc: locOf(call), ...(fnName !== null ? { fnName } : {}) }; } + if (object.type.kind !== "dyn" && canConvertToDyn(object.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) + object = lowerer.coerceInto(call.arguments[0]!, object, DYN); if (object.type.kind === "dyn") { const loc = locOf(call); if (member === "getPrototypeOf") @@ -9550,7 +9731,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression // value is the shared prototype table, not an instance to unbox. const prototypeTarget = ts.isPropertyAccessExpression(targetNode) && targetNode.name.text === "prototype" && (exactClassOfReceiver(lowerer, targetNode.expression) !== null || storedClassValueType(lowerer, targetNode.expression)?.kind === "classval"); - const declaredTarget = target?.type.kind === "dyn" && !prototypeTarget ? lowerer.mapTypeOf(lowerer.typeOf(targetNode)) : null; + const declaredTarget = target?.type.kind === "dyn" && !prototypeTarget && !isJsSourceFile(targetNode.getSourceFile()) ? lowerer.mapTypeOf(lowerer.typeOf(targetNode)) : null; if (target && declaredTarget && isDynTypedRefType(declaredTarget)) target = lowerer.coerceToExpected(target, declaredTarget); if (target && member === "defineProperty") { const native = lowerClassDataDescriptor(lowerer, call, member, target); @@ -9788,7 +9969,8 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression const isPrimitive = probed !== null && probed !== undefined && (isUnitType(probed.type) || probed.type.kind === "string" || probed.type.kind === "f64" || probed.type.kind === "bool"); - if (isDyn || isPrimitive || isFunction || (probed && isDynTypedRefType(probed.type))) { + const isJsUnion = probed?.type.kind === "union" && isJsSourceFile(call.getSourceFile()) && lowerer.dynConvertible(probed.type); + if (isDyn || isPrimitive || isFunction || isJsUnion || (probed && isDynTypedRefType(probed.type))) { const fn = member === "keys" ? "dyn.objKeys" : member === "values" ? "dyn.objValues" : "dyn.objEntries"; let v = lowerer.lowerExpr(argNode); if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) }; @@ -10034,6 +10216,15 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I params.push({ localId: argsLocal.id, name: "%arguments", type: DYN }); ctx.argumentsLocal = argsLocal; } + if (isJsSourceFile(decl.getSourceFile()) && sig.params.length === 1 && + sig.params[0]?.mode === "dynRest" && bodyReadsArguments(decl)) { + const argsLocal = lowerer.declareHiddenLocal("%arguments", DYN); + prologue.push({ kind: "varDecl", localId: argsLocal.id, init: { + kind: "dynInvoke", recv: varRef(params[0]!.localId, DYN, locOf(decl)), method: "slice", + calleeName: "Array.prototype.slice", args: [], type: DYN, loc: locOf(decl), + }, loc: locOf(decl) }); + ctx.argumentsLocal = argsLocal; + } const bodyBlock = blockBodyOf(decl); if (!bodyBlock) { lowerer.unsupported("SC1090", decl, "function declarations whose block body the frontend cannot locate"); @@ -11059,6 +11250,13 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I const recvT = lowerer.typeOf(access.expression); const propSym = lowerer.checker.getPropertyOfType(recvT, name); if (!propSym) return null; + const propertyDecl = lowerer.checker.valueDeclarationOf(propSym); + if (propertyDecl && ts.isShorthandPropertyAssignment(propertyDecl) && ts.isIdentifier(propertyDecl.name) && ts.isObjectLiteralExpression(propertyDecl.parent)) { + requireObjLitGenericReceiver(lowerer, call, access.expression, propertyDecl.parent, name); + const targetSymbol = lowerer.checker.getShorthandAssignmentValueSymbol(propertyDecl); + const target = targetSymbol ? lowerer.genericFnsBySymbol.get((targetSymbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(targetSymbol) : targetSymbol)) : undefined; + if (target) return lowerer.lowerGenericCall(call, target); + } if (!isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker)) return null; // CLASS members belong to the class path (lowerClassGenericMethodCall // claimed compilable ones; a class that failed collection keeps its diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b08292901..30000c269 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7,11 +7,11 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import type { FnCtx, Lowerer } from "./lowerer.js"; import { BOOL, DATE_T, DYN, F64, bytesOf, type IrClassDef, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, typeEquals } from "../../ir/ir.js"; -import { MAX_GENERIC_INSTANCES, appendImplicitUndefinedReturn, bodyReadsArguments, generatorMeta, genericCallInstance, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js"; +import { MAX_GENERIC_INSTANCES, appendImplicitUndefinedReturn, bodyReadsArguments, generatorMeta, genericCallInstance, hasExplicitJsDocReturn, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js"; import { isGenericCallableMemberType, jsOpenObjectType, typeKey, withUnitArm } from "../type-mapper.js"; import { cjsClassExprWholeExportOf, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeTypesPath, locOf } from "../program.js"; -import { PoisonError, dynFallbackType, dynUndefinedExpr, newFnCtx, own } from "./lowerer.js"; -import { lowerArrayConstructor, lowerMapSeedNew, lowerSetSeedNew } from "./lower-containers.js"; +import { PoisonError, dynFallbackType, dynUndefinedExpr, newFnCtx, nodeThrowExpr, own } from "./lowerer.js"; +import { lowerArrayConstructor, lowerMapSeedNew, lowerObjectConstructor, lowerSetSeedNew } from "./lower-containers.js"; import { bufEncoding } from "./containers/bytes.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { lowerSearchParamsNew, lowerTextCodecNew } from "./lower-builtins.js"; @@ -60,10 +60,17 @@ function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { return false; } -export function storedClassValueType(lowerer: Lowerer, expression: ts.Expression): IrType | null { +export function storedClassValueType(lowerer: Lowerer, expression: ts.Expression, visiting = new Set()): IrType | null { + if (visiting.has(expression)) return null; + visiting.add(expression); + if (ts.isParenthesizedExpression(expression)) return storedClassValueType(lowerer, expression.expression, visiting); + if (ts.isClassExpression(expression) && !functionLocalClass(expression) && !lowerer.collectingExprClasses.has(expression)) { + const info = lowerer.lowerClassExpressionInfo(expression); + return { kind: "classval", className: info.def.name }; + } if (ts.isPropertyAccessExpression(expression) && !expression.questionDotToken) { const member = nsMemberIdentOf(lowerer, expression); - if (member) return storedClassValueType(lowerer, member); + if (member) return storedClassValueType(lowerer, member, visiting); } if (expression.kind === ts.SyntaxKind.ThisKeyword) { const stored = lowerer.resolveThis(); @@ -71,11 +78,89 @@ export function storedClassValueType(lowerer: Lowerer, expression: ts.Expression } if (ts.isIdentifier(expression)) { const stored = lowerer.peekLocal(expression) ?? lowerer.globalOf(expression); - if (stored?.type.kind === "classval") return stored.type; + if (stored?.type.kind === "classval" && lowerer.classes.has(stored.type.className)) return stored.type; + const symbol = lowerer.resolveValueSymbol(expression); + const declaration = symbol && lowerer.checker.declarationsOf(symbol).find((decl) => + ts.isVariableDeclaration(decl) && decl.initializer && (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0); + if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { + const value = storedClassValueType(lowerer, declaration.initializer, visiting); + if (value?.kind === "classval") return value; + } } const type = lowerer.typeOf(expression); const mapped = lowerer.mapTypeOf(type); + if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) { + for (const signature of lowerer.checker.getConstructSignatures(type)) { + const constructor = lowerer.checker.signatureDeclaration(signature); + const declaration = constructor && ts.isConstructorDeclaration(constructor) ? constructor.parent : undefined; + if (declaration && ts.isClassExpression(declaration) && !functionLocalClass(declaration) && + !lowerer.collectingExprClasses.has(declaration)) { + const info = lowerer.lowerClassExpressionInfo(declaration); + return { kind: "classval", className: info.def.name }; + } + } + } if (mapped?.kind === "classval" || !ts.isCallExpression(expression)) return mapped; + let callee = expression.expression; + let calls = 1; + const aliases = new Set(); + for (;;) { + if (aliases.has(callee)) break; + aliases.add(callee); + if (ts.isParenthesizedExpression(callee)) { callee = callee.expression; continue; } + if (ts.isCallExpression(callee)) { calls++; callee = callee.expression; continue; } + if (ts.isPropertyAccessExpression(callee)) { + const member = nsMemberIdentOf(lowerer, callee); + if (member) { callee = member; continue; } + } + if (ts.isIdentifier(callee)) { + const symbol = lowerer.resolveValueSymbol(callee); + const declaration = symbol && lowerer.checker.declarationsOf(symbol).find((decl) => + ts.isVariableDeclaration(decl) && decl.initializer && (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0); + if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { + callee = declaration.initializer; + continue; + } + } + break; + } + if ((ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) && isJsSourceFile(callee.getSourceFile()) && + !callee.typeParameters?.length && !lowerer.preparingClassFactories.has(callee)) { + const signature = lowerer.checker.getSignatureFromDeclaration(callee); + let result = signature && lowerer.checker.getReturnTypeOfSignature(signature); + for (let i = 1; result && i < calls; i++) { + const signatures = lowerer.checker.getCallSignatures(result); + result = signatures.length === 1 ? lowerer.checker.getReturnTypeOfSignature(signatures[0]!) : undefined; + } + if (result && lowerer.checker.getConstructSignatures(result).length > 0) { + const returned = (value: IrExpr): IrType | null => { + let type = value.type; + for (let i = 0; i < calls && type.kind === "func"; i++) type = type.ret; + return type.kind === "classval" ? type : null; + }; + const home = callee.parent; + const generic = ts.isVariableDeclaration(home) && ts.isIdentifier(home.name) ? lowerer.genericFnOf(home.name) : null; + if (generic?.implicitParams && ts.isVariableDeclaration(home) && ts.isIdentifier(home.name)) { + const factory = lowerer.lowerGenericFnValue(home.name, generic); + const type = returned(factory); + if (type) return type; + } + lowerer.preparingClassFactories.add(callee); + const collecting = lowerer.collecting, sink = lowerer.diagSink; + lowerer.collecting = false; + lowerer.diagSink = null; + try { + const factory = lowerer.preparedClassFactories.get(callee) ?? lowerer.lowerLambda(callee); + lowerer.preparedClassFactories.set(callee, factory); + const type = returned(factory); + if (type) return type; + } finally { + lowerer.collecting = collecting; + lowerer.diagSink = sink; + lowerer.preparingClassFactories.delete(callee); + } + } + } // A checked call can erase its returned constructor's ABI while the // checker still identifies the class declaration. Collect that class in // its factory's lexical scope before choosing the derived native layout. @@ -123,6 +208,7 @@ export interface ClassMethodSignature { export interface ClassInfo { def: IrClassDef; + expressionInitializationEmitted?: true; runtimeStatics?: readonly ts.ClassElement[]; runtimePrototypeMembers?: readonly ts.ClassElement[]; localPrototypeData?: true; @@ -152,6 +238,7 @@ export interface ClassInfo { /** Redeclared field: initialize the existing slot at this position; * no new slot (def.fields excludes it). */ redeclared?: true; + errorCause?: true; }[]; /** OWN declared methods only — inherited lookups walk the base chain * (findMethodOn). An `abstract` entry is a signature with no body (and @@ -233,6 +320,7 @@ export interface ClassInfo { /** A module class may inherit a compiled ordinary JS constructor. Its * callable and prototype are captured when the class declaration runs. */ callableBase?: { expression: ts.Expression; constructorId: string; prototypeId: string }; + factoryBaseExpression?: ts.Expression; /** DIRECT subclasses, filled as derived classes collect — the frontend's * side of whole-program devirtualization (overrideBelow). */ subclasses: ClassInfo[]; @@ -1119,6 +1207,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla const computedBase = lowerer.computedClassBases.get(lowerer.classNamer(decl)); let base: ClassInfo | null = inst ? inst.family : mixin ? mixin.base : computedBase?.classInfo ?? null; let callableBase: ClassInfo["callableBase"]; + let factoryBaseExpression: ts.Expression | undefined; const adoptCallableBase = (expression: ts.Expression): boolean => { if (familyMode || inst || mixin) return false; if (lowerer.computedCallableBases.has(decl)) { @@ -1199,7 +1288,6 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla // (the class statement evaluates at its init position; a base // block below it would still be uninitialized in Node). if (!t.expression.questionDotToken && nsMemberIdentOf(lowerer, t.expression)) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); if (memberSym) fenceEarlyNsMemberRef(lowerer, t.expression, memberSym); const stored = storedClassValueType(lowerer, t.expression); const nsBase = (resolved ? lowerer.classBySymbol.get(resolved) : undefined) ?? @@ -1212,6 +1300,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla `extending the namespace member '${t.expression.name.text}' (no class lowering)`, ); } + if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); base = nsBase; continue; } @@ -1288,6 +1377,13 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla base = mixinBase; continue; } + const stored = storedClassValueType(lowerer, t.expression); + const factoryBase = stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined; + if (factoryBase) { + base = factoryBase; + factoryBaseExpression = t.expression; + continue; + } } if (!t || !ts.isIdentifier(t.expression)) { if (t && adoptCallableBase(t.expression)) continue; @@ -1370,7 +1466,11 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla } } - if (base?.localClass && !functionLocalClass(decl) && !computedBase) lowerer.unsupported("SC1090", decl, "extending a function-local class outside a function"); + if (base?.localClass && !functionLocalClass(decl) && !computedBase && !factoryBaseExpression) { + const heritage = decl.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]; + if (heritage) factoryBaseExpression = heritage.expression; + else lowerer.unsupported("SC1090", decl, "extending a function-local class outside a function"); + } if (lowerer.collectingExprClasses.has(decl)) { for (let parent = base; parent; parent = parent.base) { if (parent.builtinEmitter || parent.builtinStream !== undefined) { @@ -1471,9 +1571,9 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla } if (functionLocalClass(decl) && isJsSourceFile(decl.getSourceFile()) && (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && member.body && - ts.isComputedPropertyName(member.name) && classMemberNameOf(lowerer, member.name) === null) { + !ts.isPrivateIdentifier(member.name)) { runtimePrototypeMembers.push(member); - continue; + if (ts.isComputedPropertyName(member.name) && classMemberNameOf(lowerer, member.name) === null) continue; } if (ts.isClassStaticBlockDeclaration(member)) { // Statics live on the FAMILY (JS has one class, one static @@ -1527,7 +1627,8 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla ) { const type = lowerer.irTypeOf(member.name); if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); - if (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile())) { + if (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile()) && + (member.type !== undefined || (lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0)) { lowerer.unsupported("SC1090", member.name, "'unknown'-typed static fields"); } staticFields.push({ @@ -1591,8 +1692,13 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla // (every named member kind stores a PropertyName there). const memberName = (member as { name?: ts.PropertyName }).name; if (errorRooted && memberName && classMemberNameOf(lowerer, memberName) === "cause") { - lowerer.unsupported("SC1090", memberName, - "redeclaring Error.cause (pass the cause in the Error constructor options)"); + if (ts.isPropertyDeclaration(member) && !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) { + if (!isJsSourceFile(member.getSourceFile())) lowerer.unsupported("SC1090", memberName, "redeclaring Error.cause (pass the cause in the Error constructor options)"); + fieldOrder.push({ name: "cause", type: DYN, initializer: member.initializer, + ...(member.initializer ? {} : { undefinedInitializer: locOf(member) }), redeclared: true, errorCause: true }); + continue; + } + lowerer.unsupported("SC1090", memberName, "Error.cause accessors"); } // #PRIVATE members compile: their names ('#m') are unspellable by // any public identifier, so they ride the ordinary fields/methods @@ -1729,31 +1835,35 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla // get/set pair — declare the field and accessors explicitly. lowerer.unsupported("SC1090", member, "auto-accessor fields ('accessor x')"); } - if (ts.isComputedPropertyName(member.name)) { - const key = classSymbolKeyOf(lowerer, member.name.expression); - if (!key || (!member.initializer && !isJsSourceFile(member.getSourceFile()))) { - lowerer.unsupported("SC1090", member, "computed class fields without an initializer and a stable module-level literal Symbol()/Symbol.for() key"); + if (ts.isComputedPropertyName(member.name) || ts.isStringLiteralLike(member.name) || ts.isNumericLiteral(member.name)) { + const symbolKey = ts.isComputedPropertyName(member.name) ? classSymbolKeyOf(lowerer, member.name.expression) : null; + const stringKey = ts.isComputedPropertyName(member.name) ? lowerer.foldedStringKeyOf(member.name.expression) + : ts.isNumericLiteral(member.name) ? String(Number(member.name.text)) : member.name.text; + const fieldName = symbolKey?.fieldName ?? stringKey; + if (fieldName === null || (!member.initializer && !isJsSourceFile(member.getSourceFile()))) { + lowerer.unsupported("SC1090", member, "computed class fields without an initializer and a stable string, number, or module-level literal Symbol()/Symbol.for() key"); } const declared = lowerer.typeOf(member); const inferred = member.type || !member.initializer ? declared : lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(member.initializer)); const mapped = member.initializer ? lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred) : DYN; - const type = mapped ? jsOpenObjectType(member, mapped, lowerer.shapes, lowerer.unions) : null; + const type = mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type + ? DYN : mapped ? jsOpenObjectType(member, mapped, lowerer.shapes, lowerer.unions) : null; if (!type || type.kind === "void" || (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile()))) lowerer.badType(member, inferred); const undefinedInitializer = !member.initializer ? locOf(member) : undefined; - const previous = fields.get(key.fieldName); + const previous = fields.get(fieldName); if (previous) { - if (!symbolFields.has(key.identity)) { - lowerer.unsupported("SC1090", member.name, `distinct keys sharing the printable name '${symbolFieldDisplayName(key.fieldName)}' in one class`); + if (symbolKey && !symbolFields.has(symbolKey.identity)) { + lowerer.unsupported("SC1090", member.name, `distinct keys sharing the printable name '${symbolFieldDisplayName(fieldName)}' in one class`); } if (!typeEquals(previous, type)) { - lowerer.unsupported("SC1090", member.name, "redeclaring symbol-keyed fields at a different type"); + lowerer.unsupported("SC1090", member.name, "redeclaring computed fields at a different type"); } - fieldOrder.push({ name: key.fieldName, type, initializer: member.initializer, ...(undefinedInitializer ? { undefinedInitializer } : {}), redeclared: true }); + fieldOrder.push({ name: fieldName, type, initializer: member.initializer, ...(undefinedInitializer ? { undefinedInitializer } : {}), redeclared: true }); } else { - fields.set(key.fieldName, type); - fieldOrder.push({ name: key.fieldName, type, initializer: member.initializer, ...(undefinedInitializer ? { undefinedInitializer } : {}) }); + fields.set(fieldName, type); + fieldOrder.push({ name: fieldName, type, initializer: member.initializer, ...(undefinedInitializer ? { undefinedInitializer } : {}) }); } - symbolFields.set(key.identity, key.fieldName); + if (symbolKey) symbolFields.set(symbolKey.identity, fieldName); continue; } // #private fields ride the ordinary field machinery — the '#' @@ -1816,7 +1926,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla // callable and its receiver instead of adapting its signature. if (isJsSourceFile(member.getSourceFile()) && !member.type && !/@type\b/.test(member.getSourceFile().text.slice(member.pos, member.getStart())) && - type.kind === "func" && lowerer.dynConvertible(type)) type = DYN; + (type.kind === "func" || type.kind === "record") && lowerer.dynConvertible(type)) type = DYN; // JS permits reads before a constructor's first assignment even // when its inferred field type omits undefined. if (isJsSourceFile(member.getSourceFile()) && !member.initializer && type.kind !== "dyn") { @@ -2079,6 +2189,9 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla } } const { shapes, funcType: ft } = lowerer.lambdaSignature(member); + // JS inference may retain only a symbol sentinel while a method + // also returns values from an untyped mutable field or local. + if (isJsSourceFile(member.getSourceFile()) && !member.type && !hasExplicitJsDocReturn(member) && ft.ret.kind === "symbol") ft.ret = DYN; if (isJsSourceFile(member.getSourceFile()) && lowerer.prototypeMethodAccesses.has(mName) && !lowerer.findMethodOn(base, mName)?.declarer.def.runtime && shapes.every((p) => p.mode === "required" || p.mode === "omittable")) { @@ -2179,7 +2292,17 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla // the inherited call/dispatch ABI and refuse at method entry; // lowering this body with the base signature would change JS. const returnOnly = overridden.sig.params.length === shapes.length && - overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)); + overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)) && + !member.parameters.some((parameter, index) => { + if (!parameter.initializer) return false; + const baseMember = overridden.declarer.decl?.members.find((candidate) => + ts.isMethodDeclaration(candidate) && classMemberNameOf(lowerer, candidate.name) === mName); + const inherited = baseMember && ts.isMethodDeclaration(baseMember) ? baseMember.parameters[index]?.initializer : undefined; + if (!inherited) return false; + const ownType = lowerer.mapTypeOf(lowerer.typeOf(parameter.initializer)); + const baseType = lowerer.mapTypeOf(lowerer.typeOf(inherited)); + return ownType !== null && baseType !== null && !typeEquals(ownType, baseType); + }); methodEntryFences.set(mName, unsupportedDiag( "SC1090", locOf(member.name), @@ -2538,20 +2661,31 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla lhs.expression.kind === ts.SyntaxKind.ThisKeyword ) { const key = classSymbolKeyOf(lowerer, lhs.argumentExpression); - if (!key) continue; + const stringKey = lowerer.foldedStringKeyOf(lhs.argumentExpression); + const fieldName = key?.fieldName ?? stringKey; + if (fieldName === null) { + if (lowerer.mapTypeOf(lowerer.typeOf(lhs.argumentExpression))?.kind === "symbol") { + lowerer.unsupported("SC1090", lhs, "symbol-keyed class fields require a stable module-level key"); + } + continue; + } + if (!key && (fields.has(fieldName) || hasAccessor(fieldName))) continue; + if (!key && (methods.has(fieldName) || lowerer.findMethodOn(base, fieldName))) { + lowerer.unsupported("SC1090", lhs, "constructor-assigned fields shadowing methods"); + } // A key already declared (own or inherited) makes later // assignments writes, not declarations. - if (symbolFields.has(key.identity)) continue; - if (fields.has(key.fieldName)) { + if (key && symbolFields.has(key.identity)) continue; + if (fields.has(fieldName)) { // Two DISTINCT Symbol(...) consts with one description in // one layout would need one printable name for two slots. lowerer.unsupported( "SC1090", lhs, - `distinct symbol keys sharing the printable name '${symbolFieldDisplayName(key.fieldName)}' in one class`, + `distinct symbol keys sharing the printable name '${symbolFieldDisplayName(fieldName)}' in one class`, ); } - const propSym = lateBoundByKey.get(key.sym); + const propSym = key ? lateBoundByKey.get(key.sym) : instType ? lowerer.checker.getPropertyOfType(instType, fieldName) : undefined; // tsgo does not synthesize the late-bound `__@name@id` // property for a JS `this[k] = v` declaration (the finding-5 // family: no expando/late-bound synthesis in its stricter @@ -2570,9 +2704,9 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla let type = t ? (lowerer.mapTypeOf(t) ?? dynFallbackType(lowerer, lhs, t)) : null; if (type) type = jsOpenObjectType(lhs, type, lowerer.shapes, lowerer.unions); if (!type || type.kind === "void") lowerer.badType(lhs, t ?? lowerer.typeOf(lhs)); - fields.set(key.fieldName, type); - symbolFields.set(key.identity, key.fieldName); - fieldOrder.push({ name: key.fieldName, type, initializer: undefined }); + fields.set(fieldName, type); + if (key) symbolFields.set(key.identity, fieldName); + fieldOrder.push({ name: fieldName, type, initializer: undefined }); } } } @@ -2718,6 +2852,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla ...(paramProps.length > 0 ? { paramProps } : {}), base, ...(callableBase ? { callableBase } : {}), + ...(factoryBaseExpression ? { factoryBaseExpression } : {}), subclasses: [], throwingSetters, staticFields, @@ -2778,6 +2913,10 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla info.def.baseValueGlobal = `%g.${className}.computedBase`; lowerer.globalsList.push({ id: info.def.baseValueGlobal, name: `${className} base`, type: computedBase.value.type, mutable: false }); } + if (factoryBaseExpression && !functionLocalClass(decl)) { + info.def.baseValueGlobal = `%g.${className}.computedBase`; + lowerer.globalsList.push({ id: info.def.baseValueGlobal, name: `${className} base`, type: { kind: "classval", className: base!.def.name }, mutable: false }); + } if (callableBase && !functionLocalClass(decl)) { lowerer.globalsList.push( { id: callableBase.constructorId, name: `${className} base`, type: DYN, mutable: false }, @@ -2967,7 +3106,20 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla // Mixin instantiations lower their initializers under the // instantiation's bindings/context (a no-op for everything else — // generic FAMILIES own their statics and carry no genericInstance). - return withInstanceBindings(lowerer, info, () => lowerStaticFieldInitsInner(lowerer, info)); + const diagsBefore = lowerer.diags.length; + try { + return withInstanceBindings(lowerer, info, () => lowerStaticFieldInitsInner(lowerer, info)); + } catch (error) { + // Heritage and decorators run before the per-field recovery below. + // Keep their refusal attached to the class declaration instead of + // letting a poisoned initializer escape the module lowering pass. + if (!(error instanceof PoisonError)) throw error; + if (info.decl && isJsSourceFile(info.decl.getSourceFile()) && lowerer.diagSink === null) { + const fence = lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" }); + if (fence) return [fence]; + } + return []; + } } function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] { @@ -2977,6 +3129,15 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla // decorated result is what `this`/the class name mean inside them). const out: IrStmt[] = [...lowerClassDecoration(lowerer, info)]; const computedBase = info.decl ? lowerer.computedClassBases.get(lowerer.classNamer(info.decl)) : undefined; + if (info.factoryBaseExpression && info.def.baseValueGlobal) { + const loc = locOf(info.factoryBaseExpression); + const type: IrType = { kind: "classval", className: info.base!.def.name }; + out.push({ kind: "assign", localId: info.def.baseValueGlobal, value: lowerer.lowerExprExpecting(info.factoryBaseExpression, type), loc }); + out.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [ + lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN), + lowerer.coerceToExpected({ kind: "varRef", localId: info.def.baseValueGlobal, type, loc }, DYN), + ], type: DYN, loc }, loc }); + } if (computedBase && info.def.baseValueGlobal) { const loc = computedBase.value.loc; out.push({ kind: "assign", localId: info.def.baseValueGlobal, value: computedBase.value, loc }); @@ -3021,17 +3182,19 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla lowerer.bumpFileStat(locOf(f.initializer).file, "total"); const diagsBefore = lowerer.diags.length; try { - // `this` in a static field initializer names the CLASS (like a - // static block's), with arrows transparent and this-binding - // function forms opaque — the static-block rule verbatim, named - // here so the generic outside-a-method fence never fires first. - rejectStaticThis( - lowerer, - f.initializer, - () => "'this' in static field initializers (it names the class — reference the class by name instead)", - true, - ); - const value = lowerer.lowerExprExpecting(f.initializer, f.type); + const previousThis = lowerer.ctx.thisLocal; + const receiver = f.initializer.kind === ts.SyntaxKind.ThisKeyword || hasStaticThis(f.initializer) + ? lowerer.declareHiddenLocal("%staticThis", { kind: "classval", className: info.def.name }) : null; + if (receiver) { + lowerer.noteEdge(`%${info.def.name}.constructor`); + out.push({ kind: "varDecl", localId: receiver.id, init: { kind: "classRef", className: info.def.name, + type: receiver.type, loc: locOf(f.initializer) }, loc: locOf(f.initializer) }); + } + let value: IrExpr; + try { + if (receiver) lowerer.ctx.thisLocal = receiver; + value = lowerer.lowerExprExpecting(f.initializer, f.type); + } finally { lowerer.ctx.thisLocal = previousThis; } out.push({ kind: "assign", localId: f.globalId, value, loc: locOf(f.initializer) }); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -3454,7 +3617,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce if (info.classDecorators?.valueGlobalId !== undefined) return null; const loc = locOf(expr); const found = findStaticOn(lowerer, info, expr.name.text); - if (hasRuntimeStatics(info)) return { kind: "dynKeyGet", value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), + if (hasRuntimeStatics(info) && !found?.field) return { kind: "dynKeyGet", value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc }; // A #private static resolves only through the DECLARING class's own // name: in JS the brand lives on that one constructor object, so @@ -3595,7 +3758,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce // drains it), which is JS's order for whole-initializer positions. // Anything subtler (multi-declarator statements, arguments evaluated // after other side effects) is a named fence, never a reordering. - if (info.staticFields.length > 0 || (info.staticBlocks?.length ?? 0) > 0) { + if (!local && (info.callableBase || info.def.baseValueGlobal || info.staticFields.length > 0 || (info.staticBlocks?.length ?? 0) > 0)) { let holder: ts.Node | undefined = expr.parent; while ( ts.isParenthesizedExpression(holder) || ts.isClassExpression(holder) || @@ -3622,7 +3785,6 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce "class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement)", ); } - lowerer.pendingClassExprInits.push(...lowerer.lowerStaticFieldInits(info)); } return info; } @@ -3651,6 +3813,11 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce } } const info = lowerClassExpressionInfo(lowerer, expr); + if (!info.localClass && !info.expressionInitializationEmitted && + (info.callableBase || info.def.baseValueGlobal || info.staticFields.length > 0 || (info.staticBlocks?.length ?? 0) > 0)) { + info.expressionInitializationEmitted = true; + lowerer.pendingClassExprInits.push(...lowerer.lowerStaticFieldInits(info)); + } if (info.localClass && !info.localClass.ready) { const context = newFnCtx(true, null, null, VOID); info.localClass.context = context; @@ -3880,7 +4047,11 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce // export assignment pins it) — the direct-name rules apply. if (!ts.isIdentifier(access.expression)) { if (!isModuleExportsAccess(access.expression) || !isCjsJsFile(access.getSourceFile(), lowerer.program)) { - const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + let receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + if (receiverType?.kind !== "classval" && isJsSourceFile(call.getSourceFile()) && ts.isCallExpression(access.expression)) { + const receiver = tryLowerExpression(lowerer, access.expression); + if (receiver?.type.kind === "classval") receiverType = receiver.type; + } if (receiverType?.kind !== "classval") return null; const info = lowerer.classes.get(receiverType.className); if (!info) return null; @@ -3940,7 +4111,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce throughValue: boolean, ): IrExpr | null { const loc = locOf(call); - if (info.callableBase || hasRuntimeStatics(info)) return { kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), + if (hasRuntimeStatics(info) && !findStaticOn(lowerer, info, access.name.text)?.field || info.callableBase && !findStaticOn(lowerer, info, access.name.text) && !findGenericStaticOn(lowerer, info, access.name.text)) return { kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, calleeName: access.name.text, args: call.arguments.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc }; // #private statics: through-a-VALUE receiver the call devirtualizes @@ -4008,7 +4179,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce } args.push(absent); } - return { kind: "callValue", callee, args, type: found.field.type.ret, loc }; + return { kind: "callValue", callee, receiver: lowerer.lowerExprExpecting(access.expression, DYN), args, type: found.field.type.ret, loc }; } if (found.method.member.body && hasStaticThis(found.method.member)) { const generic = found.method.receiverInfo ??= { @@ -4062,7 +4233,7 @@ function staticMethodForReceiver(lowerer: Lowerer, access: ts.PropertyAccessExpr const loc = locOf(expr); const member = expr.name.text; const info = lowerer.classes.get(recvT.className); - if (info && (info.callableBase || hasRuntimeStatics(info))) return { kind: "dynKeyGet", value: lowerer.lowerExprExpecting(expr.expression, DYN), + if (info && (hasRuntimeStatics(info) && !findStaticOn(lowerer, info, member)?.field || info.callableBase && !findStaticOn(lowerer, info, member))) return { kind: "dynKeyGet", value: lowerer.lowerExprExpecting(expr.expression, DYN), key: { kind: "strLit", value: member, type: STRING, loc }, type: DYN, loc }; // `X.name` reads the RUNTIME class object's stored name — the one // genuinely dynamic member. It CONSUMES the receiver (no evaluation @@ -4456,10 +4627,19 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction // signature-blocked function (lowerStaticMethod's rule) instead of // crashing the whole lowering. if (!info.generic && (always || lowerer.wantBody(`%${className}.constructor`))) { + const diagsBefore = lowerer.diags.length; try { out.push(lowerer.lowerClassCtor(info)); } catch (e) { if (!(e instanceof PoisonError)) throw e; + if (info.decl && isJsSourceFile(info.decl.getSourceFile())) { + const types: IrType[] = [{ kind: "object", className }, ...info.ctorParams.map((param) => param.type)]; + const fallback = lowerer.deferToRuntimeFence(diagsBefore, info.ctor ?? info.decl, { + kind: "function", name: `%${className}.constructor`, returnType: VOID, + params: types.map((type, index) => ({ localId: `p.${index}`, name: index === 0 ? "this" : `p${index}`, type })), + }); + if (fallback) out.push(fallback); + } } } for (const { mName, member } of lowerer.classMethodMembers(info)) { @@ -5054,6 +5234,13 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction const out: IrStmt[] = []; const thisType: IrType = { kind: "object", className: info.def.name }; for (const f of info.fieldOrder) { + if (f.errorCause) { + const loc = f.initializer ? locOf(f.initializer) : f.undefinedInitializer!; + const value = f.initializer ? lowerer.lowerExprExpecting(f.initializer, DYN) : dynUndefinedExpr(loc); + out.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "error.defineCause", + args: [{ kind: "varRef", localId: thisLocal.id, type: thisType, loc }, value], type: VOID, loc }, loc }); + continue; + } if (f.undefinedInitializer) { const loc = f.undefinedInitializer; out.push({ @@ -5124,110 +5311,77 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction return out; } -/** A derived constructor's body: statements lower as usual EXCEPT the - * top-level `super(...)` statement, which becomes a direct call to the - * base constructor over the same `this`, immediately followed by this - * class's field initializers (JS runs them when super returns). tsc - * guarantees a super call exists and runs before any this-use; the - * supported form is a top-level expression statement — anywhere else - * (conditionals, expression positions) is rejected, not misordered. */ - export function lowerDerivedCtorBody(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLocal, - /** Mixin forwarding-constructor mode: `super(...args)` forwards these - * pre-declared synthetic params directly (the spread never lowers — - * the base's ABI is this constructor's ABI). */ - forward?: IrExpr[],): IrStmt[] { - const out: IrStmt[] = []; +/** Super statements initialize the base and then the derived fields on + * whichever control-flow path executes. Repeated calls are explicitly + * refused until native layouts can allocate a second receiver. */ + export function lowerDerivedCtorBody(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLocal, forward?: IrExpr[]): IrStmt[] { let superSeen = false; - const statements = info.ctor!.body!.statements; - const entry = { stmts: statements, index: 0, ctx: lowerer.ctx, frame: lowerer.scopes[lowerer.scopes.length - 1]!, out }; - lowerer.activeStmtLists.push(entry); - try { - for (let index = 0; index < statements.length; index++) { - entry.index = index; - const stmt = statements[index]!; - const superCall = - ts.isExpressionStatement(stmt) && - ts.isCallExpression(stmt.expression) && - stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword - ? stmt.expression - : null; - if (!superCall) { - out.push(...lowerer.lowerStmts([stmt])); - continue; - } - if (!lowerer.suppressStats) { - lowerer.stats.statementsTotal++; - lowerer.bumpFileStat(locOf(stmt).file, "total"); - } - try { - if (superSeen) lowerer.unsupported("SC1090", stmt, "multiple super() calls"); - superSeen = true; - if (info.callableBase) { - if (superCall.arguments.some(ts.isSpreadElement)) lowerer.unsupported("SC1090", superCall, "spreading explicit super arguments into an ordinary constructor"); - const loc = locOf(stmt); - const pack: IrExpr = { kind: "dynArrLit", elems: superCall.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }; - out.push(lowerer.superCallStmt(info, thisLocal, [pack], loc)); - out.push(...lowerer.fieldInitStmts(info, thisLocal)); - out.push(...paramPropInitStmts(lowerer, info, thisLocal)); - continue; - } - const base = superBaseOf(info)!; - if (base.builtinEmitter && superCall.arguments.length > 0) { - // @types/node admits super({ captureRejections }) — no lowering. - lowerer.unsupported("SC1090", superCall, "EventEmitter constructor options ('captureRejections')"); - } - if (base.builtinStream) { - // super(options?) into a runtime stream base: the stream spoke - // parses the options and binds overridden underscore methods. - out.push(...lowerStreamSuperCall(lowerer, info, base, superCall.arguments, thisLocal, locOf(stmt), stmt)); - out.push(...lowerer.fieldInitStmts(info, thisLocal)); - out.push(...paramPropInitStmts(lowerer, info, thisLocal)); - continue; - } + const initialized = lowerer.declareHiddenLocal("%superInitialized", BOOL); + initialized.mutable = true; + const context = lowerer.ctx; + const previous = context.superCall; + const previousInitialized = context.superInitialized; + context.superInitialized = initialized; + context.superCall = (superCall) => { + if (!ts.isExpressionStatement(superCall.parent)) lowerer.unsupported("SC1090", superCall, "super() in an expression position"); + superSeen = true; + const loc = locOf(superCall); + const statements: IrStmt[] = [{ kind: "if", cond: { kind: "varRef", localId: initialized.id, type: BOOL, loc }, then: [{ kind: "exprStmt", expr: nodeThrowExpr(0, "SC2020", "repeated super() calls require a fresh receiver and are not supported yet", DYN, loc), loc }], else_: null, loc }]; + if (info.callableBase) { + if (superCall.arguments.some(ts.isSpreadElement)) lowerer.unsupported("SC1090", superCall, "spreading explicit super arguments into an ordinary constructor"); + const pack: IrExpr = { kind: "dynArrLit", elems: superCall.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }; + statements.push(lowerer.superCallStmt(info, thisLocal, [pack], loc)); + } else { + const base = superBaseOf(info)!; + if (base.builtinEmitter && superCall.arguments.length > 0) lowerer.unsupported("SC1090", superCall, "EventEmitter constructor options ('captureRejections')"); + if (base.builtinStream) { + statements.push(...lowerStreamSuperCall(lowerer, info, base, superCall.arguments, thisLocal, loc, superCall)); + } else { const surplusSpread = forward === undefined && !base.builtinError && !base.builtinEmitter && base.ctorParams.length === 0 && superCall.arguments.some(ts.isSpreadElement); if (surplusSpread) { - // A parameterless base still evaluates and iterates every - // supplied argument before its constructor starts. - const evaluated = lowerer.completeArgs(superCall.arguments, [{ mode: "dynRest", type: DYN }], locOf(stmt), stmt); - for (const value of evaluated) out.push({ kind: "exprStmt", expr: value, loc: locOf(stmt) }); + const evaluated = lowerer.completeArgs(superCall.arguments, [{ mode: "dynRest", type: DYN }], loc, superCall); + for (const value of evaluated) statements.push({ kind: "exprStmt", expr: value, loc }); } - const args = surplusSpread ? [] : forward !== undefined - ? forward - : base.builtinError - ? lowerer.errorConstructorArgs(superCall.arguments, locOf(stmt), stmt) - : base.builtinEmitter - ? [] - : lowerer.completeArgs(superCall.arguments, base.ctorParams, locOf(stmt), stmt); - out.push(lowerer.superCallStmt(info, thisLocal, args, locOf(stmt))); - // super() returns → field initializers → parameter-property - // assignments (Node's order, probed) → the rest of the body. - out.push(...lowerer.fieldInitStmts(info, thisLocal)); - out.push(...paramPropInitStmts(lowerer, info, thisLocal)); - } catch (e) { - if (!(e instanceof PoisonError)) throw e; - if (!lowerer.suppressStats) { - lowerer.stats.statementsFailed++; - lowerer.bumpFileStat(locOf(stmt).file, "failed"); + const surplus = forward === undefined && !base.builtinError && !base.builtinEmitter && + !base.ctorParams.some((param) => param.mode === "rest" || param.mode === "dynRest" || param.mode === "islandRest") && + !superCall.arguments.some(ts.isSpreadElement) && superCall.arguments.length > base.ctorParams.length; + let args = surplusSpread ? [] : forward !== undefined ? forward : base.builtinError + ? lowerer.errorConstructorArgs(superCall.arguments, loc, superCall) : base.builtinEmitter ? [] + : lowerer.completeArgs(surplus ? superCall.arguments.slice(0, base.ctorParams.length) : superCall.arguments, base.ctorParams, loc, superCall); + if (surplus) { + args = args.map((arg) => { + const local = lowerer.declareHiddenLocal("%superArgument", arg.type); + statements.push({ kind: "varDecl", localId: local.id, init: arg, loc }); + return { kind: "varRef", localId: local.id, type: arg.type, loc }; + }); + for (const argument of superCall.arguments.slice(base.ctorParams.length)) { + statements.push({ kind: "exprStmt", expr: lowerer.lowerExpr(argument), loc: locOf(argument) }); + } } + statements.push(lowerer.superCallStmt(info, thisLocal, args, loc)); } } + statements.push({ kind: "assign", localId: initialized.id, value: { kind: "boolLit", value: true, type: BOOL, loc }, loc }); + delete context.superInitialized; + try { statements.push(...lowerer.fieldInitStmts(info, thisLocal), ...paramPropInitStmts(lowerer, info, thisLocal)); } + finally { context.superInitialized = initialized; } + const result: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc }; + return { kind: "seqExpr", stmts: statements.map((statement) => statement.kind === "runtimeFence" + ? { kind: "exprStmt", expr: nodeThrowExpr(0, statement.code, statement.message, DYN, statement.loc), loc: statement.loc } + : statement), result, type: result.type, loc }; + }; + try { + const result = lowerer.lowerStmts(info.ctor!.body!.statements); + result.push({ kind: "if", cond: { kind: "unary", op: "!", operand: { kind: "varRef", localId: initialized.id, type: BOOL, loc: locOf(info.ctor!) }, type: BOOL, loc: locOf(info.ctor!) }, then: [{ kind: "exprStmt", expr: nodeThrowExpr(5, "", "Must call super constructor in derived class before accessing 'this' or returning from derived constructor", DYN, locOf(info.ctor!)), loc: locOf(info.ctor!) }], else_: null, loc: locOf(info.ctor!) }); + if (!superSeen) lowerer.pushDiag(unsupportedDiag("SC1090", locOf(info.ctor!), "derived constructors without a lowered super() call")); + return [{ kind: "varDecl", localId: initialized.id, init: { kind: "boolLit", value: false, type: BOOL, loc: locOf(info.ctor!) }, loc: locOf(info.ctor!) }, ...result]; } finally { - lowerer.activeStmtLists.pop(); + if (previous) context.superCall = previous; + else delete context.superCall; + if (previousInitialized) context.superInitialized = previousInitialized; + else delete context.superInitialized; } - if (!superSeen) { - // tsc guarantees the call exists somewhere; if it wasn't a top-level - // statement the per-site rejection above already fired — this is the - // constructor-level backstop so a half-initialized ctor never emits. - lowerer.pushDiag( - unsupportedDiag( - "SC1090", - locOf(info.ctor!), - "super() calls anywhere but as a top-level constructor statement", - ), - ); - } - return out; } /** `super(args)` → direct call of the base constructor with the SAME @@ -5605,6 +5759,16 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (instanceConstructor) return instanceConstructor; const objectFactory = lowerObjectFactoryNew(lowerer, expr); if (objectFactory) return objectFactory; + if (ts.isIdentifier(expr.expression)) { + const symbol = lowerer.resolveValueSymbol(expr.expression); + const declaration = symbol && lowerer.checker.declarationsOf(symbol).find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined); + if (declaration && ts.isFunctionDeclaration(declaration) && declaration.body) { + const last = declaration.body.statements.at(-1); + if (last && ts.isReturnStatement(last) && last.expression && ts.isObjectLiteralExpression(last.expression)) { + lowerer.unsupported("SC1090", expr, "object-returning constructors that observe this or return mixed primitive/object values"); + } + } + } let selected: ts.Expression = expr.expression; while (ts.isParenthesizedExpression(selected)) selected = selected.expression; if (ts.isConditionalExpression(selected)) { @@ -5812,11 +5976,16 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const agent = lowerHttpAgentNew(lowerer, expr); if (agent) return agent; } - if (ts.isIdentifier(expr.expression)) { + if (ts.isIdentifier(expr.expression) || ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.name) && + lowerer.isStdlibGlobal(expr.expression.expression, "globalThis")) { // `import C = N.C; new C()` — the alias's own source-order guards // (a no-op for every non-import= binding). - fenceEarlyAliasUse(lowerer, expr.expression, expr); - const symbol = lowerer.resolveValueSymbol(expr.expression); + if (ts.isIdentifier(expr.expression)) fenceEarlyAliasUse(lowerer, expr.expression, expr); + const identifier = ts.isIdentifier(expr.expression) ? expr.expression : expr.expression.name; + const symbol = ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null; + if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Array")) { + return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []); + } if (isNativeProxyInitializer(lowerer, expr)) return lowerNativeProxy(lowerer, expr); // `new Error(msg?)` and its standard subclasses: the // runtime-provided classes construct through one libCall — the result @@ -6039,22 +6208,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // types get their half named specifically instead of the component // fence (SC2009, which names Map slots at value positions elsewhere). // Both Array() and new Array() share the elements/count lowering. - // `new Object()` — the spec's OrdinaryObjectCreate, exactly what the - // `{}` literal builds (fresh reference identity, no own properties) — - // lowers as the empty record. The ARGUMENT form is Object(x): it - // returns its argument for objects and BOXES primitives — the wrapper - // story with no lowering — so it keeps the constructor fence. - if ( - symbol?.name === "Object" && - lowerer.isStdlibSymbol(symbol) && - (expr.arguments ?? []).length === 0 - ) { - return { - kind: "recordLit", - fields: [], - type: { kind: "record", shapeId: lowerer.shapes.intern([]) }, - loc, - }; + if (symbol?.name === "Object" && lowerer.isStdlibSymbol(symbol)) { + const object = lowerObjectConstructor(lowerer, expr.arguments ?? [], loc); + if (object) return object; } if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) { return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []); @@ -6183,6 +6339,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const seeded = lowerSetSeedNew(lowerer, expr.arguments![0]!, mapped); if (seeded) return seeded; } + if (!mapped && isJsSourceFile(expr.getSourceFile()) && (expr.arguments?.length ?? 0) <= 1 && !expr.arguments?.some(ts.isSpreadElement)) { + return { kind: "libCall", fn: "dyn.nativeSetNew", args: [expr.arguments?.[0] + ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) : dynUndefinedExpr(loc)], type: DYN, loc }; + } // JavaScript's identity-Set idiom: `new Set([setTimeout, atob, // ...])` — the element TYPE (a union of stdlib signatures) has no // mapping, but the element VALUES all lower to identity tokens @@ -6351,7 +6511,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { lowerer.noLowering( `new ${expr.expression.text}`, expr, - ctorHints[expr.expression.text], + ctorHints[ts.isIdentifier(expr.expression) ? expr.expression.text : expr.expression.name.text], symbol, ); } @@ -6487,6 +6647,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ); } const callee = lowerer.lowerExpr(expr.expression); + if (callee.type.kind === "dyn") return checkedClassConstruction(lowerer, callee, (expr.arguments ?? []).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), loc); if (callee.type.kind !== "classval") lowerer.badType(expr.expression, lowerer.typeOf(expr.expression)); lowerer.noteEdge(`%${info.def.name}.constructor`); // Every constructor a value in this slot can dispatch to is a @@ -6542,8 +6703,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } // Checked construction supports emitted classes and builtin typed arrays. const callee = tryLowerExpression(lowerer, expr.expression); - if (callee?.type.kind === "dyn" && !(expr.arguments ?? []).some(ts.isSpreadElement)) { - return checkedClassConstruction(lowerer, callee, (expr.arguments ?? []).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), loc); + if (callee && (callee.type.kind === "dyn" || callee.type.kind === "func" && lowerer.dynConvertible(callee.type)) && !(expr.arguments ?? []).some(ts.isSpreadElement)) { + return checkedClassConstruction(lowerer, lowerer.coerceToExpected(callee, DYN), (expr.arguments ?? []).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), loc); } lowerer.unsupported("SC1090", expr, "constructing values other than classes declared in the program"); } diff --git a/packages/compiler/src/frontend/lowering/lower-containers.ts b/packages/compiler/src/frontend/lowering/lower-containers.ts index 51d217c91..4482a11b2 100644 --- a/packages/compiler/src/frontend/lowering/lower-containers.ts +++ b/packages/compiler/src/frontend/lowering/lower-containers.ts @@ -15,11 +15,24 @@ import { islandPrimitiveExit, lowerDynDispatchMethodCall } from "./lower-calls.j import { buildArraySortFn } from "./lower-array-sort.js"; import { arrayIndexPresent, arrayValueRead, arrayValueStore, arrayValueType, currentArrayIndexPresent } from "./array-values.js"; import { typeKey } from "../type-mapper.js"; -import { type WidthLift, newFnCtx, nodeThrowExpr } from "./lowerer.js"; +import { type WidthLift, dynUndefinedExpr, newFnCtx, nodeThrowExpr } from "./lowerer.js"; import { boolLit, countedFor, numLit, strLit, varRef } from "../../ir/build.js"; import { defaultAfterUndefined, lowerPositionArgument, lowerStaticallyUndefinedArgument, positionNumber } from "./optional-arguments.js"; import { lowerArrayCopyWithin, lowerArrayFill } from "./array-indexed-mutation.js"; +/** Object() and new Object() preserve known object inputs. Primitive + * boxing and unproven checked-dynamic values retain their refusal. */ +export function lowerObjectConstructor(lowerer: Lowerer, args: readonly ts.Expression[], loc: SrcLoc): IrExpr | null { + if (args.length === 0) { + return { kind: "recordLit", fields: [], type: { kind: "record", shapeId: lowerer.shapes.intern([]) }, loc }; + } + if (args.length !== 1 || ts.isSpreadElement(args[0]!)) return null; + const value = lowerer.lowerExpr(args[0]!); + if (value.kind === "dynObjLit" || value.kind === "dynArrLit") return value; + if (["record", "array", "object", "regex", "date", "bytes", "map", "set", "func", "classval"].includes(value.type.kind)) return value; + return null; +} + function primitivePositionType(lowerer: Lowerer, type: IrType): boolean { if (type.kind === "union") { return lowerer.unions.get(type.unionId)?.arms.every((arm) => primitivePositionType(lowerer, arm)) ?? false; @@ -1854,6 +1867,9 @@ export function lowerTupleReadMethodCall( const { fnArg, arity } = hofCallbackArg(lowerer, callback, [elem], receiver.type); const ret = fnArg.type.ret; if (ret.kind === "void" || ret.kind === "func") lowerer.badType(call, lowerer.typeOf(call)); + if (method === "flatMap" && (ret.kind === "dyn" || ret.kind === "jsval" || (ret.kind === "record" && lowerer.shapes.get(ret.shapeId)?.tuple))) { + return { kind: "dynInvoke", recv: { kind: "dynFrom", value: receiver, type: DYN, loc }, method: "flatMap", calleeName: access.getText(), args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }], type: DYN, loc }; + } if (method === "flatMap" && ret.kind === "union" && lowerer.unions.get(ret.unionId)!.arms.some(arm => arm.kind === "array")) { lowerer.noLowering("tuple .flatMap callback mixing array and scalar results", callback, "return an array from every path"); } @@ -2451,6 +2467,9 @@ export function tryLowerNumericIndexRead(lowerer: Lowerer, operand: IrExpr, loc: if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [arrayValueType(lowerer, elem)], arrT); const fnRet = fnArg.type.ret; + if (fnRet.kind === "dyn" || fnRet.kind === "jsval" || (fnRet.kind === "record" && lowerer.shapes.get(fnRet.shapeId)?.tuple)) { + return { kind: "dynInvoke", recv: { kind: "dynFrom", value: receiver, liveRef: true, type: DYN, loc }, method: "flatMap", calleeName: access.getText(), args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }], type: DYN, loc }; + } if (fnRet.kind === "union") { const def = lowerer.unions.get(fnRet.unionId); if (def?.arms.some((a) => a.kind === "array")) { @@ -3113,7 +3132,7 @@ export function lowerArrayOfCall(lowerer: Lowerer, call: ts.CallExpression, /** Mapper-less checked Array.from: acquire once, then step through emitted * property and call dispatch so native class iterators retain their methods. */ -function lowerCheckedArrayFrom(lowerer: Lowerer, source: IrExpr, loc: SrcLoc, mapper?: IrExpr, receiver?: IrExpr): Extract { +export function lowerCheckedArrayFrom(lowerer: Lowerer, source: IrExpr, loc: SrcLoc, mapper?: IrExpr, receiver?: IrExpr, numeric = false): Extract { const iterator = lowerer.declareHiddenLocal("%fromIterator", DYN); const next = lowerer.declareHiddenLocal("%fromNext", DYN); const step = lowerer.declareHiddenLocal("%fromStep", DYN); @@ -3126,7 +3145,8 @@ function lowerCheckedArrayFrom(lowerer: Lowerer, source: IrExpr, loc: SrcLoc, ma const item = get(varRef(step.id, DYN, loc), "value"); const mapped: IrExpr = mapper && index ? { kind: "ternary", cond: { kind: "dynTest", test: "undefined", value: mapper, type: BOOL, loc }, then: item, else_: { kind: "dynCall", callee: mapper, ...(receiver ? { receiver } : {}), calleeName: "Array.from mapper", args: [item, lowerer.coerceToExpected(varRef(index.id, F64, loc), DYN)], type: DYN, loc }, type: DYN, loc } : item; - let append: IrStmt = { kind: "exprStmt", expr: { kind: "dynInvoke", recv: varRef(out.id, DYN, loc), method: "push", calleeName: "Array.from", args: [mapped], type: DYN, loc }, loc }; + const element = numeric ? lowerer.coerceToExpected({ kind: "libCall", fn: "dyn.toNumberCoerce", args: [mapped], type: F64, loc }, DYN) : mapped; + let append: IrStmt = { kind: "exprStmt", expr: { kind: "dynInvoke", recv: varRef(out.id, DYN, loc), method: "push", calleeName: "Array.from", args: [element], type: DYN, loc }, loc }; if (mapper) { const error = lowerer.declareHiddenLocal("%fromError", CAUGHT); const close = lowerer.declareHiddenLocal("%fromReturn", DYN); @@ -3207,6 +3227,25 @@ export function lowerArrayFromValue(lowerer: Lowerer, loc: SrcLoc): IrExpr { if (access.name.text !== "from") return null; const loc = locOf(call); const args = call.arguments; + if (isJsSourceFile(call.getSourceFile()) && args.length >= 1 && args.length <= 3 && !args.some(ts.isSpreadElement)) { + const name = "%builtin.Array.fromDirect"; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const context = newFnCtx(false, null, null, DYN); + lowerer.fnStack.push(context); + try { + const params = ["source", "mapper", "receiver"].map((name) => { + const local = lowerer.declareHiddenLocal(name, DYN); + return { localId: local.id, name, type: DYN }; + }); + const refs = params.map((param) => varRef(param.localId, DYN, loc)); + const value = lowerCheckedArrayFrom(lowerer, refs[0]!, loc, refs[1], refs[2]); + lowerer.liftedFns.push({ name, params, returnType: DYN, locals: context.locals, + body: [...value.stmts, { kind: "return", value: value.result, loc }], loc }); + } finally { lowerer.fnStack.pop(); } + } + return { kind: "call", callee: name, args: [0, 1, 2].map((i) => args[i] + ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc)), type: DYN, loc }; + } // MAPPER-LESS `Array.from({ length: n })` (usually with an explicit // type argument — the pMap results-array idiom): a length-n array of // ABSENT slots, filled by index before any read. Union elements with @@ -3534,13 +3573,10 @@ function buildArrayFromArrayFn(lowerer: Lowerer, name: string, elem: IrType, } if (receiverIr?.kind !== "map") return null; // Collection views can refine has() while retaining its native ABI. - if (!lowerer.isStdlibMember(access) && name !== "has") return null; + if (!probedUntyped && !lowerer.isStdlibMember(access) && name !== "has") return null; const loc = locOf(call); const value = lowerer.lowerExpr(access.expression); const receiver = lowerer.runtimeOptionalPropertyReceiver(access.expression, value, receiverIr, name) ?? value; - // The lib's `set` returns the Map (chaining typechecks); the lowered - // set is a void statement, so a chained receiver has no value — fence - // it instead of emitting a void receiver. if (receiver.type.kind !== "map") { lowerer.noLowering( "chained Map method calls", @@ -3572,7 +3608,12 @@ function buildArrayFromArrayFn(lowerer: Lowerer, name: string, elem: IrType, const v = receiverIr.value.kind === "dyn" ? lowerer.lowerCollectionKey(call.arguments[1]!, receiverIr.value) : lowerer.lowerExprExpecting(call.arguments[1]!, receiverIr.value); - return { kind: "mapIntrinsic", method: "set", receiver, args: [k, v], type: VOID, loc }; + const slot = lowerer.declareHiddenLocal("%mapSetReceiver", receiver.type); + const ref = varRef(slot.id, receiver.type, loc); + return { kind: "seqExpr", stmts: [ + { kind: "varDecl", localId: slot.id, init: receiver, loc }, + { kind: "exprStmt", expr: { kind: "mapIntrinsic", method: "set", receiver: ref, args: [k, v], type: VOID, loc }, loc }, + ], result: ref, type: receiver.type, loc }; } if (name === "has" || name === "delete") { const k = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.key); @@ -3961,19 +4002,23 @@ export function lowerCollectionSpread(lowerer: Lowerer, source: IrExpr, node: ts const name = access.name.text; if (!SET_METHODS.has(name) && !SET_COMBINE_METHODS.has(name)) return null; let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + let probedUntyped = false; // The identity-Set idiom (JS): the CHECKER type is an unmappable // Set, but the VALUE lowered as a real Set of // identity tokens (the new-Set probe) — the lowered receiver's type // is the honest dispatch key. if (receiverIr?.kind !== "set" && isJsSourceFile(access.getSourceFile())) { const probed = tryLowerExpression(lowerer, access.expression); - if (probed?.type.kind === "set") receiverIr = probed.type; + if (probed?.type.kind === "set") { + receiverIr = probed.type; + probedUntyped = true; + } } if (receiverIr?.kind !== "set") return null; // A collection interface may refine has() into a type predicate. // Type mapping checked that view's native ABI; the value check below // still rejects structural mocks and assertions over other objects. - if (!lowerer.isStdlibMember(access) && name !== "has") return null; + if (!probedUntyped && !lowerer.isStdlibMember(access) && name !== "has") return null; const loc = locOf(call); const value = lowerer.lowerExpr(access.expression); const receiver = lowerer.runtimeOptionalPropertyReceiver(access.expression, value, receiverIr, name) ?? value; @@ -5914,7 +5959,8 @@ function mapFromSeedValue(lowerer: Lowerer, seed: IrExpr, mapT: IrType & { kind: } return { kind: "call", callee: name, args: [seed], type: mapT, loc }; } - if (seed.type.kind === "dyn" || seed.type.kind === "array" && seed.type.elem.kind === "dyn") { + if (seed.type.kind === "dyn" || seed.type.kind === "array" && + (seed.type.elem.kind === "dyn" || seed.type.elem.kind === "array")) { const key = `checked-seed:${typeKey(mapT)}`; let name = lowerer.mapHofHelpers.get(key); if (!name) { diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 1858afa83..ad1f85f30 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -152,7 +152,8 @@ export function lowerDgramDnsModuleCall(lowerer: Lowerer, expr: ts.CallExpressio // fence — abort-driven close has no lowering yet. const raw = lowerer.lowerExpr(prop.initializer); const provablyNot = raw.type.kind === "string" || raw.type.kind === "f64" || - raw.type.kind === "bool" || raw.type.kind === "record" || raw.type.kind === "array"; + raw.type.kind === "bool" || raw.type.kind === "record" || raw.type.kind === "array" || + ts.isObjectLiteralExpression(prop.initializer); if (provablyNot && lowerer.dynConvertible(raw.type)) { return { kind: "libCall", @@ -160,7 +161,7 @@ export function lowerDgramDnsModuleCall(lowerer: Lowerer, expr: ts.CallExpressio args: [ { kind: "strLit", value: "options.signal", type: STRING, loc }, { kind: "strLit", value: "an instance of AbortSignal", type: STRING, loc }, - { kind: "dynFrom", value: raw, type: DYN, loc }, + lowerer.coerceToExpected(raw, DYN), ], type: DGRAMSOCK_T, loc, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index b2bfba7eb..746196325 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -19,7 +19,7 @@ import { BIGINT_T, BYTES_ELEMENT_NAME, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js"; import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, COMPOUND_ASSIGN_OPS, type CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STRING_INDEX_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js"; import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; -import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js"; +import { PoisonError, dynFallbackType, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js"; import { lowerCollectionSpread, lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall, tryLowerNumericIndexRead } from "./lower-containers.js"; import { arrayValueRead, arrayValueStore, arrayValueType } from "./array-values.js"; import { lowerOptionalStringIndex } from "./string-index.js"; @@ -38,7 +38,7 @@ import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTy import { lowerYield } from "./lower-generators.js"; import { errorPropertyRead, errorPropertyWrite, errorToStringCall } from "./error-methods.js"; import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js"; -import { countedFor, numLit, varRef } from "../../ir/build.js"; +import { countedFor, numLit, strLit, varRef } from "../../ir/build.js"; import { unionWideningTags } from "../../ir/analysis.js"; import { isSafeToDiscard, isSafeToMoveConditionEarlier, isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { globalSymbolKey } from "./expressions/global-symbols.js"; @@ -59,6 +59,8 @@ import { isClassCallback } from "./class-callbacks.js"; import { builtinPrototypeMethod, lowerArrayIsArrayValue, lowerCheckedPredicateValue, lowerNumberParserValue, lowerObjectAssignValue, lowerStringCodesValue } from "./lower-builtin-values.js"; import { lowerArrayFromValue } from "./lower-containers.js"; import { lowerPerfHooksTypeof } from "./lower-builtins.js"; +import { jsBindingHasOpenWrites } from "./lower-stmts.js"; +import { checkedClassInstanceOf } from "./class-construction.js"; /** An assignable `obj.field` target — a class field, a record field, or a * class ACCESSOR property (reads become getter calls, writes setter calls; @@ -1136,21 +1138,35 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // type-mapper.ts pins the one concrete signature `(value: string) => // primitive`; direct calls `String(x)` never reach here — the call // lowering intercepts them with the wider static coercions). - const jsPrimitiveCtorSelector = - isJsSourceFile(expr.getSourceFile()) && - expr.parent !== undefined && - ts.isArrowFunction(expr.parent) && - expr.parent.body === expr; - if ( - (expr.text === "String" || expr.text === "Number" || expr.text === "Boolean") && - (!isJsSourceFile(expr.getSourceFile()) || jsPrimitiveCtorSelector) && - lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(expr)) - ) { - return primitiveCtorClosure(lowerer, expr.text, loc, jsPrimitiveCtorSelector); + const jsPrimitiveCtorSelector = isJsSourceFile(expr.getSourceFile()); + const primitiveName = stdlibGlobalNameOf(lowerer, expr); + if (primitiveName === "String" || primitiveName === "Number" || primitiveName === "Boolean" || primitiveName === "BigInt") { + return primitiveCtorClosure(lowerer, primitiveName, loc, jsPrimitiveCtorSelector); } + if (lowerer.isStdlibGlobal(expr, "ArrayBuffer")) return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc }; if (Object.values(BYTES_ELEMENT_NAME).includes(expr.text) && lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(expr))) { return { kind: "libCall", fn: "bytes.constructor", args: [{ kind: "strLit", value: expr.text, type: STRING, loc }], type: DYN, loc }; } + if ((expr.text === "encodeURIComponent" || expr.text === "encodeURI" || expr.text === "decodeURIComponent" || expr.text === "decodeURI") && lowerer.isStdlibGlobal(expr, expr.text)) { + const name = `%builtin.${expr.text}`; + const type = funcOf([DYN], STRING); + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const value: IrExpr = { kind: "libCall", fn: "dyn.toStringCoerce", args: [varRef("value", DYN, loc)], type: STRING, loc }; + const fn = expr.text === "encodeURIComponent" ? "str.encodeUriComponent" : expr.text === "encodeURI" ? "str.encodeUri" : expr.text === "decodeURI" ? "str.decodeUri" : "str.decodeUriComponent"; + lowerer.liftedFns.push({ name, params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], returnType: STRING, + body: [{ kind: "return", value: { kind: "libCall", fn, args: [value], type: STRING, loc }, loc }], loc }); + } + return { kind: "closure", fnName: name, captures: [], type, loc }; + } + if (!lowerer.dynamic && lowerer.isStdlibGlobal(expr, "crypto")) return { kind: "libCall", fn: "crypto.native", args: [], type: DYN, loc }; + const nativeAlias = stdlibGlobalNameOf(lowerer, expr); + if (nativeAlias === "Buffer" && expr.text !== "Buffer") { + return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; + } + if (!lowerer.dynamic && lowerer.isStdlibGlobal(expr, "fetch")) { + return { kind: "libCall", fn: "fetch.function", args: [], type: DYN, loc }; + } // The lib fence's IDENTIFIER chokepoint: the real standard library // resolves names the old minimal ambient world never declared // (Symbol, Reflect, Infinity, Date, ...) — and the adopted @@ -1351,7 +1367,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // `this` in plain nested functions is already a tsc error under // noImplicitThis, so the generic scope walk here is preflight-safe. const local = lowerer.resolveThis(); - if (local) return { kind: "varRef", localId: local.id, type: local.type, loc }; + if (local) { + const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc }; + const initialized = lowerer.ctx.superInitialized; + if (initialized) return { kind: "seqExpr", stmts: [{ kind: "if", cond: { kind: "unary", op: "!", operand: { kind: "varRef", localId: initialized.id, type: BOOL, loc }, type: BOOL, loc }, then: [{ kind: "exprStmt", expr: nodeThrowExpr(5, "", "Must call super constructor in derived class before accessing 'this' or returning from derived constructor", DYN, loc), loc }], else_: null, loc }], result: value, type: value.type, loc }; + return value; + } // `this` in a plain JS FUNCTION (not a method): the AMBIENT // RECEIVER (libCall dyn.this — scr_dyn_this_get). Firing sites bind // the emitting handle around each listener call (Node calls @@ -1589,6 +1610,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ["iterator", "asyncIterator", "toStringTag", "toPrimitive", "hasInstance", "isConcatSpreadable", "match", "matchAll", "replace", "search", "split", "species", "unscopables", "dispose", "asyncDispose"].includes(expr.name.text)) { return { kind: "libCall", fn: "sym.wellKnown", args: [{ kind: "strLit", value: expr.name.text, type: STRING, loc }], type: { kind: "symbol" }, loc }; } + if (!lowerer.dynamic && lowerer.stdlibGlobalMember(expr, "Object") === "freeze") { + const name = "%builtin.Object.freeze"; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({ name, + params: [{ localId: "value", name: "value", type: DYN }], locals: [{ id: "value", name: "value", type: DYN, mutable: false }], returnType: DYN, + body: [{ kind: "return", value: { kind: "libCall", fn: "dyn.freeze", args: [varRef("value", DYN, loc)], type: DYN, loc }, loc }], loc }); + return { kind: "closure", fnName: name, captures: [], type: funcOf([DYN], DYN), loc }; + } if (lowerer.stdlibGlobalMember(expr, "globalThis") === "console") { return { kind: "libCall", fn: "console.native", args: [], type: DYN, loc }; } @@ -1675,14 +1703,6 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { } return read; } - // `m.groups` on a match result whose regex is statically known: - // the compile-time record projection over the honest slice (or - // Node's undefined when the pattern has no named groups) — see - // lowerMatchGroupsRead. - { - const g = lowerMatchGroupsRead(lowerer, expr); - if (g !== null) return g; - } // `m.index` on a for-of-over-matchAll binding reads the companion- // index array at this iteration's cursor — always a number: every // drained row matched, so the lib's `.index` is never undefined @@ -1700,6 +1720,24 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { }; } } + if ((expr.name.text === "index" || expr.name.text === "input") && !expr.questionDotToken) { + const represented = tryLowerExpression(lowerer, expr.expression); + if (represented && isMatchSliceType(lowerer, represented.type) && + (lowerer.isStdlibMember(expr) || isJsSourceFile(expr.getSourceFile()))) { + const recv = represented; + const read: IrExpr = { kind: "dynKeyGet", value: lowerer.coerceToExpected(recv, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc }; + return lowerer.maybeNarrow(read, expr); + } + } + // `m.groups` on a match result whose regex is statically known: + // the compile-time record projection over the honest slice (or + // Node's undefined when the pattern has no named groups) — see + // lowerMatchGroupsRead. + { + const g = lowerMatchGroupsRead(lowerer, expr); + if (g !== null) return g; + } // `arguments.length` in a TYPED function whose signature is // FIXED-ARITY (no optional/default/rest parameters): tsc enforces // exact arity at every call site and call/apply/bind indirection is @@ -1932,6 +1970,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const canonical = stdlibGlobalNameOf(lowerer, expr); if (canonical !== null) { if (!lowerer.dynamic && canonical === "globalThis") return lowerGlobalValue(lowerer, expr); + if (!lowerer.dynamic && canonical === "ArrayBuffer") return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc }; + if (!lowerer.dynamic && canonical === "fetch") return { kind: "libCall", fn: "fetch.function", args: [], type: DYN, loc }; + if (!lowerer.dynamic && Object.values(BYTES_ELEMENT_NAME).includes(canonical)) return { kind: "libCall", fn: "bytes.constructor", args: [{ kind: "strLit", value: canonical, type: STRING, loc }], type: DYN, loc }; return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc }; } } @@ -2267,13 +2308,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { } function importMetaProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { - if (expr.questionDotToken !== undefined || !ts.isMetaProperty(expr.expression)) return null; + if (!ts.isMetaProperty(expr.expression)) return null; const meta = expr.expression; if (meta.keywordToken !== ts.SyntaxKind.ImportKeyword || meta.name.text !== "meta") return null; const sf = expr.getSourceFile(); const fileName = moduleFileName(lowerer, sf); switch (expr.name.text) { + case "env": + return dynUndefinedExpr(locOf(expr)); case "url": return { kind: "strLit", @@ -2633,6 +2676,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (expr.type.kind === "dyn") { if (isJsSourceFile(node.getSourceFile()) && ts.isIdentifier(node)) { const symbol = lowerer.resolveValueSymbol(node); + if (symbol && lowerer.checker.declarationsOf(symbol).some((decl) => + ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl))) return expr; + if (symbol && lowerer.checker.declarationsOf(symbol).some((decl) => + ts.isParameter(decl) && decl.initializer && !decl.type && + !/@(?:param|type)\b/.test(decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart())))) return expr; if (symbol && lowerer.checker.declarationsOf(symbol).some((decl) => { if (!ts.isParameter(decl) || decl.type) return false; const fn = decl.parent; @@ -3598,6 +3646,11 @@ function lowerPromiseThenPresence( * the receiver type AND the ambient-file provenance of the member are * verified — the name alone proves nothing. */ export function lowerIntrinsicProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { + for (const name of ["String", "Number", "Boolean", "BigInt"] as ("String" | "Number" | "Boolean" | "BigInt")[]) { + if (!lowerer.chainBlocked(expr) && lowerer.isStdlibGlobal(expr, name)) { + return primitiveCtorClosure(lowerer, name, locOf(expr), isJsSourceFile(expr.getSourceFile())); + } + } if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") { const value = lowerer.lowerNativeCallableValue({ fn: "dyn.ownKeys", params: [DYN], result: DYN, valueParams: [{ mode: "required", type: DYN }], @@ -3890,6 +3943,9 @@ function lowerPromiseThenPresence( ); } if (kind === "regex") { + if (name === "lastIndex") { + return { kind: "regexIntrinsic", method: "lastIndex", receiver: strictReceiver({ kind: "regex" }), args: [], type: F64, loc: locOf(expr) }; + } if (name === "source" || name === "flags") { const receiver = strictReceiver({ kind: "regex" }); return { kind: "regexIntrinsic", method: name, receiver, args: [], type: STRING, loc: locOf(expr) }; @@ -4055,6 +4111,9 @@ function lowerPromiseThenPresence( // destination can use the ordinary tuple-to-array coercion afterward. const tsType = underConstAssertion(expr) ? lowerer.typeOf(expr) : ctxType ?? lowerer.typeOf(expr); let mapped = expected ?? lowerer.mapTypeOf(tsType); + // Inferred JavaScript arrays can contain unrelated constructors with + // structurally equal checker types. Their native class ABIs still differ. + if (!expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array" && mapped.elem.kind === "classval") mapped = DYN; // An inferred destructuring pattern supplies a contextual [any, ...] // tuple. Its positions do not erase the literal's inferred element // types; explicit destinations already arrive through expected. @@ -4167,6 +4226,11 @@ function lowerPromiseThenPresence( mapped = arrayOf(first.type); } } + if (!expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array") { + const elem = mapped.elem; + const arms = elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : [elem]; + if (arms?.length && arms.every((arm) => arm.kind === "classval")) mapped = DYN; + } // An EMPTY literal under a CONST ASSERTION whose type queries panicked // (tsgo's readonly-[] TupleType conversion — the facade answers `any`, // which maps to nothing statically and to an island value under @@ -4180,6 +4244,11 @@ function lowerPromiseThenPresence( ) { mapped = arrayOf(unitOnlyUnion(lowerer.unions)); } + // Heterogeneous JavaScript object arrays must retain each literal's + // real keys. A structural union can otherwise drop descriptor fields. + if (!expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array" && mapped.elem.kind === "union" && + expr.elements.some(ts.isObjectLiteralExpression)) mapped = DYN; + // A TUPLE-typed slot (`const t: [string, number] = ["a", 1]`): the // literal constructs the tuple's record shape — one positional field // per element, source order (which IS index order, so evaluation order @@ -4331,11 +4400,7 @@ function lowerPromiseThenPresence( // the checker inferred a typed array from its producer. Drain once // and validate the elements before the ordinary spread copy. if (src.type.kind === "dyn" && (type.elem.kind === "dyn" || type.elem.kind === "f64" || type.elem.kind === "string" || type.elem.kind === "bool")) { - src = lowerer.coerceInto(el.expression, { - kind: "libCall", fn: "dyn.iterPack", args: [src, - { kind: "strLit", value: el.expression.getText(), type: STRING, loc: locOf(el) }], - type: DYN, loc: locOf(el), - }, type); + src = lowerer.coerceInto(el.expression, checkedIteratorPack(lowerer, src, locOf(el)), type); } // A same-family array whose ELEMENT lifts (string[] into a // (string | symbol)[] literal — per-element wrap/width copy): @@ -4659,8 +4724,9 @@ export function lowerOptionalNumber( if (target) return lowerer.maybeNarrow(lowerer.fieldGetExpr(target, locOf(expr), expr), expr); let receiver = lowerer.lowerExpr(expr.expression); const classInfo = receiver.type.kind === "classval" ? lowerer.classes.get(receiver.type.className) : undefined; - if (receiver.type.kind === "func" || - (receiver.type.kind === "object" && RUNTIME_ERROR_CLASSES.has(receiver.type.className)) || isDynTypedRefType(receiver.type) || + if (receiver.type.kind === "func" || receiver.type.kind === "generator" || + ((receiver.type.kind === "array" || receiver.type.kind === "string" || receiver.type.kind === "bytes") && lowerer.dynConvertible(receiver.type)) || + (receiver.type.kind === "object" && lowerer.dynConvertible(receiver.type)) || isDynTypedRefType(receiver.type) || (classInfo && (classInfo.callableBase || hasRuntimeStatics(classInfo))) || ((receiver.type.kind === "map" || receiver.type.kind === "set") && lowerer.dynConvertible(receiver.type))) { const fnName = jsFuncNameOf(expr.expression); receiver = { kind: "dynFrom", value: receiver, type: DYN, loc: locOf(expr), @@ -4762,6 +4828,11 @@ export function lowerOptionalNumber( // declared number type. if (recv.type.kind === "jsval") return islandElementRead(lowerer, expr, recv); const index = lowerer.lowerExpr(expr.argumentExpression); + if (isJsSourceFile(expr.getSourceFile()) && recv.type.kind === "bytes") { + return { kind: "dynKeyGet", value: lowerer.coerceInto(expr.expression, recv, DYN), + key: index.type.kind === "f64" ? { kind: "toString", operand: index, type: STRING, loc: locOf(expr) } : index, + type: DYN, loc: locOf(expr) }; + } if (index.type.kind === "dyn" && recv.type.kind === "bytes") { return { kind: "dynKeyGet", value: lowerer.coerceInto(expr.expression, recv, DYN), key: index, type: DYN, loc: locOf(expr) }; } @@ -5071,6 +5142,11 @@ export function lowerOptionalNumber( // is undefined, whose property lookup misses just like NaN; keep that // value instead of rejecting a checker-number index with optional IR. const index = lowerOptionalNumber(lowerer, lowerer.lowerExpr(expr.argumentExpression), locOf(expr.argumentExpression), expr.argumentExpression); + if (arr.type.kind === "array" && (index.type.kind === "dyn" || + isJsSourceFile(expr.getSourceFile()) && index.type.kind !== "f64" && lowerer.dynConvertible(index.type))) { + return { kind: "dynKeyGet", value: lowerer.coerceToExpected(arr, DYN), + key: index, type: DYN, loc: locOf(expr) }; + } if (index.type.kind !== "f64") { lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys"); } @@ -5496,11 +5572,29 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression const target = expr.left as ts.ElementAccessExpression; fenceNodeModuleMutation(lowerer, target, "assignment"); const loc = locOf(expr); - const receiverType = lowerer.mapTypeOf(lowerer.typeOf(target.expression)); + const checked = lowerer.lowerExpr(target.expression); + if (checked?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + const stmts: IrStmt[] = []; + const save = (value: IrExpr, name: string): IrExpr => { + const local = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc }); + return varRef(local.id, value.type, loc); + }; + const receiver = save(checked, "%compoundReceiver"); + const key = save(lowerer.lowerExprExpecting(target.argumentExpression, DYN), "%compoundKey"); + const old = save({ kind: "dynKeyGet", value: receiver, key, type: DYN, loc }, "%compoundOld"); + const rhs = lowerer.lowerExprExpecting(expr.right, DYN); + const numeric = ["-", "*", "/", "%", "**"].includes(op); + const value = save({ kind: "libCall", fn: op === "+" ? "dyn.add" : numeric ? "dyn.arithmetic" : "dyn.bitwise", + args: op === "+" ? [old, rhs] : [old, rhs, strLit(op, loc)], type: DYN, loc }, "%compoundResult"); + stmts.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySetComputed", args: [receiver, key, value], type: VOID, loc }, loc }); + return { kind: "seqExpr", stmts, result: value, type: DYN, loc }; + } + const receiverType = checked.type.kind === "array" || checked.type.kind === "bytes" ? checked.type : lowerer.mapTypeOf(lowerer.typeOf(target.expression)); if (receiverType?.kind !== "array" && receiverType?.kind !== "bytes") { lowerer.unsupported("SC1090", target, "compound assignment to non-array elements"); } - let receiver = lowerer.lowerExpr(target.expression); + let receiver = checked; if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0) { const present = lowerer.stripUndefinedArm(receiver.type); const helper = present.kind === receiverType.kind @@ -5533,6 +5627,11 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression computed = { kind: "strConcat", left: lowerer.ensureString(oldRef(), target), right: lowerer.ensureString(rhs, expr.right), type: STRING, loc }; } else if (valueType.kind === "f64" && numericRhs.type.kind === "f64") { computed = { kind: "bin", op, left: lowerOptionalNumber(lowerer, oldRef(), loc), right: numericRhs, type: F64, loc }; + } else if (valueType.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + const left = lowerer.coerceToExpected(oldRef(), DYN), right = lowerer.coerceToExpected(rhs, DYN); + const numeric = ["-", "*", "/", "%", "**"].includes(op); + computed = { kind: "libCall", fn: op === "+" ? "dyn.add" : numeric ? "dyn.arithmetic" : "dyn.bitwise", + args: op === "+" ? [left, right] : [left, right, strLit(op, loc)], type: DYN, loc }; } else { lowerer.unsupported("SC1043", expr); } @@ -5650,6 +5749,9 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression } const receiver = lowerer.lowerExpr(target.expression); if (receiver.type.kind === "dyn") return { kind: "exprStmt", expr: lowerDynMemberAssignment(lowerer, expr, receiver), loc: locOf(expr) }; + if (isJsSourceFile(expr.getSourceFile()) && receiver.type.kind === "object" && lowerer.dynConvertible(receiver.type)) { + return { kind: "exprStmt", expr: lowerDynMemberAssignment(lowerer, expr, lowerer.coerceToExpected(receiver, DYN)), loc: locOf(expr) }; + } lowerer.unsupported( "SC1090", target, @@ -6367,7 +6469,7 @@ export function lowerTemplate(lowerer: Lowerer, expr: ts.TemplateExpression): Ir return { kind: "jsExit", value: inner, type: target, loc: locOf(expr) }; } const targetTs = lowerer.checker.getTypeFromTypeNode(expr.type); - const target = lowerer.mapTypeOf(targetTs); + const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs); if (!target) lowerer.badType(expr.type, targetTs); if (target.kind === "dyn") return inner; // `as unknown`: erasure // An all-`unknown`-fields record target (`err as { code?: unknown }` @@ -6903,32 +7005,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr // consumer sees exactly what the checker typed. if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) { const recv = tryLowerExpression(lowerer, expr.left.expression); - if (recv && recv.type.kind === "dyn") { - const recvTmp = lowerer.declareHiddenLocal("%setRecv", DYN); - const rhsVal = lowerer.lowerExpr(expr.right); - const valTmp = lowerer.declareHiddenLocal("%setVal", rhsVal.type); - const valRef = (): IrExpr => ({ kind: "varRef", localId: valTmp.id, type: rhsVal.type, loc }); - const stored = lowerer.coerceToExpected(valRef(), DYN); - if (stored.type.kind !== "dyn") { - lowerer.unsupported("SC1101", expr.right, `assigning '${lowerer.fmt(rhsVal.type)}' values into a checked-dynamic member`); - } - const key: IrExpr = { kind: "strLit", value: expr.left.name.text, type: STRING, loc: locOf(expr.left.name) }; - return { - kind: "seqExpr", - stmts: [ - { kind: "varDecl", localId: recvTmp.id, init: recv, loc }, - { kind: "varDecl", localId: valTmp.id, init: rhsVal, loc }, - { - kind: "exprStmt", - expr: { kind: "libCall", fn: "dyn.keySet", args: [{ kind: "varRef", localId: recvTmp.id, type: DYN, loc }, key, stored], type: VOID, loc }, - loc, - }, - ], - result: valRef(), - type: rhsVal.type, - loc, - }; - } + if (recv && recv.type.kind === "dyn") return lowerDynMemberAssignment(lowerer, expr, recv); const field = lowerer.fieldTarget(expr.left); if (field) { const recvTmp = lowerer.declareHiddenLocal("%setReceiver", field.obj.type); @@ -7116,10 +7193,10 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr // Numeric operators dispatch on ToNumeric's actual Number/BigInt // result, including values flowing through native callable properties. if (isJsSourceFile(expr.getSourceFile()) || op === ts.SyntaxKind.PlusToken || + [ts.SyntaxKind.LessThanToken, ts.SyntaxKind.LessThanEqualsToken, + ts.SyntaxKind.GreaterThanToken, ts.SyntaxKind.GreaterThanEqualsToken].includes(op) || [ts.SyntaxKind.MinusToken, ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.SlashToken, ts.SyntaxKind.PercentToken, ts.SyntaxKind.AsteriskAsteriskToken].includes(op)) { - const checkNum = (e: IrExpr): IrExpr => - e.type.kind === "dyn" ? { kind: "dynCheck", value: e, type: F64, loc: e.loc } : e; const other = left.type.kind === "dyn" ? right : left; const NUM_BIN: Partial> = { [ts.SyntaxKind.MinusToken]: "-", @@ -7147,8 +7224,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr return numeric ? { kind: "dynCheck", value, type: numeric, loc } : value; } } - if (cmp && (other.type.kind === "f64" || other.type.kind === "dyn")) { - return { kind: "bin", op: cmp, left: checkNum(left), right: checkNum(right), type: BOOL, loc }; + if (cmp && lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { + return { kind: "libCall", fn: "dyn.compare", args: [lowerer.coerceToExpected(left, DYN), + lowerer.coerceToExpected(right, DYN), { kind: "strLit", value: cmp, type: STRING, loc }], type: BOOL, loc }; } // Untyped addition chooses concatenation or numeric addition only // after both operands have undergone ToPrimitive at runtime. @@ -7585,6 +7663,10 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr if (DYN_HANDLE_KINDS.has(idLeft.type.kind) && typeEquals(idLeft.type, idRight.type)) { return { kind: "bin", op: negated ? "!==" : "===", left: idLeft, right: idRight, type: BOOL, loc }; } + if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) { + return { kind: "dynScalarEq", left: lowerer.coerceToExpected(idLeft, DYN), + right: lowerer.coerceToExpected(idRight, DYN), ...(negated ? { negated: true as const } : {}), type: BOOL, loc }; + } lowerer.unsupported("SC1043", expr); break; } @@ -7597,6 +7679,10 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr : op === ts.SyntaxKind.GreaterThanToken ? ">" : ">="; if (bothNum) return { kind: "bin", op: cmpOp, left: arithmeticLeft, right: arithmeticRight, type: BOOL, loc }; if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc }; + if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { + return { kind: "libCall", fn: "dyn.compare", args: [lowerer.coerceToExpected(left, DYN), + lowerer.coerceToExpected(right, DYN), { kind: "strLit", value: cmpOp, type: STRING, loc }], type: BOOL, loc }; + } lowerer.unsupported("SC1043", expr); break; } @@ -7699,6 +7785,11 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE type: JSVAL, loc, }; } + if (isJsSourceFile(expr.getSourceFile()) && op === ts.SyntaxKind.AmpersandAmpersandToken && + left.type.kind === "union" && (lowerer.unions.get(left.type.unionId)?.arms.some((arm) => arm.kind === "bool")) && + !typeEquals(left.type, right.type) && lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { + return { kind: "logical", op: "&&", left: lowerer.coerceToExpected(left, DYN), right: lowerer.coerceToExpected(right, DYN), type: DYN, loc }; + } if (left.type.kind === "union" || right.type.kind === "union") { // Test the deciding operand in its own representation. The result // contains only the values that can survive short-circuit evaluation. @@ -8545,6 +8636,8 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE if (value.type.kind === "dyn") return { kind: "libCall", fn: collection === "map" ? "dyn.nativeMapIs" : "dyn.nativeSetIs", args: [value], type: BOOL, loc }; if (value.type.kind === collection) return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: value, loc }], result: { kind: "boolLit", value: true, type: BOOL, loc }, type: BOOL, loc }; + return { kind: "libCall", fn: collection === "map" ? "dyn.nativeMapIs" : "dyn.nativeSetIs", + args: [lowerer.coerceInto(expr.left, value, DYN)], type: BOOL, loc }; } if (lowerer.isStdlibGlobal(expr.right, "WeakMap") || lowerer.isStdlibGlobal(expr.right, "WeakSet")) { if (lowerer.dynamic) { @@ -8561,6 +8654,10 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return { kind: "libCall", fn: "dyn.nativeUrlIs", args: [value], type: BOOL, loc }; } + if (lowerer.isStdlibGlobal(expr.right, "DataView")) { + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { kind: "libCall", fn: "dyn.dataViewIs", args: [value], type: BOOL, loc }; + } if (lowerer.isStdlibGlobal(expr.right, "ArrayBuffer")) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return { kind: "libCall", fn: "arrayBuffer.is", args: [value], type: BOOL, loc }; @@ -8610,12 +8707,20 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE if ((date || url) && !lowerer.caughtLocalOf(expr.left)) { const value = lowerer.lowerExpr(expr.left); if (value.type.kind === "dyn") return { kind: "libCall", fn: date ? "dyn.nativeDateIs" : "dyn.nativeUrlIs", args: [value], type: BOOL, loc }; - if (value.type.kind === "union") lowerer.unsupported("SC1090", expr, "Date or URL instanceof over a union"); + if (value.type.kind === "union") return { kind: "libCall", fn: date ? "dyn.nativeDateIs" : "dyn.nativeUrlIs", + args: [lowerer.coerceInto(expr.left, value, DYN)], type: BOOL, loc }; return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: value, loc }], result: { kind: "boolLit", value: value.type.kind === (date ? "date" : "url"), type: BOOL, loc }, type: BOOL, loc }; } + const webBrand = ["Request", "Response", "Headers"].find((name) => lowerer.isStdlibGlobal(expr.right, name)); + if (webBrand) return { kind: "libCall", fn: "fetch.webIs", args: [lowerer.lowerExprExpecting(expr.left, DYN), + { kind: "strLit", value: webBrand, type: STRING, loc }], type: BOOL, loc }; + if (lowerer.isStdlibGlobal(expr.right, "ReadableStream")) { + const value = lowerer.lowerExprExpecting(expr.left, DYN); + return { kind: "libCall", fn: "fetch.streamIs", args: [value], type: BOOL, loc }; + } // Unknown storage retains each numeric typed array's exact native brand. - if (ts.isIdentifier(expr.right) && !lowerer.caughtLocalOf(expr.left)) { + if (!lowerer.caughtLocalOf(expr.left)) { const elem = (Object.keys(BYTES_ELEMENT_NAME) as IrBytesElem[]).find( (kind) => lowerer.isStdlibGlobal(expr.right, BYTES_ELEMENT_NAME[kind]), ); @@ -8624,12 +8729,8 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE if (left.type.kind === "dyn") { return { kind: "dynTest", test: "bytes", bytesElem: elem, value: left, type: BOOL, loc }; } - if (left.type.kind === "bytes") { - return { - kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: left, loc }], - result: { kind: "boolLit", value: left.type.elem === elem, type: BOOL, loc }, type: BOOL, loc, - }; - } + return { kind: "dynTest", test: "bytes", bytesElem: elem, + value: lowerer.coerceInto(expr.left, left, DYN), type: BOOL, loc }; } } // `x instanceof RegExp` over a union with a regex arm (the @@ -8652,9 +8753,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE return { kind: "unionIsTag", unionId: left.type.unionId, tag, negated: false, value: left, type: BOOL, loc }; } } - if (left.type.kind === "regex" && left.kind === "varRef" && ts.isIdentifier(expr.left)) { - return { kind: "boolLit", value: true, type: BOOL, loc }; - } + return { kind: "libCall", fn: "dyn.nativeRegexIs", args: [lowerer.coerceInto(expr.left, left, DYN)], type: BOOL, loc }; } // `x instanceof X` where X is a class VALUE (a classval-typed // binding): the target is DYNAMIC — a hierarchy target reads its @@ -8712,11 +8811,12 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE "statically-decided 'instanceof' on computed operands (bind the values to variables first)", ); } - lowerer.unsupported( - "SC1090", - expr.right, - "'instanceof' right-hand sides other than classes declared in the program", - ); + const constructor = tryLowerExpression(lowerer, expr.right); + if (constructor?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return checkedClassInstanceOf(lowerer, value, constructor, loc); + } + lowerer.unsupported("SC1090", expr.right, "'instanceof' right-hand sides other than classes declared in the program"); } // A catch binding on the left: the runtime test against the class's // preorder interval (false for non-hierarchy-object payloads). tsc's @@ -8816,11 +8916,8 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE } } if (left.type.kind !== "object") { - lowerer.unsupported( - "SC1090", - expr, - "'instanceof' on values other than class instances (narrow union-typed values first)", - ); + return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: left, loc }], + result: { kind: "boolLit", value: false, type: BOOL, loc }, type: BOOL, loc }; } const lhsInfo = lowerer.classes.get(left.type.className); if (!lhsInfo) throw new InternalCompilerError(`lowerer bug: unknown class ${left.type.className}`); @@ -9366,8 +9463,8 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc }; } -/** A regex literal `/ab+c/gi` → regexLit (interned per (pattern, flags) - * by the backend). The TS parser has already syntax-checked the literal; +/** A regex literal `/ab+c/gi` → regexLit (fresh state per evaluation, + * shared bytecode per (pattern, flags) pair). The TS parser has already syntax-checked the literal; * what remains here is the flag-alphabet fence (d and v are * declared-valid TS flags outside this slice). Named capture groups * `(?...)` and `\k` backreferences compile — libregexp @@ -9512,13 +9609,44 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE * caller keeps its fence), or the groups list (possibly empty — a * traced regex WITHOUT named groups is the Node-undefined case). */ export function matchResultNamedGroupsOf(lowerer: Lowerer, e: ts.Expression): { name: string; index: number }[] | null { + let producer = e; + while (ts.isParenthesizedExpression(producer) || ts.isNonNullExpression(producer)) producer = producer.expression; + if (ts.isIdentifier(producer)) { + const init = constInitializerOf(lowerer, producer); + if (init !== null) return matchResultNamedGroupsOf(lowerer, init); + } const reExpr = matchProducerRegexOf(lowerer, e); if (reExpr === null) return null; + if (ts.isCallExpression(producer) && ts.isPropertyAccessExpression(producer.expression) && + producer.expression.name.text === "match") { + const flags = staticRegexFlagsOf(lowerer, reExpr); + if (flags === null) return null; + // Global String.match returns whole matches without a groups + // property, even when the pattern contains named captures. + if (flags.includes("g")) return []; + } const pattern = staticRegexPatternOf(lowerer, reExpr); if (pattern === null) return null; return namedCaptureGroupsOfPattern(pattern); } + function staticRegexFlagsOf(lowerer: Lowerer, node: ts.Expression): string | null { + let expr = node; + while (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) expr = expr.expression; + if (ts.isRegularExpressionLiteral(expr)) return expr.text.slice(expr.text.lastIndexOf("/") + 1); + if (ts.isIdentifier(expr)) { + const init = constInitializerOf(lowerer, expr); + return init !== null ? staticRegexFlagsOf(lowerer, init) : null; + } + if (ts.isNewExpression(expr) && ts.isIdentifier(expr.expression) && + lowerer.isStdlibGlobal(expr.expression, "RegExp")) { + const flags = expr.arguments?.[1]; + if (!flags) return ""; + if (ts.isStringLiteral(flags) || ts.isNoSubstitutionTemplateLiteral(flags)) return flags.text; + } + return null; + } + /** The regex EXPRESSION whose match produced `e` (see * matchResultNamedGroupsOf for the traced shapes). */ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | null { @@ -10470,6 +10598,11 @@ function representedClassFieldTarget( `reading a property that has only a setter ('${target.field}' — Node would yield undefined)`, ); } + if (!target.field.startsWith("#") && isJsSourceFile(blame.getSourceFile()) && lowerer.dynConvertible(getF.sig.ret)) { + const value: IrExpr = { kind: "dynKeyGet", value: lowerer.coerceToExpected(target.obj, DYN), + key: { kind: "strLit", value: target.field, type: STRING, loc }, type: DYN, loc }; + return lowerer.coerceToExpected(value, getF.sig.ret); + } return lowerer.accessorCall(target.className, `get:${target.field}`, target.obj, [], getF.sig.ret, loc); } // Record accessor properties: the read IS a call of the %get: closure @@ -10887,12 +11020,12 @@ function lowerStreamObjectProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpr * so `opt.type === String` compares like JS function identity. */ function primitiveCtorClosure( lowerer: Lowerer, - name: "String" | "Number" | "Boolean", + name: "String" | "Number" | "Boolean" | "BigInt", loc: SrcLoc, dynamicInput: boolean, ): IrExpr { - const ret = name === "String" ? STRING : name === "Number" ? F64 : BOOL; - const input = dynamicInput ? DYN : STRING; + const ret = name === "String" ? STRING : name === "Number" ? F64 : name === "BigInt" ? BIGINT_T : BOOL; + const input = dynamicInput || name === "BigInt" ? DYN : STRING; const fnT = funcOf([input], ret); const key = `${name}:${dynamicInput ? "dyn" : "string"}`; let fnName = lowerer.primitiveCtorFns.get(key); @@ -10901,12 +11034,16 @@ function primitiveCtorClosure( lowerer.primitiveCtorFns.set(key, fnName); const v: IrExpr = { kind: "varRef", localId: "v.0", type: input, loc }; const value: IrExpr = dynamicInput - ? name === "String" + ? name === "BigInt" + ? { kind: "libCall", fn: "dyn.bigintConstructor", args: [v], type: BIGINT_T, loc } + : name === "String" ? { kind: "libCall", fn: "dyn.toStringCoerce", args: [v], type: STRING, loc } : name === "Number" - ? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [v], type: F64, loc } + ? { kind: "libCall", fn: "dyn.numberConstructor", args: [v], type: F64, loc } : { kind: "dynTest", test: "truthy", value: v, type: BOOL, loc } - : name === "String" + : name === "BigInt" + ? { kind: "libCall", fn: "dyn.bigintConstructor", args: [v], type: BIGINT_T, loc } + : name === "String" ? v : name === "Number" ? { kind: "libCall", fn: "num.fromString", args: [v], type: F64, loc } @@ -10923,3 +11060,25 @@ function primitiveCtorClosure( } return { kind: "closure", fnName, captures: [], type: fnT, loc }; } + +/** Drain using checked dispatch so native class and generator iterators keep their methods. */ +function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, loc: SrcLoc): IrExpr { + const name = "%iterator.pack.checked"; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: DYN, loc }); + const property = (key: string): IrExpr => ({ kind: "dynKeyGet", value: ref("step"), key: { kind: "strLit", value: key, type: STRING, loc }, type: DYN, loc }); + lowerer.liftedFns.push({ name, params: [{ localId: "source", name: "source", type: DYN }], returnType: DYN, + locals: ["source", "iterator", "step", "result"].map((id) => ({ id, name: id, type: DYN, mutable: id !== "source" })), + body: [ + { kind: "varDecl", localId: "iterator", init: { kind: "libCall", fn: "dyn.iterator", args: [ref("source"), { kind: "strLit", value: "value", type: STRING, loc }], type: DYN, loc }, loc }, + { kind: "varDecl", localId: "result", init: { kind: "dynArrLit", elems: [], type: DYN, loc }, loc }, + { kind: "while", cond: { kind: "boolLit", value: true, type: BOOL, loc }, body: [ + { kind: "varDecl", localId: "step", init: { kind: "dynInvoke", recv: ref("iterator"), method: "next", calleeName: "iterator.next", args: [], type: DYN, loc }, loc }, + { kind: "if", cond: { kind: "dynTest", test: "truthy", value: property("done"), type: BOOL, loc }, then: [{ kind: "return", value: ref("result"), loc }], else_: null, loc }, + { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.packPush", args: [ref("result"), property("value")], type: VOID, loc }, loc }, + ], loc }, + { kind: "return", value: ref("result"), loc }, + ], loc }); + } + return { kind: "call", callee: name, args: [source], type: DYN, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-island.ts b/packages/compiler/src/frontend/lowering/lower-island.ts index 0ec5378d5..a637cbea9 100644 --- a/packages/compiler/src/frontend/lowering/lower-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-island.ts @@ -6,7 +6,7 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { arrayOf, BOOL, BYTES_U8, DYN, F64, type IrExpr, type IrStmt, type IrType, JSVAL, MAX_ISLAND_CALLBACK_ARITY, STRING, VOID, canConvertToDyn, canMarshalTypedFuncIntoIsland, islandPromisePayloadTag, isUnitType } from "../../ir/ir.js"; -import { ISLAND_SURFACE, type IslandFnEntry, STATIC_MATH_FNS, STATIC_MATH_PROPS, boundaryIntoIslandMsg } from "./surfaces.js"; +import { ISLAND_SURFACE, type IslandFnEntry, STATIC_MATH_FNS, STATIC_MATH_PROPS, boundaryIntoIslandMsg, stdlibGlobalNameOf } from "./surfaces.js"; import { requiresDynamicApiDiag, requiresDynamicPackageDiag } from "../../diagnostics/diagnostic.js"; import { esmNamedImportLinkCrash, isCjsJsFile, isJsSourceFile, locOf, npmPackageNameOf } from "../program.js"; import { foldedStringKeyOf, lowerDynObjectLiteral } from "./expressions/object-literals.js"; @@ -824,11 +824,11 @@ function fenceRequestInitValue( type: STRING, loc: locOf(inputNode), } - : lowerer.coerceInto(inputNode, inputRef, STRING); + : input.type.kind === "dyn" ? inputRef : lowerer.coerceInto(inputNode, inputRef, STRING); const answer: IrExpr = { kind: "libCall", - fn: "fetch.start", - args: [url, initRef], + fn: input.type.kind === "dyn" ? "fetch.input" : "fetch.start", + args: [input.type.kind === "dyn" ? inputRef : url, initRef], type: { kind: "promise", inner: DYN }, loc, }; @@ -2429,6 +2429,25 @@ export function lowerStaticReadableStreamNew( }; } +/** Native Request and Headers values share the fetch runtime's handles. */ +export function lowerFetchWebNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + const name = stdlibGlobalNameOf(lowerer, expr.expression); + if (name !== "Request" && name !== "Headers") return null; + const args = expr.arguments ?? []; + if (args.some(ts.isSpreadElement)) return null; + const loc = locOf(expr); + if (lowerer.dynamic) return { kind: "jsOp", op: "construct", args: [ + { kind: "jsOp", op: "globalGet", name, args: [], type: JSVAL, loc }, + ...args.map((arg) => lowerer.jsvalIn(lowerer.lowerExpr(arg), arg)), + ], type: JSVAL, loc }; + if (name === "Request" && args[1]) fenceRequestInitValue(lowerer, args[1]); + const lowered = args.map((arg) => ts.isObjectLiteralExpression(arg) + ? lowerDynObjectLiteral(lowerer, arg) : lowerLiveWebValue(lowerer, arg)); + return { kind: "libCall", fn: name === "Request" ? "fetch.requestNew" : "fetch.headersNew", + args: name === "Request" ? [lowered[0] ?? dynUndefinedExpr(loc), lowered[1] ?? dynUndefinedExpr(loc)] + : [lowered[0] ?? dynUndefinedExpr(loc)], type: DYN, loc }; +} + /** `new Response(body?, init?)`. Dynamic builds construct the island's * Web Response directly. Static builds hand a checked-dynamic BodyInit and * ResponseInit snapshot to the native fetch runtime, which returns the same @@ -2437,14 +2456,7 @@ export function lowerResponseNew( lowerer: Lowerer, expr: ts.NewExpression, ): IrExpr | null { - if ( - !ts.isIdentifier(expr.expression) || - expr.expression.text !== "Response" - ) { - return null; - } - const symbol = lowerer.resolveValueSymbol(expr.expression); - if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; + if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null; const args = expr.arguments ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 6280c77ab..6a39511bf 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -18,12 +18,12 @@ import { BOOL, DYN, F64, type IrClassDef, type IrExpr, type IrFunction, type IrG import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, newFnCtx, staticImportNamespaceType, uncheckedOverloadHandleCall } from "./lowerer.js"; import { isNativeFfiRequire, builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias, stripTypeCasts } from "./lower-builtins.js"; import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, funcTypeFromParamShapes, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js"; -import { hasJsTypeAnnotation, isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js"; +import { jsBindingHasOpenWrites, hasJsTypeAnnotation, isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js"; import { objectFactorySignature } from "./object-factory-new.js"; import { streamClassAliasDecl } from "./lower-stream.js"; import { stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, stdlibGlobalNameOf } from "./surfaces.js"; import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js"; -import { collectNamespaceStmt, nsPathPrefix, trapDeclRootOf } from "./lower-namespaces.js"; +import { collectNamespaceStmt, nsMemberIdentOf, nsPathPrefix, trapDeclRootOf } from "./lower-namespaces.js"; import { collectExpandoMembers } from "./lower-expando.js"; import { recordTextCodecClass } from "../../ir/ir.js"; import { isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js"; @@ -743,6 +743,24 @@ export function appendForkModules( const pendingClasses: string[] = []; const pendingShapes: string[] = []; const pendingUnions: string[] = []; + const boxedChild = (type: IrType, seen = new Set()): boolean => { + switch (type.kind) { + case "child": return true; + case "array": case "set": return boxedChild(type.elem, seen); + case "promise": return boxedChild(type.inner, seen); + case "map": return boxedChild(type.key, seen) || boxedChild(type.value, seen); + case "func": return boxedChild(type.ret, seen); + case "record": + if (seen.has(type.shapeId)) return false; + seen.add(type.shapeId); + return lowerer.shapes.get(type.shapeId)?.fields.some((field) => boxedChild(field.type, seen)) ?? false; + case "union": + if (seen.has(type.unionId)) return false; + seen.add(type.unionId); + return lowerer.unions.get(type.unionId)?.arms.some((arm) => boxedChild(arm, seen)) ?? false; + default: return false; + } + }; const visit = (node: unknown): void => { if (Array.isArray(node)) { for (const item of node) visit(item); @@ -750,6 +768,10 @@ export function appendForkModules( } if (node === null || typeof node !== "object") return; const rec = node as Record; + if (rec["kind"] === "dynFrom" && boxedChild((rec["value"] as IrExpr).type)) { + // A generic child can expose native stdio without importing stream. + visit([{ className: "%Readable" }, { className: "%Writable" }]); + } if (typeof rec["className"] === "string" && !classNames.has(rec["className"])) { classNames.add(rec["className"]); pendingClasses.push(rec["className"]); @@ -901,6 +923,10 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi let e: ts.Expression = init; while (ts.isParenthesizedExpression(e)) e = e.expression; if (checkedGlobalReference(lowerer, e)) return true; + if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && + ["freeze", "seal", "preventExtensions"].includes(e.expression.name.text) && + lowerer.stdlibGlobalMember(e.expression, "Object") !== null && e.arguments[0] && + ts.isArrayLiteralExpression(e.arguments[0])) return true; // The built-in Array constructor can create an evolving array whose // inferred element type has no native mapping. Its checked reference // must remain shared with separately declared functions and aliases. @@ -926,6 +952,10 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi // callable whose inferred overloads cannot describe one native ABI. if ((ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) && ts.isIdentifier(e.expression) && lowerer.globalOf(e.expression)?.type.kind === "dyn") return true; + if (ts.isPropertyAccessExpression(e)) { + const member = nsMemberIdentOf(lowerer, e); + if (member && (lowerer.globalOf(member)?.type.kind === "dyn" || lowerer.genericFnOf(member)?.implicitParams)) return true; + } // Optional callable reads (for example descriptor?.get) can be held in // the same checked slot as a callable. Their nullish arm must remain // observable by functions declared outside the module initializer. @@ -938,6 +968,7 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi isUnitType(arm) || (arm.kind === "func" && canBoxFuncIntoDyn(arm, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))))) return true; } + if ((ts.isFunctionExpression(e) || ts.isArrowFunction(e)) && !e.typeParameters?.length && isJsSourceFile(e.getSourceFile())) return true; if (ts.isIdentifier(e) || ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { if (ts.isIdentifier(e)) { if (lowerer.globalOf(e)?.type.kind === "dyn") return true; @@ -1530,6 +1561,22 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts for (const nameNode of boundIdentifiersOf(decl.name)) { const diagsBefore = lowerer.diags.length; try { + // Checked JavaScript factories can expose inferred callables by + // destructuring. Keep each exported binding in module storage so + // separately lowered functions see the initialized value. + if (isJsSourceFile(sf) && !ts.isIdentifier(decl.name) && decl.initializer && + !decl.type && !hasJsTypeAnnotation(decl) && !lowerer.mapTypeOf(lowerer.typeOf(nameNode))) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (symbol && !lowerer.globalsBySymbol.has(symbol)) { + const g: IrGlobal = { id: `%g.${tag}${nsPrefix}${nameNode.text}`, name: nameNode.text, + type: DYN, mutable: isLet, source: bindingSource(nameNode), + ...(!isVarDeclared(decl) ? { tdz: true as const } : {}) }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + continue; + } // Builtin declarations do not always describe the concrete native // callable ABI. Preserve checked-native values in shared storage; // adapting the loader to (string) would replace its validation. @@ -1583,7 +1630,8 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts if (isJsSourceFile(sf) && ts.isIdentifier(decl.name) && nameNode === decl.name && decl.initializer && ts.isObjectLiteralExpression(decl.initializer) && decl.initializer.properties.some((p) => !ts.isSpreadAssignment(p) && - (ts.isMethodDeclaration(p) || ts.isAccessor(p) || ts.isComputedPropertyName(p.name)))) { + (ts.isMethodDeclaration(p) || ts.isAccessor(p) || ts.isComputedPropertyName(p.name) || + ts.isShorthandPropertyAssignment(p) && lowerer.checker.getCallSignatures(lowerer.typeOf(p.name)).length > 0))) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (symbol && !lowerer.globalsBySymbol.has(symbol)) { const g: IrGlobal = { id: `%g.${tag}${nsPrefix}${nameNode.text}`, name: nameNode.text, type: DYN, mutable: isLet, source: bindingSource(nameNode) }; @@ -1694,9 +1742,16 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts ts.isIdentifier(decl.name) && nameNode === decl.name && decl.initializer && jsDynHoldableInitializer(lowerer, decl.initializer) ) { - const fallback = dynFallbackType(lowerer, nameNode, lowerer.typeOf(nameNode)); + const initializer = decl.initializer; + const member = ts.isPropertyAccessExpression(initializer) ? nsMemberIdentOf(lowerer, initializer) : null; + const generic = member && lowerer.genericFnOf(member); + const fallback = (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) && !initializer.typeParameters?.length + ? lowerer.lambdaSignature(initializer).funcType + : member && lowerer.globalOf(member)?.type.kind === "dyn" ? DYN + : member && generic?.implicitParams ? lowerer.lowerGenericFnValue(member, generic).type + : dynFallbackType(lowerer, nameNode, lowerer.typeOf(nameNode)); const symbol = lowerer.checker.getSymbolAtLocation(nameNode); - if (symbol && fallback && (fallback.kind === "func" || fallback.kind === "union") && lowerer.dynConvertible(fallback)) { + if (symbol && fallback && (fallback.kind === "dyn" || fallback.kind === "func" || fallback.kind === "union") && lowerer.dynConvertible(fallback)) { if (!lowerer.globalsBySymbol.has(symbol)) { const g: IrGlobal = { id: `%g.${tag}${nsPrefix}${nameNode.text}`, name: nameNode.text, @@ -1744,6 +1799,11 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts const inferredJsCall = callDeclaration && isJsSourceFile(callDeclaration.getSourceFile()) && !lowerer.mapTypeOf(lowerer.typeOf(nameNode)); let type = handleT ?? factoryType ?? (inferredJsCall ? DYN : lowerer.irTypeOf(nameNode)); + if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && decl.initializer && + (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) && + !decl.initializer.typeParameters?.length) { + type = lowerer.lambdaSignature(decl.initializer).funcType; + } if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && decl.initializer && ts.isCallExpression(decl.initializer) && type.kind === "union") { const arms = lowerer.unions.get(type.unionId)?.arms; @@ -1770,6 +1830,8 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts // checked factory. Keep that callable boxed instead of asking // a fixed-signature adapter to reconstruct its hidden rest ABI. if (!decl.type && !hasJsTypeAnnotation(decl) && type.kind === "func" && type.rest === true && type.restAbi === undefined) type = DYN; + if (isJsSourceFile(sf) && isLet && !decl.type && !hasJsTypeAnnotation(decl) && + (type.kind === "object" || jsBindingHasOpenWrites(lowerer, decl))) type = DYN; // JavaScript call results may retain a checked array supplied by // the caller. A typed global here would copy it on extraction. if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && type.kind === "array" && @@ -1854,8 +1916,7 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts if ( isJsSourceFile(sf) && type.kind === "func" && - type.ret.kind === "record" && - lowerer.dynConvertible(type.ret) && + (type.ret.kind === "date" || type.ret.kind === "record" && lowerer.dynConvertible(type.ret)) && decl.initializer !== undefined && (ts.isFunctionExpression(decl.initializer) || ts.isArrowFunction(decl.initializer)) && decl.initializer.type === undefined && diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 73e6b42f3..ebcfc17bc 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,3 +1,4 @@ +import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; import { InternalCompilerError } from "../../errors.js"; /* Statement lowering: the statement dispatch (lowerStmt), variable * declarations including destructuring patterns, scoped blocks, control @@ -11,7 +12,7 @@ import { lowerForAwaitGenerator, lowerForOfGenerator, lowerYieldStarStatement, t import { lowerForAwaitBuiltin } from "./lower-async-iteration.js"; import { BOOL, BYTES_U8, CAUGHT, DYN, F64, type IrExpr, type IrGlobal, type IrLocal, type IrStmt, type IrType, JSVAL, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, isDynTypedRefType, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; import { PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, neverTaintedJsType, staticImportNamespaceType, stmtUsesIsland, uncheckedOverloadHandleCall } from "./lowerer.js"; -import { enforceLibBoundary } from "./lib-boundary.js"; +import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js"; import { cjsExportAssignmentOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, isEsModuleStamp, isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js"; import { COMPOUND_ASSIGN_OPS, type CompoundOp, STR_METHODS, UNSUPPORTED_STMT, isStdlibMember, sideEffectFreeOptionValue, stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, stdlibGlobalNameOf } from "./surfaces.js"; import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js"; @@ -428,10 +429,13 @@ export function provenanceElidedConstDecl(lowerer: Lowerer, decl: ts.VariableDec // fence — inside this statement's poison window, so a JS fence // becomes the statement's runtimeFence exactly like every other // deferred rejection. - if (lowered) enforceLibBoundary(lowerer, lowered); - if (!appended) { - if (Array.isArray(lowered)) out.push(...lowered); - else if (lowered) out.push(lowered); + if (lowered) { + const body: IrStmt[] = []; + if (Array.isArray(lowered)) body.push(...lowered); + else body.push(lowered); + const rewritten = lowerSurplusCalls(lowerer, body); + enforceLibBoundary(lowerer, rewritten); + if (!appended) out.push(...rewritten); } // Island accounting (--dynamic coverage): a statement that lowered // but carries island constructs compiles DYNAMICALLY — its work @@ -1610,6 +1614,7 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool const patternSymbol = patternType.getSymbol(); allowDynObject ||= patternSymbol?.name === "SegmentData" && lowerer.isStdlibSymbol(patternSymbol); allowDynObject ||= (patternType.flags & ts.TypeFlags.Object) !== 0 && lowerer.mapTypeOf(patternType)?.kind === "dyn"; + allowDynObject ||= (patternType.flags & ts.TypeFlags.Any) !== 0; } const present = patternSourceValue(lowerer, srcRef(), ts.isArrayBindingPattern(pattern), out, locOf(pattern), dynSpell); if (!typeEquals(present.type, srcType)) { @@ -1948,19 +1953,23 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool // scoped TypeScript exception: its declared result/token types are // deliberately carried by the checked-dynamic tree, so their ordinary // object patterns use the same exact keyed-read desugar. - if ( - srcType.kind === "dyn" && - (isJsSourceFile(pattern.getSourceFile()) || allowDynObject) - ) { + if (srcType.kind === "dyn") { for (const el of pattern.elements) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { - lowerer.unsupported( - "SC1031", - el, - "rest elements over checked-dynamic sources (the remaining-fields object has no lowering yet)", - ); + const keys: IrExpr[] = []; + for (const sibling of pattern.elements) { + if (sibling === el || sibling.dotDotDotToken) continue; + const key = sibling.propertyName ?? sibling.name; + const name = ts.isIdentifier(key) || ts.isStringLiteralLike(key) || ts.isNumericLiteral(key) || ts.isComputedPropertyName(key) + ? patternKeyNameOf(lowerer, key as ts.PropertyName) : null; + if (name === null) lowerer.unsupported("SC1031", sibling, "rest bindings beside runtime computed keys"); + keys.push({ kind: "dynFrom", value: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc }); + } + lowerer.bindPatternTarget(el.name, { kind: "libCall", fn: "dyn.objectRest", args: [srcRef(), + { kind: "dynArrLit", elems: keys, type: DYN, loc }], type: DYN, loc }, isLet, out, allowDynObject); + continue; } const prop = el.propertyName ?? el.name; const propName = ts.isIdentifier(prop) || ts.isComputedPropertyName(prop) || ts.isStringLiteralLike(prop) || ts.isNumericLiteral(prop) @@ -2216,6 +2225,16 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool lowerer.bindPatternTarget(el.name, value, isLet, out); continue; } + if (!fieldType && isJsSourceFile(el.getSourceFile())) { + // Inferred JavaScript shapes can omit optional own properties and + // inherit values from their prototype. Read the live checked view. + const loc = locOf(el); + let value: IrExpr = { kind: "dynKeyGet", value: lowerer.coerceToExpected(srcRef(), DYN), + key: { kind: "strLit", value: propName, type: STRING, loc }, type: DYN, loc }; + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + continue; + } if (!fieldType) { // The pattern names a field the source SHAPE does not carry — // `for (let {x = 1} of [{}])` (signature 08): tsc types the binding @@ -2539,6 +2558,14 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool * alike (the caller's bindPatternTarget recurses through the latter). */ function applyBindingDefault(lowerer: Lowerer, el: ts.BindingElement, value: IrExpr): IrExpr { const name = el.name!; // callers skip nameless elisions + if (value.type.kind === "dyn") { + const loc = value.loc; + const local = lowerer.declareHiddenLocal("%bindingDefault", DYN); + const ref = varRef(local.id, DYN, loc); + const result: IrExpr = { kind: "ternary", cond: { kind: "dynTest", test: "undefined", value: ref, type: BOOL, loc }, + then: lowerer.lowerExprExpecting(el.initializer!, DYN), else_: ref, type: DYN, loc }; + return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], result, type: DYN, loc }; + } if (value.type.kind !== "union") return value; // no undefined arm: dead default if (lowerer.armTag(value.type.unionId, UNDEFINED_T) < 0) return value; const bodyT = patternBindingType(lowerer, name); @@ -3710,7 +3737,7 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL } const raw = lowerVariableInitializer(lowerer, decl.initializer, g.type.kind === "dyn" && (isNativeBuiltinValueInitializer(lowerer, decl.initializer) || - ts.isArrayLiteralExpression(decl.initializer)) ? DYN : undefined); + ts.isArrayLiteralExpression(decl.initializer) || ts.isObjectLiteralExpression(decl.initializer)) ? DYN : undefined); const arithmetic = raw.type.kind === "union" ? lowerer.unions.get(raw.type.unionId)?.arms : undefined; @@ -3785,7 +3812,9 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL const inferredObjectType = isJsSourceFile(decl.getSourceFile()) ? lowerer.mapTypeOf(lowerer.typeOf(decl.name)) : null; const inferredObjectArms = inferredObjectType?.kind === "union" ? lowerer.unions.get(inferredObjectType.unionId)?.arms ?? [] : inferredObjectType ? [inferredObjectType] : []; - const expandsObject = inferredObjectArms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm)); + const expandsObject = !decl.type && !hasJsTypeAnnotation(decl) && inferredObjectType !== null && + jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn" || + inferredObjectArms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm)); let init: IrExpr; try { init = expandsObject || (isJsSourceFile(decl.getSourceFile()) && !decl.type && @@ -3841,6 +3870,7 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL staticImportNamespaceType(lowerer, decl.initializer) ?? (init.type.kind === "moduleNs" ? init.type : null) ?? (init.type.kind === "dyn" ? DYN : null); + if (isJsSourceFile(decl.getSourceFile()) && !decl.type && !hasJsTypeAnnotation(decl) && init.type.kind === "func") type = init.type; if ( lowerer.implicitParamTypes !== null && init.type.kind === "dyn" && @@ -3877,6 +3907,10 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL const shape = lowerer.shapes.get(type.shapeId); if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN; } + if (isLet && type !== null && isJsSourceFile(decl.getSourceFile()) && !decl.type && !hasJsTypeAnnotation(decl) && + (type.kind === "object" || type.kind === "symbol" || jsBindingHasOpenWrites(lowerer, decl))) { + type = DYN; + } const returnedEmptyArrayType = npmStaticReturnedEmptyArrayType(lowerer, decl); if (returnedEmptyArrayType !== null) { type = returnedEmptyArrayType; @@ -4148,6 +4182,8 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL ? lowerer.lowerExprExpecting(node, expected) : lowerer.lowerExpr(node); } + if (expected?.kind === "dyn") return lowerDynObjectLiteral(lowerer, value); + const previous = new Map(); for (const property of value.properties) { if (!ts.isPropertyAssignment(property) || !ts.isComputedPropertyName(property.name)) continue; @@ -4226,6 +4262,12 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL if (name === null || (name !== undefined && !names.has(name))) expands = true; } if (ts.isDeleteExpression(node) && property(node.expression) !== undefined) expands = true; + if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "setPrototypeOf" && + lowerer.isStdlibGlobal(node.expression.expression, "Object")) { + const target = node.arguments[0]; + const sym = target && ts.isIdentifier(target) ? lowerer.resolveValueSymbol(target) : undefined; + if (sym && aliases.has(sym)) expands = true; + } ts.forEachChild(node, visit); }; visit(scope); @@ -4237,6 +4279,28 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL return !!statement && /@type\b/.test(decl.getSourceFile().text.slice(statement.pos, decl.getStart())); } +/** Inferred JS scalar initializers do not constrain later writes through + * an any-valued callback. Keep those bindings open, including captured writes. */ +export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if (!isJsSourceFile(decl.getSourceFile()) || decl.type || hasJsTypeAnnotation(decl) || !ts.isIdentifier(decl.name)) return false; + const symbol = lowerer.resolveValueSymbol(decl.name); + if (!symbol) return false; + let owner: ts.Node = decl; + while (owner.parent && !ts.isFunctionLike(owner) && !ts.isSourceFile(owner)) owner = owner.parent; + let open = false; + ts.walkPreorder(owner, (node) => { + if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && ts.isIdentifier(node.left) && + lowerer.resolveValueSymbol(node.left) === symbol && + (lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) { + open = true; + return "stop"; + } + return undefined; + }); + return open; +} + function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { if (decl.type || !ts.isIdentifier(decl.name) || !decl.initializer) return false; const statement = decl.parent?.parent; @@ -4878,7 +4942,7 @@ function isStrictDelete(node: ts.DeleteExpression): boolean { const info = lowerer.lowerClassExpressionInfo(rhs); const exportSym = lowerer.checker.getSymbolAtLocation(expr.left); if (exportSym && !lowerer.classBySymbol.has(exportSym)) lowerer.classBySymbol.set(exportSym, info); - return { kind: "block", body: [], loc }; + return { kind: "exprStmt", expr: lowerer.lowerClassExpression(rhs), loc }; } const g = lowerer.globalsByDeclNode.get(expr); if (!g) return null; @@ -5346,6 +5410,11 @@ function isStrictDelete(node: ts.DeleteExpression): boolean { const viaStream = lowerStreamUnderscoreAssign(lowerer, expr); if (viaStream) return viaStream; } + if (expr.left.name.text === "lastIndex" && lowerer.mapTypeOf(lowerer.typeOf(expr.left.expression))?.kind === "regex") { + const loc = locOf(expr); + return { kind: "exprStmt", expr: { kind: "libCall", fn: "regex.resetLastIndex", + args: [lowerer.lowerExpr(expr.left.expression), lowerer.lowerExprExpecting(expr.right, F64)], type: VOID, loc }, loc }; + } const unionWrite = lowerUnionFieldWrite(lowerer, expr.left, expr.right); if (unionWrite) return unionWrite; const target = lowerer.fieldTarget(expr.left); diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 6d266f789..7b00ae575 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1185,7 +1185,7 @@ function lowerStreamArg(lowerer: Lowerer, node: ts.Expression, what: "finished" v.type.kind === "bool" || v.type.kind === "nullT") { argTypeThrow("body", PIPELINE_STAGE_EXPECTED); } - } else if (v.type.kind === "record" || v.type.kind === "string" || v.type.kind === "f64" || + } else if ((ts.isObjectLiteralExpression(node) && node.properties.length === 0) || v.type.kind === "record" || v.type.kind === "string" || v.type.kind === "f64" || v.type.kind === "bool" || v.type.kind === "array" || v.type.kind === "nullT") { argTypeThrow("stream", "an instance of ReadableStream, WritableStream, or Stream"); } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 1b0db95e7..3e1ac065f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,4 @@ +import { pathModuleValue } from "./lower-builtin-values.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { RuntimeOptionalLocals } from "./runtime-optional-locals.js"; import { sanitizeUnregisteredClassTypes } from "./sanitize-class-types.js"; @@ -125,7 +126,7 @@ import { lowerRegexMethodCall, lowerStringMethodCall } from "./containers/string import { lowerStreamModuleCall } from "./lower-stream.js"; import { lowerEmitOverrideSpec, type ComputedEventPattern, type EmitSpecCtx, type EmitSpecRequest, type EventSig } from "./lower-event-emitter.js"; import { builtinImportOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleOf, createRequireSpecOf, stripTypeCasts, lowerBuiltinModuleCall, lowerNodeModuleCall, lowerTimersPromisesSetInterval, lowerFsToUnixTimestampCall, lowerFsLadderCall, lowerChildArgsArg, lowerSpawnSyncCall, lowerSpawnCall, lowerExecFileCall, lowerExecSyncCall, recordToEnvPairs, lowerJsonMethodCall, fencedBuiltinImportOf, lowerCryptoComposedCall, lowerUrlMethodCall, lowerSearchParamsMethodCall, lowerStatsMethodCall, lowerChildMethodCall, lowerAtomicsCall, lowerBuiltinExtraProperty, registerPromisifiedBuiltinDecl, lowerExecFileAsyncCall, execFileAsyncHelper, lowerStringDecoderMethodCall, strdecHelper, lowerReadlineMethodCall, lowerDcChannelMethodCall, lowerDcChannelProperty, lowerAlsMethodCall, lowerDcTracingChannelMethodCall, lowerDcTracingChannelProperty, lowerJsonProperty, lowerErrorCodeProperty, lowerProcessProperty, isProcessEnv, envValueType, lowerProcessEnvGet, lowerProcessMethodCall, lowerProcessOptionalMethodCall, lowerTimeoutMethodCall, envSnapshotHelper, isConsoleLog, consoleCallMember, lowerNumberStaticCall, lowerNumberStaticProperty, lowerDateCall, lowerTextCodecCall, lowerCryptoModuleCall, lowerFsConstantsProperty, lowerBuiltinConstantsProperty, builtinConstantBindingOf, builtinConstantsDestructureDecl, lowerProcessStreamProperty, lowerStringStaticCall, lowerStringLastIndexOfCall, lowerPromiseStaticCall } from "./lower-builtins.js"; -import { fenceFetchObjectAssignment, fenceFetchObjectBinding, fenceStaticAbortControllerMemberRead, fenceStaticHeadersIteration, fenceStaticHeadersMember, fenceStaticReadableStreamMember, fenceStaticResponseMember, fenceUnsupportedFetchConstructorMember, isIslandExpr, islandFuncValueFence, islandRegexpOf, jsvalIn, requireDynamicApi, islandGlobalFnOf, lowerAbortControllerNew, lowerDynamicHeadersIteratorCall, lowerDynamicHeadersSpread, lowerDynamicImportCall, lowerFetchCall, lowerFetchElementMethodCall, lowerResponseNew, lowerStaticFetchCompanionCall, lowerStaticAbortControllerCall, lowerStaticAbortSignalListenerCall, lowerStaticReadableStreamCancelCall, lowerStaticReadableStreamControllerCall, lowerStaticReadableStreamNew, lowerStaticReadableStreamReaderCall, lowerStaticResponseCall, lowerIslandMethodCall, lowerMathProperty, npmPackageOf, npmMemberFence, npmPackageOfSymbol } from "./lower-island.js"; +import { fenceFetchObjectAssignment, fenceFetchObjectBinding, fenceStaticAbortControllerMemberRead, fenceStaticHeadersIteration, fenceStaticHeadersMember, fenceStaticReadableStreamMember, fenceStaticResponseMember, fenceUnsupportedFetchConstructorMember, isIslandExpr, islandFuncValueFence, islandRegexpOf, jsvalIn, requireDynamicApi, islandGlobalFnOf, lowerAbortControllerNew, lowerDynamicHeadersIteratorCall, lowerDynamicHeadersSpread, lowerDynamicImportCall, lowerFetchCall, lowerFetchElementMethodCall, lowerResponseNew, lowerFetchWebNew, lowerStaticFetchCompanionCall, lowerStaticAbortControllerCall, lowerStaticAbortSignalListenerCall, lowerStaticReadableStreamCancelCall, lowerStaticReadableStreamControllerCall, lowerStaticReadableStreamNew, lowerStaticReadableStreamReaderCall, lowerStaticResponseCall, lowerIslandMethodCall, lowerMathProperty, npmPackageOf, npmMemberFence, npmPackageOfSymbol } from "./lower-island.js"; import { lowerHttpHeadersElement, lowerNetModuleCall, lowerServerMethodCall, lowerServerProperty, lowerTlsRootCertificates } from "./lower-server.js"; import { lowerDgramDnsModuleCall, lowerDgramMethodCall } from "./lower-dgram.js"; import { lowerNodeTestModuleCall, lowerTestDirectCall, lowerTestMethodCall, lowerTestCtxProperty } from "./lower-test.js"; @@ -705,7 +706,7 @@ export function dynUndefinedExpr(loc: SrcLoc): IrExpr { * symbol-to-string TypeError). kind 0 Error / 1 TypeError / 2 RangeError; * an empty code means no code slot. `type` is the replaced expression's * own (never materialized — the global.undefRead pattern). */ -export function nodeThrowExpr(kind: 0 | 1 | 2, code: string, message: string, type: IrType, loc: SrcLoc): IrExpr { +export function nodeThrowExpr(kind: 0 | 1 | 2 | 5, code: string, message: string, type: IrType, loc: SrcLoc): IrExpr { return { kind: "libCall", fn: "error.nodeThrow", @@ -767,6 +768,11 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir if (t.flags & ts.TypeFlags.Void) return null; if (!isJsSourceFile(node.getSourceFile())) { if (t.flags & ts.TypeFlags.Any) return DYN; + const symbol = t.getSymbol(); + const args = symbol && lowerer.isStdlibSymbol(symbol) ? lowerer.checker.getTypeArguments(t as ts.TypeReference) : []; + if (symbol?.name === "Promise" && args[0] && args[0].flags & ts.TypeFlags.Any) return jsFallbackPromiseType(lowerer, t); + if (symbol && ["Generator", "AsyncGenerator", "IterableIterator"].includes(symbol.name) && args.some((arg) => arg.flags & ts.TypeFlags.Any)) return jsFallbackGeneratorType(lowerer, t); + if (lowerer.checker.isArrayType(t) && ((lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]?.flags ?? 0) & ts.TypeFlags.Any) !== 0) return DYN; // TS single-call-signature function types: per-piece fallback, but // ONLY `any` pieces fall to dyn — any other unmappable piece keeps // the whole type's own fence. @@ -4552,6 +4558,7 @@ export class Lowerer { } badType(node: ts.Node, type: ts.Type): never { + const widened = this.checker.getBaseTypeOfLiteralType(type); // Types declared by the ADOPTED @types/node (Buffer, NodeJS.Timeout, // the undici Response, ...) are supported-surface provenance, not npm @@ -4759,7 +4766,7 @@ export class Lowerer { TextEncoder: "TextEncoder instances support encode(string) and encode(); other codec operations require a dedicated lowering", TextDecoder: - "TextDecoder instances support recognized literal labels with default options and whole-buffer decode(Uint8Array); runtime labels and options remain unsupported", + "TextDecoder instances support recognized labels, fatal/ignoreBOM option literals, and streaming UTF-8/UTF-16/single-byte decode; runtime option objects remain unsupported", }; this.pushDiag( noLoweringDiag(this.checker.typeToString(type), locOf(node), typeHints[stdlibOwnSym?.name ?? ""]), @@ -5396,7 +5403,10 @@ export class Lowerer { }; } if (expr.kind === "unitLit" || this.dynConvertible(expr.type)) { - return { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc }; + const shape = expr.type.kind === "record" ? this.shapes.get(expr.type.shapeId) : undefined; + const liveRef = expr.type.kind === "array" || !!shape && (shape.fields.length > 0 || shape.indexValue !== undefined); + return { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc, + ...(liveRef ? { liveRef: true as const } : {}) }; } return expr; } @@ -5829,8 +5839,26 @@ export class Lowerer { (src.kind === "record" && arm.kind === "object"); if (sameFamily && this.widthLiftPlan(src, arm) !== null) candidates.push({ tag: i, arm }); }); - if (candidates.length !== 1) return null; - return { how: "liftWrap", tag: candidates[0]!.tag, arm: candidates[0]!.arm }; + let selected = candidates; + if (selected.length > 1 && src.kind === "record") { + const source = this.shapes.get(src.shapeId); + // An inferred JS union may widen its tag while retaining variants + // with and without a value. Prefer the unique layout whose omitted + // fields are explicitly undefined, rather than filling an optional + // unknown slot and making both variants look equally compatible. + const absentOnly = selected.filter(({ arm }) => { + if (!source || arm.kind !== "record") return false; + const target = this.shapes.get(arm.shapeId); + return target !== undefined && target.fields.every((field) => { + if (source.fields.some((present) => present.name === field.name)) return true; + return isUnitType(field.type) || field.type.kind === "union" && + this.unions.get(field.type.unionId)?.arms.every(isUnitType) === true; + }); + }); + if (absentOnly.length === 1) selected = absentOnly; + } + if (selected.length !== 1) return null; + return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } // A UNION source into a slot that is ONE of its arms (a width copy // whose target field narrowed — the option-table choices shape: @@ -7896,7 +7924,9 @@ export class Lowerer { } } } - let e = this.coerceToExpected(expr, expected); + let e = this.dynamic && expr.type.kind === "dyn" && expected.kind === "array" && expected.elem.kind === "jsval" + ? { kind: "jsExit" as const, value: this.jsvalIn(expr, node), type: expected, loc: expr.loc } + : this.coerceToExpected(expr, expected); // Existing JavaScript arrays retain mutations and identity across checked // slots. Fresh literals have no prior identity and use checked storage // directly, so later writes can change their inferred element type. @@ -8146,7 +8176,7 @@ export class Lowerer { } // An OBJECT LITERAL against a checked-dynamic slot in a JS file (the // getSupportInfo options argument — a dyn-ABI param), or against the - // standard RequestInit/ErrorOptions types in TypeScript: the value's world IS the + // standard native option-bag types in TypeScript: the value's world IS the // checked-dynamic tree — build the dyn literal directly. if (expected?.kind === "dyn") { let x: ts.Expression = node; @@ -8165,7 +8195,7 @@ export class Lowerer { return sym?.name === name && this.isStdlibSymbol(sym); }); if (ts.isObjectLiteralExpression(x)) { - if (isJsSourceFile(x.getSourceFile()) || builtinOption("RequestInit") || builtinOption("ErrorOptions") || + if (isJsSourceFile(x.getSourceFile()) || builtinOption("RequestInit") || builtinOption("ErrorOptions") || builtinOption("SpawnOptions") || !this.mapTypeOf(this.typeOf(x))) { return lowerDynObjectLiteral(this, x); } @@ -10109,7 +10139,7 @@ export class Lowerer { const arg = this.lowerExpr(expr.arguments[0]!); return { kind: "jsOp", op: "construct", args: [ctor, arg], type: JSVAL, loc }; } - const builtin = lowerAbortControllerNew(this, expr) ?? lowerResponseNew(this, expr) ?? lowerStaticReadableStreamNew(this, expr); + const builtin = lowerAbortControllerNew(this, expr) ?? lowerFetchWebNew(this, expr) ?? lowerResponseNew(this, expr) ?? lowerStaticReadableStreamNew(this, expr); return builtin ?? this.withRuntimeOptionalClassValue(expr.expression, () => lowerNew(this, expr)); } @@ -10400,13 +10430,13 @@ export class Lowerer { if (!expr.questionDotToken && ts.isPropertyAccessExpression(expr.expression)) { const inner = this.builtinMemberOf(expr.expression); if (inner && inner.module === "fs" && inner.member === "constants") { - const MODES: Record = { F_OK: 0, X_OK: 1, W_OK: 2, R_OK: 4 }; + const MODES: Record = { F_OK: 0, X_OK: 1, W_OK: 2, R_OK: 4, COPYFILE_EXCL: 1, COPYFILE_FICLONE: 2, COPYFILE_FICLONE_FORCE: 4 }; const value = own(MODES, expr.name.text); if (value === undefined) { this.noLowering( `fs.constants.${expr.name.text}`, expr, - "F_OK, R_OK, W_OK, and X_OK are the lowered constants", + "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants", ); } return { kind: "numLit", value, type: F64, loc }; @@ -10414,6 +10444,10 @@ export class Lowerer { } const bi = this.builtinMemberOf(expr); if (!bi) return null; + if ((bi.module === "path" || bi.module === "path/posix" || bi.module === "path/win32") && + (bi.member === "posix" || bi.member === "win32")) { + return pathModuleValue(this, `path/${bi.member}`, loc); + } // events.defaultMaxListeners READS the process-wide default (its // write twin routes through emitter.setDefaultMaxChk). if (bi.module === "events" && bi.member === "defaultMaxListeners") { @@ -10446,7 +10480,7 @@ export class Lowerer { } if (bi.member === "constants" && bi.module === "fs") { // A bare `fs.constants` read (not one of the baked bits above). - this.noLowering(`fs.constants`, expr, "F_OK, R_OK, W_OK, and X_OK are the lowered constants"); + this.noLowering(`fs.constants`, expr, "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants"); } if (builtinModuleFnOf(this, bi.module, bi.member)) { this.unsupported( @@ -10472,6 +10506,9 @@ export class Lowerer { bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr | null { + if (bi.module === "fs" && builtinModuleFnOf(this, bi.module, bi.member)?.fn === "fs.callbackCall") { + return { kind: "libCall", fn: "fs.callbackValue", args: [{ kind: "strLit", value: bi.member, type: STRING, loc }], type: DYN, loc }; + } const fn = builtinModuleFnOf(this, bi.module, bi.member); return fn ? this.lowerNativeCallableValue(fn, `${bi.module}.${bi.member}`, loc) : null; } diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 5457920c1..32df3915f 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -805,7 +805,7 @@ export const BUILTIN_MODULE_FNS: Record(table: Record, key: string): T | und /** One MEMBER's table entry, own-property-safe (`path.toString` must not * answer Object.prototype.toString as a BuiltinModuleFn). */ export function builtinModuleFnOf(lowerer: Lowerer, module: string, member: string): BuiltinModuleFn | undefined { + if (module === "fs" && FS_CALLBACK_MEMBERS.has(member)) return { fn: "fs.callbackCall", params: [], result: DYN }; const mod = builtinModuleFnsOf(lowerer, module); return mod ? ownEntry(mod, member) : undefined; } +/** Native error-first function values used by platform service adapters. */ +export const FS_CALLBACK_MEMBERS = new Set([ + "access", "cp", "copyFile", "chmod", "chown", "glob", "link", "mkdir", "mkdtemp", "rm", "rmdir", "unlink", + "open", "close", "fstat", "ftruncate", "fsync", "read", "write", "readlink", "realpath", "rename", "stat", "lstat", + "symlink", "truncate", "utimes", "readFile", "writeFile", "appendFile", "readdir", +]); + /** One member's fence hint, own-property-safe (a collision would print an * inherited function into the diagnostic text). */ export function builtinFenceHintOf(module: string, member: string): string | undefined { @@ -1792,7 +1807,7 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record = (() => { type StaticTextDecoderEncoding = { kind: "utf8" } | { kind: "legacy"; id: number }; +/** Shared input for the native constructor's generated label registry. */ +export const textDecoderLabels: readonly (readonly [string, number])[] = [ + ...[...TEXT_DECODER_UTF8_LABELS].map((label) => [label, -1] as const), + ...Object.entries(TEXT_DECODER_LEGACY_LABELS).map(([label, id]) => [label, id!] as const), +]; + /** TextDecoder's get-an-encoding normalization: trim ASCII whitespace and * fold ASCII case only (Unicode case folding must not manufacture a label). */ export function staticTextDecoderEncoding(label: string): StaticTextDecoderEncoding | null { diff --git a/packages/compiler/src/frontend/npm-static-prune.test.ts b/packages/compiler/src/frontend/npm-static-prune.test.ts index 39278fec4..af688eac2 100644 --- a/packages/compiler/src/frontend/npm-static-prune.test.ts +++ b/packages/compiler/src/frontend/npm-static-prune.test.ts @@ -50,4 +50,13 @@ describe("npm static namespace re-export pruning", () => { expect(files.some((file) => file.endsWith("/cycle-b/spare.js"))).toBe(true); expect(files.some((file) => file.endsWith("/cycle-impure/index.js"))).toBe(true); }); + test("per-file sideEffects patterns retain initialization through imports and cycles", () => { + const { files, diagnostics } = moduleOrder("selectivebarrel-cli.ts", "selectivebarrel"); + expect(diagnostics).toEqual([]); + expect(files.some((file) => file.endsWith("/selectivebarrel/spare.js"))).toBe(false); + for (const name of ["feature.js", "effects/start.js", "bridge.js", "loop-a.js", "loop-b.js", "nested/custom.init.js", "braced/a.js"]) { + expect(files.some((file) => file.endsWith(`/selectivebarrel/${name}`)), name).toBe(true); + } + }); + }); diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts index 10da3cf8d..83cfe5ce3 100644 --- a/packages/compiler/src/frontend/npm-static-prune.ts +++ b/packages/compiler/src/frontend/npm-static-prune.ts @@ -5,7 +5,7 @@ * the link checks, and emitted module init headers must see one graph. */ import * as ts from "./ts7/adapter.js"; -import { dirname, join, resolve } from "node:path"; +import { dirname, join, relative, resolve } from "node:path"; import { nearestPackageType, resolveBareModule } from "./resolve.js"; import { npmStaticPackageOfPath } from "./npm-static.js"; import { trackedReadFile } from "./input-tracker.js"; @@ -28,8 +28,29 @@ function packageJson(path: string): Record | null { } } -function noSideEffects(value: unknown): boolean { - return value === false || (Array.isArray(value) && value.length === 0); +/** sideEffects patterns use package-relative POSIX paths. A pattern with no + * slash matches a basename at any depth, as in bundler package metadata. */ +function sideEffectPattern(pattern: string): RegExp { + // Unrecognized glob syntax must retain the module rather than silently + // treating a possible side-effect match as a literal path. + if (/[\[\]\\!@+()]/.test(pattern)) throw new Error("unsupported sideEffects glob"); + pattern = pattern.replace(/^\.\//, ""); + if (!pattern.includes("/")) pattern = "**/" + pattern; + let source = "^"; + for (let i = 0; i < pattern.length; i++) { + const ch = pattern[i]!; + if (ch === "*" && pattern[i + 1] === "*") { + i++; + if (pattern[i + 1] === "/") { source += "(?:.*/)?"; i++; } + else source += ".*"; + } else if (ch === "*") source += "[^/]*"; + else if (ch === "?") source += "[^/]"; + else if (ch === "{") source += "(?:"; + else if (ch === "}") source += ")"; + else if (ch === ",") source += "|"; + else source += ch.replace(/[\\^$+?.()|[\]]/g, "\\$&"); + } + return new RegExp(source + "$"); } /** A nested format scope such as dist/esm/package.json is not the package's @@ -48,49 +69,24 @@ function packageRootJsonPath(fromFile: string, packageName: string): string | nu return root; } -/** A package's own sideEffects declaration cannot cover imports of a - * dependency that runs at module init. Require the whole declared runtime - * dependency tree to make the same promise before removing an edge. A cycle - * back-edge is provisionally pure, but its result cannot be cached for a - * descendant until all dependencies of the ancestor have been checked. */ -type PackagePurity = { pure: boolean; provisional: boolean }; - -function purePackageTree( - path: string, - memo: Map, - visiting: Set, -): PackagePurity { - const cached = memo.get(path); - if (cached !== undefined) return { pure: cached, provisional: false }; - if (visiting.has(path)) return { pure: true, provisional: true }; +// An external package's executable sources may not be in this program. +// Preserve the conservative whole-dependency promise in that case. +function pureExternalPackage(path: string, visiting = new Set()): boolean { + if (visiting.has(path)) return true; const json = packageJson(path); - if (json === null || !noSideEffects(json["sideEffects"])) { - memo.set(path, false); - return { pure: false, provisional: false }; - } + const value = json?.["sideEffects"]; + if (json === null || !(value === false || Array.isArray(value) && value.length === 0)) return false; visiting.add(path); - let pure = true; - let provisional = false; for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) { const dependencies = json[field]; - if (dependencies === null || typeof dependencies !== "object" || Array.isArray(dependencies)) continue; - for (const name of Object.keys(dependencies as Record)) { + if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue; + for (const name of Object.keys(dependencies)) { const resolved = resolveBareModule(path, name, "js-only"); - const depPath = resolved === null ? null : packageRootJsonPath(resolved.typesFile, name); - const dep = depPath === null ? { pure: false, provisional: false } : purePackageTree(depPath, memo, visiting); - if (!dep.pure) { - pure = false; - break; - } - provisional ||= dep.provisional; + const root = resolved === null ? null : packageRootJsonPath(resolved.typesFile, name); + if (root === null || !pureExternalPackage(root, visiting)) return false; } - if (!pure) break; } - visiting.delete(path); - // False is final. A true result is final only when it did not borrow an - // ancestor's optimistic back-edge, or when the entire traversal is done. - if (!pure || !provisional || visiting.size === 0) memo.set(path, pure); - return { pure, provisional }; + return true; } type Demand = Set | null; // null requests the entire namespace @@ -131,15 +127,60 @@ export function planNpmStaticReexports( request(entry, null); for (const path of extraRoots) request(program.getSourceFile(path) ?? null, null); - const pureMemo = new Map(); + const pureMemo = new Map(); + const filePure = (sf: ts.SourceFile): boolean => { + const pkg = npmPackageNameOf(sf.fileName) ?? npmStaticPackageOfPath(sf.fileName); + const path = pkg === null ? null : packageRootJsonPath(sf.fileName, pkg); + const json = path === null ? null : packageJson(path); + const value = json?.["sideEffects"]; + if (value === false) return true; + if (!Array.isArray(value) || !value.every((pattern) => typeof pattern === "string") || path === null) return false; + const name = relative(dirname(path), sf.fileName).replaceAll("\\", "/"); + try { return !value.some((pattern: string) => sideEffectPattern(pattern).test(name)); } + catch { return false; } + }; + // Check the actual imported closure: metadata for one file never vouches + // for its side-effectful descendants, even across a package or a cycle. + const pureModuleTree = (root: ts.SourceFile): boolean => { + const cached = pureMemo.get(root); + if (cached !== undefined) return cached; + const visited = new Set(); + const visit = (sf: ts.SourceFile): boolean => { + if (visited.has(sf)) return true; + if (pureMemo.get(sf) === false || !filePure(sf)) return false; + visited.add(sf); + const edges: { spec: string; kind?: "import" | "require" }[] = []; + for (const stmt of sf.statements) { + if (ts.isImportDeclaration(stmt) && stmt.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword && ts.isStringLiteral(stmt.moduleSpecifier)) edges.push({ spec: stmt.moduleSpecifier.text }); + else if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly && stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) edges.push({ spec: stmt.moduleSpecifier.text }); + } + ts.walkPreorder(sf, (node) => { + if (!ts.isCallExpression(node) || node.arguments.length !== 1) return; + const arg = node.arguments[0]; + if (!arg || !ts.isStringLiteralLike(arg)) return; + if (node.expression.kind === ts.SyntaxKind.ImportKeyword) edges.push({ spec: arg.text, kind: "import" }); + else if (ts.isIdentifier(node.expression) && node.expression.text === "require") edges.push({ spec: arg.text, kind: "require" }); + }); + return edges.every((edge) => { + const dep = resolveEdge(sf, edge.spec, edge.kind); + if (dep !== null) return visit(dep); + const resolved = resolveBareModule(sf.fileName, edge.spec, "js-only"); + const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile); + const root = pkg === null || resolved === null ? null : packageRootJsonPath(resolved.typesFile, pkg); + return root !== null && pureExternalPackage(root); + }); + }; + const pure = visit(root); + if (pure) for (const sf of visited) pureMemo.set(sf, true); + else pureMemo.set(root, false); + return pure; + }; const canPrune = (sf: ts.SourceFile, stmt: ts.ExportDeclaration, dep: ts.SourceFile | null): boolean => { if (dep === null || stmt.exportClause === undefined || !ts.isNamespaceExport(stmt.exportClause)) return false; const pkg = npmStaticPackageOfPath(sf.fileName); if (pkg === null || npmStaticPackageOfPath(dep.fileName) !== pkg) return false; - const pkgPath = packageRootJsonPath(sf.fileName, pkg); - if (pkgPath === null) return false; if (nearestPackageType(sf.fileName) !== "module") return false; - if (!purePackageTree(pkgPath, pureMemo, new Set()).pure) return false; + if (!pureModuleTree(dep)) return false; const names = demanded.get(sf); return names !== undefined && names !== null && !names.has(stmt.exportClause.name.text); }; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 977496f5c..903a20f9c 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -54,6 +54,7 @@ export function jsOpenObjectType( * same names keep their ordinary structural representation. */ export const ISLAND_AMBIENT_TYPES = [ "Response", + "Request", "ResponseInit", "RequestInit", "Event", @@ -1658,7 +1659,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return { kind: "record", - shapeId: ctx.shapes.intern([{ name: `%${psym.name}`, type: F64 }], false, undefined, []), + shapeId: ctx.shapes.intern([{ name: `%${psym.name}`, type: F64 }, ...(psym.name === "TextDecoder" ? [{ name: "%fatal", type: BOOL }, { name: "%ignoreBOM", type: BOOL }, { name: "%state", type: DYN }] : [])], false, undefined, []), }; } // string_decoder.StringDecoder: the decoder value is a two-field record @@ -1738,6 +1739,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // no-op cast, error-typed listener params accept it, and the `.code` // read has its own lowering (errno/syscall/path stay per-member fences). if (isStdlibInterface("ErrnoException")) return { kind: "object", className: "%Error" }; + // spawn consumes a presence-sensitive option bag, including optional + // stdio and env values. Keep it checked-dynamic for native normalization. + if (isStdlibInterface("SpawnOptions")) return DYN; // The fetch ambient slice: under --dynamic these are island handles // exactly like npm-declared types. Static fetch has one deliberately // narrower native representation: Response is an opaque checked- @@ -2719,6 +2723,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (widened.isIntersectionType() && checker.getConstructSignatures(widened).length === 0) { const hybrid = mapHybridCallableIntersection(widened, ctx); if (hybrid) return hybrid; + // Recursive and Symbol-keyed callable objects need their complete live + // property table; dropping those members into a plain closure loses data. + if (!ctx.dynamic && checker.getCallSignatures(widened).length > 0 && + checker.getPropertiesOfType(widened).length > 0) return DYN; } const callSigs = checker.getCallSignatures(widened); if (callSigs.length === 1) { @@ -2758,6 +2766,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ts.isParameter(decl) && (decl.questionToken !== undefined || decl.initializer !== undefined); let pt = mapType(checker.getTypeOfSymbol(p), ctx); + // An inferred JavaScript binding pattern consumes an iterable; it + // does not require the caller's array to have exactly this length. + if (decl && ts.isParameter(decl) && ts.isArrayBindingPattern(decl.name) && + isJsSourceFile(decl.getSourceFile()) && !decl.type && + !/@(?:param|type)\b/.test(decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()))) pt = DYN; if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions); // Belt and braces for non-strict type worlds: an optional param's // ABI slot is always the undefined-armed union (strictNullChecks @@ -2786,7 +2799,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const sigDecl = checker.signatureDeclaration(sig); const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) && (retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; - let ret = jsUnitReturn ? DYN : retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx); + let ret = jsUnitReturn || (!ctx.dynamic && (retT.flags & ts.TypeFlags.Any) !== 0) + ? DYN : retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx); if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array" && ret.elem.kind === "f64") ret = DYN; if (!ret) return null; return typedRest diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 34c4a53a8..d66fe0c67 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1973,16 +1973,18 @@ export const MAY_THROW_BYTES_METHODS: ReadonlySet = new * capture groups (JS would splice the captured values into the result) — * both catchable: backends' may-throw analyses must seed on these two * methods like a `throw`. */ -/** `match` takes a STRING receiver with args[0] the regex (non-g/y — the - * frontend fences literal g/y flags; a g-flagged value reaching the - * runtime aborts like test()) and produces the PROGRAM-DEPENDENT - * `string[] | null` union: the matched slice [whole, ...captures] wrapped - * into the array arm, or the interned null-arm instance for no match. A +/** `match` takes a STRING receiver with args[0] the regex and produces + * the PROGRAM-DEPENDENT `string[] | null` union: all whole matches for a + * global regex, otherwise [whole, ...captures], or null for no match. A * NONPARTICIPATING capture holds "" where Node's slot is undefined * (SEMANTICS.md divergence). Never throws. */ export type IrRegexIntrinsicMethod = | "test" | "match" + /** exec uses the same string-first operands but keeps the g/y + * lastIndex refusal, independently of global String.match iteration. */ + | "exec" + | "lastIndex" /** `s.matchAll(re)` — every match as its honest string[] slice (match's * rule), drained EAGERLY into a fresh string[][]: the lazy iterator is * unobservable across the lowered surface (strings are immutable; the @@ -2029,7 +2031,9 @@ export type IrLibFn = | "weakSet.new" | "dyn.fromEntries" | "bytes.constructor" + | "bytes.instanceOf" | "bytes.construct" + | "arrayBuffer.constructor" | "arrayBuffer.new" | "arrayBuffer.is" | "arrayBuffer.isView" @@ -2050,6 +2054,10 @@ export type IrLibFn = * consume the native body stream. AbortSignal and ReadableStream values * are opaque checked-dynamic handles. */ | "fetch.start" + | "fetch.input" + | "fetch.function" + | "fetch.requestNew" + | "fetch.headersNew" | "fetch.responseNew" | "fetch.responseArrayBuffer" | "fetch.responseJson" @@ -2059,6 +2067,8 @@ export type IrLibFn = | "fetch.abortTimeout" | "fetch.abortNow" | "fetch.abortAny" + | "fetch.webIs" + | "fetch.streamIs" | "fetch.streamNew" | "fetch.streamFrom" | "fetch.readerRead" @@ -2384,6 +2394,7 @@ export type IrLibFn = * spec's URIError ("URI malformed"), catchable. Borrow; results +1. */ | "str.encodeUriComponent" | "str.decodeUriComponent" + | "str.decodeUri" /** RegExp.escape (ES2025): per-code-point EncodeForRegExpEscape — * leading ASCII alphanumeric hex-escapes, syntax characters and '/' * take a backslash, other punctuators/whitespace/line terminators @@ -2417,6 +2428,7 @@ export type IrLibFn = | "num.toExponential" | "num.toFixed0" | "num.toFixed" + | "num.toStringRadix" /** Object.is over two numbers — the spec's SameValue on doubles: NaN * equals NaN, +0 differs from -0, everything else is `===`. Plain bool * result; never throws. (Union-armed operands take unionEq's sameValue @@ -2487,6 +2499,7 @@ export type IrLibFn = * the for-of/forEach desugar's index reads (live — the loop re-reads * sp.size each pass). */ | "sp.new" + | "sp.newChecked" | "sp.parse" | "sp.copy" | "sp.fromPairs" @@ -2650,6 +2663,9 @@ export type IrLibFn = | "child.disconnect" | "child.onMessage" | "child.onDisconnect" + | "child.onSpawn" + /** Rest listeners use the checked function bridge to pack event arguments. */ + | "child.onDyn" | "process.connected" | "process.send" | "process.sendCb" @@ -2684,6 +2700,7 @@ export type IrLibFn = | "child.kill" | "child.killNum" | "child.unref" + | "child.ref" /** The piped-output streams (stdio mode 3 — scr_child.c's stream * slice). child.stdout/child.stderr answer the checker's * `Readable | null` union (type-directed construction in the backend @@ -3443,6 +3460,7 @@ export type IrLibFn = * runtime algorithm string (md5/sha1/sha256); update returns the same * handle by retained identity, copy snapshots Hash state, and digest * finalizes the handle and returns either a Buffer or encoded string. */ + | "crypto.native" | "crypto.hashNew" | "crypto.hmacNewStr" | "crypto.hmacNewBytes" @@ -3485,6 +3503,7 @@ export type IrLibFn = /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like * fromStr's — and Buffer.isEncoding(name) over a runtime string * (case-insensitive against Node's alias set). Pure; never throw. */ + | "buffer.byteLenDyn" | "buffer.byteLenStr" | "buffer.isEncoding" /** Buffer.concat(list, totalLength): the concatenation truncated or @@ -3669,6 +3688,8 @@ export type IrLibFn = * apply/call thisArg), or the undefined dyn singleton with none bound * (the strict-mode plain-call answer, the old constant). Zero args → * dyn (+1). Never throws. */ + | "dyn.dataViewIs" + | "dyn.construct" | "dyn.this" | "dyn.generatorThis" /** process.execPath: the compiled binary's own resolved absolute path @@ -3685,6 +3706,8 @@ export type IrLibFn = | "process.builtinId" | "process.builtinModule" | "process.builtinUnsupported" + | "fs.callbackValue" + | "fs.callbackCall" | "process.hrtimeValue" /** process.versions.node: the runtime's Node COMPATIBILITY TARGET — * there is no Node under the binary, so this reports the version whose @@ -3898,6 +3921,7 @@ export type IrLibFn = | "error.hasCause" /** Assignment borrows both operands and retains the new cause. */ | "error.setCause" + | "error.defineCause" | "error.deleteCause" /** The compiler-resolved Node-parity throw for always-throwing lowered * arms (ERR_INVALID_THIS receivers, ERR_MISSING_ARGS arity ladders, @@ -3922,9 +3946,11 @@ export type IrLibFn = * f64 result, or a throw. Used by statically lowered numeric coercions * whose checker type remained any. */ | "dyn.numberConstructor" + | "dyn.bigintConstructor" | "dyn.toNumberCoerce" | "dyn.add" | "dyn.arithmetic" + | "dyn.compare" | "dyn.bitwise" | "dyn.proxyNew" /** A read of a `declare`d const NOTHING defines (the bundler-define @@ -3997,6 +4023,7 @@ export type IrLibFn = | "dyn.propertyIsEnumerable" | "dyn.assign" | "dyn.copyDataProperties" + | "dyn.objectRest" /** Variadic Object.assign over CHECKED-DYNAMIC targets (`Object.assign( * {}, ...arr.map(f), tail)` — the option-table merge): the lowering * builds one fresh dyn pack of sources (packPush retains a plain source @@ -4017,6 +4044,7 @@ export type IrLibFn = * Symbol(Symbol.iterator))"). The frontend picks by position. */ | "dyn.packPushSpreadIter" | "dyn.assignAll" + | "dyn.reflectApply" /** `Object.create(null)` (scr_json.c): a fresh NULL-PROTOTYPE dyn * dictionary. The checked-dynamic tree's OBJ dispatch is already own-member-only — * Node's null-proto answer — so the flag's whole job is the observations @@ -4069,6 +4097,7 @@ export type IrLibFn = * The result TYPE is the regex kind, so the link switch pulls the * engine exactly like a literal. */ | "regex.new" + | "regex.resetLastIndex" | "regex.newChecked" /** structuredClone with a NON-EMPTY transfer array of static values: * nothing static is transferable, so the call always throws Node's @@ -4220,6 +4249,8 @@ export type IrLibFn = | "readable.unshift" | "readable.unshiftStr" | "readable.read" + | "readable.readDyn" + | "stream.onDyn" | "readable.pause" | "readable.resume" | "readable.setEncoding" @@ -4701,6 +4732,11 @@ export type IrLibFn = * lower to buffer.fromStr(s, "utf8") (identical bytes — ScrStr storage * is well-formed UTF-8). Borrowed arg; owned (+1) string; never throws. */ | "text.decode" + | "bytes.bufferSource" + | "text.decodeBufferSource" + | "text.decodeOptions" + | "text.decodeStream" + | "text.decodeLegacyOptions" /** TextDecoder.decode for a compile-time non-UTF-8 WHATWG label. The * second f64 is the frontend-owned encoding id consumed by scr_bytes.c; * label aliases/case/ASCII whitespace are canonicalized before IR. The @@ -4708,6 +4744,7 @@ export type IrLibFn = * output portable without an ICU/iconv dependency. Borrowed args; owned * (+1) string; never throws. */ | "text.decodeLegacy" + | "text.decoderEncoding" /** The wider sync fs slice (scr_lib.c), all throwing catchably with * Node's errno message shapes and `.code` stamped like the rest of * sync fs. unlink/chmod/chown wrap the syscalls 1:1 (Node reports the @@ -4821,6 +4858,9 @@ export type IrLibFn = * pairs like cp.execSync's), cwd ""=inherit. Same event/loop story as * cp.spawn. */ | "cp.spawnOpts" + /** Runtime options normalized into the native spawn core; unsupported + * process features retain explicit refusals. (command, args, options). */ + | "cp.spawnDynamic" /** Atomics.wait(int32Array, idx, expected, timeoutMs) → "not-equal" * when the element differs from `expected`, else a real nanosleep for * the timeout and "timed-out" (scr_lib.c). scriptc has no threads — @@ -6404,6 +6444,7 @@ function canBoxDynCompositeAt( case "bytes": case "regex": case "url": + case "searchParams": return true; case "func": return canBoxFuncIntoDyn(t, getRecord, getUnion, visiting); @@ -6491,12 +6532,12 @@ function canDynCheckToAt( // serializable. Backends already retain dyn fields and fill missing // unknown record fields with the undefined value. if (isJsonSafeAt(t, getRecord, getUnion, false, false, new Set(), true)) return true; - if (t.kind === "bigint" || t.kind === "symbol" || t.kind === "date") return true; + if (t.kind === "bigint" || t.kind === "symbol" || t.kind === "date" || t.kind === "searchParams") return true; if (t.kind === "map" || t.kind === "set") return canBoxDynComposite(t, getRecord, getUnion, visiting); if (t.kind === "bytes") return true; if (t.kind === "classval") return true; if (t.kind === "generator") return true; - if (t.kind === "promise") return t.inner.kind === "dyn"; + if (t.kind === "promise") return t.inner.kind === "dyn" || t.inner.kind === "void" || canDynCheckToAt(t.inner, getRecord, getUnion, visiting); if (t.kind === "object" && t.className === "%Error") return true; // Native class capsules already support checked extraction at ordinary // boundaries. Callable adapters use the same identity/brand check. @@ -6678,7 +6719,8 @@ export interface RuntimeFeatures { } const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ - "async.awaitDyn", "timers.immediatePromise", + "fs.callbackValue", "fs.callbackCall", + "async.awaitDyn", "timers.immediatePromise", "crypto.native", "process.onUncaughtException", "process.offUncaughtException", "process.onUnhandledRejection", "process.offUnhandledRejection", "process.onRejectionHandled", "process.offRejectionHandled", @@ -6712,7 +6754,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run if (node.kind === "libCall") { const fn = node.fn; if (fn === "regexp.escape" || fn === "dyn.nativeRegexIs") features.regex = true; - if (fn === "text.decodeLegacy") features.legacyTextDecoder = true; + if (fn === "text.decodeLegacy" || fn === "text.decodeLegacyOptions" || fn === "text.decodeStream") features.legacyTextDecoder = true; if (fn.startsWith("fetch.")) features.fetch = true; if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; if (fn.startsWith("emitter.")) features.emitter = true; @@ -7123,6 +7165,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ // exclude — refuse the surface like the rest of the event-loop family. ["fs.existsChk", "the async fs callback surface (fs.exists)"], ["fs.renameCb", "the async fs callback surface (fs.rename)"], + ["fs.callback", "the async filesystem callback surface"], ["zlib.deflateCb", "the async node:zlib callback surface"], ["zlib.inflateCb", "the async node:zlib callback surface"], ["zlib.deflateRawCb", "the async node:zlib callback surface"], @@ -7342,6 +7385,14 @@ export function moduleLibNondeterministicSurface(mod: IrModule): string | null { * seed on `dynCheck` and `awaitExpr` nodes, which throw on validation * failure / promise rejection). */ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ + "cp.spawnDynamic", + "child.onDyn", + "text.decoderEncoding", + "dyn.construct", + "dyn.reflectApply", + "text.decodeOptions", + "text.decodeStream", + "text.decodeLegacyOptions", "dyn.nativeSetNew", "ffi.argument", "ffi.memoryModule", @@ -7354,6 +7405,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "weakSet.new", "dyn.fromEntries", "bytes.constructor", + "bytes.instanceOf", + "buffer.byteLenDyn", "bytes.construct", "arrayBuffer.new", "arrayBuffer.byteLengthGetter", @@ -7367,6 +7420,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "arrayBuffer.viewF32", "arrayBuffer.viewF64", "arrayBuffer.viewDV", + "bytes.bufferSource", + "text.decodeBufferSource", "bigint.parse", "bigint.fromF64", @@ -7382,12 +7437,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "bigint.bufferWrite", "bigint.dataViewGet", "bigint.dataViewSet", + "fetch.requestNew", + "fetch.headersNew", "fetch.responseNew", "fetch.abortTimeout", "fetch.abortAny", "fetch.streamNew", "fetch.streamFrom", "num.toFixed", + "num.toStringRadix", "insp.jsonDyn", // diagnostics_channel: publish runs subscribers synchronously (a throw // propagates — the documented divergence from triggerUncaughtException); @@ -7547,6 +7605,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "readable.unshift", "readable.unshiftStr", "readable.read", + "readable.readDyn", + "stream.onDyn", "readable.resume", "readable.pipe", "readable.unpipe", @@ -7602,9 +7662,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objectTag", // Numeric coercion runs user valueOf/toString — throws propagate. "dyn.numberConstructor", + "dyn.bigintConstructor", "dyn.toNumberCoerce", "dyn.add", "dyn.arithmetic", + "dyn.compare", "dyn.bitwise", "dyn.proxyNew", "dyn.classBasePrototype", @@ -7654,6 +7716,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // decodeURIComponent throws the spec's URIError on bad hex/invalid // UTF-8 octets (encodeURIComponent never throws — see the IrLibFn doc). "str.decodeUriComponent", + "str.decodeUri", // The base64 globals: atob/btoa throw the catchable DOMException // InvalidCharacterError on malformed input; the zero-argument form // always throws Node's TypeError [ERR_MISSING_ARGS]. @@ -7678,6 +7741,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.propertyIsEnumerable", "dyn.assign", "dyn.copyDataProperties", + "dyn.objectRest", // variadic Object.assign: spread flattening throws V8's spread-call // TypeErrors; the final copy throws ToObject on a nullish target "dyn.packPushSpread", @@ -7701,12 +7765,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // new RegExp compiles the pattern eagerly: an invalid pattern or flag // throws Node's catchable SyntaxError at construction. "regex.new", + "regex.resetLastIndex", "regex.newChecked", "dyn.keySet", "dyn.keySetComputed", "process.builtinId", "process.builtinModule", "process.builtinUnsupported", + "fs.callbackValue", + "fs.callbackCall", "dyn.keyDelete", "dyn.keyDeleteComputed", "dyn.hasKeyComputed", @@ -7753,6 +7820,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // on a row that is not a [name, value] pair. The rest of the sp family // never throws. "sp.fromPairs", + "sp.newChecked", "fs.statSync", "crypto.randomBytesToString", "crypto.randomBytes", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index af156519c..711c49f04 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -68,6 +68,8 @@ export const REGEX_INTRINSIC_SIGS: Record< // VOID here is the process.envGet sentinel; the regexIntrinsic case // checks the union's arms. match: { receiver: STRING, argTypes: [REGEX], result: VOID }, + exec: { receiver: STRING, argTypes: [REGEX], result: VOID }, + lastIndex: { receiver: REGEX, argTypes: [], result: F64 }, matchAll: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) }, matchAllInto: { receiver: STRING, argTypes: [REGEX, arrayOf(F64)], result: arrayOf(arrayOf(STRING)) }, search: { receiver: STRING, argTypes: [REGEX], result: F64 }, @@ -89,6 +91,10 @@ export const REGEX_INTRINSIC_SIGS: Record< export const LIB_FN_SIGS: Record = { "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, + "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "fetch.function": { argTypes: [], result: DYN }, + "fetch.requestNew": { argTypes: [DYN, DYN], result: DYN }, + "fetch.headersNew": { argTypes: [DYN], result: DYN }, "fetch.responseArrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseJson": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, @@ -101,6 +107,8 @@ export const LIB_FN_SIGS: Record; validated below. "fetch.readerRead": { argTypes: [DYN], result: VOID }, @@ -310,11 +318,13 @@ export const LIB_FN_SIGS: Record void", e.loc); + } if (e.fn === "child.onExit" || e.fn === "child.onClose" || e.fn === "child.onError") { // The listener: a closure with no params, or exactly the // supported parameter shapes per event — exit takes (code: @@ -5719,7 +5760,7 @@ function validateFunction( } function checkLibEmitterCall(e: IrExpr & { kind: "libCall" }, result: IrType): boolean { - if (e.fn.startsWith("emitter.") && e.fn !== "emitter.setDefaultMax" && + if ((e.fn.startsWith("emitter.") || e.fn === "stream.onDyn") && e.fn !== "emitter.setDefaultMax" && e.fn !== "emitter.setDefaultMaxChk" && e.fn !== "emitter.getDefaultMax" && e.fn !== "emitter.checkListener") { // Receiver: an emitter-hierarchy object (the %EventEmitter class @@ -5746,7 +5787,7 @@ function validateFunction( } // The chaining forms return the receiver's own static class. if (e.fn === "emitter.on" || e.fn === "emitter.off" || - e.fn === "emitter.onDyn" || e.fn === "emitter.onFlex" || e.fn === "emitter.offDyn" || + e.fn === "emitter.onDyn" || (e.fn === "emitter.onFlex" || e.fn === "stream.onDyn") || e.fn === "emitter.offDyn" || e.fn === "emitter.onData" || e.fn === "emitter.onDataDyn" || e.fn === "emitter.removeAll" || e.fn === "emitter.setMax" || e.fn === "emitter.setMaxChk") { diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index dfd3621ae..53abeae00 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -195,14 +195,6 @@ "code": "SC1120", "note": "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates" }, - { - "id": "diagnostic.sc1121", - "kind": "diagnostic-fence", - "name": "'.test()' on a regex with the 'g' or 'y' flag", - "status": "unsupported", - "code": "SC1121", - "note": "g/y regexes carry mutable lastIndex state between calls, which is not modeled; drop the flag for a plain match test, or use replace/replaceAll/split (their iteration is internal)" - }, { "id": "diagnostic.sc2001", "kind": "diagnostic-fence", @@ -896,6 +888,13 @@ "name": "fs.appendFileSync", "status": "static" }, + { + "id": "node-builtin.fs.callbacks", + "kind": "node-builtin", + "name": "fs error-first callbacks", + "status": "static", + "note": "native filesystem callbacks, including stored platform adapter functions" + }, { "id": "node-builtin.fs.chmodSync", "kind": "node-builtin", @@ -2624,9 +2623,8 @@ "id": "stdlib.headers.constructor", "kind": "stdlib", "name": "Headers constructor", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; the interface constructor has no compiler bridge in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: webidl-conversion, mutation, identity; supported scope: Record and iterable header pairs, including copying native Headers; differential evidence: fixture:static-request" }, { "id": "stdlib.headers.delete", @@ -3211,9 +3209,8 @@ "id": "stdlib.request.arrayBuffer", "kind": "stdlib", "name": "Request.arrayBuffer", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: body-consumption, promise-settlement, state-machine, error-shape; differential evidence: fixture:static-request" }, { "id": "stdlib.request.attribute", @@ -3235,33 +3232,29 @@ "id": "stdlib.request.body", "kind": "stdlib", "name": "Request.body", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.bodyUsed", "kind": "stdlib", "name": "Request.bodyUsed", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.bytes", "kind": "stdlib", "name": "Request.bytes", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: body-consumption, promise-settlement, state-machine, error-shape; differential evidence: fixture:static-request" }, { "id": "stdlib.request.cache", "kind": "stdlib", "name": "Request.cache", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.clone", @@ -3275,33 +3268,29 @@ "id": "stdlib.request.constructor", "kind": "stdlib", "name": "Request constructor", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: webidl-conversion, body-consumption, error-shape; supported scope: Absolute URLs with method, headers, body, duplex, redirect, and signal; other RequestInit members retain explicit fences; differential evidence: fixture:static-request" }, { "id": "stdlib.request.credentials", "kind": "stdlib", "name": "Request.credentials", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.destination", "kind": "stdlib", "name": "Request.destination", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.duplex", "kind": "stdlib", "name": "Request.duplex", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.formData", @@ -3315,113 +3304,99 @@ "id": "stdlib.request.headers", "kind": "stdlib", "name": "Request.headers", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.integrity", "kind": "stdlib", "name": "Request.integrity", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.isHistoryNavigation", "kind": "stdlib", "name": "Request.isHistoryNavigation", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.isReloadNavigation", "kind": "stdlib", "name": "Request.isReloadNavigation", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.json", "kind": "stdlib", "name": "Request.json", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: body-consumption, promise-settlement, state-machine, error-shape; differential evidence: fixture:static-request" }, { "id": "stdlib.request.keepalive", "kind": "stdlib", "name": "Request.keepalive", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.method", "kind": "stdlib", "name": "Request.method", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.mode", "kind": "stdlib", "name": "Request.mode", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.redirect", "kind": "stdlib", "name": "Request.redirect", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.referrer", "kind": "stdlib", "name": "Request.referrer", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.referrerPolicy", "kind": "stdlib", "name": "Request.referrerPolicy", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.signal", "kind": "stdlib", "name": "Request.signal", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.request.text", "kind": "stdlib", "name": "Request.text", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: body-consumption, promise-settlement, state-machine, error-shape; differential evidence: fixture:static-request" }, { "id": "stdlib.request.url", "kind": "stdlib", "name": "Request.url", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.15.0 / Undici 7.24.4; typed source has no compiler bridge for this interface in either tier" + "status": "static", + "note": "Node 24.15.0 / Undici 7.24.4; facets: property-read, state-machine; supported scope: Reads on native Requests within the constructor's supported RequestInit subset; differential evidence: fixture:static-request" }, { "id": "stdlib.response-init.headers", diff --git a/packages/compiler/test/native-class-descriptors.test.ts b/packages/compiler/test/native-class-descriptors.test.ts index 09172c8bb..b1f055d0a 100644 --- a/packages/compiler/test/native-class-descriptors.test.ts +++ b/packages/compiler/test/native-class-descriptors.test.ts @@ -16,7 +16,6 @@ test.each([ "Object.defineProperties(value, {x: {value: 2}});", "Object.defineProperty(value, 'data', {get() { return this.x; }});", "Object.defineProperties(value, {data: {get() { return this.x; }}});", - "Object.defineProperty(value, String('data'), {value: 2});", ])("keeps unsafe native descriptor changes fenced: %s", (operation) => { const { dir, entry } = fixture(`class Value { constructor() { this.x = 1; } } const value = new Value(); ${operation}`); try { @@ -72,3 +71,26 @@ try { value.clone(); } catch (error) { console.log(error.message.includes('repla rmSync(dir, { recursive: true, force: true }); } }); + + +test("derived constructor boundaries preserve initialization and refuse repeated super", async () => { + const { dir, entry } = fixture(` +class Base { constructor(value) { this.value = value; } } +class Before extends Base { constructor() { +// @ts-expect-error Exercise the runtime initialization guard. +this.value; super(1); } } +class Missing extends Base { constructor(flag) { if (flag) super(1); } } +class Repeated extends Base { constructor() { super(1); super(2); } } +try { new Before(); } catch (error) { console.log(error instanceof ReferenceError); } +try { new Missing(false); } catch (error) { console.log(error instanceof ReferenceError); } +try { new Repeated(); } catch (error) { console.log(error.code === 'SC2020'); } +`); + try { + const result = await compile(entry, { outDir: dir, outPath: join(dir, "program"), backend: "llvm", optimization: "dev" }); + if (!result.ok) throw new Error(JSON.stringify(result.diagnostics)); + const execution = spawnSync(result.binaryPath, [], { encoding: "utf8" }); + expect(execution.status).toBe(0); + expect(execution.stdout).toBe("true\ntrue\ntrue\n"); + expect(execution.stderr).toBe(""); + } finally { rmSync(dir, { recursive: true, force: true }); } +}); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 40d3e7131..9807bfa74 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -166,7 +166,7 @@ describe.runIf(supported)("LLVM native helper integration", () => { expect(await symbols(helperObject, ["-gU"])) .toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v5"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v6"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -460,7 +460,7 @@ describe.runIf(supported)("LLVM native helper integration", () => { .stdout.trim().split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v5"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v6"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 09544889f..d4b408427 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -205,6 +205,12 @@ ], "diags": [] }, + "/tests/corpus/110-number-radix.ts": { + "order": [ + "/tests/corpus/110-number-radix.ts" + ], + "diags": [] + }, "/tests/corpus/1100-island-eval-basics.ts": { "order": [ "/tests/corpus/1100-island-eval-basics.ts" @@ -223,6 +229,12 @@ ], "diags": [] }, + "/tests/corpus/111-record-entries.ts": { + "order": [ + "/tests/corpus/111-record-entries.ts" + ], + "diags": [] + }, "/tests/corpus/1110-math-members.ts": { "order": [ "/tests/corpus/1110-math-members.ts" @@ -283,6 +295,12 @@ ], "diags": [] }, + "/tests/corpus/112-class-descriptor-key.js": { + "order": [ + "/tests/corpus/112-class-descriptor-key.js" + ], + "diags": [] + }, "/tests/corpus/1120-web-globals-encoders.ts": { "order": [ "/tests/corpus/1120-web-globals-encoders.ts" @@ -313,6 +331,48 @@ ], "diags": [] }, + "/tests/corpus/113-promise-checked-exits.ts": { + "order": [ + "/tests/corpus/113-promise-checked-exits.ts" + ], + "diags": [] + }, + "/tests/corpus/114-live-record-delete.ts": { + "order": [ + "/tests/corpus/114-live-record-delete.ts" + ], + "diags": [] + }, + "/tests/corpus/115-array-compound.js": { + "order": [ + "/tests/corpus/115-array-compound.js" + ], + "diags": [] + }, + "/tests/corpus/116-text-decoder-runtime.js": { + "order": [ + "/tests/corpus/116-text-decoder-runtime.js" + ], + "diags": [] + }, + "/tests/corpus/117-weak-cache-cycle.js": { + "order": [ + "/tests/corpus/117-weak-cache-cycle.js" + ], + "diags": [] + }, + "/tests/corpus/118-fs-stored-callbacks.cjs": { + "order": [ + "/tests/corpus/118-fs-stored-callbacks.cjs" + ], + "diags": [] + }, + "/tests/corpus/119-native-patterns.js": { + "order": [ + "/tests/corpus/119-native-patterns.js" + ], + "diags": [] + }, "/tests/corpus/1200-regex-test-basics.ts": { "order": [ "/tests/corpus/1200-regex-test-basics.ts" @@ -7697,6 +7757,62 @@ ], "diags": [] }, + "/tests/corpus/3125-regex-global-match.ts": { + "order": [ + "/tests/corpus/3125-regex-global-match.ts" + ], + "diags": [] + }, + "/tests/corpus/3126-js-number-radix/main.ts": { + "order": [ + "/tests/corpus/3126-js-number-radix/format.js", + "/tests/corpus/3126-js-number-radix/main.ts" + ], + "diags": [] + }, + "/tests/corpus/3127-spawn-runtime-options.ts": { + "order": [ + "/tests/corpus/3127-spawn-runtime-options.ts" + ], + "diags": [] + }, + "/tests/corpus/3128-regex-last-index.ts": { + "order": [ + "/tests/corpus/3128-regex-last-index.ts" + ], + "diags": [] + }, + "/tests/corpus/3129-js-token-union/main.ts": { + "order": [ + "/tests/corpus/3129-js-token-union/lexer.js", + "/tests/corpus/3129-js-token-union/main.ts" + ], + "diags": [] + }, + "/tests/corpus/3130-child-generic-streams.ts": { + "order": [ + "/tests/corpus/3130-child-generic-streams.ts" + ], + "diags": [] + }, + "/tests/corpus/3131-child-rest-listeners.js": { + "order": [ + "/tests/corpus/3131-child-rest-listeners.js" + ], + "diags": [] + }, + "/tests/corpus/3132-child-generic-stream-completion.ts": { + "order": [ + "/tests/corpus/3132-child-generic-stream-completion.ts" + ], + "diags": [] + }, + "/tests/corpus/3133-text-decoder-streaming.ts": { + "order": [ + "/tests/corpus/3133-text-decoder-streaming.ts" + ], + "diags": [] + }, "/tests/corpus/400-fib.ts": { "order": [ "/tests/corpus/400-fib.ts" diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index defc031ff..c9d31fbb2 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -14,7 +14,7 @@ const BASE_RUNTIME_SOURCES = [ "scr_number.c", "scr_bigint.c", "scr_string.c", "scr_grapheme.c", "scr_array.c", "scr_bytes.c", "scr_bytes_io.c", "scr_map.c", "scr_closure.c", "scr_ffi.c", "scr_object.c", "scr_union.c", "scr_exception.c", "scr_error.c", - "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_json.c", "scr_node_builtin.c", + "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_url_params.c", "scr_json.c", "scr_node_builtin.c", "scr_async.c", "scr_crypto_async.c", "scr_child.c", "scr_cycle.c", ]; @@ -43,7 +43,6 @@ const optional = [ ["scr_dyn_handle.c", any("emitter", "netEffective")], ["scr_symbol.c", "symbol"], ["scr_bigint_assert.c", all("assert", "bigint")], - ["scr_url_params.c", "searchParams"], ["scr_qs.c", "qs"], ["scr_util.c", "parseArgs"], ["scr_stream.c", "stream"], diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 222fb2d43..e5fc42db4 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -166,6 +166,7 @@ async function build() { [join(runtimeRoot, "LICENSE"), "artifacts/licenses/scriptc-runtime.txt", "Apache-2.0"], [join(quickjs, "LICENSE"), "artifacts/licenses/quickjs-ng.txt", "MIT"], [join(vendorRoot, "ryu", "LICENSE-Boost"), "artifacts/licenses/ryu.txt", "BSL-1.0"], + [join(vendorRoot, "v8-number-radix", "LICENSE"), "artifacts/licenses/v8-number-radix.txt", "BSD-3-Clause"], [join(vendorRoot, "unicode", "LICENSE"), "artifacts/licenses/unicode.txt", "Unicode-3.0"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"], @@ -181,7 +182,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 5, marker: "scr_runtime_abi_v5" }, + runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 8eb217fa3..130d74404 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -12,7 +12,7 @@ const BASE_RUNTIME_SOURCES = [ "scr_number.c", "scr_bigint.c", "scr_string.c", "scr_grapheme.c", "scr_array.c", "scr_bytes.c", "scr_bytes_io.c", "scr_map.c", "scr_closure.c", "scr_ffi.c", "scr_object.c", "scr_union.c", "scr_exception.c", "scr_error.c", - "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_json.c", "scr_node_builtin.c", + "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_url_params.c", "scr_json.c", "scr_node_builtin.c", "scr_async.c", "scr_crypto_async.c", "scr_child.c", "scr_cycle.c", ]; @@ -24,7 +24,7 @@ const OPTIONAL = [ ["scr_zlib.c", "zlib"], ["scr_zlib_island.c", all("zlib", "dynamic")], ["scr_events.c", "events"], ["scr_readline.c", "events"], ["scr_events_emitter.c", "emitter"], ["scr_dyn_handle.c", any("emitter", "netEffective")], - ["scr_symbol.c", "symbol"], ["scr_url_params.c", "searchParams"], + ["scr_symbol.c", "symbol"], ["scr_bigint_assert.c", all("assert", "bigint")], ["scr_qs.c", "qs"], ["scr_util.c", "parseArgs"], ["scr_stream.c", "stream"], ["scr_loop_kqueue.c", any("netEffective", "dgram")], diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 338e473aa..cfae7ffc1 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -126,10 +126,11 @@ async function build() { ]; await assertArtifactsExcludeStrings(stagedOutputRoot, config.forbiddenArtifactStrings ?? []); const archiveSpecs = new Map(matrix.archives.map((entry) => [entry.id, entry])); - const licensed = [[join(runtimeRoot, "LICENSE"), "artifacts/licenses/scriptc-runtime.txt", "Apache-2.0"], [join(quickjs, "LICENSE"), "artifacts/licenses/quickjs-ng.txt", "MIT"], [join(vendorRoot, "ryu", "LICENSE-Boost"), "artifacts/licenses/ryu.txt", "BSL-1.0"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"]]; + const licensed = [[join(runtimeRoot, "LICENSE"), "artifacts/licenses/scriptc-runtime.txt", "Apache-2.0"], [join(quickjs, "LICENSE"), "artifacts/licenses/quickjs-ng.txt", "MIT"], [join(vendorRoot, "ryu", "LICENSE-Boost"), "artifacts/licenses/ryu.txt", "BSL-1.0"], + [join(vendorRoot, "v8-number-radix", "LICENSE"), "artifacts/licenses/v8-number-radix.txt", "BSD-3-Clause"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"]]; licensed.push([join(vendorRoot, "unicode", "LICENSE"), "artifacts/licenses/unicode.txt", "Unicode-3.0"]); await Promise.all(licensed.map(async ([source, destination]) => { const output = join(buildRoot, destination); await mkdir(dirname(output), { recursive: true }); await copyFile(source, output); })); - const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: matrix.target, runtime_abi: { version: 5, marker: "scr_runtime_abi_v5" }, compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required" }, flavors, archives: archives.map((entry) => ({ ...entry, predicate: archiveSpecs.get(entry.id).predicate })), system_libraries: matrix.system_libraries, licenses: licensed.map(([, path, license]) => ({ path, license })) }; + const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: matrix.target, runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" }, compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required" }, flavors, archives: archives.map((entry) => ({ ...entry, predicate: archiveSpecs.get(entry.id).predicate })), system_libraries: matrix.system_libraries, licenses: licensed.map(([, path, license]) => ({ path, license })) }; await writeFile(stagedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`); const suffix = `${process.pid}-${Math.random().toString(36).slice(2)}`; await installRuntimePack({ outputRoot, manifestPath, stagedOutputRoot, stagedManifestPath, backupRoot: join(packageRoot, `.runtime-pack-artifacts-backup-${suffix}`), backupManifestPath: join(packageRoot, `.runtime-pack-manifest-backup-${suffix}`) }); diff --git a/packages/runtime-pack-common/test/library-matrix.test.ts b/packages/runtime-pack-common/test/library-matrix.test.ts index c5f80c08b..2e2cebad4 100644 --- a/packages/runtime-pack-common/test/library-matrix.test.ts +++ b/packages/runtime-pack-common/test/library-matrix.test.ts @@ -5,6 +5,14 @@ const matrix = createRuntimePackMatrix({ target: { object_format: "macho" }, compileFlags: [], systemLibraries: [], }); +test("checked URL handles include their search-parameter dependency without a feature opt-in", () => { + for (const object_format of ["elf", "macho", "coff", "wasm"]) { + const pack = createRuntimePackMatrix({ target: { object_format }, compileFlags: [], systemLibraries: [] }); + const unit = pack.runtime_units.find((unit) => unit.source === "scr_url_params.c"); + expect(unit?.predicate).toBe(true); + } +}); + test("library pack modes preserve pure runtime features and exclude ambient event-loop code", () => { for (const optimization of ["release", "dev"]) { for (const mode of ["library", "library-thread"]) { diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c5a979228..4834549d4 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -420,6 +420,7 @@ ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) { a->props = NULL; a->prop_len = 0; a->prop_cap = 0; + a->metadata = NULL; if (initial_cap > 0) scr_arr_grow_dense(a, initial_cap); #ifdef SCR_RC_AUDIT scr_live_arrays++; @@ -433,6 +434,7 @@ ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) { * below releases none — the complement contract in scr_runtime.h. */ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { ScrArr *a = (ScrArr *)a0; + visit(a->metadata, ctx); for (size_t i = 0; i < a->cap; i++) { if (a->present[i] == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); } @@ -486,6 +488,7 @@ ScrArr *scr_arr_new_ref(void *(*elem_retain)(void *), a->props = NULL; a->prop_len = 0; a->prop_cap = 0; + a->metadata = NULL; if (initial_cap > 0) scr_arr_grow_dense(a, initial_cap); #ifdef SCR_RC_AUDIT scr_live_arrays++; @@ -497,6 +500,8 @@ void scr_arr_release(ScrArr *a) { if (!a || a->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--a->rc == 0) { if (a->elem_trace) scr_cyc_on_dead(a); + scr_dyn_release(a->metadata); + a->metadata = NULL; if (scr_elem_is_ref(a->elem)) { for (size_t i = 0; i < a->cap; i++) { if (a->present[i] != SCR_ARR_HOLE) { @@ -534,6 +539,7 @@ void scr_arr_release(ScrArr *a) { #ifdef SCR_RC_AUDIT scr_live_arrays--; #endif + scr_weak_dispose(a); free(a); } } else if (a->elem_trace) { @@ -1442,3 +1448,13 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { free(buf); return out; } + +void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { + if (!a->metadata) return; + for (size_t i = 0; i < a->metadata->v.obj.len; i++) { + ScrDynEntry *entry = &a->metadata->v.obj.entries[i]; + ScrStr *key = scr_str_new(entry->key, entry->key_len); + scr_dyn_key_set(target, key, entry->value); + scr_str_release(key); + } +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index f07340e6e..ee0feebe1 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -153,8 +153,11 @@ enum { SCR_PROM_PENDING = 0, SCR_PROM_FULFILLED = 1, SCR_PROM_REJECTED = 2 }; typedef struct ScrFiber ScrFiber; +typedef struct { size_t rc; } ScrPromiseIdentity; + struct ScrPromise { - size_t rc; + size_t rc; /* The cycle collector reads the reference count at offset zero. */ + ScrPromiseIdentity *identity; int state; bool top_level_handled; /* uncaughtException handled entry evaluation */ /* Payload (fulfillment value or rejection reason), ScrExcCell-style. @@ -257,6 +260,7 @@ static void scr_promise_gcfree(void *o) { for (size_t i = 0; i < p->ncbs; i++) { if (p->cbs[i].all) scr_promise_all_state_release(p->cbs[i].all); } + if (--p->identity->rc == 0) free(p->identity); free(p->waiters); free(p->cbs); #ifdef SCR_RC_AUDIT @@ -268,12 +272,26 @@ static void scr_promise_gcfree(void *o) { ScrPromise *scr_promise_new(void) { ScrPromise *p = scr_cyc_alloc(sizeof *p, &scr_promise_trace, &scr_promise_gcfree); p->rc = 1; + p->identity = malloc(sizeof *p->identity); + if (!p->identity) scr_oom(); + p->identity->rc = 1; #ifdef SCR_RC_AUDIT scr_live_promises++; #endif return p; } +void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src) { + if (dst->identity == src->identity) return; + if (--dst->identity->rc == 0) free(dst->identity); + dst->identity = src->identity; + dst->identity->rc++; +} + +bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b) { + return a->identity == b->identity; +} + /* The 'rejectionHandled' hook (scr_async_dyn.c installs it at listener * registration — the scr_urj_deliver_fn pattern, so listener-free * binaries keep their size class): called when a promise the checkpoint @@ -328,6 +346,7 @@ void scr_promise_release(ScrPromise *p) { if (--p->rc == 0) { scr_cyc_on_dead(p); scr_promise_release_payload(p); + if (--p->identity->rc == 0) free(p->identity); free(p->waiters); for (size_t i = 0; i < p->ncbs; i++) { scr_promise_release(p->cbs[i].dst); diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index b35e678f7..4596566dc 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -732,22 +732,6 @@ void scr_process_emit_warning(ScrDyn *args) { * (SEMANTICS.md 67). Shared by the dc trace choreography, the checked-dynamic tree * promise reactions, and the unhandled-rejection dispatch. Borrows the * box; result +1. */ -ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { - switch (c->kind) { - case SCR_EXC_F64: return scr_dyn_new_num(c->f64); - case SCR_EXC_BOOL: return scr_dyn_new_bool(c->b); - case SCR_EXC_STR: return scr_dyn_new_str((ScrStr *)c->payload); - case SCR_EXC_REF: - case SCR_EXC_PRIMITIVE_REF: - if (c->retain_fn == scr_dyn_retain_v) return scr_dyn_retain((ScrDyn *)c->payload); - return scr_dyn_new_obj(); - case SCR_EXC_OBJ: - if (scr_error_is(c->payload)) return scr_dyn_from_error((const ScrError *)c->payload); - return scr_dyn_new_obj(); - default: - return scr_dyn_new_obj(); - } -} /* Await a dyn-CROSSING promise (SCR_DYN_PROMISE's boundary contract — * dyn or void fulfillment): the payload as a dyn value (+1; a void @@ -820,7 +804,10 @@ ScrDyn *scr_promise_reason_dyn(const ScrPromise *p) { * promise directly and every other inner type through the adapting * constructor below. */ +extern bool (*scr_dyn_promise_identity_fn)(ScrPromise *, ScrPromise *); + ScrDyn *scr_dyn_new_promise(ScrPromise *p) { + scr_dyn_promise_identity_fn = scr_promise_identity_equal; ScrDyn *d = scr_dyn_alloc_promise(scr_promise_release); d->v.promise = scr_promise_retain(p); return d; @@ -835,6 +822,8 @@ ScrDyn *scr_dyn_new_promise(ScrPromise *p) { ScrDyn *scr_dyn_new_promise_adapting(ScrPromise *src, void (*adapt)(ScrPromise *dst, ScrPromise *src)) { ScrPromise *dst = scr_promise_new(); + scr_promise_share_identity(dst, src); + scr_dyn_promise_identity_fn = scr_promise_identity_equal; scr_promise_race_add(dst, src, adapt); ScrDyn *d = scr_dyn_alloc_promise(scr_promise_release); d->v.promise = dst; /* the constructor's +1 moves in */ @@ -844,3 +833,45 @@ ScrDyn *scr_dyn_new_promise_adapting(ScrPromise *src, ScrPromise *scr_dyn_promise_of(const ScrDyn *d) { return d->kind == SCR_DYN_PROMISE ? d->v.promise : NULL; } + +/* The checked native WebCrypto digest entry keeps promise and ArrayBuffer results native. */ +static ScrDyn *scr_crypto_digest_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrPromise *promise = scr_promise_new(); + ScrDyn *algorithm = argc ? args[0] : scr_dyn_undefined(); + ScrDyn *name = algorithm->kind == SCR_DYN_OBJ ? scr_dyn_obj_read(algorithm, "name", 4) : scr_dyn_retain(algorithm); + ScrStr *text = name ? scr_dyn_string_coerce_js(name) : NULL; + scr_dyn_release(name); + ScrBytes *bytes = argc > 1 && args[1]->kind == SCR_DYN_BYTES ? scr_dyn_bytes_unbox(args[1]) : NULL; + if (!bytes && argc > 1 && scr_array_buffer_is(args[1])) bytes = scr_array_buffer_view(SCR_BYTES_U8, args[1], scr_dyn_undefined(), scr_dyn_undefined()); + const char *native = NULL; + if (text) { + if (text->len == 7 && strncasecmp(text->data, "SHA-256", 7) == 0) native = "sha256"; + else if (text->len == 5 && strncasecmp(text->data, "SHA-1", 5) == 0) native = "sha1"; + } + if (!text || !bytes || !native) { + static const char message[] = "Unsupported WebCrypto digest algorithm or BufferSource"; + if (!scr_exc_pending()) scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "SC2020"); + scr_promise_reject_pending(promise); + } else { + ScrStr *alg = scr_str_new(native, strlen(native)); + ScrCryptoHash *hash = scr_crypto_hash_new(alg); + scr_str_release(alg); + if (hash) { + scr_crypto_hash_update_bytes(hash, bytes); + ScrBytes *digest = scr_exc_pending() ? NULL : scr_crypto_hash_digest_buffer(hash); + scr_crypto_hash_release(hash); + if (digest) { + ScrDyn *buffer = scr_array_buffer_from_bytes(digest); scr_bytes_release(digest); + scr_promise_resolve_dyn(promise, buffer); + } else scr_promise_reject_pending(promise); + } else scr_promise_reject_pending(promise); + } + scr_str_release(text); scr_bytes_release(bytes); + ScrDyn *result = scr_dyn_new_promise(promise); scr_promise_release(promise); return result; +} +ScrDyn *scr_crypto_native(void) { + ScrDyn *subtle = scr_dyn_new_obj(); + scr_dyn_obj_set(subtle, "digest", 6, scr_dyn_new_func(scr_closure_new(NULL, 0), scr_crypto_digest_call, 2, "crypto.subtle.digest", "digest")); + ScrDyn *result = scr_dyn_new_obj(); scr_dyn_obj_set(result, "subtle", 6, subtle); return result; +} diff --git a/packages/runtime/src/scr_bigint.c b/packages/runtime/src/scr_bigint.c index af31c6b94..5451ab782 100644 --- a/packages/runtime/src/scr_bigint.c +++ b/packages/runtime/src/scr_bigint.c @@ -597,6 +597,14 @@ static ScrBigInt *bi_parse_string(ScrStr *s, bool report_error) { ScrBigInt *scr_bigint_parse(ScrStr *s) { return bi_parse_string(s, true); } +double scr_bigint_cmp_string(ScrBigInt *a, ScrStr *b) { + ScrBigInt *parsed = bi_parse_string(b, false); + if (!parsed) return 2; + double result = scr_bigint_cmp_f64(a, parsed); + scr_bigint_release(parsed); + return result; +} + /* Abstract Equality's String/BigInt arm uses StringToBigInt without * surfacing its parse failure: an invalid string compares false instead * of throwing. BigInt(string) keeps the public throwing parser above. */ diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 2bced75fd..373486c70 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -7,6 +7,7 @@ * encoding conversions (utf8 with WHATWG replacement, hex, base64) match * Node byte-for-byte — the differential corpus holds them to it. */ #include "scr_runtime.h" +#include "scr_text_decoder_labels.h" #ifdef SCR_TEXT_DECODER_LEGACY #include "scr_text_decoder_data.h" #endif @@ -16,6 +17,46 @@ #include #include +static bool scr_td_label_space(unsigned char c) { + return c == 9 || c == 10 || c == 12 || c == 13 || c == 32; +} + +double scr_text_decoder_encoding(const ScrDyn *label) { + if (!label || label->kind == SCR_DYN_UNDEF) return -1; + ScrStr *text = scr_dyn_string_coerce_js(label); + if (!text) return -1; + size_t start = 0, end = text->len; + while (start < end && scr_td_label_space((unsigned char)text->data[start])) start++; + while (end > start && scr_td_label_space((unsigned char)text->data[end - 1])) end--; + for (size_t i = 0; i < sizeof scr_td_labels / sizeof scr_td_labels[0]; i++) { + const char *candidate = scr_td_labels[i].label; + if (strlen(candidate) != end - start) continue; + bool matches = true; + for (size_t j = start; j < end; j++) { + unsigned char c = (unsigned char)text->data[j]; + if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + if (c != (unsigned char)candidate[j - start]) { matches = false; break; } + } + if (matches) { + double result = scr_td_labels[i].encoding; + scr_str_release(text); + return result; + } + } + static const char prefix[] = "The \""; + static const char suffix[] = "\" encoding is not supported"; + size_t length = sizeof prefix - 1 + text->len + sizeof suffix - 1; + char *message = malloc(length + 1); + if (!message) scr_trap("scriptc: out of memory\n"); + memcpy(message, prefix, sizeof prefix - 1); + memcpy(message + sizeof prefix - 1, text->data, text->len); + memcpy(message + sizeof prefix - 1 + text->len, suffix, sizeof suffix); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, length, "ERR_ENCODING_NOT_SUPPORTED"); + free(message); + scr_str_release(text); + return -1; +} + #ifdef SCR_RC_AUDIT static SCR_TL long scr_live_bytes = 0; long scr_bytes_live_count(void) { return scr_live_bytes; } @@ -70,6 +111,7 @@ static ScrBytes *scr_bytes_alloc(ScrBytesElem elem, size_t len) { if (!b->data) scr_bytes_oom(); b->backing = NULL; b->is_buffer = false; + b->is_data_view = false; b->external = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; @@ -129,6 +171,7 @@ ScrBytes *scr_bytes_raw_view(ScrBytes *bytes) { ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) { bytes->is_buffer = true; + bytes->is_data_view = false; return scr_bytes_retain(bytes); } @@ -341,6 +384,7 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) { v->data = b->data + s * scr_bytes_elem_size(b->elem); v->backing = scr_bytes_retain(owner); v->is_buffer = b->is_buffer; + v->is_data_view = false; v->external = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; @@ -554,6 +598,7 @@ ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double v->data = owner->data + (size_t)off; v->backing = scr_bytes_retain(owner); v->is_buffer = false; + v->is_data_view = true; v->external = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; @@ -581,6 +626,7 @@ ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem, if (!view) return NULL; view->elem = elem; view->len /= width; + view->is_data_view = false; return view; } @@ -699,7 +745,13 @@ static const char scr_hex_digits[] = "0123456789abcdef"; /* WHATWG UTF-8 decode with U+FFFD replacement per maximal invalid subpart * — what Buffer.prototype.toString("utf8") does. Output re-encodes as * (now valid) UTF-8: worst case 3 bytes per input byte. */ -static ScrStr *scr_bytes_decode_utf8(const uint8_t *in, size_t n) { +static void scr_td_invalid(const char *encoding) { + char message[160]; + int n = snprintf(message, sizeof message, "The encoded data was not valid for encoding %s", encoding); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_ENCODING_INVALID_ENCODED_DATA"); +} + +static ScrStr *scr_bytes_decode_utf8_options(const uint8_t *in, size_t n, bool fatal) { if (in == NULL && n != 0) { scr_trap("scriptc: native callback passed a NULL span with nonzero length\n"); } @@ -713,6 +765,7 @@ static ScrStr *scr_bytes_decode_utf8(const uint8_t *in, size_t n) { for (size_t i = 0; i <= n; i++) { if (i == n) { if (needed > 0) { /* EOF inside a sequence: one replacement */ + if (fatal) { free(out); scr_td_invalid("utf-8"); return NULL; } memcpy(out + o, "\xef\xbf\xbd", 3); o += 3; } @@ -733,6 +786,7 @@ static ScrStr *scr_bytes_decode_utf8(const uint8_t *in, size_t n) { if (byte == 0xf4) upper = 0x8f; needed = 3; cp = byte & 0x7; } else { + if (fatal) { free(out); scr_td_invalid("utf-8"); return NULL; } memcpy(out + o, "\xef\xbf\xbd", 3); o += 3; } @@ -740,6 +794,7 @@ static ScrStr *scr_bytes_decode_utf8(const uint8_t *in, size_t n) { } if (byte < lower || byte > upper) { /* Invalid continuation: replace the subpart, REPROCESS this byte. */ + if (fatal) { free(out); scr_td_invalid("utf-8"); return NULL; } memcpy(out + o, "\xef\xbf\xbd", 3); o += 3; needed = 0; lower = 0x80; upper = 0xbf; @@ -772,6 +827,10 @@ static ScrStr *scr_bytes_decode_utf8(const uint8_t *in, size_t n) { return s; } +static ScrStr *scr_bytes_decode_utf8(const uint8_t *in, size_t n) { + return scr_bytes_decode_utf8_options(in, n, false); +} + ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len) { return scr_bytes_decode_utf8(bytes, len); } @@ -780,14 +839,47 @@ ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len) { * subpart replacement decode as toString("utf8") above, with one * difference — a leading UTF-8 BOM is stripped (ignoreBOM defaults to * false in the spec; Buffer.toString keeps the BOM as U+FEFF). */ -ScrStr *scr_text_decode(const ScrBytes *b) { +ScrStr *scr_text_decode_options(const ScrBytes *b, bool fatal, bool ignore_bom) { const uint8_t *in = b->data; size_t n = b->len; - if (n >= 3 && in[0] == 0xef && in[1] == 0xbb && in[2] == 0xbf) { + if (!ignore_bom && n >= 3 && in[0] == 0xef && in[1] == 0xbb && in[2] == 0xbf) { in += 3; n -= 3; } - return scr_bytes_decode_utf8(in, n); + return scr_bytes_decode_utf8_options(in, n, fatal); +} + +ScrBytes *scr_bytes_buffer_source(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *materialized = scr_dyn_typed_ref_materialize(value); + if (!materialized) return NULL; + ScrBytes *result = scr_bytes_buffer_source(materialized); + scr_dyn_release(materialized); + return result; + } + if (value->kind == SCR_DYN_UNDEF) return scr_bytes_new(SCR_BYTES_U8, 0); + if (value->kind == SCR_DYN_BYTES) { + ScrBytes *bytes = value->v.bytes; + return scr_bytes_buffer_view(bytes, SCR_BYTES_U8, scr_bytes_byte_offset(bytes), true, scr_bytes_byte_len(bytes)); + } + if (scr_array_buffer_is(value)) { + return scr_array_buffer_view(SCR_BYTES_U8, value, scr_dyn_undefined(), scr_dyn_undefined()); + } + static const char message[] = "The input argument must be an instance of SharedArrayBuffer, ArrayBuffer or ArrayBufferView."; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_ARG_TYPE"); + return NULL; +} + +ScrStr *scr_text_decode_buffer_source(const ScrDyn *value) { + ScrBytes *view = scr_bytes_buffer_source(value); + if (!view) return NULL; + ScrStr *out = scr_text_decode(view); + scr_bytes_release(view); + return out; +} + +ScrStr *scr_text_decode(const ScrBytes *b) { + return scr_text_decode_options(b, false, false); } /* ── node:string_decoder, the utf8 StringDecoder ───────────────────────── @@ -1067,11 +1159,12 @@ enum { typedef struct { char *data; size_t len; + bool failed; } ScrTdOut; static ScrTdOut scr_td_out_new(size_t input_len) { if (input_len > (SIZE_MAX - 8) / 3) scr_bytes_oom(); - ScrTdOut out = { malloc(input_len * 3 + 8), 0 }; + ScrTdOut out = { malloc(input_len * 3 + 8), 0, false }; if (!out.data) scr_bytes_oom(); return out; } @@ -1081,39 +1174,42 @@ static void scr_td_put(ScrTdOut *out, uint32_t cp) { } static void scr_td_error(ScrTdOut *out) { + out->failed = true; scr_td_put(out, 0xfffd); } -static ScrStr *scr_td_finish(ScrTdOut *out) { +static ScrStr *scr_td_finish(ScrTdOut *out, bool fatal) { + if (fatal && out->failed) { free(out->data); scr_td_invalid("legacy encoding"); return NULL; } ScrStr *str = scr_str_new(out->data, out->len); free(out->data); return str; } -static ScrStr *scr_td_single_byte(const ScrBytes *b, unsigned encoding) { +static ScrStr *scr_td_single_byte(const ScrBytes *b, unsigned encoding, bool fatal) { ScrTdOut out = scr_td_out_new(b->len); for (size_t i = 0; i < b->len; i++) { uint8_t byte = b->data[i]; - scr_td_put(&out, byte < 0x80 ? byte : scr_td_single[encoding][byte - 0x80]); + uint32_t cp = byte < 0x80 ? byte : scr_td_single[encoding][byte - 0x80]; + if (cp == 0xfffd) scr_td_error(&out); else scr_td_put(&out, cp); } - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } -static ScrStr *scr_td_x_user_defined(const ScrBytes *b) { +static ScrStr *scr_td_x_user_defined(const ScrBytes *b, bool fatal) { ScrTdOut out = scr_td_out_new(b->len); for (size_t i = 0; i < b->len; i++) { uint8_t byte = b->data[i]; scr_td_put(&out, byte < 0x80 ? byte : 0xf780 + byte - 0x80); } - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } -static ScrStr *scr_td_utf16(const ScrBytes *b, bool be) { +static ScrStr *scr_td_utf16(const ScrBytes *b, bool be, bool fatal, bool ignore_bom) { const uint8_t *in = b->data; size_t n = b->len; /* TextDecoder's BOM handling strips only the BOM matching the selected * endian decoder. The opposite BOM decodes to U+FFFE and remains. */ - if (n >= 2 && ((!be && in[0] == 0xff && in[1] == 0xfe) || + if (!ignore_bom && n >= 2 && ((!be && in[0] == 0xff && in[1] == 0xfe) || (be && in[0] == 0xfe && in[1] == 0xff))) { in += 2; n -= 2; @@ -1146,7 +1242,7 @@ static ScrStr *scr_td_utf16(const ScrBytes *b, bool be) { else scr_td_put(&out, cu); } if (i < n) scr_td_error(&out); /* odd trailing byte */ - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } static uint32_t scr_td_gb_range(uint32_t pointer) { @@ -1165,7 +1261,7 @@ static uint32_t scr_td_gb_range(uint32_t pointer) { /* A tiny prepend stack models the Encoding Standard's I/O queue restore. * The gb18030 decoder is the only legacy decoder which can restore three * bytes after discovering a malformed four-byte sequence. */ -static ScrStr *scr_td_gb18030_decode(const ScrBytes *b) { +static ScrStr *scr_td_gb18030_decode(const ScrBytes *b, bool fatal) { ScrTdOut out = scr_td_out_new(b->len); uint8_t first = 0, second = 0, third = 0; uint8_t replay[3]; @@ -1238,10 +1334,10 @@ static ScrStr *scr_td_gb18030_decode(const ScrBytes *b) { else scr_td_error(&out); } if (first || second || third) scr_td_error(&out); - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } -static ScrStr *scr_td_big5_decode(const ScrBytes *b) { +static ScrStr *scr_td_big5_decode(const ScrBytes *b, bool fatal) { ScrTdOut out = scr_td_out_new(b->len); uint8_t lead = 0; for (size_t i = 0; i < b->len; i++) { @@ -1267,10 +1363,10 @@ static ScrStr *scr_td_big5_decode(const ScrBytes *b) { else scr_td_error(&out); } if (lead) scr_td_error(&out); - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } -static ScrStr *scr_td_euc_jp_decode(const ScrBytes *b) { +static ScrStr *scr_td_euc_jp_decode(const ScrBytes *b, bool fatal) { ScrTdOut out = scr_td_out_new(b->len); uint8_t lead = 0; bool jis0212 = false; @@ -1330,10 +1426,10 @@ static ScrStr *scr_td_euc_jp_decode(const ScrBytes *b) { else scr_td_error(&out); } if (lead) scr_td_error(&out); - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } -static ScrStr *scr_td_shift_jis_decode(const ScrBytes *b) { +static ScrStr *scr_td_shift_jis_decode(const ScrBytes *b, bool fatal) { ScrTdOut out = scr_td_out_new(b->len); uint8_t lead = 0; for (size_t i = 0; i < b->len; i++) { @@ -1367,10 +1463,10 @@ static ScrStr *scr_td_shift_jis_decode(const ScrBytes *b) { else scr_td_error(&out); } if (lead) scr_td_error(&out); - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } -static ScrStr *scr_td_euc_kr_decode(const ScrBytes *b) { +static ScrStr *scr_td_euc_kr_decode(const ScrBytes *b, bool fatal) { ScrTdOut out = scr_td_out_new(b->len); uint8_t lead = 0; for (size_t i = 0; i < b->len; i++) { @@ -1393,7 +1489,7 @@ static ScrStr *scr_td_euc_kr_decode(const ScrBytes *b) { else scr_td_error(&out); } if (lead) scr_td_error(&out); - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } enum ScrTdIsoState { @@ -1406,7 +1502,7 @@ enum ScrTdIsoState { SCR_TD_ISO_ESCAPE, }; -static ScrStr *scr_td_iso_2022_jp_decode(const ScrBytes *b) { +static ScrStr *scr_td_iso_2022_jp_decode(const ScrBytes *b, bool fatal) { ScrTdOut out = scr_td_out_new(b->len); enum ScrTdIsoState state = SCR_TD_ISO_ASCII; enum ScrTdIsoState output_state = SCR_TD_ISO_ASCII; @@ -1574,27 +1670,95 @@ static ScrStr *scr_td_iso_2022_jp_decode(const ScrBytes *b) { } } } - return scr_td_finish(&out); + return scr_td_finish(&out, fatal); } -ScrStr *scr_text_decode_legacy(const ScrBytes *b, double encoding_value) { +ScrStr *scr_text_decode_legacy_options(const ScrBytes *b, double encoding_value, bool fatal, bool ignore_bom) { unsigned encoding = (unsigned)encoding_value; - if (encoding < SCR_TD_SINGLE_COUNT) return scr_td_single_byte(b, encoding); + if (encoding < SCR_TD_SINGLE_COUNT) return scr_td_single_byte(b, encoding, fatal); switch (encoding) { - case SCR_TD_X_USER_DEFINED: return scr_td_x_user_defined(b); - case SCR_TD_UTF16LE: return scr_td_utf16(b, false); - case SCR_TD_UTF16BE: return scr_td_utf16(b, true); - case SCR_TD_GB18030: return scr_td_gb18030_decode(b); - case SCR_TD_BIG5: return scr_td_big5_decode(b); - case SCR_TD_EUC_JP: return scr_td_euc_jp_decode(b); - case SCR_TD_ISO_2022_JP: return scr_td_iso_2022_jp_decode(b); - case SCR_TD_SHIFT_JIS: return scr_td_shift_jis_decode(b); - case SCR_TD_EUC_KR: return scr_td_euc_kr_decode(b); + case SCR_TD_X_USER_DEFINED: return scr_td_x_user_defined(b, fatal); + case SCR_TD_UTF16LE: return scr_td_utf16(b, false, fatal, ignore_bom); + case SCR_TD_UTF16BE: return scr_td_utf16(b, true, fatal, ignore_bom); + case SCR_TD_GB18030: return scr_td_gb18030_decode(b, fatal); + case SCR_TD_BIG5: return scr_td_big5_decode(b, fatal); + case SCR_TD_EUC_JP: return scr_td_euc_jp_decode(b, fatal); + case SCR_TD_ISO_2022_JP: return scr_td_iso_2022_jp_decode(b, fatal); + case SCR_TD_SHIFT_JIS: return scr_td_shift_jis_decode(b, fatal); + case SCR_TD_EUC_KR: return scr_td_euc_kr_decode(b, fatal); default: return scr_str_new("", 0); /* compiler invariant */ } } +ScrStr *scr_text_decode_legacy(const ScrBytes *b, double encoding_value) { + return scr_text_decode_legacy_options(b, encoding_value, false, false); +} #endif /* SCR_TEXT_DECODER_LEGACY */ +/* Streaming decoders retain only an incomplete character and whether the + * initial BOM has been handled. The state object belongs to the codec's + * private record, so aliases and closure captures share the same decoder. */ +static size_t scr_td_utf8_tail(const uint8_t *bytes, size_t n) { + size_t tail = scr_strdec_tail(bytes, n); + if (!tail) return 0; + uint8_t lead = bytes[n - tail]; + if (lead < 0xc2 || lead > 0xf4) return 0; + if (tail > 1) { + uint8_t second = bytes[n - tail + 1]; + if ((lead == 0xe0 && second < 0xa0) || (lead == 0xed && second > 0x9f) || + (lead == 0xf0 && second < 0x90) || (lead == 0xf4 && second > 0x8f)) return 0; + } + return tail; +} + +ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *input, + double encoding, bool fatal, bool ignore_bom, bool stream) { + ScrDyn *saved = scr_dyn_obj_read(state, "pending", 7); + size_t pending = saved->kind == SCR_DYN_BYTES ? saved->v.bytes->len : 0; + if (input->len > SIZE_MAX - pending) scr_bytes_oom(); + ScrBytes *combined = scr_bytes_new(SCR_BYTES_U8, (double)(pending + input->len)); + if (pending) memcpy(combined->data, saved->v.bytes->data, pending); + if (input->len) memcpy(combined->data + pending, input->data, input->len); + scr_dyn_release(saved); + ScrDyn *bom = scr_dyn_obj_read(state, "bom", 3); + bool seen_bom = scr_dyn_truthy(bom); + scr_dyn_release(bom); + size_t hold = stream && encoding < 0 ? scr_td_utf8_tail(combined->data, combined->len) : 0; +#ifdef SCR_TEXT_DECODER_LEGACY + if (stream && ((unsigned)encoding == SCR_TD_UTF16LE || (unsigned)encoding == SCR_TD_UTF16BE)) { + size_t complete = combined->len & ~(size_t)1; + hold = combined->len - complete; + if (complete >= 2) { + const uint8_t *last = combined->data + complete - 2; + uint16_t unit = (unsigned)encoding == SCR_TD_UTF16BE + ? ((uint16_t)last[0] << 8) | last[1] : last[0] | ((uint16_t)last[1] << 8); + if (unit >= 0xd800 && unit <= 0xdbff) hold += 2; + } + } + if (stream && encoding >= SCR_TD_GB18030) { + scr_bytes_release(combined); + static const char message[] = "Streaming TextDecoder for this legacy encoding has no native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } +#endif + size_t complete = combined->len - hold; + ScrBytes *tail = scr_bytes_new(SCR_BYTES_U8, (double)hold); + if (hold) memcpy(tail->data, combined->data + complete, hold); + scr_dyn_obj_set(state, "pending", 7, scr_dyn_new_bytes(tail)); + scr_bytes_release(tail); + scr_dyn_obj_set(state, "bom", 3, scr_dyn_new_bool(stream && (seen_bom || complete > 0))); + combined->len = complete; + ScrStr *result; + if (encoding < 0) result = scr_text_decode_options(combined, fatal, ignore_bom || seen_bom); +#ifdef SCR_TEXT_DECODER_LEGACY + else result = scr_text_decode_legacy_options(combined, encoding, fatal, ignore_bom || seen_bom); +#else + else { result = NULL; scr_trap("legacy TextDecoder requires its runtime pack variant"); } +#endif + scr_bytes_release(combined); + return result; +} + /* Decode the next code point from an ScrStr's storage — ALWAYS valid * UTF-8 (the string runtime never stores ill-formed sequences), so no * validation re-runs here. */ diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 5944f59c5..d5c804dbc 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -12,6 +12,17 @@ #include #include #include +#include +#include +#include +#include +#include +#ifndef O_SYNC +#define O_SYNC 0 /* Windows CRT opens have no separate synchronous flag. */ +#endif +#if !defined(_WIN32) && !defined(__wasi__) +#include +#endif #ifdef __APPLE__ #include /* lchmod(2) — the fs.lchmodSync ladder's real tail */ #endif @@ -140,6 +151,548 @@ ScrPromise *scr_fsp_read_file_bytes(ScrStr *path) { return scr_promise_settled_ref(b, &scr_bytes_retain_v, &scr_bytes_release_v, NULL); } +/* Native error-first callbacks remain callable values when a platform layer + * captures them. Argument failures throw at the call; filesystem failures + * arrive through the callback on the next event-loop turn. The synchronous + * filesystem spine does the work, as it does for our promise adapters. */ +static bool scr_fs_dyn_absent(const ScrDyn *value); +static bool scr_fs_cb_chk(const ScrDyn *callback, const char *name); +static bool scr_fs_path_chk(const ScrDyn *value, const char *name); +static ScrDyn *scr_fs_cb_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +static ScrDyn *scr_fs_cb_option(const ScrDyn *options, const char *name) { + if (scr_fs_dyn_absent(options)) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *object = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : scr_dyn_retain((ScrDyn *)options); + if (!object) return NULL; + ScrDyn *value = scr_dyn_obj_read(object, name, strlen(name)); + scr_dyn_release(object); + return value; +} + +static double scr_fs_cb_number(const ScrDyn *value, const char *name, double fallback) { + if (scr_fs_dyn_absent(value)) return fallback; + if (value->kind == SCR_DYN_BIGINT) return scr_bigint_to_f64(value->v.bigint); + if (value->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail(name, "of type number", value); + return fallback; + } + return value->v.num; +} + +static double scr_fs_cb_option_number(const ScrDyn *options, const char *name, double fallback) { + ScrDyn *value = scr_fs_cb_option(options, name); + if (!value) return fallback; + double number = scr_fs_cb_number(value, name, fallback); + scr_dyn_release(value); + return number; +} + +static bool scr_fs_cb_option_bool(const ScrDyn *options, const char *name) { + ScrDyn *value = scr_fs_cb_option(options, name); + bool answer = value && scr_dyn_truthy(value); + scr_dyn_release(value); + return answer; +} + +static bool scr_fs_cb_refuse_option(const ScrDyn *options, const char *name) { + ScrDyn *value = scr_fs_cb_option(options, name); + bool present = value && value->kind != SCR_DYN_UNDEF && value->kind != SCR_DYN_NULL; + scr_dyn_release(value); + if (present) { + char message[160]; + int length = snprintf(message, sizeof message, "Filesystem callback option '%s' has no native lowering", name); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020"); + } + return present; +} + +static bool scr_fs_cb_signal(const ScrDyn *options) { + ScrDyn *signal = scr_fs_cb_option(options, "signal"); + if (!signal) return false; + if (scr_fs_dyn_absent(signal)) { scr_dyn_release(signal); return true; } + if (signal->kind != SCR_DYN_HANDLE || signal->v.handle.tag != SCR_DYNH_ABORT_SIGNAL) { + scr_dyn_arg_type_fail("options.signal", "an instance of AbortSignal", signal); + scr_dyn_release(signal); + return false; + } + ScrStr *key = scr_str_new("aborted", 7); + ScrDyn *aborted = scr_dyn_handle_key_get(signal, key); + scr_str_release(key); + bool cancelled = aborted && scr_dyn_truthy(aborted); + scr_dyn_release(aborted); scr_dyn_release(signal); + if (cancelled) { + static const char message[] = "The operation was aborted"; + ScrStr *text = scr_str_new(message, sizeof message - 1); + ScrError *error = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_str_release(error->name); + error->name = scr_str_new("AbortError", 10); + error->name_present = true; + scr_error_set_code(error, "ABORT_ERR"); + scr_throw_obj(error, scr_error_retain_v, scr_error_release_v, scr_error_trace_arg()); + } + return !scr_exc_pending(); +} + +static double scr_fs_cb_open(const ScrStr *path, const ScrDyn *flag, double mode, const char *fallback) { + const char *f = flag->kind == SCR_DYN_UNDEF ? fallback : flag->kind == SCR_DYN_STR ? flag->v.str->data : NULL; + if (!f) { + static const char message[] = "Runtime numeric filesystem flags have no portable native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return -1; + } + int bits; + if (!strcmp(f, "r")) bits = O_RDONLY; + else if (!strcmp(f, "r+")) bits = O_RDWR; + else if (!strcmp(f, "rs") || !strcmp(f, "sr")) bits = O_RDONLY | O_SYNC; + else if (!strcmp(f, "rs+") || !strcmp(f, "sr+")) bits = O_RDWR | O_SYNC; + else if (!strcmp(f, "w")) bits = O_WRONLY | O_CREAT | O_TRUNC; + else if (!strcmp(f, "w+")) bits = O_RDWR | O_CREAT | O_TRUNC; + else if (!strcmp(f, "wx") || !strcmp(f, "xw")) bits = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL; + else if (!strcmp(f, "wx+") || !strcmp(f, "xw+")) bits = O_RDWR | O_CREAT | O_TRUNC | O_EXCL; + else if (!strcmp(f, "a")) bits = O_WRONLY | O_CREAT | O_APPEND; + else if (!strcmp(f, "a+")) bits = O_RDWR | O_CREAT | O_APPEND; + else if (!strcmp(f, "ax") || !strcmp(f, "xa")) bits = O_WRONLY | O_CREAT | O_APPEND | O_EXCL; + else if (!strcmp(f, "ax+") || !strcmp(f, "xa+")) bits = O_RDWR | O_CREAT | O_APPEND | O_EXCL; + else if (!strcmp(f, "as") || !strcmp(f, "sa")) bits = O_WRONLY | O_CREAT | O_APPEND | O_SYNC; + else if (!strcmp(f, "as+") || !strcmp(f, "sa+")) bits = O_RDWR | O_CREAT | O_APPEND | O_SYNC; + else { + char message[160]; + int length = snprintf(message, sizeof message, "The argument 'flags' is invalid. Received '%s'", f); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)length, "ERR_INVALID_ARG_VALUE"); + return -1; + } + if (scr_exc_pending()) return -1; + int fd = open(path->data, bits, (mode_t)mode); + if (fd < 0) scr_fs_throw(errno, "open", path); + return fd; +} + +static ScrStr *scr_fs_cb_path(const ScrDyn *value, const char *name) { + if (!scr_fs_path_chk(value, name)) return NULL; + ScrStr *path = value->kind == SCR_DYN_STR ? scr_str_retain(value->v.str) + : scr_str_new((const char *)value->v.bytes->data, value->v.bytes->len); + if (memchr(path->data, 0, path->len)) { + scr_dyn_arg_value_fail(name, "must be a string, Uint8Array, or URL without null bytes", value); + scr_str_release(path); + return NULL; + } + return path; +} + +static ScrDyn *scr_fs_cb_string(ScrStr *text) { + if (!text) return NULL; + ScrDyn *value = scr_dyn_new_str(text); + scr_str_release(text); + return value; +} + +static ScrStr *scr_fs_cb_join(const ScrStr *directory, const char *name) { + ScrStr *suffix = scr_str_new("/", 1); + ScrStr *base = scr_str_concat((ScrStr *)directory, suffix); + scr_str_release(suffix); + suffix = scr_str_new(name, strlen(name)); + ScrStr *path = scr_str_concat(base, suffix); + scr_str_release(base); scr_str_release(suffix); + return path; +} + +static void scr_fs_cb_copy(const ScrStr *source, const ScrStr *destination, bool recursive, bool force, bool preserve) { + struct stat status; + if ( +#ifdef _WIN32 + stat(source->data, &status) +#else + lstat(source->data, &status) +#endif + < 0) { scr_fs_throw(errno, "stat", source); return; } +#ifndef _WIN32 + if (S_ISLNK(status.st_mode)) { + const char *message = "Symbolic-link cp entries have no native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); + return; + } +#endif + if (S_ISDIR(status.st_mode)) { + if (!recursive) { scr_fs_throw(EISDIR, "cp", source); return; } + scr_fs_mkdir_recursive_mode((ScrStr *)destination, status.st_mode & 0777); + if (scr_exc_pending()) return; + DIR *directory = opendir(source->data); + if (!directory) { scr_fs_throw(errno, "opendir", source); return; } + struct dirent *entry; + while (!scr_exc_pending() && (entry = readdir(directory))) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; + ScrStr *from = scr_fs_cb_join(source, entry->d_name), *to = scr_fs_cb_join(destination, entry->d_name); + scr_fs_cb_copy(from, to, true, force, preserve); + scr_str_release(from); scr_str_release(to); + } + closedir(directory); + } else { + if (!force && scr_fs_exists((ScrStr *)destination)) return; + scr_fs_copyfile((ScrStr *)source, (ScrStr *)destination); + } + if (preserve && !scr_exc_pending()) { + struct utimbuf times = { status.st_atime, status.st_mtime }; + if (utime(destination->data, ×) < 0) scr_fs_throw(errno, "utime", destination); + } +} + +static ScrDyn *scr_fs_cb_stat_test(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; (void)argc; + return scr_dyn_new_bool(scr_box_get_bool(closure->caps[0])); +} + +static void scr_fs_cb_stat_method(ScrDyn *object, const char *name, bool answer) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_BOOL); + scr_box_set_bool(closure->caps[0], answer); + scr_dyn_obj_set(object, name, strlen(name), scr_dyn_new_func(closure, scr_fs_cb_stat_test, 0, "native:fs.Stats", name)); +} + +static void scr_fs_cb_stat_number(ScrDyn *object, const char *name, int64_t number, bool bigint) { + ScrDyn *value; + if (bigint) { + ScrBigInt *integer = scr_bigint_from_i64(number); + value = scr_dyn_new_bigint(integer); + scr_bigint_release(integer); + } else value = scr_dyn_new_num((double)number); + scr_dyn_obj_set(object, name, strlen(name), value); +} + +static void scr_fs_cb_stat_date(ScrDyn *object, const char *name, double milliseconds) { + ScrDyn *arguments = scr_dyn_new_arr(); + scr_dyn_arr_push(arguments, scr_dyn_new_num(milliseconds)); + ScrDyn *date = scr_dyn_native_date_new(arguments); + scr_dyn_release(arguments); + if (date) scr_dyn_obj_set(object, name, strlen(name), date); +} + +static ScrDyn *scr_fs_cb_stats(const ScrStr *path, double fd, const ScrDyn *options, bool nofollow) { + struct stat status; +#ifdef _WIN32 + (void)nofollow; + int rc = path ? stat(path->data, &status) : fstat((int)fd, &status); +#else + int rc = path ? (nofollow ? lstat(path->data, &status) : stat(path->data, &status)) : fstat((int)fd, &status); +#endif + if (rc < 0) { scr_fs_throw(errno, path ? (nofollow ? "lstat" : "stat") : "fstat", path); return NULL; } + bool bigint = scr_fs_cb_option_bool(options, "bigint"); + ScrDyn *object = scr_dyn_new_obj(); +#define SCR_FS_STAT_FIELD(field) scr_fs_cb_stat_number(object, #field, (int64_t)status.st_##field, bigint) + SCR_FS_STAT_FIELD(dev); SCR_FS_STAT_FIELD(ino); SCR_FS_STAT_FIELD(mode); SCR_FS_STAT_FIELD(nlink); + SCR_FS_STAT_FIELD(uid); SCR_FS_STAT_FIELD(gid); SCR_FS_STAT_FIELD(rdev); SCR_FS_STAT_FIELD(size); +#ifndef _WIN32 + SCR_FS_STAT_FIELD(blksize); SCR_FS_STAT_FIELD(blocks); +#endif +#undef SCR_FS_STAT_FIELD + double atime = (double)status.st_atime * 1000, mtime = (double)status.st_mtime * 1000, ctime = (double)status.st_ctime * 1000; + double birthtime = 0; +#ifdef __APPLE__ + atime += status.st_atimespec.tv_nsec / 1000000.0; mtime += status.st_mtimespec.tv_nsec / 1000000.0; + ctime += status.st_ctimespec.tv_nsec / 1000000.0; + birthtime = (double)status.st_birthtimespec.tv_sec * 1000 + status.st_birthtimespec.tv_nsec / 1000000.0; +#elif !defined(_WIN32) && !defined(__wasi__) + atime += status.st_atim.tv_nsec / 1000000.0; mtime += status.st_mtim.tv_nsec / 1000000.0; + ctime += status.st_ctim.tv_nsec / 1000000.0; +#endif + scr_fs_cb_stat_date(object, "atime", atime); scr_fs_cb_stat_date(object, "mtime", mtime); + scr_fs_cb_stat_date(object, "ctime", ctime); scr_fs_cb_stat_date(object, "birthtime", birthtime); + if (bigint) { + scr_fs_cb_stat_number(object, "atimeMs", (int64_t)atime, true); + scr_fs_cb_stat_number(object, "mtimeMs", (int64_t)mtime, true); + scr_fs_cb_stat_number(object, "ctimeMs", (int64_t)ctime, true); + scr_fs_cb_stat_number(object, "birthtimeMs", (int64_t)birthtime, true); + } else { + scr_dyn_obj_set(object, "atimeMs", 7, scr_dyn_new_num(atime)); + scr_dyn_obj_set(object, "mtimeMs", 7, scr_dyn_new_num(mtime)); + scr_dyn_obj_set(object, "ctimeMs", 7, scr_dyn_new_num(ctime)); + scr_dyn_obj_set(object, "birthtimeMs", 11, scr_dyn_new_num(birthtime)); + } + scr_fs_cb_stat_method(object, "isFile", S_ISREG(status.st_mode)); + scr_fs_cb_stat_method(object, "isDirectory", S_ISDIR(status.st_mode)); +#ifndef _WIN32 + scr_fs_cb_stat_method(object, "isSymbolicLink", S_ISLNK(status.st_mode)); + scr_fs_cb_stat_method(object, "isBlockDevice", S_ISBLK(status.st_mode)); + scr_fs_cb_stat_method(object, "isCharacterDevice", S_ISCHR(status.st_mode)); + scr_fs_cb_stat_method(object, "isFIFO", S_ISFIFO(status.st_mode)); + scr_fs_cb_stat_method(object, "isSocket", S_ISSOCK(status.st_mode)); +#else + scr_fs_cb_stat_method(object, "isSymbolicLink", false); + scr_fs_cb_stat_method(object, "isBlockDevice", false); + scr_fs_cb_stat_method(object, "isCharacterDevice", S_ISCHR(status.st_mode)); + scr_fs_cb_stat_method(object, "isFIFO", false); scr_fs_cb_stat_method(object, "isSocket", false); +#endif + return object; +} + +#ifndef SCR_LIB +static void scr_fs_cb_fire(ScrClosure *closure) { + ScrDyn *callback = scr_box_get_ref(closure->caps[0]); + ScrDyn *arguments = scr_box_get_ref(closure->caps[1]); + ScrDyn *result = scr_dyn_apply(callback, arguments, "the filesystem callback"); + scr_dyn_release(result); scr_dyn_release(callback); scr_dyn_release(arguments); +} +#endif + +static void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer) { + ScrDyn *arguments = scr_dyn_new_arr(); + if (scr_exc_pending()) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *error = scr_caught_to_dyn(caught); + scr_caught_release(caught); + scr_dyn_arr_push(arguments, error); + scr_dyn_release(result); + } else { + scr_dyn_arr_push(arguments, scr_dyn_new_null()); + if (result) scr_dyn_arr_push(arguments, result); + if (buffer) scr_dyn_arr_push(arguments, scr_dyn_retain((ScrDyn *)buffer)); + } +#ifndef SCR_LIB + ScrClosure *closure = scr_closure_new((void *)scr_fs_cb_fire, 2); + closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + closure->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(closure->caps[0], scr_dyn_retain((ScrDyn *)callback)); + scr_box_set_ref(closure->caps[1], arguments); + scr_set_timeout(closure, 0); +#else + (void)callback; + scr_dyn_release(arguments); + scr_throw_error_msg_code(SCR_ERR_ERROR, "Filesystem callbacks require an executable event loop", 52, "SC4005"); +#endif +} + +static bool scr_fs_read_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *); +static bool scr_fs_encoding_chk(const ScrDyn *); + +static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc) { + const char *op = member->data; + bool fileCallback = !strcmp(op, "readFile") || !strcmp(op, "mkdtemp"); + const ScrDyn *callback = argc >= (fileCallback ? 2u : 1u) ? args[argc - 1] : scr_dyn_undefined(); + if (!scr_fs_cb_chk(callback, fileCallback ? "cb" : "callback")) return NULL; + size_t count = argc - 1; +#define ARG(index) scr_fs_cb_arg(args, count, index) + if (!strcmp(op, "readFile") && !scr_fs_encoding_chk(ARG(1))) return NULL; + if (!strcmp(op, "read") && count >= 4 && !scr_fs_read_validate(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4))) return NULL; + bool descriptor = !strcmp(op, "close") || !strcmp(op, "fstat") || !strcmp(op, "ftruncate") || !strcmp(op, "fsync") || !strcmp(op, "read") || !strcmp(op, "write"); + ScrStr *path = descriptor ? NULL : scr_fs_cb_path(ARG(0), !strcmp(op, "mkdtemp") ? "prefix" : "path"); + if (!descriptor && !path) return NULL; + double fd = descriptor ? scr_fs_cb_number(ARG(0), "fd", -1) : -1; + if (scr_exc_pending()) { scr_str_release(path); return NULL; } + ScrDyn *result = NULL; + const ScrDyn *buffer = NULL; + if (!strcmp(op, "readFile")) { + const ScrDyn *options = ARG(1); + if (options->kind != SCR_DYN_STR && (!scr_fs_cb_signal(options) || scr_fs_cb_refuse_option(options, "flag"))) goto done; + ScrDyn *encoding = options->kind == SCR_DYN_STR ? scr_dyn_retain((ScrDyn *)options) : scr_fs_cb_option(options, "encoding"); + if (encoding) result = scr_fs_read_file_sync_dyn(path, encoding); + scr_dyn_release(encoding); + } else if (!strcmp(op, "writeFile") || !strcmp(op, "appendFile")) { + const ScrDyn *options = ARG(2); + if (options->kind != SCR_DYN_STR && !scr_fs_cb_signal(options)) goto done; + ScrDyn *encoding = options->kind == SCR_DYN_STR ? scr_dyn_retain((ScrDyn *)options) : scr_fs_cb_option(options, "encoding"); + ScrStr *codec = encoding && encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : scr_str_new("utf8", 4); + ScrBytes *data = encoding ? scr_buffer_from_dyn(ARG(1), codec) : NULL; + scr_str_release(codec); + scr_dyn_release(encoding); + if (data) { + ScrDyn *flag = scr_fs_cb_option(options->kind == SCR_DYN_STR ? scr_dyn_undefined() : options, "flag"); + double mode = scr_fs_cb_option_number(options->kind == SCR_DYN_STR ? scr_dyn_undefined() : options, "mode", 0666); + double fd = flag ? scr_fs_cb_open(path, flag, mode, !strcmp(op, "appendFile") ? "a" : "w") : -1; + scr_dyn_release(flag); + if (fd >= 0) { + size_t offset = 0; + while (offset < data->len && !scr_exc_pending()) { + double written = scr_fs_write_sync(fd, data, (double)offset, (double)(data->len - offset), -1); + if (written <= 0) break; + offset += (size_t)written; + } + int saved = errno; + close((int)fd); + errno = saved; + } + scr_bytes_release(data); + } + } else if (!strcmp(op, "access")) scr_fs_access(path, scr_fs_cb_number(ARG(1), "mode", 0)); + else if (!strcmp(op, "mkdir")) { + const ScrDyn *options = ARG(1); + double mode = options->kind == SCR_DYN_NUM ? options->v.num : scr_fs_cb_option_number(options, "mode", 0777); + if (scr_fs_cb_option_bool(options->kind == SCR_DYN_NUM ? scr_dyn_undefined() : options, "recursive")) scr_fs_mkdir_recursive_mode(path, mode); + else scr_fs_mkdir_mode(path, mode); + } else if (!strcmp(op, "mkdtemp")) result = scr_fs_cb_string(scr_fs_mkdtemp(path)); + else if (!strcmp(op, "rm")) scr_fs_rm_opts_retry(path, scr_fs_cb_option_bool(ARG(1), "recursive"), scr_fs_cb_option_bool(ARG(1), "force"), scr_fs_cb_option_number(ARG(1), "maxRetries", 0), scr_fs_cb_option_number(ARG(1), "retryDelay", 100)); + else if (!strcmp(op, "rmdir")) scr_fs_rmdir(path); + else if (!strcmp(op, "unlink")) scr_fs_unlink(path); + else if (!strcmp(op, "chmod")) scr_fs_chmod(path, scr_fs_cb_number(ARG(1), "mode", 0)); + else if (!strcmp(op, "chown")) scr_fs_chown(path, scr_fs_cb_number(ARG(1), "uid", 0), scr_fs_cb_number(ARG(2), "gid", 0)); + else if (!strcmp(op, "realpath")) result = scr_fs_cb_string(scr_fs_realpath(path)); + else if (!strcmp(op, "stat") || !strcmp(op, "lstat")) result = scr_fs_cb_stats(path, -1, ARG(1), !strcmp(op, "lstat")); + else if (!strcmp(op, "fstat")) result = scr_fs_cb_stats(NULL, fd, ARG(1), false); + else if (!strcmp(op, "close")) scr_fs_close(fd); + else if (!strcmp(op, "fsync")) scr_fs_fsync(fd); + else if (!strcmp(op, "open")) { + double opened = scr_fs_cb_open(path, ARG(1), scr_fs_cb_number(ARG(2), "mode", 0666), "r"); + if (!scr_exc_pending()) result = scr_dyn_new_num(opened); + } else if (!strcmp(op, "cp")) { + if (scr_fs_cb_refuse_option(ARG(2), "filter") || scr_fs_cb_refuse_option(ARG(2), "dereference") || scr_fs_cb_refuse_option(ARG(2), "verbatimSymlinks") || scr_fs_cb_refuse_option(ARG(2), "errorOnExist") || scr_fs_cb_refuse_option(ARG(2), "mode")) goto done; + ScrStr *destination = scr_fs_cb_path(ARG(1), "dest"); + if (destination) { + ScrDyn *force = scr_fs_cb_option(ARG(2), "force"); + scr_fs_cb_copy(path, destination, scr_fs_cb_option_bool(ARG(2), "recursive"), + force && (force->kind == SCR_DYN_UNDEF || scr_dyn_truthy(force)), scr_fs_cb_option_bool(ARG(2), "preserveTimestamps")); + scr_dyn_release(force); scr_str_release(destination); + } + } else if (!strcmp(op, "glob")) { +#if !defined(_WIN32) && !defined(__wasi__) + if (strstr(path->data, "**") || scr_fs_cb_option_bool(ARG(1), "withFileTypes")) { + const char *message = "Recursive glob patterns and Dirent results have no native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); + goto done; + } + ScrDyn *cwd = scr_fs_cb_option(ARG(1), "cwd"); + ScrDyn *exclude = scr_fs_cb_option(ARG(1), "exclude"); + if (exclude && exclude->kind != SCR_DYN_UNDEF) { + const char *message = "fs.glob exclude options have no native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); + } else { + ScrStr *pattern = cwd && cwd->kind == SCR_DYN_STR ? scr_fs_cb_join(cwd->v.str, path->data) : scr_str_retain(path); + glob_t matches = {0}; + int rc = glob(pattern->data, 0, NULL, &matches); + if (rc && rc != GLOB_NOMATCH) scr_fs_throw(rc == GLOB_NOSPACE ? ENOMEM : EIO, "glob", path); + else { + result = scr_dyn_new_arr(); + size_t prefix = cwd && cwd->kind == SCR_DYN_STR ? cwd->v.str->len + 1 : 0; + for (size_t i = 0; i < matches.gl_pathc; i++) { + const char *name = matches.gl_pathv[i] + prefix; + scr_dyn_arr_push(result, scr_fs_cb_string(scr_str_new(name, strlen(name)))); + } + } + globfree(&matches); scr_str_release(pattern); + } + scr_dyn_release(cwd); scr_dyn_release(exclude); +#else + scr_fs_throw(ENOSYS, "glob", path); +#endif + } else if (!strcmp(op, "read") || !strcmp(op, "write")) { + bool readOp = !strcmp(op, "read"); + const ScrDyn *options = ARG(1); + ScrDyn *ownedBuffer = NULL; + if (readOp && !scr_dyn_bytes_is(options, SCR_BYTES_U8)) { ownedBuffer = scr_fs_cb_option(options, "buffer"); buffer = ownedBuffer; } + else buffer = options; + ScrBytes *bytes = buffer && buffer->kind == SCR_DYN_BYTES ? scr_dyn_bytes_unbox(buffer) : NULL; + if (!bytes) scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer ? buffer : scr_dyn_undefined()); + else { + double offset = ownedBuffer ? scr_fs_cb_option_number(options, "offset", 0) : scr_fs_cb_number(ARG(2), "offset", 0); + double length = ownedBuffer ? scr_fs_cb_option_number(options, "length", bytes->len - offset) : scr_fs_cb_number(ARG(3), "length", bytes->len - offset); + double position = ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) : scr_fs_cb_number(ARG(4), "position", -1); + if (!scr_exc_pending()) result = scr_dyn_new_num(readOp ? scr_fs_read_sync(fd, bytes, offset, length, position) : scr_fs_write_sync(fd, bytes, offset, length, position)); + } + scr_bytes_release(bytes); + /* The completion retains the original buffer, including options.buffer. */ + if (ownedBuffer) { + scr_fs_cb_schedule(callback, result, ownedBuffer); + scr_dyn_release(ownedBuffer); scr_str_release(path); + return scr_dyn_retain(scr_dyn_undefined()); + } + } else if (!strcmp(op, "copyFile") || !strcmp(op, "rename") || !strcmp(op, "link") || !strcmp(op, "symlink")) { + ScrStr *destination = scr_fs_cb_path(ARG(1), "dest"); + if (destination) { + if (!strcmp(op, "rename")) scr_fs_rename(path, destination); + else if (!strcmp(op, "link")) scr_fs_link(path, destination); + else if (!strcmp(op, "copyFile")) { + double flags = scr_fs_cb_number(ARG(2), "mode", 0); + if (flags == 1 && scr_fs_exists(destination)) scr_fs_throw(EEXIST, "copyfile", destination); + else if (flags != 0 && flags != 1) { + static const char message[] = "fs.copyFile clone flags have no native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } else if (!scr_exc_pending()) scr_fs_copyfile(path, destination); + } + else { +#if !defined(_WIN32) + if (symlink(path->data, destination->data) < 0) scr_fs_throw(errno, "symlink", path); +#else + scr_fs_throw(ENOSYS, "symlink", path); +#endif + } + scr_str_release(destination); + } + } else if (!strcmp(op, "truncate") || !strcmp(op, "ftruncate")) { + double length = scr_fs_cb_number(ARG(1), "len", 0); + if (!scr_exc_pending()) { + int rc = descriptor ? ftruncate((int)fd, (off_t)length) : truncate(path->data, (off_t)length); + if (rc < 0) scr_fs_throw(errno, descriptor ? "ftruncate" : "truncate", path); + } + } else if (!strcmp(op, "readlink")) { +#ifndef _WIN32 + size_t capacity = 256; + char *text = NULL; + for (;;) { + char *grown = realloc(text, capacity); + if (!grown) scr_bytes_io_oom(); + text = grown; + ssize_t size = readlink(path->data, text, capacity); + if (size < 0) { scr_fs_throw(errno, "readlink", path); break; } + if ((size_t)size < capacity) { result = scr_fs_cb_string(scr_str_new(text, (size_t)size)); break; } + capacity *= 2; + } + free(text); +#else + scr_fs_throw(ENOSYS, "readlink", path); +#endif + } else if (!strcmp(op, "utimes")) { + double access = scr_fs_to_unix_timestamp(ARG(1)), modified = scr_fs_to_unix_timestamp(ARG(2)); +#if !defined(_WIN32) + struct timespec times[2] = { + { (time_t)floor(access), (long)((access - floor(access)) * 1000000000) }, + { (time_t)floor(modified), (long)((modified - floor(modified)) * 1000000000) }, + }; + if (!scr_exc_pending() && utimensat(AT_FDCWD, path->data, times, 0) < 0) scr_fs_throw(errno, "utime", path); +#else + struct utimbuf times = { (time_t)access, (time_t)modified }; + if (!scr_exc_pending() && utime(path->data, ×) < 0) scr_fs_throw(errno, "utime", path); +#endif + } else if (!strcmp(op, "readdir")) { + if (scr_fs_cb_refuse_option(ARG(1), "encoding") || scr_fs_cb_option_bool(ARG(1), "withFileTypes") || scr_fs_cb_option_bool(ARG(1), "recursive")) { + if (!scr_exc_pending()) { const char *message = "Recursive readdir and Dirent results have no native lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); } + goto done; + } + ScrArr *names = scr_fs_readdir(path); + if (names) { + result = scr_dyn_new_arr(); + for (size_t i = 0; i < (size_t)scr_arr_len(names); i++) scr_dyn_arr_push(result, scr_fs_cb_string(scr_arr_get_ref(names, (double)i))); + scr_arr_release(names); + } + } else { + char message[160]; + int length = snprintf(message, sizeof message, "Native filesystem callback fs.%s with these options has no lowering", op); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020"); + } +#undef ARG +done: + scr_str_release(path); + scr_fs_cb_schedule(callback, result, buffer); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *scr_fs_cb_value_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrStr *member = scr_box_get_ref(closure->caps[0]); + ScrDyn *result = scr_fs_cb_invoke(member, args, argc); + scr_str_release(member); + return result; +} + +ScrDyn *scr_fs_callback_value(ScrStr *member) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_STR); + scr_box_set_ref(closure->caps[0], scr_str_retain(member)); + return scr_dyn_new_func(closure, scr_fs_cb_value_call, 0, "native:fs.callback", "filesystemCallback"); +} + +ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args) { + return scr_fs_cb_invoke(member, args->v.arr.items, args->v.arr.len); +} + /* ── crypto.randomBytes → a real Buffer ────────────────────────────────── */ ScrBytes *scr_crypto_random_bytes(double n) { @@ -383,6 +936,12 @@ static bool scr_fs_path_chk(const ScrDyn *p, const char *name) { * options record's `encoding` member): Node throws ERR_INVALID_ARG_VALUE * for any truthy value Buffer.isEncoding rejects. */ static bool scr_fs_encoding_chk(const ScrDyn *opts) { + if (opts->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(opts); + bool valid = view && !scr_exc_pending() && scr_fs_encoding_chk(view); + scr_dyn_release(view); + return valid; + } const ScrDyn *enc = opts; if (opts->kind == SCR_DYN_OBJ) { enc = scr_dyn_obj_get(opts, "encoding", 8); @@ -670,15 +1229,15 @@ static bool scr_fs_int_range_chk(const ScrDyn *v, const char *name, double min, return true; } -void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, - const ScrDyn *length, const ScrDyn *position, const ScrStr *fence) { +static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position) { if (buffer->kind != SCR_DYN_BYTES) { scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); - return; + return false; } if (fd->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("fd", "of type number", fd); - return; + return false; } double buflen = scr_bytes_byte_len(buffer->v.bytes); if (!scr_fs_dyn_absent(offset)) { @@ -686,12 +1245,12 @@ void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offse * Node renders each with its own max. */ if (!scr_fs_int_range_chk(offset, "offset", 0, 9007199254740991.0, ">= 0 && <= 9007199254740991")) { - return; + return false; } if (offset->v.num > buflen) { char range[64]; snprintf(range, sizeof range, ">= 0 && <= %.0f", buflen); - if (!scr_fs_int_range_chk(offset, "offset", 0, buflen, range)) return; + if (!scr_fs_int_range_chk(offset, "offset", 0, buflen, range)) return false; } } double off = offset->kind == SCR_DYN_NUM ? offset->v.num : 0; @@ -708,10 +1267,10 @@ void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offse int len = snprintf(msg, sizeof msg, "The value of \"length\" is out of range. %s Received %s", shape, recv); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); - return; + return false; } scr_dyn_arg_type_fail("length", "of type number", length); - return; + return false; } if (length->v.num > buflen - off) { char recv[48], msg[160]; @@ -720,20 +1279,26 @@ void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offse "The value of \"length\" is out of range. It must be <= %.0f. Received %s", buflen - off, recv); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); - return; + return false; } } if (!scr_fs_dyn_absent(position)) { if (position->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("position", "of type bigint or integer", position); - return; + return false; } if (!scr_fs_int_range_chk(position, "position", -1, 9007199254740991.0, ">= -1 && <= 9007199254740991")) { - return; + return false; } } - scr_throw_lowering_fence(fence); + return true; +} + + +void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position, const ScrStr *fence) { + if (scr_fs_read_validate(fd, buffer, offset, length, position)) scr_throw_lowering_fence(fence); } /* createReadStream/createWriteStream(path, options?): getOptions' diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index c74c35a26..23db0c6ee 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -28,6 +28,8 @@ static void scr_ipc_release(ScrIpc *ipc); static void scr_ipc_service(void); static bool scr_ipc_pending(void); static bool scr_ipc_close_ready(const ScrIpc *ipc); +static void scr_child_fire_spawn(ScrChild *c); +static void scr_child_drop_spawn_listeners(ScrChild *c); static ScrArr *scr_fork_argv(double target, uintptr_t read_handle, uintptr_t write_handle, ScrArr *args) { @@ -1273,6 +1275,9 @@ struct ScrChild { ScrChildState state; bool settled; bool close_done; + bool spawn_done; + ScrClosure **spawn_cbs; + size_t n_spawn, cap_spawn; bool has_code; bool killed; bool reffed; @@ -1294,6 +1299,7 @@ struct ScrChild { ScrChildStream *out_stream; ScrChildStream *err_stream; ScrChildWriter *in_stream; + ScrDyn *out_dyn, *err_dyn, *in_dyn; ScrIpc *ipc; struct ScrChild *next; /* the pending registry */ }; @@ -1308,6 +1314,7 @@ static size_t scr_children_reffed_n = 0; static size_t scr_children_unwatched = 0; static void scr_child_drop_terminal_listeners(ScrChild *c) { + scr_child_drop_spawn_listeners(c); for (size_t i = 0; i < c->n_exit; i++) scr_closure_release(c->exit_cbs[i].cb); for (size_t i = 0; i < c->n_err; i++) scr_closure_release(c->err_cbs[i].cb); free(c->exit_cbs); @@ -1343,6 +1350,9 @@ void scr_child_release(ScrChild *c) { scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */ scr_str_release(c->err_msg); scr_str_release(c->exec_display); + scr_dyn_release(c->out_dyn); + scr_dyn_release(c->err_dyn); + scr_dyn_release(c->in_dyn); scr_child_stream_release(c->out_stream); scr_child_stream_release(c->err_stream); scr_child_writer_release(c->in_stream); @@ -2466,7 +2476,8 @@ bool scr_children_pending(void) { bool scr_children_failed_pending(void) { for (ScrChild *c = scr_children; c; c = c->next) { - if (c->state == SCR_CHILD_SPAWN_FAILED && !c->settled) return true; + if ((c->state == SCR_CHILD_SPAWN_FAILED && !c->settled) || + (c->state == SCR_CHILD_RUNNING && !c->spawn_done)) return true; } return false; } @@ -2592,6 +2603,10 @@ static void scr_child_settle(ScrChild *c) { * ordering), then every running child answers WaitForSingleObject(h, 0) * — the WNOHANG analogue; spawn failures settle on their first pass. */ void scr_children_poll(void) { + for (ScrChild *c = scr_children; c; c = c->next) { + if (c->state == SCR_CHILD_RUNNING && !c->spawn_done) scr_child_fire_spawn(c); + if (scr_exc_pending()) return; + } scr_ipc_service(); if (scr_exc_pending()) return; scr_child_writers_service(); @@ -3541,6 +3556,9 @@ struct ScrChild { ScrChildState state; bool settled; /* terminal event delivered; listeners released */ bool close_done; + bool spawn_done; + ScrClosure **spawn_cbs; + size_t n_spawn, cap_spawn; bool unwatched; /* no kqueue NOTE_EXIT armed: the loop must poll for it */ bool has_code; bool killed; /* a kill() successfully sent a signal (Node's flag) */ @@ -3565,6 +3583,7 @@ struct ScrChild { ScrChildStream *out_stream; ScrChildStream *err_stream; ScrChildWriter *in_stream; + ScrDyn *out_dyn, *err_dyn, *in_dyn; ScrIpc *ipc; struct ScrChild *next; /* the pending registry */ }; @@ -3716,6 +3735,7 @@ bool scr_children_wait(double max_wait_ms) { } static void scr_child_drop_terminal_listeners(ScrChild *c) { + scr_child_drop_spawn_listeners(c); for (size_t i = 0; i < c->n_exit; i++) scr_closure_release(c->exit_cbs[i].cb); for (size_t i = 0; i < c->n_err; i++) scr_closure_release(c->err_cbs[i].cb); free(c->exit_cbs); @@ -3751,6 +3771,9 @@ void scr_child_release(ScrChild *c) { scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */ scr_str_release(c->err_msg); scr_str_release(c->exec_display); + scr_dyn_release(c->out_dyn); + scr_dyn_release(c->err_dyn); + scr_dyn_release(c->in_dyn); scr_child_stream_release(c->out_stream); scr_child_stream_release(c->err_stream); scr_child_writer_release(c->in_stream); @@ -5080,7 +5103,8 @@ bool scr_children_pending(void) { * the loop's only-unref'd-children exit gate must not skip it. */ bool scr_children_failed_pending(void) { for (ScrChild *c = scr_children; c; c = c->next) { - if (c->state == SCR_CHILD_SPAWN_FAILED && !c->settled) return true; + if ((c->state == SCR_CHILD_SPAWN_FAILED && !c->settled) || + (c->state == SCR_CHILD_RUNNING && !c->spawn_done)) return true; } return false; } @@ -5200,6 +5224,10 @@ static void scr_child_settle(ScrChild *c) { * callbacks — a throw stops the pass and leaves the exception pending * for the loop. */ void scr_children_poll(void) { + for (ScrChild *c = scr_children; c; c = c->next) { + if (c->state == SCR_CHILD_RUNNING && !c->spawn_done) scr_child_fire_spawn(c); + if (scr_exc_pending()) return; + } scr_ipc_service(); if (scr_exc_pending()) return; scr_child_writers_service(); @@ -5252,6 +5280,224 @@ void scr_children_poll(void) { #endif /* !_WIN32 */ +static void scr_child_drop_spawn_listeners(ScrChild *c) { + for (size_t i = 0; i < c->n_spawn; i++) scr_closure_release(c->spawn_cbs[i]); + free(c->spawn_cbs); + c->spawn_cbs = NULL; + c->n_spawn = c->cap_spawn = 0; +} + +void scr_child_ref(ScrChild *c) { + if (!c->settled && !c->reffed) { + c->reffed = true; + scr_children_reffed_n++; + } +} + +void scr_child_on_spawn(ScrChild *c, ScrClosure *cb) { + if (c->spawn_done || c->settled) { scr_closure_release(cb); return; } + if (c->n_spawn == c->cap_spawn) { + c->cap_spawn = c->cap_spawn ? c->cap_spawn * 2 : 2; + ScrClosure **grown = realloc(c->spawn_cbs, c->cap_spawn * sizeof *grown); + if (!grown) scr_trap("scriptc: out of memory\n"); + c->spawn_cbs = grown; + } + c->spawn_cbs[c->n_spawn++] = cb; +} + +/* Success is observable on the next loop pass, before any data or exit + * event. Detaching the snapshot keeps listeners registered by a callback + * from participating in an event that has already started. */ +static void scr_child_fire_spawn(ScrChild *c) { + c->spawn_done = true; + ScrClosure **callbacks = c->spawn_cbs; + size_t count = c->n_spawn; + c->spawn_cbs = NULL; + c->n_spawn = c->cap_spawn = 0; + for (size_t i = 0; i < count; i++) { + if (!scr_exc_pending()) ((void (*)(ScrClosure *))callbacks[i]->fn)(callbacks[i]); + scr_closure_release(callbacks[i]); + } + free(callbacks); +} + +/* Options produced by JavaScript helpers use the same native spawn core + * as inline options. Materialize typed views only for normalization; + * descriptor reads still execute getters and propagate their exceptions. */ +static ScrDyn *scr_spawn_option(const ScrDyn *options, const char *name) { + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(options); + if (!view) return NULL; + ScrDyn *value = scr_spawn_option(view, name); + scr_dyn_release(view); + return value; + } + return scr_dyn_obj_read(options, name, strlen(name)); +} + +static void scr_spawn_refuse(const char *feature) { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "spawn with "); + scr_jb_puts(&message, feature); + scr_jb_puts(&message, " has no native lowering"); + ScrStr *text = scr_jb_finish(&message); + scr_throw_error_msg_code(SCR_ERR_ERROR, text->data, text->len, "SC2020"); + scr_str_release(text); +} + +static bool scr_spawn_stdio_mode(const ScrDyn *value, int slot, + double *mode, double *fd) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) { + *mode = 3; + return true; + } + if (value->kind == SCR_DYN_STR) { + const ScrStr *s = value->v.str; + if (s->len == 4 && !memcmp(s->data, "pipe", 4)) { *mode = 3; return true; } + if (s->len == 6 && !memcmp(s->data, "ignore", 6)) { *mode = 0; return true; } + if (s->len == 7 && !memcmp(s->data, "inherit", 7)) { *mode = 1; return true; } + } + if (slot > 0 && value->kind == SCR_DYN_NUM && isfinite(value->v.num) && + value->v.num >= 0 && value->v.num <= 2147483647 && floor(value->v.num) == value->v.num) { + *mode = 2; + *fd = value->v.num; + return true; + } + scr_spawn_refuse("this stdio slot"); + return false; +} + +ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options) { + ScrDyn *view = options->kind == SCR_DYN_TYPED_REF + ? scr_dyn_typed_ref_materialize(options) : scr_dyn_retain((ScrDyn *)options); + ScrDyn *stdio = NULL, *value = NULL, *env = NULL, *keys = NULL; + ScrArr *pairs = NULL; + ScrStr *cwd = NULL; + ScrChild *child = NULL; + bool detached = false, shell = false, has_env = false; + double modes[3] = {3, 3, 3}, fds[3] = {0, 0, 0}; + if (!view) goto done; + if (view->kind != SCR_DYN_UNDEF && + (view->kind == SCR_DYN_NULL || !scr_dyn_is_object(view))) { + scr_dyn_arg_type_fail("options", "of type object", view); + goto done; + } + if (view->kind == SCR_DYN_UNDEF) { + scr_dyn_release(view); + view = scr_dyn_new_obj(); + } + + /* Retain explicit boundaries for native process features that the + * inline lowering does not implement either. Undefined is an omission. */ + static const char *const unsupported[] = { + "uid", "gid", "argv0", "signal", "timeout", "killSignal", + }; + for (size_t i = 0; i < sizeof unsupported / sizeof unsupported[0]; i++) { + value = scr_spawn_option(view, unsupported[i]); + if (!value || scr_exc_pending()) goto done; + if (value->kind != SCR_DYN_UNDEF) { + scr_spawn_refuse(unsupported[i]); + goto done; + } + scr_dyn_release(value); value = NULL; + } + value = scr_spawn_option(view, "windowsVerbatimArguments"); + if (!value || scr_exc_pending()) goto done; + if (scr_dyn_truthy(value)) { scr_spawn_refuse("windowsVerbatimArguments"); goto done; } + scr_dyn_release(value); value = NULL; + + cwd = scr_str_new("", 0); + value = scr_spawn_option(view, "cwd"); + if (!value || scr_exc_pending()) goto done; + if (value->kind != SCR_DYN_UNDEF && value->kind != SCR_DYN_NULL) { + if (value->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.cwd", "of type string or an instance of Buffer or URL", value); + goto done; + } + scr_str_release(cwd); cwd = scr_str_retain(value->v.str); + } + scr_dyn_release(value); value = NULL; + static const char *const booleans[] = {"detached", "shell", "windowsHide"}; + for (size_t i = 0; i < sizeof booleans / sizeof booleans[0]; i++) { + value = scr_spawn_option(view, booleans[i]); + if (!value || scr_exc_pending()) goto done; + if (value->kind != SCR_DYN_UNDEF) { + if (i == 1 && value->kind == SCR_DYN_STR) { + scr_spawn_refuse("a custom shell executable"); + goto done; + } + if (value->kind != SCR_DYN_BOOL) { + char name[40]; snprintf(name, sizeof name, "options.%s", booleans[i]); + scr_dyn_prop_type_fail(name, i == 1 ? "of type boolean or string" : "of type boolean", value); + goto done; + } + if (i == 0) detached = value->v.b; + else if (i == 1) shell = value->v.b; + } + scr_dyn_release(value); value = NULL; + } + if (shell && scr_arr_len(args) != 0) { + scr_spawn_refuse("shell enabled and separate arguments"); + goto done; + } + stdio = scr_spawn_option(view, "stdio"); + if (!stdio || scr_exc_pending()) goto done; + if (stdio->kind == SCR_DYN_TYPED_REF) { + ScrDyn *expanded = scr_dyn_typed_ref_materialize(stdio); + scr_dyn_release(stdio); stdio = expanded; + if (!stdio) goto done; + } + if (stdio->kind == SCR_DYN_ARR) { + if (stdio->v.arr.len > 3) { scr_spawn_refuse("additional stdio descriptors"); goto done; } + for (int i = 0; i < 3; i++) { + value = scr_dyn_arr_at(stdio, i); + if (!scr_spawn_stdio_mode(value, i, &modes[i], &fds[i])) goto done; + scr_dyn_release(value); value = NULL; + } + } else if (stdio->kind == SCR_DYN_STR || stdio->kind == SCR_DYN_UNDEF || stdio->kind == SCR_DYN_NULL) { + if (!scr_spawn_stdio_mode(stdio, 0, &modes[0], &fds[0])) goto done; + modes[1] = modes[2] = modes[0]; + } else { scr_spawn_refuse("this stdio option"); goto done; } + + pairs = scr_arr_new(SCR_ELEM_STR, 8); + env = scr_spawn_option(view, "env"); + if (!env || scr_exc_pending()) goto done; + if (env->kind == SCR_DYN_TYPED_REF) { + ScrDyn *expanded = scr_dyn_typed_ref_materialize(env); + scr_dyn_release(env); env = expanded; + if (!env) goto done; + } + if (env->kind != SCR_DYN_UNDEF && env->kind != SCR_DYN_NULL) { + has_env = true; + keys = scr_dyn_for_in_keys(env); + if (!keys || scr_exc_pending()) goto done; + for (size_t i = 0; i < keys->v.arr.len; i++) { + ScrStr *key = keys->v.arr.items[i]->v.str; + value = scr_spawn_option(env, key->data); + if (!value || scr_exc_pending()) goto done; + if (value->kind != SCR_DYN_UNDEF) { + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) goto done; + scr_arr_push_ref(pairs, scr_str_retain(key)); + scr_arr_push_ref(pairs, text); + } + scr_dyn_release(value); value = NULL; + } + } + child = scr_spawn_opts(cmd, args, modes[0], modes[1], modes[2], + fds[1], fds[2], detached, shell, has_env, pairs, cwd); +done: + scr_dyn_release(view); + scr_dyn_release(stdio); + scr_dyn_release(value); + scr_dyn_release(env); + scr_dyn_release(keys); + scr_arr_release(pairs); + scr_str_release(cwd); + return child; +} + /* ── fork JSON IPC (shared by Windows and POSIX) ────────────────────── */ typedef struct { @@ -5744,30 +5990,300 @@ void scr_process_ipc_on_disconnect(ScrClosure *cb, bool once) { scr_ipc_add_disconnect(scr_process_ipc, cb, once); } -/* ── checked-dynamic ChildProcess identity bridge ──────────────────── - * Typed code normally checks the box back to ChildProcess before member - * access, so this table's essential contract is retain/release identity. - * The three modeled properties also remain useful while a JavaScript - * package carries the handle through an inferred checked-dynamic slot. - * Method calls stay loud until their callback signatures have dedicated - * dyn adapters; they never silently manufacture a different child. */ +static SCR_TL const ScrChildStreamOps *scr_child_stream_ops; +static SCR_TL ScrDyn *(*scr_child_readable_box)(void *); +static SCR_TL ScrDyn *(*scr_child_writable_box)(void *); + +void scr_child_dyn_streams_install(const ScrChildStreamOps *ops, + ScrDyn *(*readable_box)(void *), ScrDyn *(*writable_box)(void *)) { + scr_child_stream_ops = ops; + scr_child_readable_box = readable_box; + scr_child_writable_box = writable_box; +} + +static ScrClosure *scr_child_bridge_callback(void *stream, void *fire) { + ScrClosure *cb = scr_closure_new(fire, 1); + cb->caps[0] = scr_box_new_obj(scr_child_stream_ops->retain, + scr_child_stream_ops->release, scr_child_stream_ops->trace); + scr_box_set_ref(cb->caps[0], scr_child_stream_ops->retain(stream)); + return cb; +} +static void scr_child_bridge_data(ScrClosure *cb, ScrBytes *chunk) { + ScrStream *stream = scr_box_get_ref(cb->caps[0]); + scr_child_stream_ops->push(stream, chunk); + scr_child_stream_ops->release(stream); +} +static void scr_child_bridge_end(ScrClosure *cb) { + ScrStream *stream = scr_box_get_ref(cb->caps[0]); + scr_child_stream_ops->end(stream); + scr_child_stream_ops->release(stream); +} +static ScrDyn *scr_child_bridge_readable(ScrChildStream *reader, ScrDyn **cached) { + if (!reader) return scr_dyn_new_null(); + if (!scr_child_stream_ops) { + static const char message[] = "ChildProcess standard streams require the native stream surface"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + if (!*cached) { + ScrStream *stream = scr_child_stream_ops->readable(); + scr_child_stream_on_data(reader, scr_child_bridge_callback(stream, (void *)scr_child_bridge_data), + scr_child_bridge_data, false); + scr_child_stream_on_end(reader, scr_child_bridge_callback(stream, (void *)scr_child_bridge_end), false); + *cached = scr_child_readable_box(stream); + scr_child_stream_ops->release(stream); + } + return scr_dyn_retain(*cached); +} +static void scr_child_bridge_write_error(ScrClosure *, ScrStr *); +static void scr_child_bridge_final_error(ScrClosure *, ScrStr *); +static void scr_child_bridge_write_drained(ScrClosure *); +static void scr_child_bridge_final_finished(ScrClosure *); + +/* Each pending native stream operation owns its callback until the pipe + * actually drains or closes. Remove the alternate completion listener + * before finishing the operation, which may synchronously start another. */ +static void scr_child_bridge_remove_error(ScrChildWriter *writer, ScrChildErrFn fn) { + for (size_t i = writer->n_write_err; i > 0; i--) { + if (writer->err_ls[i - 1].fn != fn) continue; + scr_closure_release(writer->err_ls[i - 1].cb); + memmove(writer->err_ls + i - 1, writer->err_ls + i, + (writer->n_write_err - i) * sizeof *writer->err_ls); + writer->n_write_err--; + } +} +static void scr_child_bridge_remove_zero(ScrChildStreamEndL *listeners, size_t *count, void *fn) { + for (size_t i = *count; i > 0; i--) { + if (listeners[i - 1].cb->fn != fn) continue; + scr_closure_release(listeners[i - 1].cb); + memmove(listeners + i - 1, listeners + i, (*count - i) * sizeof *listeners); + (*count)--; + } +} +static ScrClosure *scr_child_bridge_operation(ScrStream *stream, ScrChildWriter *writer, void *fn) { + ScrClosure *cb = scr_closure_new(fn, 2); + cb->caps[0] = scr_box_new_obj(scr_child_stream_ops->retain, + scr_child_stream_ops->release, scr_child_stream_ops->trace); + cb->caps[1] = scr_box_new_obj(scr_child_writer_retain_v, scr_child_writer_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_child_stream_ops->retain(stream)); + scr_box_set_ref(cb->caps[1], scr_child_writer_retain(writer)); + return cb; +} +static void scr_child_bridge_write_drained(ScrClosure *cb) { + ScrStream *stream = scr_box_get_ref(cb->caps[0]); + ScrChildWriter *writer = scr_box_get_ref(cb->caps[1]); + scr_child_bridge_remove_error(writer, scr_child_bridge_write_error); + scr_child_stream_ops->write_done(stream, NULL); + scr_child_writer_release(writer); + scr_child_stream_ops->release(stream); +} +static void scr_child_bridge_final_finished(ScrClosure *cb) { + ScrStream *stream = scr_box_get_ref(cb->caps[0]); + ScrChildWriter *writer = scr_box_get_ref(cb->caps[1]); + scr_child_bridge_remove_error(writer, scr_child_bridge_final_error); + scr_child_stream_ops->final_done(stream, NULL); + scr_child_writer_release(writer); + scr_child_stream_ops->release(stream); +} +static void scr_child_bridge_operation_error(ScrClosure *cb, ScrStr *message, bool final) { + ScrStream *stream = scr_box_get_ref(cb->caps[0]); + ScrChildWriter *writer = scr_box_get_ref(cb->caps[1]); + if (final) scr_child_bridge_remove_zero(writer->finish_ls, &writer->n_finish, (void *)scr_child_bridge_final_finished); + else scr_child_bridge_remove_zero(writer->drain_ls, &writer->n_drain, (void *)scr_child_bridge_write_drained); + ScrError *error = scr_error_new(SCR_ERR_ERROR, message); + if (writer->err_code) scr_error_set_code(error, writer->err_code); + if (final) scr_child_stream_ops->final_done(stream, error); + else scr_child_stream_ops->write_done(stream, error); + scr_child_writer_release(writer); + scr_child_stream_ops->release(stream); +} +static void scr_child_bridge_write_error(ScrClosure *cb, ScrStr *message) { + scr_child_bridge_operation_error(cb, message, false); +} +static void scr_child_bridge_final_error(ScrClosure *cb, ScrStr *message) { + scr_child_bridge_operation_error(cb, message, true); +} +static void scr_child_bridge_write(ScrClosure *cb, ScrStream *stream, ScrBytes *chunk) { + ScrChildWriter *writer = scr_box_get_ref(cb->caps[0]); + scr_child_writer_write_bytes(writer, chunk); + bool pending = scr_child_writer_queued(writer) > 0; +#ifdef _WIN32 + pending = pending || writer->write_pending; +#endif + if (pending || writer->error_pending) { + scr_child_writer_on_error(writer, + scr_child_bridge_operation(stream, writer, (void *)scr_child_bridge_write_error), + scr_child_bridge_write_error, true); + if (!writer->error_pending) { + writer->need_drain = true; + scr_child_writer_on_drain(writer, + scr_child_bridge_operation(stream, writer, (void *)scr_child_bridge_write_drained), true); + } + } else scr_child_stream_ops->write_done(stream, NULL); + scr_child_writer_release(writer); +} +static void scr_child_bridge_final(ScrClosure *cb, ScrStream *stream) { + ScrChildWriter *writer = scr_box_get_ref(cb->caps[0]); + scr_child_writer_on_error(writer, + scr_child_bridge_operation(stream, writer, (void *)scr_child_bridge_final_error), + scr_child_bridge_final_error, true); + scr_child_writer_on_finish(writer, + scr_child_bridge_operation(stream, writer, (void *)scr_child_bridge_final_finished), true); + scr_child_writer_end(writer); + scr_child_writer_release(writer); +} +static ScrClosure *scr_child_writer_bridge_callback(ScrChildWriter *writer, void *fire) { + ScrClosure *cb = scr_closure_new(fire, 1); + cb->caps[0] = scr_box_new_obj(scr_child_writer_retain_v, scr_child_writer_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_child_writer_retain(writer)); + return cb; +} +static ScrDyn *scr_child_bridge_writable(ScrChildWriter *writer, ScrDyn **cached) { + if (!writer) return scr_dyn_new_null(); + if (!scr_child_stream_ops) { + static const char message[] = "ChildProcess standard streams require the native stream surface"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + if (!*cached) { + ScrStream *stream = scr_child_stream_ops->writable( + scr_child_writer_bridge_callback(writer, (void *)scr_child_bridge_write), scr_child_bridge_write, + scr_child_writer_bridge_callback(writer, (void *)scr_child_bridge_final), scr_child_bridge_final); + *cached = scr_child_writable_box(stream); + scr_child_stream_ops->release(stream); + } + return scr_dyn_retain(*cached); +} + +/* ── checked-dynamic ChildProcess dispatch ──────────────────────────── */ +static void scr_child_dyn_fire(ScrClosure *cb, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = scr_box_get_ref(cb->caps[0]); + ScrDyn *self = scr_box_get_ref(cb->caps[1]); + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, "child listener"); + scr_dyn_this_pop(); + scr_dyn_release(result); + scr_dyn_release(self); + scr_dyn_release(fn); +} + +static void scr_child_dyn_spawn(ScrClosure *cb) { scr_child_dyn_fire(cb, NULL, 0); } +static void scr_child_dyn_exit(ScrClosure *cb, bool has_code, double code, const char *signal) { + ScrDyn *args[2] = { has_code ? scr_dyn_new_num(code) : scr_dyn_new_null(), + signal ? NULL : scr_dyn_new_null() }; + if (signal) { + ScrStr *name = scr_str_new(signal, strlen(signal)); + args[1] = scr_dyn_new_str(name); + scr_str_release(name); + } + scr_child_dyn_fire(cb, args, 2); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); +} +static void scr_child_dyn_error(ScrClosure *cb, ScrStr *message) { + ScrError *error = scr_error_new(SCR_ERR_ERROR, message); + if (scr_child_err_code) scr_error_set_code(error, scr_child_err_code); + else if (scr_child_writer_err_code) scr_error_set_code(error, scr_child_writer_err_code); + ScrDyn *arg = scr_dyn_from_error(error); + scr_error_release(error); + scr_child_dyn_fire(cb, &arg, 1); + scr_dyn_release(arg); +} + +static ScrClosure *scr_child_dyn_listener(ScrDyn *fn, ScrDyn *self, void *fire) { + ScrClosure *cb = scr_closure_new(fire, 2); + cb->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + cb->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(cb->caps[0], scr_dyn_retain(fn)); + scr_box_set_ref(cb->caps[1], scr_dyn_retain(self)); + return cb; +} + +static bool scr_child_dyn_matches(ScrClosure *cb, void *fire, const ScrDyn *fn) { + if (cb->fn != fire) return false; + ScrDyn *listener = scr_box_get_ref(cb->caps[0]); + bool matches = scr_dyn_strict_eq(listener, fn); + scr_dyn_release(listener); + return matches; +} + static ScrDyn *scr_child_dyn_invoke( void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - (void)h; - (void)self; - (void)method; - (void)args; - (void)argc; (void)what; - static const char msg[] = - "ChildProcess method calls through an 'unknown' value are not supported yet — narrow the value to ChildProcess first"; - scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + ScrChild *child = h; + if (!strcmp(method, "ref") || !strcmp(method, "unref")) { + if (!strcmp(method, "ref")) scr_child_ref(child); else scr_child_unref(child); + return scr_dyn_retain(self); + } + if (!strcmp(method, "kill")) { + const ScrDyn *signal = argc ? args[0] : scr_dyn_undefined(); + bool killed; + if (signal->kind == SCR_DYN_NUM) killed = scr_child_kill_num(child, signal->v.num); + else if (signal->kind == SCR_DYN_STR) killed = scr_child_kill(child, signal->v.str); + else if (signal->kind == SCR_DYN_UNDEF) { + ScrStr *name = scr_str_new("SIGTERM", 7); + killed = scr_child_kill(child, name); + scr_str_release(name); + } else { scr_dyn_arg_type_fail("signal", "of type string or number", signal); return NULL; } + return scr_exc_pending() ? NULL : scr_dyn_new_bool(killed); + } + bool remove = !strcmp(method, "removeListener") || !strcmp(method, "off"); + if (!strcmp(method, "on") || !strcmp(method, "once") || !strcmp(method, "addListener") || remove) { + const ScrDyn *event = argc ? args[0] : scr_dyn_undefined(); + ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); + if (listener->kind != SCR_DYN_FUNC) scr_dyn_arg_type_fail("listener", "of type function", listener); + if (scr_exc_pending()) return NULL; + if (event->kind != SCR_DYN_STR) { scr_dyn_arg_type_fail("event", "of type string", event); return NULL; } + const ScrStr *name = event->v.str; + bool spawn = name->len == 5 && !memcmp(name->data, "spawn", 5); + bool exit = name->len == 4 && !memcmp(name->data, "exit", 4); + bool close = name->len == 5 && !memcmp(name->data, "close", 5); + bool error = name->len == 5 && !memcmp(name->data, "error", 5); + if (spawn || exit || close || error) { + void *fire = spawn ? (void *)scr_child_dyn_spawn : error ? (void *)scr_child_dyn_error : (void *)scr_child_dyn_exit; + if (remove) { + if (spawn) { + for (size_t i = child->n_spawn; i > 0; i--) if (scr_child_dyn_matches(child->spawn_cbs[i-1], fire, listener)) { + scr_closure_release(child->spawn_cbs[i-1]); + memmove(child->spawn_cbs+i-1, child->spawn_cbs+i, (child->n_spawn-i)*sizeof *child->spawn_cbs); + child->n_spawn--; break; + } + } else if (error) { + for (size_t i = child->n_err; i > 0; i--) if (scr_child_dyn_matches(child->err_cbs[i-1].cb, fire, listener)) { + scr_closure_release(child->err_cbs[i-1].cb); + memmove(child->err_cbs+i-1, child->err_cbs+i, (child->n_err-i)*sizeof *child->err_cbs); + child->n_err--; break; + } + } else { + ScrChildExitEntry *entries = close ? child->close_cbs : child->exit_cbs; + size_t *count = close ? &child->n_close : &child->n_exit; + for (size_t i = *count; i > 0; i--) if (scr_child_dyn_matches(entries[i-1].cb, fire, listener)) { + scr_closure_release(entries[i-1].cb); + memmove(entries+i-1, entries+i, (*count-i)*sizeof *entries); + (*count)--; break; + } + } + } else { + ScrClosure *cb = scr_child_dyn_listener(listener, self, fire); + if (spawn) scr_child_on_spawn(child, cb); + else if (error) scr_child_on_error(child, cb, scr_child_dyn_error); + else if (close) scr_child_on_close(child, cb, scr_child_dyn_exit); + else scr_child_on_exit(child, cb, scr_child_dyn_exit); + } + return scr_dyn_retain(self); + } + } + static const char msg[] = "This ChildProcess method or event has no native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); return NULL; } static ScrDyn *scr_child_dyn_get(void *h, const char *key, size_t key_len) { ScrChild *child = (ScrChild *)h; + if (key_len == 6 && !memcmp(key, "stdout", 6)) return scr_child_bridge_readable(child->out_stream, &child->out_dyn); + if (key_len == 6 && !memcmp(key, "stderr", 6)) return scr_child_bridge_readable(child->err_stream, &child->err_dyn); + if (key_len == 5 && !memcmp(key, "stdin", 5)) return scr_child_bridge_writable(child->in_stream, &child->in_dyn); if (key_len == 6 && memcmp(key, "killed", 6) == 0) { return scr_dyn_new_bool(scr_child_killed(child)); } @@ -5806,3 +6322,14 @@ static const ScrDynHandleOps scr_child_dyn_ops = { void scr_child_dyn_install(void) { scr_dyn_handle_install(SCR_DYNH_CHILD, &scr_child_dyn_ops); } + +void scr_child_on_dyn(ScrChild *child, ScrStr *event, ScrDyn *listener) { + scr_child_dyn_install(); + ScrDyn *self = scr_dyn_new_handle(child, SCR_DYNH_CHILD); + ScrDyn *name = scr_dyn_new_str(event); + ScrDyn *args[] = { name, listener }; + ScrDyn *result = scr_child_dyn_invoke(child, self, "on", args, 2, "child listener"); + scr_dyn_release(result); + scr_dyn_release(name); + scr_dyn_release(self); +} diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index bf0b81c17..87d7e5fa5 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -107,7 +107,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v5(void) {} +void scr_runtime_abi_v6(void) {} void scr_init(void) { #ifdef _WIN32 diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 3ba2ce7b9..660624ee6 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -555,7 +555,15 @@ static bool dyn_name_is(const char *m, const char *n) { return strcmp(m, n) == 0 * accessor reports). */ static ScrDyn *scr_dynh_dispatch(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what) { const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(recv); - return ops->invoke(recv->v.handle.ptr, recv, method, args, argc, what); + if (ops->invoke) return ops->invoke(recv->v.handle.ptr, recv, method, args, argc, what); + ScrDyn *callable = ops->get ? ops->get(recv->v.handle.ptr, method, strlen(method)) : NULL; + if (scr_exc_pending()) { scr_dyn_release(callable); return NULL; } + if (!callable) callable = scr_dyn_retain(scr_dyn_undefined()); + scr_dyn_this_push_dyn(recv); + ScrDyn *result = scr_dyn_call(callable, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(callable); + return result; } /* JS Array.prototype.sort over a dyn array: the spec's snapshot-sort — @@ -698,6 +706,29 @@ static ScrDyn *scr_dyn_invoke_impl( ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv); +ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method) { + if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) + return scr_dyn_invoke(recv, method, NULL, 0, method); + if (recv->kind == SCR_DYN_OBJ) + return scr_dyn_obj_read(recv, method, strlen(method)); + if (recv->kind == SCR_DYN_PROXY) { + ScrStr *key = scr_str_new(method, strlen(method)); + ScrDyn *result = scr_dyn_proxy_get(recv, key); + scr_str_release(key); + return result; + } + return NULL; +} + +ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + if (!callee) return scr_dyn_invoke(recv, method, args, argc, what); + scr_dyn_this_push_dyn(recv); + ScrDyn *result = scr_dyn_call(callee, args, argc, what); + scr_dyn_this_pop(); + return result; +} + ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what) { @@ -861,9 +892,11 @@ static ScrDyn *scr_dyn_invoke_impl( if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) && argc >= 1 && args[0]->kind == SCR_DYN_STR) { - if (dyn_name_is(method, "includes")) return scr_dyn_new_bool(scr_str_includes(s, args[0]->v.str)); - if (dyn_name_is(method, "indexOf")) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, 0)); - return scr_dyn_new_num(scr_str_last_index_of(s, args[0]->v.str)); + double position = dyn_index_arg(args, argc, 1, dyn_name_is(method, "lastIndexOf") ? INFINITY : 0, what); + if (scr_exc_pending()) return NULL; + if (dyn_name_is(method, "includes")) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); + if (dyn_name_is(method, "indexOf")) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); + return scr_dyn_new_num(scr_str_last_index_of_from(s, args[0]->v.str, position)); } dyn_throw_unsupported("String", method); return NULL; @@ -1302,6 +1335,12 @@ static ScrDyn *scr_dyn_invoke_impl( cb, item, i, callback_recv ? callback_recv : recv); scr_dyn_release(item); if (!r) { scr_dyn_release(out); return NULL; } + if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(r)) { + ScrDyn *view = scr_dyn_typed_ref_materialize(r); + scr_dyn_release(r); + r = view; + if (!r) { scr_dyn_release(out); return NULL; } + } if (r->kind == SCR_DYN_ARR) { for (size_t j = 0; j < r->v.arr.len; j++) { scr_dyn_arr_push(out, scr_dyn_retain(r->v.arr.items[j])); diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 01c12720c..0441d7dbd 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -3176,6 +3176,7 @@ static SfResponse *sf_response_retain(SfResponse *r) { static void sf_response_release(SfResponse *r) { if (!r || --r->rc > 0) return; + scr_weak_dispose(r); sf_stream_release(r->body); sf_headers_release(r->headers); scr_str_release(r->url); @@ -4057,11 +4058,17 @@ static ScrArr *sf_headers_convert_init(const ScrDyn *headers) { if (headers->kind == SCR_DYN_ARR) { for (size_t i = 0; i < headers->v.arr.len; i++) { ScrDyn *pair = scr_dyn_retain(headers->v.arr.items[i]); + if (pair->kind == SCR_DYN_TYPED_REF) { + ScrDyn *materialized = scr_dyn_typed_ref_materialize(pair); + scr_dyn_release(pair); + pair = materialized; + } if (pair->kind != SCR_DYN_ARR || pair->v.arr.len != 2) { + bool sequence = pair->kind == SCR_DYN_ARR; size_t pair_len = pair->kind == SCR_DYN_ARR ? pair->v.arr.len : 0; scr_dyn_release(pair); if (!scr_exc_pending()) { - if (pair_len > 0 || headers->v.arr.items[i]->kind == SCR_DYN_ARR) { + if (sequence) { sf_headers_pair_length_error(pair_len); } else { sf_headers_constructor_type_error(); @@ -5169,7 +5176,250 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { return NULL; } +/* Request uses the same body stream and Headers ownership as Response. */ +typedef struct SfRequest { + size_t rc; + SfResponse *payload; + ScrStr *url; + ScrStr *method; + ScrStr *redirect; + SfSignal *signal; +} SfRequest; + +static void *sf_request_retain_v(void *ptr) { + SfRequest *r = ptr; r->rc++; return r; +} +static void sf_request_release_v(void *ptr) { + SfRequest *r = ptr; + if (--r->rc) return; + scr_weak_dispose(r); + sf_response_release(r->payload); + scr_str_release(r->url); scr_str_release(r->method); scr_str_release(r->redirect); + sf_signal_release(r->signal); free(r); +} +static ScrDyn *sf_request_get(void *ptr, const char *key, size_t len) { + SfRequest *r = ptr; + if (sf_name(key, len, "url")) return scr_dyn_new_str(r->url); + if (sf_name(key, len, "method")) return scr_dyn_new_str(r->method); + if (sf_name(key, len, "redirect")) return scr_dyn_new_str(r->redirect); + if (sf_name(key, len, "signal")) return scr_dyn_new_handle(r->signal, SCR_DYNH_ABORT_SIGNAL); + if (sf_name(key, len, "duplex")) { ScrStr *s = scr_str_new("half", 4); ScrDyn *v = scr_dyn_new_str(s); scr_str_release(s); return v; } + const char *default_value = NULL; + if (sf_name(key, len, "destination") || sf_name(key, len, "integrity") || sf_name(key, len, "referrerPolicy")) default_value = ""; + if (sf_name(key, len, "mode")) default_value = "cors"; + if (sf_name(key, len, "credentials")) default_value = "same-origin"; + if (sf_name(key, len, "cache")) default_value = "default"; + if (sf_name(key, len, "referrer")) default_value = "about:client"; + if (default_value) { ScrStr *s = scr_str_new(default_value, strlen(default_value)); ScrDyn *v = scr_dyn_new_str(s); scr_str_release(s); return v; } + if (sf_name(key, len, "keepalive") || sf_name(key, len, "isReloadNavigation") || sf_name(key, len, "isHistoryNavigation")) return scr_dyn_new_bool(false); + if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || sf_name(key, len, "headers")) return sf_response_get(r->payload, key, len); + return scr_dyn_undefined(); +} +static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { + SfRequest *r = ptr; + return sf_response_invoke(r->payload, self, method, args, argc, what); +} + +ScrDyn *scr_fetch_headers_new(ScrDyn *init) { + ScrArr *converted = sf_headers_convert_init(init); + if (!converted) return NULL; + ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, 8); + bool ok = sf_add_converted_headers(pairs, converted, false); + scr_arr_release(converted); + if (!ok) { scr_arr_release(pairs); return NULL; } + SfHeaders *headers = sf_headers_new(pairs, false); + ScrDyn *result = scr_dyn_new_handle(headers, SCR_DYNH_FETCH_HEADERS); + sf_headers_release(headers); + return result; +} + +ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init) { + SfRequest *source = input && input->kind == SCR_DYN_HANDLE && input->v.handle.tag == SCR_DYNH_FETCH_REQUEST ? input->v.handle.ptr : NULL; + if (init && init->kind != SCR_DYN_UNDEF && init->kind != SCR_DYN_NULL && !sf_response_init_object_like(init)) { + sf_type_error("Request constructor init must be an object"); return NULL; + } + ScrStr *url_text = source ? scr_str_retain(source->url) : scr_dyn_string_coerce_js(input); + if (!url_text) return NULL; + ScrUrl *url = scr_url_new(url_text); + scr_str_release(url_text); + if (!url) return NULL; + if (url->userinfo->len) { scr_url_release(url); sf_type_error("Request cannot be constructed from a URL that includes credentials"); return NULL; } + ScrDyn *dictionary = scr_dyn_new_obj(); + ScrDyn *method = sf_response_init_get(init, "method", 6); + if ((!method || method->kind == SCR_DYN_UNDEF) && source) { scr_dyn_release(method); method = scr_dyn_new_str(source->method); } + if (method) scr_dyn_obj_set(dictionary, "method", 6, method); + ScrStr *method_text = sf_method_value(dictionary); + scr_dyn_release(dictionary); + if (!method_text || !sf_method_validate(&method_text)) { scr_str_release(method_text); scr_url_release(url); return NULL; } + ScrDyn *body = sf_response_init_get(init, "body", 4); + bool supplied = body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL; + if (!supplied && source && !source->payload->null_body) { + if (source->payload->body->disturbed || source->payload->body->reader || source->payload->body->internal_lock) { + scr_dyn_release(body); scr_str_release(method_text); scr_url_release(url); + sf_type_error("Cannot construct a Request with a Request object that has already been used."); return NULL; + } + scr_dyn_release(body); body = scr_dyn_new_handle(source->payload->body, SCR_DYNH_WEB_STREAM); + } + bool has_body = body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL; + if (has_body && (sf_name(method_text->data, method_text->len, "GET") || sf_name(method_text->data, method_text->len, "HEAD"))) { + scr_dyn_release(body); scr_str_release(method_text); scr_url_release(url); sf_type_error("Request with GET/HEAD method cannot have body."); return NULL; + } + if (supplied && body->kind == SCR_DYN_HANDLE && body->v.handle.tag == SCR_DYNH_WEB_STREAM) { + ScrDyn *duplex = sf_response_init_get(init, "duplex", 6); + ScrStr *text = duplex && duplex->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(duplex) : NULL; + scr_dyn_release(duplex); + bool half = text && sf_name(text->data, text->len, "half"); scr_str_release(text); + if (!half) { scr_dyn_release(body); scr_str_release(method_text); scr_url_release(url); sf_type_error("RequestInit: duplex option is required when sending a body."); return NULL; } + } + ScrDyn *headers = sf_response_init_get(init, "headers", 7); + if ((!headers || headers->kind == SCR_DYN_UNDEF) && source) { scr_dyn_release(headers); headers = scr_dyn_new_handle(source->payload->headers, SCR_DYNH_FETCH_HEADERS); } + ScrDyn *payload_init = scr_dyn_new_obj(); + if (headers) scr_dyn_obj_set(payload_init, "headers", 7, headers); + ScrDyn *payload = scr_fetch_response_new(body, payload_init); + scr_dyn_release(payload_init); scr_dyn_release(body); + if (!payload) { scr_str_release(method_text); scr_url_release(url); return NULL; } + ScrDyn *signal = sf_response_init_get(init, "signal", 6); + if ((!signal || signal->kind == SCR_DYN_UNDEF) && source) { scr_dyn_release(signal); signal = scr_dyn_new_handle(source->signal, SCR_DYNH_ABORT_SIGNAL); } + SfSignal *following = NULL; + if (signal && signal->kind != SCR_DYN_UNDEF && signal->kind != SCR_DYN_NULL) { + ScrDyn *signals = scr_dyn_new_arr(); scr_dyn_arr_push(signals, scr_dyn_retain(signal)); + ScrDyn *dependent = scr_fetch_abort_any(signals); scr_dyn_release(signals); + if (dependent) { following = sf_signal_retain(dependent->v.handle.ptr); scr_dyn_release(dependent); } + } else following = sf_signal_new(); + scr_dyn_release(signal); + if (!following) { scr_dyn_release(payload); scr_str_release(method_text); scr_url_release(url); return NULL; } + ScrDyn *redirect = sf_response_init_get(init, "redirect", 8); + ScrStr *redirect_text = redirect && redirect->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(redirect) : source ? scr_str_retain(source->redirect) : scr_str_new("follow", 6); + scr_dyn_release(redirect); + if (!redirect_text || !(sf_name(redirect_text->data, redirect_text->len, "follow") || sf_name(redirect_text->data, redirect_text->len, "manual") || sf_name(redirect_text->data, redirect_text->len, "error"))) { + scr_str_release(redirect_text); sf_signal_release(following); scr_dyn_release(payload); scr_str_release(method_text); scr_url_release(url); sf_type_error("Invalid Request redirect value"); return NULL; + } + SfRequest *r = calloc(1, sizeof *r); if (!r) sf_oom(); + r->rc = 1; r->payload = sf_response_retain(payload->v.handle.ptr); scr_dyn_release(payload); + r->url = scr_url_href(url); scr_url_release(url); r->method = method_text; r->redirect = redirect_text; r->signal = following; + ScrDyn *result = scr_dyn_new_handle(r, SCR_DYNH_FETCH_REQUEST); sf_request_release_v(r); return result; +} + +ScrPromise *scr_fetch_input(ScrDyn *input, ScrDyn *init) { + if (init && init->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(init); + if (scr_exc_pending()) { scr_dyn_release(view); ScrPromise *p = scr_promise_new(); scr_promise_reject_pending(p); return p; } + ScrPromise *result = scr_fetch_input(input, view); + scr_dyn_release(view); + return result; + } + if (input && input->kind == SCR_DYN_HANDLE && input->v.handle.tag == SCR_DYNH_FETCH_REQUEST) { + ScrDyn *converted = scr_fetch_request_new(input, init); + if (!converted) { ScrPromise *p = scr_promise_new(); scr_promise_reject_pending(p); return p; } + SfRequest *r = converted->v.handle.ptr; + ScrDyn *dictionary = scr_dyn_new_obj(); + scr_dyn_obj_set(dictionary, "method", 6, scr_dyn_new_str(r->method)); + scr_dyn_obj_set(dictionary, "headers", 7, scr_dyn_new_handle(r->payload->headers, SCR_DYNH_FETCH_HEADERS)); + scr_dyn_obj_set(dictionary, "signal", 6, scr_dyn_new_handle(r->signal, SCR_DYNH_ABORT_SIGNAL)); + scr_dyn_obj_set(dictionary, "redirect", 8, scr_dyn_new_str(r->redirect)); + if (!r->payload->null_body) { + scr_dyn_obj_set(dictionary, "body", 4, scr_dyn_new_handle(r->payload->body, SCR_DYNH_WEB_STREAM)); + ScrStr *half = scr_str_new("half", 4); scr_dyn_obj_set(dictionary, "duplex", 6, scr_dyn_new_str(half)); scr_str_release(half); + } + ScrPromise *result = scr_fetch_static(r->url, dictionary); + scr_dyn_release(dictionary); scr_dyn_release(converted); return result; + } + ScrStr *url = scr_dyn_string_coerce_js(input); + if (!url) { ScrPromise *p = scr_promise_new(); scr_promise_reject_pending(p); return p; } + ScrPromise *result = scr_fetch_static(url, init); scr_str_release(url); return result; +} + +static ScrDyn *sf_fetch_function_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrPromise *p = scr_fetch_input(argc ? args[0] : scr_dyn_undefined(), argc > 1 ? args[1] : scr_dyn_undefined()); + ScrDyn *result = scr_dyn_new_promise(p); scr_promise_release(p); return result; +} +static ScrDyn *sf_fetch_function_value; +static void sf_fetch_function_cleanup(void) { + ScrDyn *value = sf_fetch_function_value; + sf_fetch_function_value = NULL; + scr_dyn_release(value); +} +bool scr_fetch_web_is(const ScrDyn *value, const ScrStr *name) { + if (value->kind != SCR_DYN_HANDLE) return false; + if (sf_name(name->data, name->len, "Response")) return value->v.handle.tag == SCR_DYNH_FETCH_RESPONSE; + if (sf_name(name->data, name->len, "Request")) return value->v.handle.tag == SCR_DYNH_FETCH_REQUEST; + return sf_name(name->data, name->len, "Headers") && value->v.handle.tag == SCR_DYNH_FETCH_HEADERS; +} + +bool scr_fetch_stream_is(const ScrDyn *value) { + return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_WEB_STREAM; +} +ScrDyn *scr_fetch_function(void) { + if (!sf_fetch_function_value) { + sf_fetch_function_value = scr_dyn_new_func(scr_closure_new(NULL, 0), sf_fetch_function_call, 1, "fetch", "fetch"); + scr_atexit(sf_fetch_function_cleanup); + } + return scr_dyn_retain(sf_fetch_function_value); +} + +static int sf_data_hex(unsigned char ch) { + if (ch >= '0' && ch <= '9') return ch - '0'; + if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; + if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; + return -1; +} + +static ScrDyn *sf_data_response(ScrStr *url) { + size_t end = url->len; + for (size_t i = 5; i < end; i++) if (url->data[i] == '#') { end = i; break; } + size_t comma = 5; + while (comma < end && url->data[comma] != ',') comma++; + if (comma == end) { sf_type_error("fetch failed"); return NULL; } + size_t meta_end = comma; + bool base64 = meta_end >= 12 && strncasecmp(url->data + meta_end - 7, ";base64", 7) == 0; + if (base64) meta_end -= 7; + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)(end - comma - 1)); + size_t n = 0; + for (size_t i = comma + 1; i < end; i++) { + int a, b; + if (url->data[i] == '%' && i + 2 < end && (a = sf_data_hex(url->data[i + 1])) >= 0 && (b = sf_data_hex(url->data[i + 2])) >= 0) { + bytes->data[n++] = (unsigned char)((a << 4) | b); i += 2; + } else bytes->data[n++] = (unsigned char)url->data[i]; + } + bytes->len = n; + if (base64) { + ScrStr *encoded = scr_str_new((const char *)bytes->data, bytes->len); + ScrStr *encoding = scr_str_new("base64", 6); + ScrBytes *decoded = scr_bytes_from_str(encoded, encoding); + scr_str_release(encoded); scr_str_release(encoding); scr_bytes_release(bytes); bytes = decoded; + } + ScrDyn *body = scr_dyn_new_bytes(bytes); scr_bytes_release(bytes); + ScrStr *mime = meta_end == 5 ? scr_str_new("text/plain;charset=US-ASCII", 26) : scr_str_new(url->data + 5, meta_end - 5); + if (mime->len && mime->data[0] == ';') { + ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "text/plain"); for (size_t i = 0; i < mime->len; i++) scr_jb_putc(&b, mime->data[i]); + scr_str_release(mime); mime = scr_jb_finish(&b); + } + ScrDyn *headers = scr_dyn_new_obj(); scr_dyn_obj_set(headers, "content-type", 12, scr_dyn_new_str(mime)); scr_str_release(mime); + ScrDyn *init = scr_dyn_new_obj(); scr_dyn_obj_set(init, "headers", 7, headers); + ScrStr *ok = scr_str_new("OK", 2); scr_dyn_obj_set(init, "statusText", 10, scr_dyn_new_str(ok)); scr_str_release(ok); + ScrDyn *result = scr_fetch_response_new(body, init); scr_dyn_release(body); scr_dyn_release(init); + if (result) { + SfResponse *response = result->v.handle.ptr; + scr_str_release(response->url); response->url = scr_str_new(url->data, end); + response->headers->immutable = true; + } + return result; +} + ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) { + if (init && init->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(init); + if (scr_exc_pending()) { + scr_dyn_release(view); + ScrPromise *promise = scr_promise_new(); + scr_promise_reject_pending(promise); + return promise; + } + ScrPromise *result = scr_fetch_static(url, view); + scr_dyn_release(view); + return result; + } ScrPromise *promise = scr_promise_new(); if (init && init->kind != SCR_DYN_OBJ && init->kind != SCR_DYN_UNDEF && init->kind != SCR_DYN_NULL) { @@ -5324,7 +5574,8 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) { return sf_reject_now(promise, "fetch failed"); } bool https = sf_eq_ci(u->scheme, "https"); - if (!https && !sf_eq_ci(u->scheme, "http")) { + bool data_url = sf_eq_ci(u->scheme, "data"); + if (!https && !sf_eq_ci(u->scheme, "http") && !data_url) { scr_dyn_release(coerced_body); scr_arr_release(headers); scr_str_release(method); @@ -5406,6 +5657,14 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) { return promise; } + if (data_url) { + ScrDyn *response = sf_data_response(url); + scr_dyn_release(coerced_body); scr_arr_release(headers); scr_str_release(method); scr_url_release(u); + if (response) { scr_promise_resolve_dyn(promise, response); } + else scr_promise_reject_pending(promise); + return promise; + } + SfTransfer *t = calloc(1, sizeof *t); if (!t) sf_oom(); t->rc = 1; /* registry */ @@ -5457,6 +5716,9 @@ static const ScrDynHandleOps sf_controller_ops = { "ReadableStreamDefaultController", &sf_stream_retain_v, &sf_stream_release_v, &sf_controller_invoke, &sf_controller_get, &sf_no_set, NULL}; +static const ScrDynHandleOps sf_request_ops = { + "Request", &sf_request_retain_v, &sf_request_release_v, + &sf_request_invoke, &sf_request_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_response_ops = { "Response", &sf_response_retain_v, &sf_response_release_v, &sf_response_invoke, &sf_response_get, &sf_no_set, NULL}; @@ -5497,6 +5759,7 @@ void scr_fetch_install(void) { if (installed) return; installed = true; scr_tls_ca_install(); + scr_global_fetch_install(scr_fetch_function); sf_proxy_snapshot(); scr_net_install(); scr_dyn_handle_install(SCR_DYNH_ABORT_SIGNAL, &sf_signal_ops); @@ -5505,6 +5768,7 @@ void scr_fetch_install(void) { scr_dyn_handle_install(SCR_DYNH_WEB_STREAM, &sf_stream_ops); scr_dyn_handle_install(SCR_DYNH_WEB_READER, &sf_reader_ops); scr_dyn_handle_install(SCR_DYNH_WEB_CONTROLLER, &sf_controller_ops); + scr_dyn_handle_install(SCR_DYNH_FETCH_REQUEST, &sf_request_ops); scr_dyn_handle_install(SCR_DYNH_FETCH_RESPONSE, &sf_response_ops); scr_dyn_handle_install(SCR_DYNH_FETCH_HEADERS, &sf_headers_ops); scr_dyn_handle_install(SCR_DYNH_EVENT, &sf_event_ops); diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index 4397cd9c3..90a74335d 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -5567,6 +5567,8 @@ static ScrDyn *scr_http_dynh_agent_invoke(void *h, ScrDyn *self, const char *met } if (strcmp(method, "getName") == 0) { const ScrDyn *opts = argc > 0 ? args[0] : scr_dyn_undefined(); + ScrDyn *view = opts->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(opts) : NULL; + if (view != NULL) opts = view; ScrStr *host = scr_http_dynh_opt_str(opts, "host"); ScrStr *port = scr_http_dynh_opt_str(opts, "port"); ScrStr *laddr = scr_http_dynh_opt_str(opts, "localAddress"); @@ -5587,6 +5589,7 @@ static ScrDyn *scr_http_dynh_agent_invoke(void *h, ScrDyn *self, const char *met scr_str_release(port); scr_str_release(laddr); scr_str_release(spath); + scr_dyn_release(view); ScrDyn *d = scr_dyn_new_str(name); scr_str_release(name); return d; diff --git a/packages/runtime/src/scr_island.c b/packages/runtime/src/scr_island.c index 826efd019..81f5adb73 100644 --- a/packages/runtime/src/scr_island.c +++ b/packages/runtime/src/scr_island.c @@ -1002,6 +1002,12 @@ static const char *isl_dyn_unmarshalable(const ScrDyn *d) { return "a promise"; case SCR_DYN_PROXY: return "a native Proxy"; + case SCR_DYN_TYPED_REF: { + ScrDyn *view = scr_dyn_typed_ref_materialize(d); + const char *result = isl_dyn_unmarshalable(view); + scr_dyn_release(view); + return result; + } case SCR_DYN_ARR: for (size_t i = 0; i < d->v.arr.len; i++) { const char *r = isl_dyn_unmarshalable(d->v.arr.items[i]); @@ -1040,6 +1046,12 @@ static JSValue isl_from_bytes(const ScrBytes *b) { static JSValue isl_from_dyn(const ScrDyn *d) { switch (d->kind) { + case SCR_DYN_TYPED_REF: { + ScrDyn *view = scr_dyn_typed_ref_materialize(d); + JSValue result = isl_from_dyn(view); + scr_dyn_release(view); + return result; + } case SCR_DYN_FUNC: /* A boxed dyn function enters as ONE generic host-function shim over * its uniform call thunk (ScrDynThunk): engine args wrap as dyn diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 8182031ec..be05553a8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -71,7 +71,7 @@ static bool scr_builtin_iterable(const ScrDyn *value) { (scr_dyn_generator(value) && value->v.typed_ref.type_key[0] != 'a') || (value->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(value)) || (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_ITERATOR || value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET || - scr_dyn_handle_ops_of(value)->iter_pack)); + scr_dyn_handle_ops_of(value)->iter_pack || scr_dyn_handle_ops_of(value)->iter_step)); } static bool scr_iterator_object(const ScrDyn *value) { switch (value->kind) { @@ -436,6 +436,8 @@ void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) { case SCR_DYN_TYPED_REF: visit(d->v.typed_ref.materialized, ctx); if (d->v.typed_ref.traced) visit(d->v.typed_ref.ptr, ctx); + for (ScrDynTypedCast *cast = d->v.typed_ref.casts; cast; cast = cast->next) + if (cast->traced) visit(cast->ptr, ctx); break; case SCR_DYN_HANDLE: if (d->v.handle.traced) visit(d->v.handle.ptr, ctx); @@ -444,6 +446,56 @@ void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) { } } +typedef struct ScrTypedRefIdentity { + ScrDyn *value; /* weak; removed before capsule disposal */ + struct ScrTypedRefIdentity *next; +} ScrTypedRefIdentity; +static SCR_TL ScrTypedRefIdentity **scr_typed_ref_identities; +static SCR_TL size_t scr_typed_ref_capacity, scr_typed_ref_count; +static size_t scr_typed_ref_bucket(void *ptr) { + uintptr_t hash = (uintptr_t)ptr >> 4; + hash ^= hash >> 17; + hash *= (uintptr_t)0x9e3779b1u; + hash ^= hash >> 13; + return hash & (scr_typed_ref_capacity - 1); +} +static void scr_typed_ref_reserve(void) { + if (scr_typed_ref_capacity && scr_typed_ref_count < scr_typed_ref_capacity) return; + size_t old_capacity = scr_typed_ref_capacity; + ScrTypedRefIdentity **old = scr_typed_ref_identities; + scr_typed_ref_capacity = old_capacity ? old_capacity * 2 : 1024; + scr_typed_ref_identities = calloc(scr_typed_ref_capacity, sizeof *scr_typed_ref_identities); + if (!scr_typed_ref_identities) scr_json_oom(); + for (size_t i = 0; i < old_capacity; i++) { + ScrTypedRefIdentity *entry = old[i]; + while (entry) { + ScrTypedRefIdentity *next = entry->next; + size_t bucket = scr_typed_ref_bucket(entry->value->v.typed_ref.ptr); + entry->next = scr_typed_ref_identities[bucket]; + scr_typed_ref_identities[bucket] = entry; + entry = next; + } + } + free(old); +} +static void scr_typed_ref_forget(ScrDyn *value) { + ScrTypedRefIdentity **link = &scr_typed_ref_identities[scr_typed_ref_bucket(value->v.typed_ref.ptr)]; + while (*link) { + if ((*link)->value == value) { + ScrTypedRefIdentity *old = *link; + *link = old->next; + free(old); + if (--scr_typed_ref_count == 0) { + free(scr_typed_ref_identities); + scr_typed_ref_identities = NULL; + scr_typed_ref_capacity = 0; + } + return; + } + link = &(*link)->next; + } +} + static void scr_dyn_gcfree(void *ptr); static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { @@ -555,11 +607,12 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { scr_dyn_jsval_ops()->release(d->v.jsval.cell); break; case SCR_DYN_TYPED_REF: + scr_typed_ref_forget(d); if (!collected) scr_dyn_release(d->v.typed_ref.materialized); while (d->v.typed_ref.casts) { ScrDynTypedCast *cast = d->v.typed_ref.casts; d->v.typed_ref.casts = cast->next; - cast->release(cast->ptr); + if (!collected || !cast->traced) cast->release(cast->ptr); free(cast); } if (!collected || !d->v.typed_ref.traced) d->v.typed_ref.release(d->v.typed_ref.ptr); @@ -1102,6 +1155,15 @@ ScrDyn *scr_dyn_get_prototype(ScrDyn *object) { ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { if (scr_dyn_class_reflection_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; + if (object->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(object); + if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } + ScrDyn *result = scr_dyn_set_prototype(view, prototype); + scr_dyn_release(view); + if (!result) return NULL; + scr_dyn_release(result); + return scr_dyn_retain(object); + } if (object->kind == SCR_DYN_FUNC) { if (prototype->kind == SCR_DYN_FUNC && object->v.fn.clo == prototype->v.fn.clo) { static const char message[] = "Cyclic __proto__ value"; @@ -1237,9 +1299,20 @@ ScrDyn *scr_dyn_proxy_new(const ScrDyn *target, const ScrDyn *handler) { scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); return NULL; } - /* Plain dyn objects expose their live property descriptors. Other - * representations cannot promise the Proxy invariants or live identity. */ - if (target->kind != SCR_DYN_OBJ || handler->kind != SCR_DYN_OBJ) { + bool native_record = target->kind == SCR_DYN_TYPED_REF && + strncmp(target->v.typed_ref.type_key, "record:", 7) == 0; + if (native_record) { + ScrDyn *view = scr_dyn_typed_ref_materialize(target); + bool plain = view && view->kind == SCR_DYN_OBJ; + if (plain) for (size_t i = 0; i < view->v.obj.len; i++) { + ScrDynEntry *entry = &view->v.obj.entries[i]; + if (entry->accessor || !entry->configurable || !entry->writable) plain = false; + } + scr_dyn_release(view); + if (scr_exc_pending()) return NULL; + native_record = plain; + } + if ((target->kind != SCR_DYN_OBJ && !native_record) || handler->kind != SCR_DYN_OBJ) { scr_dyn_proxy_unsupported("non-plain targets or handlers"); return NULL; } @@ -1267,6 +1340,11 @@ static ScrDyn *scr_dyn_proxy_trap(const ScrDyn *proxy, const char *name) { scr_dyn_release(method); return NULL; } + if (proxy->v.proxy.target->kind == SCR_DYN_TYPED_REF) { + scr_dyn_release(method); + scr_dyn_proxy_unsupported("traps over typed native targets"); + return NULL; + } return method; } @@ -1296,6 +1374,12 @@ ScrDyn *scr_dyn_proxy_get(const ScrDyn *proxy, const ScrStr *key) { if (scr_exc_pending()) return NULL; if (!trap) { ScrDyn *target = proxy->v.proxy.target; + if (target->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(target); + ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_obj_read_receiver(view, key->data, key->len, proxy); + scr_dyn_release(view); + return result; + } if (!scr_dyn_obj_get(target, key->data, key->len) && scr_dyn_object_proto_key(target, key)) { scr_dyn_proxy_unsupported("inherited Object.prototype member reads"); return NULL; @@ -1501,6 +1585,10 @@ void scr_dyn_proxy_set(ScrDyn *proxy, ScrStr *key, ScrDyn *value) { ScrDyn *trap = scr_dyn_proxy_trap(proxy, "set"); if (scr_exc_pending()) return; if (!trap) { + if (proxy->v.proxy.target->kind == SCR_DYN_TYPED_REF) { + scr_dyn_key_set(proxy->v.proxy.target, key, value); + return; + } /* OrdinarySet uses the Proxy receiver's [[GetOwnProperty]] and * [[DefineOwnProperty]]. Keep that boundary explicit for now. */ scr_dyn_proxy_unsupported("assignment without a set trap"); @@ -1567,6 +1655,15 @@ ScrDyn *scr_dyn_new_typed_ref( const char *type_key, size_t type_key_len, ScrDyn *(*materialize)(void *), void (*commit)(void *, const ScrDyn *)) { + scr_typed_ref_reserve(); + size_t bucket = scr_typed_ref_bucket(ptr); + for (ScrTypedRefIdentity *entry = scr_typed_ref_identities[bucket]; entry; entry = entry->next) { + ScrDyn *value = entry->value; + if (value->v.typed_ref.ptr == ptr && value->v.typed_ref.retain == retain && + value->v.typed_ref.materialize == materialize && value->v.typed_ref.commit == commit && + value->v.typed_ref.type_key_len == type_key_len && !memcmp(value->v.typed_ref.type_key, type_key, type_key_len)) + return scr_dyn_retain(value); + } ScrDyn *d = scr_dyn_alloc(SCR_DYN_TYPED_REF); d->v.typed_ref.ptr = retain(ptr); d->v.typed_ref.retain = retain; @@ -1578,6 +1675,12 @@ ScrDyn *scr_dyn_new_typed_ref( d->v.typed_ref.materialized = NULL; d->v.typed_ref.casts = NULL; d->v.typed_ref.traced = false; + d->v.typed_ref.observed = false; + ScrTypedRefIdentity *identity = malloc(sizeof *identity); + if (!identity) scr_json_oom(); + identity->value = d; identity->next = scr_typed_ref_identities[bucket]; + scr_typed_ref_identities[bucket] = identity; + scr_typed_ref_count++; return d; } @@ -1589,6 +1692,16 @@ ScrDyn *scr_dyn_new_typed_ref_traced( ScrDyn *value = scr_dyn_new_typed_ref(ptr, retain, release, type_key, type_key_len, materialize, commit); value->v.typed_ref.traced = true; + value->v.typed_ref.observed = true; + return value; +} + +ScrDyn *scr_dyn_new_typed_ref_observed( + void *ptr, void *(*retain)(void *), void (*release)(void *), + const char *type_key, size_t type_key_len, + ScrDyn *(*materialize)(void *), void (*commit)(void *, const ScrDyn *)) { + ScrDyn *value = scr_dyn_new_typed_ref(ptr, retain, release, type_key, type_key_len, materialize, commit); + value->v.typed_ref.observed = true; return value; } @@ -1670,6 +1783,34 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { * detached contents without changing the cached node's address. */ scr_dyn_typed_ref_preserve_children( capsule->v.typed_ref.materialized, fresh); + /* Record layouts cannot store properties added through a checked + * view. Keep those descriptors in the view across native refreshes. */ + ScrDyn *cached = capsule->v.typed_ref.materialized; + if (fresh->kind == SCR_DYN_OBJ && cached->kind == SCR_DYN_OBJ && + capsule->v.typed_ref.type_key_len >= 7 && + memcmp(capsule->v.typed_ref.type_key, "record:", 7) == 0) { + for (size_t i = 0; i < cached->v.obj.len; i++) { + ScrDynEntry *entry = &cached->v.obj.entries[i]; + if (scr_dyn_obj_get(fresh, entry->key, entry->key_len)) continue; + ScrStr *name = scr_str_new(entry->key, entry->key_len); + ScrDyn *key = scr_dyn_new_str(name); + ScrDyn *descriptor = scr_dyn_own_descriptor(cached, name); + ScrDyn *defined = scr_dyn_define_property(fresh, key, descriptor); + scr_dyn_release(defined); + scr_dyn_release(descriptor); + scr_dyn_release(key); + scr_str_release(name); + } + } + /* A native record stores its own fields, while its checked view owns + * prototype changes. Refreshing fields must preserve that prototype. */ + scr_dyn_release(fresh->prototype); + fresh->prototype = capsule->v.typed_ref.materialized->prototype ? scr_dyn_retain(capsule->v.typed_ref.materialized->prototype) : NULL; + fresh->null_proto = capsule->v.typed_ref.materialized->null_proto; + scr_dyn_release(fresh->symbol_properties); + scr_dyn_release(fresh->symbol_keys); + fresh->symbol_properties = capsule->v.typed_ref.materialized->symbol_properties ? scr_dyn_retain(capsule->v.typed_ref.materialized->symbol_properties) : NULL; + fresh->symbol_keys = capsule->v.typed_ref.materialized->symbol_keys ? scr_dyn_retain(capsule->v.typed_ref.materialized->symbol_keys) : NULL; size_t cached_rc = capsule->v.typed_ref.materialized->rc; size_t fresh_rc = fresh->rc; ScrDyn old = *capsule->v.typed_ref.materialized; @@ -1704,7 +1845,7 @@ void *scr_dyn_typed_ref_cached_cast( void scr_dyn_typed_ref_cache_cast( ScrDyn *d, const char *type_key, size_t type_key_len, void *ptr, - void *(*retain)(void *), void (*release)(void *)) { + void *(*retain)(void *), void (*release)(void *), bool traced) { if (!ptr) return; ScrDynTypedCast *cast = malloc(sizeof *cast); if (!cast) scr_trap("scriptc: out of memory\n"); @@ -1713,10 +1854,15 @@ void scr_dyn_typed_ref_cache_cast( cast->ptr = retain(ptr); cast->retain = retain; cast->release = release; + cast->traced = traced; cast->next = d->v.typed_ref.casts; d->v.typed_ref.casts = cast; } +bool scr_dyn_typed_array_is(const ScrDyn *d, int elem) { + return d && d->kind == SCR_DYN_BYTES && !d->v.bytes->is_data_view && d->v.bytes->elem == (ScrBytesElem)elem; +} + bool scr_dyn_bytes_is(const ScrDyn *d, int elem) { return d && d->kind == SCR_DYN_BYTES && d->v.bytes->elem == (ScrBytesElem)elem; } @@ -1773,7 +1919,43 @@ ScrDyn *scr_bytes_constructor(const ScrStr *name) { } scr_trap("scriptc: invalid typed array constructor name\n"); } +static SCR_TL ScrDyn *scr_array_buffer_ctor; +static void scr_array_buffer_ctor_cleanup(void) { + scr_dyn_release(scr_array_buffer_ctor); + scr_array_buffer_ctor = NULL; +} +static ScrDyn *scr_array_buffer_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + static const char message[] = "Constructor ArrayBuffer requires 'new'"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; +} +ScrDyn *scr_array_buffer_constructor(void) { + if (!scr_array_buffer_ctor) { + scr_array_buffer_ctor = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_array_buffer_ctor_call, 1, "native:ArrayBuffer", "ArrayBuffer"); + scr_atexit(scr_array_buffer_ctor_cleanup); + } + return scr_dyn_retain(scr_array_buffer_ctor); +} + +bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { + if (!callee || callee->kind != SCR_DYN_FUNC) { + static const char message[] = "Right-hand side of 'instanceof' is not callable"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + if (scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_is(value); + for (size_t i = 0; i < 9; i++) { + if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) + return scr_dyn_typed_array_is(value, (int)i); + } + static const char message[] = "instanceof against this native constructor is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); + return false; +} ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { + if (callee->kind == SCR_DYN_FUNC && scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) + return scr_array_buffer_new(args->v.arr.len ? args->v.arr.items[0] : scr_dyn_undefined()); for (size_t i = 0; i < 9; i++) { if (callee->kind != SCR_DYN_FUNC || !scr_bytes_ctors[i] || callee->v.fn.clo != scr_bytes_ctors[i]->v.fn.clo) continue; const ScrDyn *input = args->v.arr.len > 0 ? args->v.arr.items[0] : scr_dyn_undefined(); @@ -2219,8 +2401,9 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) { ScrDyn *d = scr_dyn_alloc(SCR_DYN_HANDLE); d->v.handle.ptr = scr_dyn_handle_ops(tag)->retain(h); d->v.handle.tag = tag; - d->v.handle.traced = (tag == SCR_DYNH_SET || tag == SCR_DYNH_MAP) && - (((ScrMap *)h)->key_trace != NULL || ((ScrMap *)h)->val_trace != NULL); + d->v.handle.traced = tag == SCR_DYNH_WEAK_MAP || tag == SCR_DYNH_WEAK_SET || + ((tag == SCR_DYNH_SET || tag == SCR_DYNH_MAP) && + (((ScrMap *)h)->key_trace != NULL || ((ScrMap *)h)->val_trace != NULL)); return d; } @@ -2229,6 +2412,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) { * scr_promise_release edge installs HERE — a promise-free link never * references the fiber machinery (the unit-test subset links). */ void (*scr_dyn_promise_release_fn)(ScrPromise *p) = NULL; +bool (*scr_dyn_promise_identity_fn)(ScrPromise *a, ScrPromise *b) = NULL; ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p)) { scr_dyn_promise_release_fn = release_fn; @@ -2257,6 +2441,7 @@ const ScrDynJsvalOps *scr_dyn_jsval_ops(void) { } bool scr_dyn_isl_typeof_is(const ScrDyn *d, const char *name) { + if (scr_dyn_generator(d)) return strcmp(name, "object") == 0; if (d->kind == SCR_DYN_TYPED_REF) { if (d->v.typed_ref.type_key_len >= 7 && memcmp(d->v.typed_ref.type_key, "object:", 7) == 0) @@ -2302,6 +2487,7 @@ bool scr_dyn_isl_is_array(const ScrDyn *d) { } bool scr_dyn_isl_is_error(const ScrDyn *d) { + if (scr_dyn_generator(d)) return false; /* Registered native Error capsules retain their brand even when their * public property view omits the internal error marker. */ ScrError *error = scr_errdyn_err_of(d); @@ -2468,6 +2654,7 @@ static ScrStr *scr_dyn_property_key(const ScrDyn *key) { } static ScrDynEntry *scr_dyn_entry(ScrDyn *obj, const ScrStr *key) { + if (obj->kind != SCR_DYN_OBJ) return NULL; for (size_t i = 0; i < obj->v.obj.len; i++) { ScrDynEntry *entry = &obj->v.obj.entries[i]; if (entry->key_len == key->len && memcmp(entry->key, key->data, key->len) == 0) return entry; @@ -2574,6 +2761,35 @@ void scr_dyn_class_super(ScrDyn *constructor, ScrDyn *receiver, ScrDyn *argument scr_dyn_release(result); } +bool scr_dyn_data_view_is(const ScrDyn *value) { + return value->kind == SCR_DYN_BYTES && value->v.bytes->is_data_view; +} + +/* Ordinary native function constructors retain their live prototype table. + * A constructor may replace the fresh receiver with another object. */ +ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { + if (callee->kind != SCR_DYN_FUNC || callee->v.fn.class_obj || callee->v.fn.clo->function_kind != 4) + return scr_bytes_construct(callee, args, what); + ScrDyn *prototype = scr_dyn_fn_get(callee, "prototype", 9); + if (!prototype) return NULL; + ScrDyn *receiver = scr_iterator_object(prototype) ? scr_dyn_obj_create(prototype) : scr_dyn_new_obj(); + scr_dyn_release(prototype); + if (!receiver) return NULL; + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(callee, args->v.arr.items, args->v.arr.len, what->data); + scr_dyn_this_pop(); + if (!result) { + scr_dyn_release(receiver); + return NULL; + } + if (scr_iterator_object(result)) { + scr_dyn_release(receiver); + return result; + } + scr_dyn_release(result); + return receiver; +} + /* Symbol descriptors use the ordinary descriptor implementation in an * inaccessible table. Keep a retained key list so identities cannot be * recycled while properties exist, and reflection returns real symbols. */ @@ -2584,11 +2800,11 @@ static ScrStr *scr_dyn_symbol_storage_key(const ScrDyn *key) { } static ScrDyn *scr_dyn_symbol_receiver(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(value); if (value->kind == SCR_DYN_FUNC) return scr_dyn_fn_properties(value); - if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR) + if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_HANDLE) return scr_dyn_retain((ScrDyn *)value); - if (value->kind == SCR_DYN_TYPED_REF || value->kind == SCR_DYN_HANDLE || - value->kind == SCR_DYN_PROXY || value->kind == SCR_DYN_JSVAL) { + if (value->kind == SCR_DYN_PROXY || value->kind == SCR_DYN_JSVAL) { static const char message[] = "Symbol properties on this native value have no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); } @@ -2631,11 +2847,11 @@ typedef struct { static ScrDyn *scr_iterator_read(const ScrDyn *value, const char *key, size_t len); -static SCR_TL ScrDyn *scr_iterator_functions[10]; +static SCR_TL ScrDyn *scr_iterator_functions[11]; static SCR_TL bool scr_iterator_functions_registered; static void scr_iterator_functions_cleanup(void) { - for (size_t i = 0; i < 10; i++) { + for (size_t i = 0; i < 11; i++) { ScrDyn *function = scr_iterator_functions[i]; scr_iterator_functions[i] = NULL; scr_dyn_release(function); @@ -2662,19 +2878,28 @@ static void scr_native_iterator_release(void *ptr) { ScrNativeIterator *iterator = ptr; if (--iterator->rc) return; scr_weak_dispose(iterator); - if (iterator->source && iterator->kind >= 2) scr_map_iter_exit(iterator->source->v.handle.ptr); + if (iterator->source && (iterator->kind == 2 || iterator->kind == 3)) scr_map_iter_exit(iterator->source->v.handle.ptr); scr_dyn_release(iterator->source); free(iterator); } static ScrDyn *scr_native_iterator_next(ScrNativeIterator *iterator) { ScrDyn *source = iterator->source; + if (source && iterator->kind == 4) { + ScrDyn *result = scr_dyn_handle_ops_of(source)->iter_step(source->v.handle.ptr, &iterator->index, iterator->selection); + if (!result) return NULL; + if (scr_dyn_truthy(scr_dyn_obj_get(result, "done", 4))) { + iterator->source = NULL; + scr_dyn_release(source); + } + return result; + } /* Native array capsules retain the live array, so refresh its view at each * step rather than iterating a snapshot taken when the iterator opened. */ ScrDyn *view = source && source->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(source) : NULL; if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } ScrDyn *items = view ? view : source; - ScrMap *map = source && iterator->kind >= 2 ? source->v.handle.ptr : NULL; + ScrMap *map = source && (iterator->kind == 2 || iterator->kind == 3) ? source->v.handle.ptr : NULL; if (map) while (iterator->index < scr_map_iter_count(map) && !scr_map_iter_live(map, iterator->index)) iterator->index++; double length = !items ? 0 : iterator->array_like ? iterator->limit : map ? scr_map_iter_count(map) : items->kind == SCR_DYN_ARR ? (double)items->v.arr.len : items->kind == SCR_DYN_BYTES ? (double)items->v.bytes->len : scr_str_utf16_len(items->v.str); @@ -2752,9 +2977,15 @@ static ScrDyn *scr_native_set_iterator_next_call(ScrClosure *closure, ScrDyn *co return scr_native_iterator_next_checked(3); } +static ScrDyn *scr_native_handle_iterator_next_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_native_iterator_next_checked(4); +} + static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) { if (len != 4 || memcmp(key, "next", 4)) return NULL; unsigned kind = ((ScrNativeIterator *)ptr)->kind; + if (kind == 4) return scr_iterator_function(10, &scr_native_handle_iterator_next_call, "next"); return kind == 3 ? scr_iterator_function(8, &scr_native_set_iterator_next_call, "next") : kind == 2 ? scr_iterator_function(7, &scr_native_map_iterator_next_call, "next") : kind == 1 ? scr_iterator_function(5, &scr_native_string_iterator_next_call, "next") @@ -2779,9 +3010,10 @@ static ScrDyn *scr_native_iterator_new(ScrDyn *source) { if (!iterator) scr_json_oom(); *iterator = (ScrNativeIterator){ .rc = 1, .source = scr_dyn_retain(source), .index = 0, .kind = source->kind == SCR_DYN_STR ? 1 : source->kind == SCR_DYN_HANDLE && source->v.handle.tag == SCR_DYNH_MAP ? 2 - : source->kind == SCR_DYN_HANDLE && source->v.handle.tag == SCR_DYNH_SET ? 3 : 0, - .selection = source->kind == SCR_DYN_HANDLE && source->v.handle.tag == SCR_DYNH_MAP ? 2 : 0 }; - if (iterator->kind >= 2) scr_map_iter_enter(source->v.handle.ptr); + : source->kind == SCR_DYN_HANDLE && source->v.handle.tag == SCR_DYNH_SET ? 3 + : source->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(source)->iter_step ? 4 : 0, + .selection = source->kind == SCR_DYN_HANDLE && (source->v.handle.tag == SCR_DYNH_MAP || scr_dyn_handle_ops_of(source)->iter_step) ? 2 : 0 }; + if (iterator->kind == 2 || iterator->kind == 3) scr_map_iter_enter(source->v.handle.ptr); ScrDyn *result = scr_dyn_new_handle(iterator, SCR_DYNH_ITERATOR); scr_native_iterator_release(iterator); return result; @@ -2793,6 +3025,10 @@ static ScrDyn *scr_native_collection_iterator(ScrDyn *source, unsigned selection return result; } +ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection) { + return scr_native_collection_iterator(source, selection); +} + static ScrDyn *scr_builtin_iterator_checked(int kind) { ScrDyn *self = scr_dyn_this_get(); ScrDyn *result = NULL; @@ -2829,7 +3065,7 @@ static ScrDyn *scr_iterator_self_call(ScrClosure *closure, ScrDyn *const *args, static ScrDyn *scr_handle_iterator_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; (void)args; (void)argc; ScrDyn *self = scr_dyn_this_get(); - if (self->kind == SCR_DYN_HANDLE && (self->v.handle.tag == SCR_DYNH_MAP || self->v.handle.tag == SCR_DYNH_SET)) { + if (self->kind == SCR_DYN_HANDLE && (self->v.handle.tag == SCR_DYNH_MAP || self->v.handle.tag == SCR_DYNH_SET || scr_dyn_handle_ops_of(self)->iter_step)) { ScrDyn *result = scr_native_iterator_new(self); scr_dyn_release(self); return result; @@ -2850,7 +3086,7 @@ static ScrDyn *scr_builtin_iterator_method(const ScrDyn *value) { if (value->kind == SCR_DYN_ARR || (value->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(value))) return scr_dyn_array_values_function(); if (value->kind == SCR_DYN_BYTES) return scr_iterator_function(2, &scr_builtin_bytes_iterator_call, "values"); if (value->kind == SCR_DYN_STR) return scr_iterator_function(3, &scr_builtin_string_iterator_call, "[Symbol.iterator]"); - if (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET || scr_dyn_handle_ops_of(value)->iter_pack)) + if (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET || scr_dyn_handle_ops_of(value)->iter_pack || scr_dyn_handle_ops_of(value)->iter_step)) return scr_iterator_function(6, &scr_handle_iterator_call, "[Symbol.iterator]"); return scr_iterator_function(4, &scr_iterator_self_call, "[Symbol.iterator]"); } @@ -2875,6 +3111,21 @@ static ScrDyn *scr_iterator_method(const ScrDyn *source) { } /* Compiled class methods are dispatched before this checked-object path. */ +ScrDyn *scr_dyn_iterator_optional(const ScrDyn *source) { + ScrDyn *method = scr_iterator_method(source); + if (!method) return NULL; + if (method->kind == SCR_DYN_UNDEF || method->kind == SCR_DYN_NULL) { + scr_dyn_release(method); + return NULL; + } + ScrDyn *iterator = scr_iterator_call(method, source, "value[Symbol.iterator]"); + scr_dyn_release(method); + if (!iterator) return NULL; + ScrDyn *checked = scr_dyn_iterator_result(iterator); + scr_dyn_release(iterator); + return checked; +} + ScrDyn *scr_dyn_iterator(const ScrDyn *source, const ScrStr *spell) { if (source->kind == SCR_DYN_JSVAL) return scr_dyn_jsval_ops()->iterator(source->v.jsval.cell, spell, false); if (source->kind == SCR_DYN_NULL || source->kind == SCR_DYN_UNDEF) return scr_dyn_not_iterable(source, spell); @@ -3148,6 +3399,20 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) { } ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { + if (target->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(target); + ScrDyn *defined = scr_exc_pending() ? NULL : scr_dyn_define_property(view, key, descriptor); + if (!scr_exc_pending()) scr_dyn_typed_ref_commit(target); + scr_dyn_release(defined); + scr_dyn_release(view); + return scr_exc_pending() ? NULL : scr_dyn_retain(target); + } + if (descriptor->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(descriptor); + ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_define_property(target, key, view); + scr_dyn_release(view); + return result; + } if (key->kind == SCR_DYN_SYMBOL) return scr_dyn_define_symbol(target, key, descriptor); if (target->kind == SCR_DYN_PROXY || descriptor->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.defineProperty"); @@ -3693,6 +3958,7 @@ static void scr_errdyn_teardown(void) { ScrDyn *scr_error_dyn_fields(const ScrError *e) { ScrDyn *d = scr_dyn_new_obj(); scr_dyn_obj_set(d, "%error", 6, scr_dyn_new_bool(true)); /* the checked-dynamic tree's error marker */ + d->v.obj.entries[d->v.obj.len - 1].enumerable = false; if (e->name_present) { scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name)); d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable; @@ -4043,20 +4309,7 @@ ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, cons scr_str_release(encoding); return result; } - if (d->kind == SCR_DYN_NUM && argument->kind != SCR_DYN_UNDEF) { - double radix; - if (!scr_dyn_number_coerce_js(argument, &radix)) return NULL; - if (!isfinite(radix) || trunc(radix) < 2 || trunc(radix) > 36) { - static const char message[] = "toString() radix argument must be between 2 and 36"; - scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1); - return NULL; - } - if (trunc(radix) != 10) { - static const char message[] = "Number.toString with a non-decimal radix has no native lowering"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); - return NULL; - } - } + if (d->kind == SCR_DYN_NUM) return scr_num_to_string_radix(d->v.num, argument); if (d->kind == SCR_DYN_OBJ && !scr_dyn_property_owner(d, "toString", 8) && !scr_dyn_has_object_prototype(d)) { ScrJsonBuf b; scr_jb_init(&b); @@ -4285,6 +4538,26 @@ double scr_dyn_number_constructor(const ScrDyn *d) { return result; } +ScrBigInt *scr_dyn_bigint_constructor(const ScrDyn *d) { + ScrDyn *primitive = scr_dyn_add_primitive(d); + if (!primitive) return NULL; + ScrBigInt *result = NULL; + switch (primitive->kind) { + case SCR_DYN_BIGINT: result = scr_bigint_retain(primitive->v.bigint); break; + case SCR_DYN_STR: result = scr_bigint_parse(primitive->v.str); break; + case SCR_DYN_NUM: result = scr_bigint_from_f64(primitive->v.num); break; + case SCR_DYN_BOOL: result = scr_bigint_from_i64(primitive->v.b ? 1 : 0); break; + default: { + const char *message = primitive->kind == SCR_DYN_NULL ? "Cannot convert null to a BigInt" + : primitive->kind == SCR_DYN_UNDEF ? "Cannot convert undefined to a BigInt" : "Cannot convert Symbol() to a BigInt"; + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); + break; + } + } + scr_dyn_release(primitive); + return result; +} + /* OrdinaryToPrimitive's default hint is numeric for the represented * ordinary objects. Retain the primitive itself: addition must choose * string concatenation before either side is converted to a number. */ @@ -4396,6 +4669,44 @@ ScrDyn *scr_dyn_increment(const ScrDyn *value, bool increment) { return scr_dyn_new_num(value->v.num + (increment ? 1 : -1)); } +bool scr_dyn_compare(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation) { + // ToPrimitive is observable and always visits the original left operand + // first, including the > and <= spellings of relational comparison. + ScrDyn *l = scr_dyn_add_primitive(left); + if (!l) return false; + ScrDyn *r = scr_dyn_add_primitive(right); + if (!r) { scr_dyn_release(l); return false; } + double order = 2; + if (l->kind == SCR_DYN_STR && r->kind == SCR_DYN_STR) { + int compared = scr_str_cmp_u16(l->v.str, r->v.str); + order = compared < 0 ? -1 : compared > 0 ? 1 : 0; + } else if (l->kind == SCR_DYN_BIGINT && r->kind == SCR_DYN_STR) { + order = scr_bigint_cmp_string(l->v.bigint, r->v.str); + } else if (l->kind == SCR_DYN_STR && r->kind == SCR_DYN_BIGINT) { + double opposite = scr_bigint_cmp_string(r->v.bigint, l->v.str); + order = opposite == 2 ? 2 : -opposite; + } else if (l->kind == SCR_DYN_BIGINT && r->kind == SCR_DYN_BIGINT) { + order = scr_bigint_cmp_f64(l->v.bigint, r->v.bigint); + } else { + double a = 0, b = 0; + bool valid = (l->kind == SCR_DYN_BIGINT || scr_dyn_number_coerce_js(l, &a)) && + (r->kind == SCR_DYN_BIGINT || scr_dyn_number_coerce_js(r, &b)); + if (valid) { + if (l->kind == SCR_DYN_BIGINT) order = scr_bigint_cmp_number(l->v.bigint, b); + else if (r->kind == SCR_DYN_BIGINT) { + double opposite = scr_bigint_cmp_number(r->v.bigint, a); + order = opposite == 2 ? 2 : -opposite; + } else if (!isnan(a) && !isnan(b)) order = a < b ? -1 : a > b ? 1 : 0; + } + } + scr_dyn_release(l); + scr_dyn_release(r); + if (scr_exc_pending() || order == 2) return false; + return operation->data[0] == '<' + ? (operation->len == 2 ? order <= 0 : order < 0) + : (operation->len == 2 ? order >= 0 : order > 0); +} + ScrDyn *scr_dyn_arithmetic(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation) { ScrDyn *l = scr_dyn_to_numeric(left); if (!l) return NULL; @@ -4477,6 +4788,12 @@ static const char *scr_dyn_kind_name(const ScrDyn *d); * valid dense index, every other kind false (tsc admits `in` only on * object-typed operands). Proxy traps may throw. Borrows both. */ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) { + if (scr_dyn_generator(v)) { + static const char *const inherited[] = { "next", "return", "throw", "constructor", "toString", "valueOf", "hasOwnProperty", "propertyIsEnumerable", "isPrototypeOf", "toLocaleString", "__proto__" }; + for (size_t i = 0; i < sizeof inherited / sizeof inherited[0]; i++) + if (key->len == strlen(inherited[i]) && memcmp(key->data, inherited[i], key->len) == 0) return true; + return false; + } if (scr_dyn_class_reflection_fence(v)) return false; if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key); if (v->kind == SCR_DYN_JSVAL) return scr_dyn_isl_fence(v, "'in'"); @@ -5505,6 +5822,14 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { if (scr_dyn_class_reflection_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; } + if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit && + !strncmp(recv->v.typed_ref.type_key, "record:", 7)) { + ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); + if (!scr_exc_pending()) scr_dyn_key_delete(materialized, key, strict); + if (!scr_exc_pending()) scr_dyn_typed_ref_commit(recv); + scr_dyn_release(materialized); + return; + } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; ScrDyn *table = scr_dyn_fn_properties(recv); @@ -5816,7 +6141,7 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b) { return a->v.handle.tag == b->v.handle.tag && a->v.handle.ptr == b->v.handle.ptr; case SCR_DYN_PROMISE: /* And the PROMISE: one promise crossing twice is one JS value. */ - return a->v.promise == b->v.promise; + return a->v.promise == b->v.promise || (scr_dyn_promise_identity_fn && scr_dyn_promise_identity_fn(a->v.promise, b->v.promise)); case SCR_DYN_JSVAL: /* Identity is the ENGINE VALUE, not the box or even the cell: two * wraps of one engine value compare ===-equal (the engine's own @@ -5925,6 +6250,12 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { void scr_sc_validate_options(const ScrDyn *options) { if (options == NULL || options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL) return; + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(options); + if (view && !scr_exc_pending()) scr_sc_validate_options(view); + scr_dyn_release(view); + return; + } /* An engine-held options bag IS a dictionary to Node — the "cannot be * converted" TypeError would be a wrong claim. Loud fence. */ if (options->kind == SCR_DYN_JSVAL) { @@ -5939,10 +6270,14 @@ void scr_sc_validate_options(const ScrDyn *options) { } ScrDyn *tr = scr_dyn_obj_get(options, "transfer", 8); /* borrowed */ if (tr == NULL || tr->kind == SCR_DYN_UNDEF) return; + ScrDyn *transferView = tr->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(tr) : NULL; + if (scr_exc_pending()) { scr_dyn_release(transferView); return; } + if (transferView) tr = transferView; if (tr->kind != SCR_DYN_ARR) { static const char msg[] = "Failed to execute 'structuredClone': transfer in Options can not be converted to sequence."; scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE"); + scr_dyn_release(transferView); return; } if (tr->v.arr.len > 0) { @@ -5950,6 +6285,7 @@ void scr_sc_validate_options(const ScrDyn *options) { * a non-transferable list member. */ scr_throw_domex("DataCloneError", "Found invalid value in transferList."); } + scr_dyn_release(transferView); } /* The parent chain rides the C stack: a revisit is a cycle. */ @@ -6860,6 +7196,33 @@ ScrDyn *scr_dyn_copy_data_properties(ScrDyn *target, const ScrDyn *src) { return scr_exc_pending() ? NULL : scr_dyn_retain(target); } +ScrDyn *scr_dyn_object_rest(const ScrDyn *source, const ScrDyn *excluded) { + ScrDyn *out = scr_dyn_new_obj(); + ScrDyn *names = scr_dyn_get_own_property_names(source); + if (!names) { scr_dyn_release(out); return NULL; } + ScrDyn *symbols = scr_dyn_get_own_property_symbols(source); + if (!symbols) { scr_dyn_release(names); scr_dyn_release(out); return NULL; } + for (size_t i = 0; i < symbols->v.arr.len; i++) scr_dyn_arr_push(names, scr_dyn_retain(symbols->v.arr.items[i])); + scr_dyn_release(symbols); + for (size_t i = 0; i < names->v.arr.len && !scr_exc_pending(); i++) { + ScrDyn *key = names->v.arr.items[i]; + bool skip = false; + for (size_t j = 0; j < excluded->v.arr.len; j++) { + if (scr_dyn_strict_eq(key, excluded->v.arr.items[j])) { skip = true; break; } + } + if (skip || !scr_dyn_key_probe_computed(source, key, 2)) continue; + ScrDyn *value = key->kind == SCR_DYN_SYMBOL ? scr_dyn_symbol_key_get(source, key, false) : scr_iterator_read(source, key->v.str->data, key->v.str->len); + if (!scr_exc_pending()) { + if (key->kind == SCR_DYN_SYMBOL) scr_dyn_symbol_key_set(out, key, value); + else scr_dyn_obj_set(out, key->v.str->data, key->v.str->len, scr_dyn_retain(value)); + } + scr_dyn_release(value); + } + scr_dyn_release(names); + if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } + return out; +} + /* Compiler-owned native class views retain every property and its flags, * including nonenumerable data, without invoking accessors. */ ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) { @@ -6903,6 +7266,19 @@ void scr_dyn_pack_push_spread(ScrDyn *pack, const ScrDyn *src, const ScrStr *wha * Symbol(Symbol.iterator))". The compiler picks the variant by the * spread's syntactic position. MAY THROW (pending). Borrows src. */ void scr_dyn_pack_push_spread_iter(ScrDyn *pack, const ScrDyn *src) { + if (src->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(src); + if (!scr_exc_pending()) scr_dyn_pack_push_spread_iter(pack, view); + scr_dyn_release(view); + return; + } + if (src->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(src)->iter_pack) { + ScrDyn *items = scr_dyn_handle_ops_of(src)->iter_pack(src->v.handle.ptr); + if (!items) return; + scr_dyn_pack_push_spread_iter(pack, items); + scr_dyn_release(items); + return; + } if (src->kind == SCR_DYN_ARR || src->kind == SCR_DYN_BYTES || src->kind == SCR_DYN_STR) { scr_dyn_arr_push_spread(pack, src, ""); /* iterable kinds never throw */ @@ -6944,6 +7320,10 @@ void scr_dyn_pack_push_spread_iter(ScrDyn *pack, const ScrDyn *src) { * ToObject TypeError first (Node throws before looking at sources), then * each source's own-member copy left to right, answering the target * retained (+1) — identity, like JS. */ +ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list) { + return scr_dyn_apply_array_like(target, receiver, arguments_list, "Reflect.apply"); +} + ScrDyn *scr_dyn_assign_all(ScrDyn *target, const ScrDyn *sources) { if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) { const char *m = "Cannot convert undefined or null to object"; @@ -7072,6 +7452,12 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) { bool scr_dyn_property_is_enumerable(const ScrDyn *value, const ScrStr *key) { if (scr_dyn_class_reflection_fence(value)) return false; + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + bool result = !scr_exc_pending() && scr_dyn_property_is_enumerable(view, key); + scr_dyn_release(view); + return result; + } if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) { static const char message[] = "Cannot convert undefined or null to object"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -7176,6 +7562,12 @@ void scr_error_set_cause(ScrError *e, ScrDyn *value) { e->error_cause = replacement; } +void scr_error_define_cause(ScrError *e, ScrDyn *value) { + scr_error_delete_cause(e); + scr_error_set_cause(e, value); +} + + void scr_error_delete_cause(ScrError *e) { for (size_t i = 0; i < scr_errdyn_n; i++) { if (scr_errdyn_cache[i].err != e) continue; @@ -7820,7 +8212,7 @@ typedef struct ScrWeakEntry { } ScrWeakEntry; struct ScrWeakMap { size_t rc; ScrWeakEntry *entries; bool is_set; }; -/* Weak observers borrow key identities. Values remain ordinary RC edges; +/* Weak observers borrow key identities. Values are traced strong edges; * value-to-key cycles require explicit deletion or container disposal. */ #define SCR_WEAK_BUCKETS 1024 static SCR_TL ScrWeakEntry *scr_weak_entries[SCR_WEAK_BUCKETS]; @@ -7829,7 +8221,7 @@ static size_t scr_weak_bucket(void *key) { return ((bits >> 4) ^ (bits >> 14)) & (SCR_WEAK_BUCKETS - 1); } -static void scr_weak_remove(ScrWeakEntry *entry) { +static void scr_weak_remove_impl(ScrWeakEntry *entry, bool collected) { ScrWeakEntry **slot = &entry->owner->entries; while (*slot != entry) slot = &(*slot)->next; *slot = entry->next; @@ -7839,7 +8231,22 @@ static void scr_weak_remove(ScrWeakEntry *entry) { ScrDyn *value = entry->value; free(entry); /* Unlink before release: releasing a value can dispose another key. */ - scr_dyn_release(value); + if (!collected) scr_dyn_release(value); +} + +static void scr_weak_remove(ScrWeakEntry *entry) { + scr_weak_remove_impl(entry, scr_cyc_hdr(entry->owner)->color == SCR_CYC_DOOMED); +} + +static void scr_weak_map_trace(void *ptr, ScrTraceVisit visit, void *ctx) { + for (ScrWeakEntry *entry = ((ScrWeakMap *)ptr)->entries; entry; entry = entry->next) + visit(entry->value, ctx); +} + +static void scr_weak_map_gcfree(void *ptr) { + ScrWeakMap *map = ptr; + while (map->entries) scr_weak_remove_impl(map->entries, true); + scr_cyc_free(map); } static void scr_weak_disposed(void *key) { @@ -7858,10 +8265,11 @@ static void *scr_weak_map_retain(void *ptr) { static void scr_weak_map_release(void *ptr) { ScrWeakMap *map = ptr; - if (--map->rc) return; + if (--map->rc) { scr_cyc_on_release(map); return; } + scr_cyc_on_dead(map); scr_weak_dispose(map); while (map->entries) scr_weak_remove(map->entries); - free(map); + scr_cyc_free(map); } /* Return 1 for an observed identity, 0 for a primitive, and -1 for a @@ -7882,10 +8290,12 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) { *kind = key->v.fn.class_obj ? 3 : 2; return 1; case SCR_DYN_HANDLE: if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP && - key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO) return -1; + key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO && + key->v.handle.tag != SCR_DYNH_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE && + key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET) return -1; *ptr = key->v.handle.ptr; *kind = 4 + key->v.handle.tag; return 1; case SCR_DYN_TYPED_REF: - if (!key->v.typed_ref.traced) return -1; + if (!key->v.typed_ref.observed) return -1; *ptr = key->v.typed_ref.ptr; *kind = 32; return 1; case SCR_DYN_PROMISE: *ptr = key->v.promise; *kind = 33; return 1; @@ -8021,7 +8431,7 @@ static ScrDyn *scr_weak_collection_new(ScrDyn *seed, bool is_set) { ScrDynHandleTag tag = is_set ? SCR_DYNH_WEAK_SET : SCR_DYNH_WEAK_MAP; scr_weak_dispose_hook = &scr_weak_disposed; scr_dyn_handle_install(tag, is_set ? &set_ops : &map_ops); - ScrWeakMap *map = calloc(1, sizeof *map); + ScrWeakMap *map = scr_cyc_alloc(sizeof *map, scr_weak_map_trace, scr_weak_map_gcfree); if (!map) scr_json_oom(); map->rc = 1; map->is_set = is_set; @@ -8257,10 +8667,21 @@ static ScrDyn *scr_native_set_invoke(void *ptr, ScrDyn *self, const char *method return NULL; } +static ScrDyn *scr_native_set_pack(void *ptr) { + ScrMap *set = ptr; + ScrDyn *values = scr_dyn_new_arr(); + scr_map_iter_enter(set); + for (double i = 0; i < scr_map_iter_count(set); i++) { + if (scr_map_iter_live(set, i)) scr_dyn_arr_push(values, scr_map_dyn_key(set, i)); + } + scr_map_iter_exit(set); + return values; +} + ScrDyn *scr_dyn_native_set(ScrMap *value) { static const ScrDynHandleOps ops = { "Set", &scr_map_retain_v, &scr_map_release_v, &scr_native_set_invoke, - &scr_native_set_get, &scr_weak_map_write, NULL, NULL, + &scr_native_set_get, &scr_weak_map_write, NULL, &scr_native_set_pack, }; scr_dyn_handle_install(SCR_DYNH_SET, &ops); return scr_dyn_new_handle(value, SCR_DYNH_SET); @@ -8371,7 +8792,7 @@ static ScrDyn *scr_native_map_pack(void *ptr) { ScrDyn *scr_dyn_native_map(ScrMap *value) { static const ScrDynHandleOps ops = { "Map", &scr_map_retain_v, &scr_map_release_v, &scr_native_map_invoke, - &scr_native_map_get, &scr_weak_map_write, NULL, NULL, + &scr_native_map_get, &scr_weak_map_write, NULL, &scr_native_map_pack, }; scr_dyn_handle_install(SCR_DYNH_MAP, &ops); return scr_dyn_new_handle(value, SCR_DYNH_MAP); @@ -8475,3 +8896,88 @@ bool scr_dyn_is_frozen(const ScrDyn *value) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return false; } + +double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *encoding) { + if (value->kind == SCR_DYN_STR) return scr_bytes_byte_length_str(value->v.str, encoding); + if (value->kind == SCR_DYN_BYTES) return scr_bytes_byte_len(value->v.bytes); + if (scr_array_buffer_is(value)) return scr_bytes_byte_len(value->v.handle.ptr); + static const char message[] = "The string argument must be of type string or an instance of Buffer or ArrayBuffer"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_ARG_TYPE"); + return 0; +} + +/* Radix digit generation follows V8's DoubleToRadixStringView algorithm. + * Copyright 2012 the V8 project authors. BSD license in + * ../vendor/v8-number-radix/LICENSE. The decimal path retains Ryū. */ +ScrStr *scr_num_to_string_radix(double value, const ScrDyn *argument) { + double requested = 10; + if (argument && argument->kind != SCR_DYN_UNDEF && + !scr_dyn_number_coerce_js(argument, &requested)) return NULL; + requested = isnan(requested) ? 0 : trunc(requested); + if (requested < 2 || requested > 36 || !isfinite(requested)) { + static const char message[] = "toString() radix argument must be between 2 and 36"; + scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1); + return NULL; + } + int radix = (int)requested; + if (radix == 10 || !isfinite(value) || value == 0) return scr_f64_to_scrstr(value); + static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; + char buffer[2200]; + const size_t middle = sizeof buffer / 2; + size_t begin = middle, end = middle; + bool negative = value < 0; + value = fabs(value); + double integer = floor(value), fraction = value - integer; + double tolerance = 0.5 * (nextafter(value, INFINITY) - value); + if (tolerance <= 0) tolerance = nextafter(0, INFINITY); + if (fraction >= tolerance) { + buffer[end++] = '.'; + do { + fraction *= radix; + tolerance *= radix; + int digit = (int)fraction; + buffer[end++] = digits[digit]; + fraction -= digit; + if ((fraction > 0.5 || (fraction == 0.5 && (digit & 1))) && fraction + tolerance > 1) { + for (;;) { + end--; + if (end == middle) { integer += 1; break; } + char last = buffer[end]; + digit = last > '9' ? last - 'a' + 10 : last - '0'; + if (digit + 1 < radix) { buffer[end++] = digits[digit + 1]; break; } + } + break; + } + } while (fraction >= tolerance); + } + /* V8's Double::Exponent is the unbiased exponent minus 52. Beyond + * that precision boundary, trailing digits are unrepresented zeros. */ + while (integer / radix >= 0x1p53) { + integer /= radix; + buffer[--begin] = '0'; + } + do { + double remainder = fmod(integer, radix); + buffer[--begin] = digits[(int)remainder]; + integer = (integer - remainder) / radix; + } while (integer > 0); + if (negative) buffer[--begin] = '-'; + return scr_str_new(buffer + begin, end - begin); +} + +ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { + switch (c->kind) { + case SCR_EXC_F64: return scr_dyn_new_num(c->f64); + case SCR_EXC_BOOL: return scr_dyn_new_bool(c->b); + case SCR_EXC_STR: return scr_dyn_new_str((ScrStr *)c->payload); + case SCR_EXC_REF: + case SCR_EXC_PRIMITIVE_REF: + if (c->retain_fn == scr_dyn_retain_v) return scr_dyn_retain((ScrDyn *)c->payload); + return scr_dyn_new_obj(); + case SCR_EXC_OBJ: + if (scr_error_is(c->payload)) return scr_dyn_from_error((const ScrError *)c->payload); + return scr_dyn_new_obj(); + default: + return scr_dyn_new_obj(); + } +} diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index b8c8d8230..2a0eb41bc 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -10,6 +10,9 @@ static SCR_TL ScrDyn *scr_global_known; static SCR_TL ScrDyn *scr_global_own; static SCR_TL bool scr_global_cleanup_registered; static SCR_TL ScrDyn *(*scr_global_console_get)(void); +static SCR_TL ScrDyn *(*scr_global_fetch_get)(void); + +void scr_global_fetch_install(ScrDyn *(*get)(void)) { scr_global_fetch_get = get; } static SCR_TL ScrDyn *scr_hrtime_value; static void scr_hrtime_cleanup(void) { @@ -28,6 +31,12 @@ static ScrDyn *scr_hrtime_bigint_call(ScrClosure *closure, ScrDyn *const *args, static ScrDyn *scr_hrtime_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; + if (argc && args[0]->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(args[0]); + ScrDyn *result = view && !scr_exc_pending() ? scr_hrtime_call(closure, &view, 1) : NULL; + scr_dyn_release(view); + return result; + } uint64_t time = scr_hrtime_ns(); double seconds = (double)(time / UINT64_C(1000000000)); double nanos = (double)(time % UINT64_C(1000000000)); @@ -111,6 +120,7 @@ static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) { return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); if (length == 7 && memcmp(key, "console", length) == 0 && scr_global_console_get) return scr_global_console_get(); + if (length == 5 && memcmp(key, "fetch", length) == 0 && scr_global_fetch_get) return scr_global_fetch_get(); if (length == 9 && memcmp(key, "undefined", length) == 0) return scr_dyn_undefined(); if (length == 3 && memcmp(key, "NaN", length) == 0) return scr_dyn_new_num(NAN); if (length == 8 && memcmp(key, "Infinity", length) == 0) return scr_dyn_new_num(INFINITY); diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 2bd115853..e522b26b3 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -7,10 +7,10 @@ * the always-compiled runtime source list (see native-toolchain.ts), so regex-free * programs keep a byte-identical link line. * - * - Every ScrRegex today is an immortal interned literal (the compiler - * emits one static per distinct (pattern, flags) pair, like string - * literals). The pattern is compiled to bytecode LAZILY on first use and - * cached on the struct; a pattern lre_compile rejects aborts with a clear + * - Each literal evaluation creates a refcounted regex with fresh state. + * Instances share an immortal template per (pattern, flags) pair. The + * template compiles and caches bytecode LAZILY on first use; a pattern + * lre_compile rejects aborts with a clear * message (Node throws SyntaxError at parse time — documented divergence; * rare, since tsc's parser has already syntax-checked the literal). * - Subjects are matched as UTF-16: lre_exec runs over a per-call @@ -23,14 +23,14 @@ * temporary registers past the capture slots), unmatched groups leave * NULL pointers, and zero-length matches advance by one code unit — or by * a whole surrogate pair under /u (AdvanceStringIndex). - * - Statefulness fence: /g regexes carry mutable lastIndex in JS. This - * slice supports /g (and /y) only where the iteration is internal — - * replace/replaceAll/split; test() on a g/y-flagged regex aborts with a - * clear message (the frontend already rejects the cases it can see). + * - Global and sticky exec/test use numeric lastIndex and update or reset + * it after matching. String matching/replacement follows its own state + * rules; matchAll, search, and split preserve the original regex state. */ #include "scr_runtime.h" #include +#include #include #include @@ -176,6 +176,7 @@ static char *scr_pattern_cesu8(const ScrStr *src, size_t *plen) { /* The compiled bytecode, compiling (and caching) on first use. */ static uint8_t *scr_regex_bc(ScrRegex *re) { if (re->bc) return re->bc; + if (re->literal) return re->bc = scr_regex_bc(re->literal); int flags = scr_lre_flags(re->flags); const char *pat = re->source->data; size_t pat_len = re->source->len; @@ -204,15 +205,15 @@ static uint8_t *scr_regex_bc(ScrRegex *re) { } /* ── RC entry points ────────────────────────────────────────────────── - * Dead paths today (every regex value is an immortal interned literal); - * correct anyway so dynamic construction can arrive without surprises. */ + * Literal instances borrow bytecode from their immortal templates; + * constructed regexes own their compiled bytecode. */ void scr_regex_release(ScrRegex *re) { if (!re || re->rc == SIZE_MAX) return; if (--re->rc == 0) { scr_str_release(re->source); scr_str_release(re->flags); - if (re->bc) lre_realloc(lre_opaque(), re->bc, 0); + if (re->bc && !re->literal) lre_realloc(lre_opaque(), re->bc, 0); free(re); } } @@ -220,6 +221,18 @@ void scr_regex_release(ScrRegex *re) { void *scr_regex_retain_v(void *re) { return scr_regex_retain(re); } void scr_regex_release_v(void *re) { scr_regex_release(re); } +ScrRegex *scr_regex_literal(ScrRegex *literal) { + ScrRegex *re = calloc(1, sizeof *re); + if (!re) scr_regex_oom(); + re->rc = 1; + re->source = scr_str_retain(literal->source); + re->flags = scr_str_retain(literal->flags); + re->literal = literal; + return re; +} + +double scr_regex_last_index(ScrRegex *re) { return re->last_index; } + ScrStr *scr_regex_source(ScrRegex *re) { return scr_str_retain(re->source); } ScrStr *scr_regex_flags(ScrRegex *re) { return scr_str_retain(re->flags); } ScrStr *scr_regex_to_string(ScrRegex *re) { @@ -403,52 +416,49 @@ static int scr_advance(const uint16_t *u, int len, int i, bool unicode) { /* ── test ─────────────────────────────────────────────────────────────── */ +void scr_regex_reset_last_index(ScrRegex *re, double index) { + re->last_index = index; +} + bool scr_regex_test(ScrRegex *re, ScrStr *s) { - uint8_t *bc = scr_regex_bc(re); - if (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) { - /* JS test() on a g/y regex reads AND writes lastIndex — stateful - * iteration this slice does not model (the frontend rejects the sites - * it can see; values that flow through variables land here). */ - fflush(stdout); - scr_trap("scriptc: test() on a regex with the 'g' or 'y' flag is not " - "supported (stateful lastIndex); drop the flag, or use " - "replace/replaceAll/split\n"); - } - int len; - uint16_t *u = scr_to_utf16(s, &len); - uint8_t **capture = scr_capture_alloc(bc); - int rc = scr_exec(capture, bc, u, 0, len); - free(capture); - free(u); - return rc == 1; + ScrArr *result = scr_regex_exec(s, re); + bool matched = result != NULL; + scr_arr_release(result); + return matched; +} + +static int scr_regex_start(ScrRegex *re, int len) { + double index = re->last_index; + if (isnan(index) || index <= 0) return 0; + if (index > len) return -1; + return (int)floor(index); } /* ── match ────────────────────────────────────────────────────────────── */ -/* s.match(re) for non-g/y regexes: Node's exec-shaped result reduced to - * the honest slice — a fresh string[] of [whole match, ...captures], or - * NULL for no match (the compiler wraps the `string[] | null` union). A - * NONPARTICIPATING capture holds "" where Node's slot is undefined - * (truthiness tests agree; SEMANTICS.md documents the divergence). g/y - * regexes abort like test() — a g-flagged match returns EVERY match, a - * different shape the frontend fences on literals. */ -ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re) { +/* re.exec(s): a fresh string[] of [whole match, ...captures], or NULL + * for no match (the compiler wraps the `string[] | null` union). Global + * and sticky regexes start at lastIndex, set it to the match end, and + * reset it on failure. Nonparticipating captures retain the documented + * empty-string representation. */ +ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re) { uint8_t *bc = scr_regex_bc(re); - if (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) { - fflush(stdout); - scr_trap("scriptc: match() on a regex with the 'g' or 'y' flag is not " - "supported (an every-match array is a different shape); drop the " - "flag, or use replace/replaceAll/split\n"); - } + bool stateful = (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) != 0; int len; uint16_t *u = scr_to_utf16(s, &len); uint8_t **capture = scr_capture_alloc(bc); - int rc = scr_exec(capture, bc, u, 0, len); + int pos = stateful ? scr_regex_start(re, len) : 0; + int rc = pos < 0 ? 0 : scr_exec(capture, bc, u, pos, len); + if (stateful) re->last_index = rc == 1 + ? (double)((capture[1] - (const uint8_t *)u) >> 1) : 0; ScrArr *out = NULL; if (rc == 1) { const uint8_t *ubase = (const uint8_t *)u; int count = lre_get_capture_count(bc); out = scr_arr_new(SCR_ELEM_STR, (size_t)count); + out->metadata = scr_dyn_new_obj(); + scr_dyn_obj_set(out->metadata, "index", 5, scr_dyn_new_num((double)((capture[0] - ubase) >> 1))); + scr_dyn_obj_set(out->metadata, "input", 5, scr_dyn_new_str(s)); for (int k = 0; k < count; k++) { const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1]; if (cs == NULL || ce == NULL) { @@ -464,12 +474,42 @@ ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re) { return out; /* +1, or NULL (no match) */ } +/* Global Symbol.match always starts at zero and drains whole matches, + * ignoring capture groups. Its terminating failed exec resets lastIndex + * to zero, so no state escapes this internal iteration. Sticky globals + * stop at the first gap; empty matches advance by a Unicode code point + * under /u and by one UTF-16 unit otherwise. */ +ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re) { + uint8_t *bc = scr_regex_bc(re); + int flags = lre_get_flags(bc); + if (!(flags & LRE_FLAG_GLOBAL)) return scr_regex_exec(s, re); + re->last_index = 0; + bool unicode = (flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; + int len; + uint16_t *u = scr_to_utf16(s, &len); + uint8_t **capture = scr_capture_alloc(bc); + const uint8_t *ubase = (const uint8_t *)u; + ScrArr *out = NULL; + int pos = scr_regex_start(re, len); + if (pos < 0) pos = len + 1; + while (pos <= len && scr_exec(capture, bc, u, pos, len) == 1) { + int start = (int)((capture[0] - ubase) >> 1); + int end = (int)((capture[1] - ubase) >> 1); + if (!out) out = scr_arr_new(SCR_ELEM_STR, 4); + scr_arr_push_ref(out, scr_str_from_utf16(u, start, end)); + pos = start == end ? scr_advance(u, len, end, unicode) : end; + } + free(capture); + free(u); + return out; +} + /* ── search ───────────────────────────────────────────────────────────── */ /* s.search(re): the first match's UTF-16 index, or -1 — Symbol.search's * fresh exec from position 0. lastIndex is neither read nor written in JS - * (the spec saves and restores it), so no flag fence applies here, unlike - * test/match: /g is irrelevant and /y anchors at 0 (libregexp's sticky + * (the spec saves and restores it): /g is irrelevant and /y anchors at 0 + * (libregexp's sticky * bytecode omits the implicit forward scan) — exactly Node. */ double scr_regex_search(ScrStr *s, ScrRegex *re) { uint8_t *bc = scr_regex_bc(re); @@ -509,7 +549,8 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices uint8_t **capture = scr_capture_alloc(bc); const uint8_t *ubase = (const uint8_t *)u; ScrArr *out = scr_arr_new(SCR_ELEM_ARR, 4); - int pos = 0; + int pos = scr_regex_start(re, len); + if (pos < 0) pos = len + 1; while (pos <= len) { if (scr_exec(capture, bc, u, pos, len) != 1) break; int start = (int)((capture[0] - ubase) >> 1); @@ -643,6 +684,7 @@ static ScrStr *scr_replace_impl(ScrStr *s, ScrRegex *re, ScrStr *rep) { uint8_t *bc = scr_regex_bc(re); int re_flags = lre_get_flags(bc); bool global = (re_flags & LRE_FLAG_GLOBAL) != 0; + bool sticky = (re_flags & LRE_FLAG_STICKY) != 0; bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; int capture_count = lre_get_capture_count(bc); const char *groupnames = lre_get_groupnames(bc); @@ -652,11 +694,15 @@ static ScrStr *scr_replace_impl(ScrStr *s, ScrRegex *re, ScrStr *rep) { const uint8_t *ubase = (const uint8_t *)u; ScrJsonBuf b; scr_jb_init(&b); - int next = 0, pos = 0; + if (global) re->last_index = 0; + int next = 0, pos = sticky ? scr_regex_start(re, len) : 0; + if (pos < 0) pos = len + 1; + if (sticky && !global) re->last_index = 0; while (pos <= len) { if (scr_exec(capture, bc, u, pos, len) != 1) break; int start = (int)((capture[0] - ubase) >> 1); int end = (int)((capture[1] - ubase) >> 1); + if (sticky && !global) re->last_index = end; scr_jb_put_utf16(&b, u, next, start); scr_put_substitution(&b, u, len, start, end, capture, capture_count, groupnames, rep); next = end; @@ -878,26 +924,10 @@ ScrStr *scr_str_to_lower(const ScrStr *s) { return scr_str_case_conv(s, 1); } ScrStr *scr_str_to_upper(const ScrStr *s) { return scr_str_case_conv(s, 0); } /* ── assert.match / assert.doesNotMatch ────────────────────────────────── - * Lives here (not scr_assert.c) because it needs the matcher, and every - * call site carries a regex value — the regex link switch is already on, - * so assert-only binaries never pull libregexp. A fresh exec from index 0: - * Node's exec on a fresh regex literal; lastIndex statefulness is not - * modeled anywhere in this runtime, so g/y-flagged regexes test like - * their flag-free twins here instead of aborting (assert.match(s, /x/g) - * works in Node — matching that beats scr_regex_test's fence; - * SEMANTICS.md 107 covers reused stateful regexes). */ -/* A fresh exec from index 0 (assert.match's stance: g/y-flagged regexes - * test like their flag-free twins here — SEMANTICS.md 107 covers reused - * stateful regexes). */ + * Lives here because it needs the matcher. Assertion matching follows + * RegExp.test, including global and sticky lastIndex transitions. */ static bool scr_assert_regex_hits(ScrRegex *re, ScrStr *s) { - uint8_t *bc = scr_regex_bc(re); - int len; - uint16_t *u = scr_to_utf16(s, &len); - uint8_t **capture = scr_capture_alloc(bc); - bool matched = scr_exec(capture, bc, u, 0, len) == 1; - free(capture); - free(u); - return matched; + return scr_regex_test(re, s); } /* The regex's own inspection — /source/ + FLAGS IN GETTER ORDER @@ -1209,6 +1239,7 @@ static ScrDyn *scr_native_regex_get(void *ptr, const char *key, size_t len) { ScrRegex *re = ptr; if (len == 6 && memcmp(key, "source", 6) == 0) return scr_native_regex_string(scr_regex_source(re)); if (len == 5 && memcmp(key, "flags", 5) == 0) return scr_native_regex_string(scr_regex_flags(re)); + if (len == 9 && memcmp(key, "lastIndex", 9) == 0) return scr_dyn_new_num(re->last_index); static const struct { const char *name; char flag; } flags[] = { {"global", 'g'}, {"ignoreCase", 'i'}, {"multiline", 'm'}, {"dotAll", 's'}, {"unicode", 'u'}, {"sticky", 'y'}, {"hasIndices", 'd'}, {"unicodeSets", 'v'}, @@ -1217,10 +1248,10 @@ static ScrDyn *scr_native_regex_get(void *ptr, const char *key, size_t len) { if (strlen(flags[i].name) == len && memcmp(key, flags[i].name, len) == 0) return scr_dyn_new_bool(memchr(re->flags->data, flags[i].flag, re->flags->len) != NULL); } - static const char *const methods[] = {"test", "exec", "toString", "compile", "lastIndex"}; + static const char *const methods[] = {"test", "exec", "toString", "compile"}; for (size_t i = 0; i < sizeof methods / sizeof methods[0]; i++) { if (strlen(methods[i]) == len && memcmp(key, methods[i], len) == 0) { - static const char message[] = "Native RegExp method values and lastIndex have no lowering"; + static const char message[] = "Native RegExp method values have no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); break; } @@ -1229,7 +1260,15 @@ static ScrDyn *scr_native_regex_get(void *ptr, const char *key, size_t len) { } static bool scr_native_regex_set(void *ptr, const char *key, size_t len, const ScrDyn *value) { - (void)ptr; (void)key; (void)len; (void)value; + if (len == 9 && memcmp(key, "lastIndex", 9) == 0) { + if (value->kind != SCR_DYN_NUM) { + static const char message[] = "Non-numeric RegExp.lastIndex values have no native lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return true; + } + ((ScrRegex *)ptr)->last_index = value->v.num; + return true; + } return false; } diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index bec436fbc..5d9bcba9b 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -62,7 +62,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v5(void); +void scr_runtime_abi_v6(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── * Every unrecoverable runtime trap — OOM, semantic range traps, internal- @@ -653,6 +653,7 @@ void scr_error_init_options(void *obj, int kind, const struct ScrDyn *message, c void scr_error_install_cause_drop(void (*fn)(void *obj)); bool scr_error_has_cause(ScrError *e); struct ScrDyn *scr_error_cause(ScrError *e); /* +1, undefined when absent */ +void scr_error_define_cause(ScrError *e, struct ScrDyn *value); void scr_error_set_cause(ScrError *e, struct ScrDyn *value); /* borrowed operands */ void scr_error_delete_cause(ScrError *e); /* ECMA Error.prototype.toString: "", name, message, or "name: message". @@ -852,6 +853,7 @@ ScrStr *scr_str_encode_uri_component(ScrStr *s); * hex or an invalid sequence THROWS the spec's catchable URIError ("URI * malformed") and returns NULL. Borrows s; result +1. */ ScrStr *scr_str_decode_uri_component(ScrStr *s); +ScrStr *scr_str_decode_uri(ScrStr *s); /* The non-throwing core of decodeURIComponent: NULL on malformed input * instead of the URIError — the try/catch shape node:querystring's @@ -1005,6 +1007,7 @@ typedef struct ScrArr { struct ScrArrProp *props; /* noncanonical numeric properties */ size_t prop_len; size_t prop_cap; + struct ScrDyn *metadata; /* heterogeneous named properties of match results */ } ScrArr; typedef struct ScrArrSparseSlot { @@ -1042,6 +1045,7 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) { * NULL-tolerant (uninitialized locals). Cycle-capable arrays (elem_trace * non-NULL) feed the candidate-root buffer like every headered type. */ void scr_arr_release(ScrArr *a); +void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); /* Trace entry point for cycle-capable arrays — stored in their collector * header and passed wherever a container stores an ARRAY payload's RC entry @@ -1200,16 +1204,15 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs); /* ── regular expressions (scr_regex.c — linked ONLY when the program * contains a regex literal; see native-toolchain.ts) ──────────────────────────────── * The engine is quickjs-ng's libregexp (the same bytecode interpreter the - * dynamic island uses), compiled standalone. A ScrRegex is today ALWAYS an - * immortal interned literal: the compiler emits one static per distinct - * (pattern, flags) pair, exactly like string literals, and the bytecode is - * compiled lazily on first use (cached on the struct, freed at exit). + * dynamic island uses), compiled standalone. Each regex literal evaluation + * creates a fresh refcounted instance sharing an immortal template per + * (pattern, flags) pair. Template bytecode compiles lazily on first use + * and is freed at exit; constructed regexes own their bytecode. * Subjects are matched as UTF-16 (converted per call from the UTF-8 * storage), so all observable index behavior is UTF-16-exact like Node. * - * Statefulness fence: /g and /y regexes carry mutable lastIndex in JS. - * Supported only where iteration is internal (replace/replaceAll/split); - * test() on a g/y-flagged regex aborts with a clear message. + * Global and sticky regexes carry numeric lastIndex. Exec/test update it + * after matching; String methods apply their specified state rules. * * Ownership: subjects/replacements are BORROWED; string/array results * return +1. replace_all without /g THROWS a catchable TypeError @@ -1217,10 +1220,12 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs); * checks); every other failure mode aborts. */ typedef struct ScrRegex { - size_t rc; /* SIZE_MAX = immortal (every regex literal) */ + size_t rc; /* SIZE_MAX = immortal literal template */ ScrStr *source; /* pattern text between the slashes */ ScrStr *flags; /* flags text, source order (alphabet fenced to gimsuy) */ uint8_t *bc; /* lazily compiled libregexp bytecode; NULL until first use */ + double last_index; + struct ScrRegex *literal; /* immutable bytecode owner for literal instances */ } ScrRegex; static inline ScrRegex *scr_regex_retain(ScrRegex *re) { @@ -1231,6 +1236,8 @@ static inline ScrRegex *scr_regex_retain(ScrRegex *re) { void scr_regex_release(ScrRegex *re); /* NULL-tolerant */ void *scr_regex_retain_v(void *re); void scr_regex_release_v(void *re); +ScrRegex *scr_regex_literal(ScrRegex *literal); /* fresh state, shared cached bytecode */ +double scr_regex_last_index(ScrRegex *re); /* new RegExp(pattern, flags): a heap ScrRegex over the same engine. The * pattern compiles EAGERLY — an invalid pattern or flag throws Node's @@ -1239,11 +1246,14 @@ void scr_regex_release_v(void *re); * spec's "(?:)" source. Borrows both; +1, NULL after a throw. */ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags); -bool scr_regex_test(ScrRegex *re, ScrStr *s); /* aborts on /g or /y */ -/* s.match(re): +1 string[] of [whole, ...captures] (nonparticipating - * captures hold "" — SEMANTICS.md), or NULL for no match. Aborts on /g - * or /y like test(). Borrows both. */ +bool scr_regex_test(ScrRegex *re, ScrStr *s); /* exec state transitions */ +/* s.match(re): +1 string[] of whole matches under /g, otherwise + * [whole, ...captures] (nonparticipating captures hold ""), or NULL for + * no match. Non-global /y uses exec state transitions. Borrows both. */ ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re); +/* re.exec(s), string-first ABI: match slice with global/sticky lastIndex + * transitions. Borrows both; +1 or NULL. */ +ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re); /* s.search(re): the first match's UTF-16 index, or -1 — a fresh exec from * position 0 (Symbol.search never touches lastIndex, so no flag fence: * /g is irrelevant, /y anchors at 0). Borrows both; never throws. */ @@ -2055,7 +2065,9 @@ ScrStream *scr_stream_set_push_encoding(ScrStream *s, ScrStr *enc); bool scr_stream_push_null(ScrStream *s); void scr_stream_unshift(ScrStream *s, ScrBytes *chunk); void scr_stream_unshift_str(ScrStream *s, ScrStr *str); -ScrBytes *scr_stream_read(ScrStream *s, double size); /* +1 or NULL; size < 0 = all/first; throws once encoded (the static type is Buffer|null) */ +ScrBytes *scr_stream_read(ScrStream *s, double size); +ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, bool prepend); +ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *size); /* +1 or NULL; size < 0 = all/first; throws once encoded (the static type is Buffer|null) */ /* setEncoding(enc): string-chunk mode — pushes decode through the * StringDecoder, 'data' delivers strings (the emit ABI carries both * payload slots; see scr_stream_emit_data), lengths count JS string @@ -2871,7 +2883,11 @@ ScrChild *scr_fork(double target, ScrArr *args, double in_mode, ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, double out_mode, double err_mode, double out_fd, double err_fd, bool detached, bool shell, bool has_env, - ScrArr *env_pairs, ScrStr *cwd); + ScrArr *env_pairs, ScrStr *cwd); +/* Runtime options use the same spawn core; validation and explicit + * unsupported-option refusals throw before starting a child. Borrows. */ +ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options); +void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */ /* The callback slice: file + args, default options. The returned child has * all three stdio slots piped; stdout/stderr are captured internally and * the callback moves into the child registry until settlement. */ @@ -2959,9 +2975,28 @@ bool scr_child_killed(ScrChild *c); bool scr_child_kill(ScrChild *c, const ScrStr *signal); bool scr_child_kill_num(ScrChild *c, double signum); void scr_child_unref(ScrChild *c); +void scr_child_ref(ScrChild *c); /* Installs ChildProcess's checked-dynamic handle identity bridge. Emitted * programs call this only when the child_process surface is present. */ void scr_child_dyn_install(void); +void scr_child_on_dyn(ScrChild *child, ScrStr *event, ScrDyn *listener); /* borrowed */ +/* Optional native-stream bridge. The table avoids a stream dependency + * in programs that only use the child-process surface. */ +typedef struct ScrChildStreamOps { + ScrStream *(*readable)(void); + ScrStream *(*writable)(ScrClosure *, ScrStreamChunkInv, ScrClosure *, ScrStreamPlainInv); + void *(*retain)(void *); + void (*release)(void *); + ScrTraceFn trace; + bool (*push)(ScrStream *, ScrBytes *); + bool (*end)(ScrStream *); + void (*write_done)(ScrStream *, ScrError *); + void (*final_done)(ScrStream *, ScrError *); +} ScrChildStreamOps; +extern const ScrChildStreamOps scr_stream_child_ops; +ScrDyn *scr_stream_dyn_view(ScrStream *stream); /* borrowed; +1 live method/property view */ +void scr_child_dyn_streams_install(const ScrChildStreamOps *ops, + ScrDyn *(*readable_box)(void *), ScrDyn *(*writable_box)(void *)); bool scr_children_reffed_pending(void); void scr_children_teardown(void); /* Node's signal-name table (scr_lib.c), shared with child.kill: the @@ -3000,6 +3035,7 @@ ScrStr *scr_crypto_hash_digest_str(ScrStr *alg, ScrStr *data, ScrStr *enc); ScrStr *scr_crypto_hash_digest_bytes(ScrStr *alg, ScrBytes *data, ScrStr *enc); typedef struct ScrCryptoHash ScrCryptoHash; ScrCryptoHash *scr_crypto_hash_new(ScrStr *alg); +ScrDyn *scr_crypto_native(void); ScrCryptoHash *scr_crypto_hmac_new_str(ScrStr *alg, ScrStr *key); ScrCryptoHash *scr_crypto_hmac_new_bytes(ScrStr *alg, ScrBytes *key); ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h); @@ -3268,6 +3304,7 @@ ScrBigInt *scr_bigint_shl(ScrBigInt *a, ScrBigInt *count); ScrBigInt *scr_bigint_shr(ScrBigInt *a, ScrBigInt *count); bool scr_bigint_eq(ScrBigInt *a, ScrBigInt *b); bool scr_bigint_eq_string(ScrBigInt *a, ScrStr *b); /* StringToBigInt failure is false */ +double scr_bigint_cmp_string(ScrBigInt *a, ScrStr *b); /* 2 = invalid StringToBigInt */ double scr_bigint_cmp_f64(ScrBigInt *a, ScrBigInt *b); /* -1, 0, 1 */ double scr_bigint_cmp_number(ScrBigInt *a, double b); /* -1, 0, 1; 2 = NaN */ bool scr_bigint_truthy(ScrBigInt *value); @@ -3495,6 +3532,8 @@ typedef enum { SCR_DYNH_DATE, /* JavaScript Date with stable native identity */ SCR_DYNH_ITERATOR, /* live native array/string/byte iterator */ SCR_DYNH_CONSOLE, /* stored global console value */ + SCR_DYNH_SEARCH_PARAMS, /* live WHATWG URLSearchParams */ + SCR_DYNH_FETCH_REQUEST, SCR_DYNH_COUNT, } ScrDynHandleTag; @@ -3507,6 +3546,7 @@ typedef struct ScrDynTypedCast { void *ptr; void *(*retain)(void *); void (*release)(void *); + bool traced; struct ScrDynTypedCast *next; } ScrDynTypedCast; typedef struct ScrJsval ScrJsval; /* opaque island cell (C11 repeat; the @@ -3583,10 +3623,7 @@ struct ScrDyn { /* SCR_DYN_FUNC: the boxed closure (owned) + its call descriptor. `sig` * and `name` are static compiler-emitted literals (never freed); name * may be NULL (anonymous — inspect prints [Function (anonymous)]). - * The dyn→closure edge is NOT visible to the cycle collector (ScrDyn - * has no trace header): trial deletion treats it as an external root, - * so nothing dangles — a cycle THROUGH a dyn-boxed function is merely - * never collected (documented divergence). */ + * The dyn→closure ownership edge is traced by the cycle collector. */ struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; } fn; /* Native handles normally keep conservative ownership edges. Sets * backed by cycle-capable maps expose their edge to the collector. @@ -3610,6 +3647,7 @@ struct ScrDyn { ScrDyn *materialized; ScrDynTypedCast *casts; bool traced; /* native referent has a cycle header */ + bool observed; /* native release reports this identity's disposal */ } typed_ref; /* SCR_DYN_PROMISE: the retained promise. The boundary contract: it * settles with a dyn payload (SCR_EXC_REF ScrDyn fulfillment or a @@ -3689,6 +3727,7 @@ bool scr_dyn_property_is_enumerable(const ScrDyn *value, const ScrStr *key); * and scalar/function/handle sources copy nothing. */ ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src); ScrDyn *scr_dyn_copy_data_properties(ScrDyn *target, const ScrDyn *src); +ScrDyn *scr_dyn_object_rest(const ScrDyn *source, const ScrDyn *excluded); ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src); /* Variadic Object.assign (the spread-source form): the compiler packs * every source into one fresh dyn array — pack_push retains a plain @@ -3723,9 +3762,12 @@ ScrDyn *scr_dyn_native_set(ScrMap *value); /* borrowed backing, +1 box */ bool scr_dyn_native_set_is(const ScrDyn *value); ScrDyn *scr_dyn_native_map(ScrMap *value); /* borrowed backing, +1 box */ bool scr_dyn_native_map_is(const ScrDyn *value); +void scr_regex_reset_last_index(ScrRegex *re, double index); ScrRegex *scr_regex_new_checked(const ScrDyn *pattern, const ScrDyn *flags); ScrDyn *scr_dyn_native_url(ScrUrl *value); bool scr_dyn_native_url_is(const ScrDyn *value); +ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value); +bool scr_dyn_native_search_params_is(const ScrDyn *value); bool scr_dyn_native_date_is(const ScrDyn *value); ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments); double scr_dyn_native_date_value(const ScrDyn *value); @@ -3753,6 +3795,7 @@ ScrDyn *scr_dyn_new_str(ScrStr *s); ScrDyn *scr_dyn_new_arr(void); ScrDyn *scr_dyn_new_obj(void); ScrDyn *scr_dyn_iterator(const ScrDyn *value, const ScrStr *spell); +ScrDyn *scr_dyn_iterator_optional(const ScrDyn *value); void scr_dyn_install_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); ScrDyn *scr_dyn_array_values_function(void); @@ -3790,6 +3833,9 @@ void scr_dyn_proxy_unsupported(const char *operation); * allocation across the checked-dynamic boundary. Borrows b. */ ScrDyn *scr_dyn_new_bytes(const ScrBytes *b); ScrDyn *scr_bytes_constructor(const ScrStr *name); +bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee); +bool scr_dyn_data_view_is(const ScrDyn *value); +ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key); /* The Buffer-flavored twin (stream chunks): string coercion/toString @@ -3810,6 +3856,11 @@ ScrDyn *scr_dyn_new_typed_ref_traced( const char *type_key, size_t type_key_len, ScrDyn *(*materialize)(void *), void (*commit)(void *, const ScrDyn *)); +ScrDyn *scr_dyn_new_typed_ref_observed( + void *ptr, void *(*retain)(void *), void (*release)(void *), + const char *type_key, size_t type_key_len, + ScrDyn *(*materialize)(void *), + void (*commit)(void *, const ScrDyn *)); bool scr_dyn_typed_ref_is( const ScrDyn *d, const char *type_key, size_t type_key_len); bool scr_dyn_typed_ref_is_key(const ScrDyn *d, const ScrStr *type_key); @@ -3821,11 +3872,12 @@ void *scr_dyn_typed_ref_cached_cast( const ScrDyn *d, const char *type_key, size_t type_key_len); /* +1/NULL */ void scr_dyn_typed_ref_cache_cast( ScrDyn *d, const char *type_key, size_t type_key_len, void *ptr, - void *(*retain)(void *), void (*release)(void *)); + void *(*retain)(void *), void (*release)(void *), bool traced); /* Retains a SCR_DYN_BYTES view (+1) for checked extraction. */ ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d); /* Exact numeric typed-array brand; elem is a ScrBytesElem tag. */ bool scr_dyn_bytes_is(const ScrDyn *d, int elem); +bool scr_dyn_typed_array_is(const ScrDyn *d, int elem); void scr_dyn_arr_push(ScrDyn *arr, ScrDyn *item); /* Spread completion for a runtime-arity argument list (`f(...xs)` in the * checked-dynamic tier): flattens `src` into `arr` per JS's spread over the @@ -3898,10 +3950,12 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc); ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what); ScrDyn *scr_dyn_object_prototype(void); ScrDyn *scr_dyn_function_apply(void); +ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list); ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what); bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right); ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method); ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, const ScrStr *what); +ScrStr *scr_num_to_string_radix(double value, const ScrDyn *radix); /* JS String() over the dyn kind (units render "null"/"undefined" where * scr_dyn_to_string throws) — the web globals' WebIDL ToString. +1. */ ScrStr *scr_dyn_string_coerce(const ScrDyn *d); @@ -3914,8 +3968,10 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out); * with the exception pending when an object hook throws/refuses. */ double scr_dyn_number_coerce(const ScrDyn *d); double scr_dyn_number_constructor(const ScrDyn *d); +ScrBigInt *scr_dyn_bigint_constructor(const ScrDyn *d); ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right); /* borrowed; +1 or NULL/pending */ ScrDyn *scr_dyn_arithmetic(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); +bool scr_dyn_compare(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); ScrDyn *scr_dyn_bitwise(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); /* `d instanceof TypeError` (and the other builtin error classes) on a @@ -3974,6 +4030,8 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * a name the kind's prototype lacks throws Node's " is not a * function"; OBJ receivers call the own member. recv/args borrowed, * result owned (+1). MAY THROW (NULL with the exception pending). */ +ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); +ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" @@ -4016,6 +4074,8 @@ typedef struct ScrDynPath { const char *key; size_t index; } ScrDynPath; +ScrSearchParams *scr_dyn_native_search_params_check(const ScrDyn *value, const ScrDynPath *path); +ScrSearchParams *scr_sp_new_checked(const ScrDyn *value); ScrMap *scr_dyn_native_set_check(const ScrDyn *value, const ScrDynPath *path); ScrMap *scr_dyn_native_map_check(const ScrDyn *value, const ScrDynPath *path); bool scr_dyn_native_collection_is(const ScrDyn *value, int map, const char *type); @@ -4069,7 +4129,10 @@ typedef struct ScrDynHandleOps { /* Immutable native iterables may drain into a fresh array. Mutable or * effectful iterators need a separate step/close protocol. NULL = not iterable. */ ScrDyn *(*iter_pack)(void *h); + /* A live native iterator step; index advances only when a value is emitted. */ + ScrDyn *(*iter_step)(void *h, double *index, unsigned selection); } ScrDynHandleOps; +ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection); ScrDyn *scr_intl_segmenter_new(void); ScrStr *scr_process_builtin_id(ScrDyn *id, ScrArr *known); @@ -4396,6 +4459,8 @@ long scr_obj_live_count(void); * tiebreaks) drives everything after %main returns. See scr_async.c. */ ScrPromise *scr_promise_new(void); +void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src); +bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b); ScrPromise *scr_promise_retain(ScrPromise *p); void scr_promise_release(ScrPromise *p); void *scr_promise_retain_v(void *p); @@ -4935,6 +5000,11 @@ long scr_promise_live_count(void); * --dynamic the same TU boots the broader engine web surface. The * emitted main calls scr_fetch_install in either form. */ void scr_fetch_install(void); +ScrPromise *scr_fetch_input(ScrDyn *input, ScrDyn *init); +ScrDyn *scr_fetch_function(void); +void scr_global_fetch_install(ScrDyn *(*get)(void)); +ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init); +ScrDyn *scr_fetch_headers_new(ScrDyn *init); ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise */ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */ ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise */ @@ -5542,6 +5612,7 @@ typedef struct ScrBytes { * points into backing->data — released, never freed. */ struct ScrBytes *backing; bool is_buffer; /* Buffer brand belongs to the view, not its backing. */ + bool is_data_view; /* DataView is distinct from a numeric typed array. */ bool external; /* Root aliases foreign memory; releasing it never frees data. */ } ScrBytes; @@ -5587,6 +5658,7 @@ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory o * byte storage, including when src is itself a subarray. */ ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem, double offset, bool has_len, double length); +ScrDyn *scr_array_buffer_constructor(void); ScrDyn *scr_array_buffer_new(ScrDyn *length); ScrDyn *scr_array_buffer_from_bytes(ScrBytes *view); bool scr_array_buffer_is(const ScrDyn *value); @@ -5748,6 +5820,12 @@ ScrStr *scr_bytes_to_str_checked_range(const ScrBytes *b, const ScrStr *enc, dou * same replacement decode as toString("utf8") with the leading BOM * stripped. Borrows; +1; never throws. */ ScrStr *scr_text_decode(const ScrBytes *b); +double scr_text_decoder_encoding(const ScrDyn *label); +ScrBytes *scr_bytes_buffer_source(const ScrDyn *value); +ScrStr *scr_text_decode_buffer_source(const ScrDyn *value); +ScrStr *scr_text_decode_options(const ScrBytes *b, bool fatal, bool ignore_bom); +ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *b, double encoding, bool fatal, bool ignore_bom, bool stream); +ScrStr *scr_text_decode_legacy_options(const ScrBytes *b, double encoding, bool fatal, bool ignore_bom); /* TextDecoder with a compile-time WHATWG legacy-encoding id. The frontend * owns label canonicalization and emits only the ids understood by @@ -5768,6 +5846,7 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc); * Buffer.isEncoding(name) (case-insensitive over Node's alias set). * Borrow; never throw. */ double scr_bytes_byte_length_str(ScrStr *s, const ScrStr *enc); +double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *enc); bool scr_bytes_is_encoding(const ScrStr *s); /* equals / compare / indexOf-lastIndexOf / fill / copy / swap / write on @@ -5943,6 +6022,8 @@ ScrBytes *scr_fs_read_file_bytes(ScrStr *path); /* +1 */ * value — a Buffer box for undefined/null, a string for utf8 — or NULL * with the exception pending. */ ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc); +ScrDyn *scr_fs_callback_value(ScrStr *member); +ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args); ScrBytes *scr_fs_read_fd_bytes(double fd); /* +1; the fd form (scr_lib.c) */ void scr_fs_write_file_bytes(ScrStr *path, const ScrBytes *data); void scr_fs_append_file_bytes(ScrStr *path, const ScrBytes *data); @@ -7238,10 +7319,8 @@ void scr_assert_iferror_bool(bool b); * %error-marked objects throw with the error's message, everything else * with the inspection. */ void scr_assert_iferror_dyn(const ScrDyn *v); -/* assert.match / assert.doesNotMatch (scr_regex.c): a fresh exec from - * index 0 — Node's exec on a fresh regex; lastIndex statefulness is not - * modeled (SEMANTICS.md's regex stance), so g/y-flagged regexes test like - * their flag-free twins instead of aborting. */ +/* assert.match / assert.doesNotMatch (scr_regex.c): RegExp.test semantics, + * including global/sticky lastIndex transitions. */ void scr_assert_match(ScrStr *s, ScrRegex *re, bool negated, ScrStr *msg, bool has_msg); /* assert.throws(fn, /regex/) whose thrown ERROR did not match: Node tests * String(actual) ("Name: message") and reports the regex-mismatch message @@ -7279,4 +7358,7 @@ void scr_console_log(size_t n, const ScrLogArg *args); * keeps source order. */ void scr_console_error(size_t n, const ScrLogArg *args); +bool scr_fetch_web_is(const ScrDyn *value, const ScrStr *name); +bool scr_fetch_stream_is(const ScrDyn *value); + #endif /* SCR_RUNTIME_H */ diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 443880171..1324fd50a 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -2446,6 +2446,59 @@ void scr_stream_unshift_str(ScrStream *s, ScrStr *str) { scr_bytes_release(b); } +static void scr_stream_listener_error(ScrClosure *adapter, ScrError *error) { + ScrDyn *fn = scr_dyn_listener_fn(adapter); + ScrDyn *arg = scr_dyn_from_error(error); + ScrDyn *answer = scr_dyn_call(fn, &arg, 1, "listener"); + scr_dyn_release(answer); + scr_dyn_release(arg); + scr_dyn_release(fn); +} + +static void scr_stream_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *string) { + ScrDyn *fn = scr_dyn_listener_fn(adapter); + ScrDyn *arg = string ? scr_dyn_new_str(string) : scr_dyn_new_bytes(bytes); + ScrDyn *answer = scr_dyn_call(fn, &arg, 1, "listener"); + scr_dyn_release(answer); + scr_dyn_release(arg); + scr_dyn_release(fn); +} + +ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, bool prepend) { + if (name->len == 4 && !memcmp(name->data, "data", 4)) { + return (ScrStream *)scr_emitter_on_dyn((ScrEmitter *)s, name, cb, + scr_dyn_listener_closure_fn(cb, (void *)scr_stream_listener_data), + scr_ee_inv_fixed2, once, prepend); + } + bool error = name->len == 5 && memcmp(name->data, "error", 5) == 0; + bool empty = (name->len == 8 && memcmp(name->data, "readable", 8) == 0) || + (name->len == 3 && memcmp(name->data, "end", 3) == 0) || + (name->len == 5 && memcmp(name->data, "close", 5) == 0) || + (name->len == 6 && memcmp(name->data, "finish", 6) == 0) || + (name->len == 5 && memcmp(name->data, "drain", 5) == 0) || + (name->len == 6 && memcmp(name->data, "resume", 6) == 0) || + (name->len == 5 && memcmp(name->data, "pause", 5) == 0) || + (name->len == 9 && memcmp(name->data, "prefinish", 9) == 0); + if (error || empty) { + ScrClosure *adapter = error ? scr_dyn_listener_closure_fn(cb, (void *)&scr_stream_listener_error) : scr_dyn_listener_closure0(cb); + return (ScrStream *)scr_emitter_on_dyn((ScrEmitter *)s, name, cb, adapter, + error ? scr_ee_inv_fixed1 : scr_ee_inv_fixed0, once, prepend); + } + static const char message[] = "this stream event has no checked-dynamic listener adapter"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} + +ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *size) { + double n = size && size->kind != SCR_DYN_UNDEF ? scr_dyn_number_coerce(size) : -1; + if (scr_exc_pending()) return scr_dyn_undefined(); + void *chunk = s->st->has_r ? scr_stream_read_n(s, n) : NULL; + if (!chunk) return scr_dyn_new_null(); + ScrDyn *result = s->st->r.encoded ? scr_dyn_new_str(chunk) : scr_dyn_new_buffer(chunk); + scr_stream_entry_release(s->st, chunk); + return result; +} + ScrBytes *scr_stream_read(ScrStream *s, double size) { /* a destroyed readable still DRAINS its buffer (Node: destroy leaves * buffered chunks readable; only _read refills are blocked) */ @@ -3147,3 +3200,147 @@ void scr_stream_install(void) { scr_emitter_on_hook = &scr_stream_on_listener; scr_loop_set_stream(&scr_stream_ticks_pending, &scr_stream_dispatch); } + +/* Child stdio uses the standard stream machinery once it crosses generic + * storage. Child-only programs never reference this optional table. */ +static void scr_stream_child_read(ScrClosure *cb, ScrStream *s, double size) { + (void)cb; (void)s; (void)size; +} +static ScrStream *scr_stream_child_readable(void) { + return scr_stream_new_readable(-1, true, true, + scr_closure_new((void *)scr_stream_child_read, 0), scr_stream_child_read, + NULL, NULL); +} +static ScrStream *scr_stream_child_writable(ScrClosure *write, ScrStreamChunkInv write_inv, + ScrClosure *final_cb, ScrStreamPlainInv final_inv) { + return scr_stream_new_writable(-1, true, true, write, write_inv, + final_cb, final_inv, NULL, NULL); +} +const ScrChildStreamOps scr_stream_child_ops = { + scr_stream_child_readable, scr_stream_child_writable, + scr_stream_retain_v, scr_stream_release_v, scr_stream_trace, + scr_stream_push, scr_stream_push_null, + scr_stream_write_done, scr_stream_final_done, +}; + +static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && + !memcmp(value->v.typed_ref.type_key, "object:", 7)) { + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + const ScrVt *vt = stream->vt; + if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream; + value->v.typed_ref.release(stream); + } + scr_dyn_arg_type_fail("destination", "an instance of Stream", value); + return NULL; +} + +static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t argc) { + ScrStream *stream = scr_box_get_ref(cb->caps[0]); + ScrStr *name = scr_box_get_ref(cb->caps[1]); + const char *method = name->data; + ScrDyn *result = NULL; + ScrDyn *first = argc ? args[0] : scr_dyn_undefined(); + if (!strcmp(method, "on") || !strcmp(method, "once") || !strcmp(method, "addListener") || + !strcmp(method, "prependListener") || !strcmp(method, "prependOnceListener")) { + if (first->kind != SCR_DYN_STR) scr_dyn_arg_type_fail("event", "of type string", first); + else { + ScrStream *registered = scr_stream_on_dyn(stream, first->v.str, + argc > 1 ? args[1] : scr_dyn_undefined(), + !strcmp(method, "once") || !strcmp(method, "prependOnceListener"), + !strncmp(method, "prepend", 7)); + scr_stream_release(registered); + } + } else if (!strcmp(method, "off") || !strcmp(method, "removeListener")) { + if (first->kind != SCR_DYN_STR) scr_dyn_arg_type_fail("event", "of type string", first); + else { + ScrEmitter *registered = scr_emitter_off_dyn((ScrEmitter *)stream, first->v.str, + argc > 1 ? args[1] : scr_dyn_undefined()); + scr_emitter_release(registered); + } + } else if (!strcmp(method, "pipe") || !strcmp(method, "unpipe")) { + ScrStream *destination = first->kind == SCR_DYN_UNDEF && !strcmp(method, "unpipe") + ? NULL : scr_stream_view_receiver(first); + if (!scr_exc_pending()) { + bool end = true; + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) { + ScrDyn *option = scr_dyn_obj_read(args[1], "end", 3); + end = option->kind == SCR_DYN_UNDEF || scr_dyn_truthy(option); + scr_dyn_release(option); + } + ScrStream *returned = !strcmp(method, "pipe") + ? scr_stream_pipe(stream, destination, end) : scr_stream_unpipe(stream, destination); + scr_stream_release(returned); + if (!strcmp(method, "pipe") && !scr_exc_pending()) result = scr_dyn_retain(first); + } + scr_stream_release(destination); + } else if (!strcmp(method, "read")) result = scr_stream_read_dyn(stream, first); + else if (!strcmp(method, "pause") || !strcmp(method, "resume")) { + ScrStream *returned = !strcmp(method, "pause") ? scr_stream_pause(stream) : scr_stream_resume(stream); + scr_stream_release(returned); + } else if (!strcmp(method, "isPaused")) result = scr_dyn_new_bool(scr_stream_is_paused(stream)); + else if (!strcmp(method, "setEncoding")) { + if (first->kind != SCR_DYN_STR) scr_dyn_arg_type_fail("encoding", "of type string", first); + else scr_stream_release(scr_stream_set_encoding(stream, first->v.str)); + } else if (!strcmp(method, "write")) { + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) { + static const char message[] = "Native stream method values with write encodings or callbacks have no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } else result = scr_dyn_new_bool(scr_stream_write_dyn(stream, first, NULL)); + } else if (!strcmp(method, "end")) { + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) { + static const char message[] = "Native stream method values with end encodings or callbacks have no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } else if (first->kind == SCR_DYN_UNDEF || first->kind == SCR_DYN_NULL) { + scr_stream_release(scr_stream_end(stream, NULL, NULL, NULL)); + } else if (first->kind == SCR_DYN_STR) { + scr_stream_release(scr_stream_end(stream, NULL, first->v.str, NULL)); + } else if (first->kind == SCR_DYN_BYTES) { + scr_stream_release(scr_stream_end(stream, first->v.bytes, NULL, NULL)); + } else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", first); + } else if (!strcmp(method, "destroy")) { + ScrError *error = first->kind == SCR_DYN_UNDEF || first->kind == SCR_DYN_NULL ? NULL : scr_error_from_dyn(first); + scr_stream_release(scr_stream_destroy(stream, error)); + scr_error_release(error); + } else if (!strcmp(method, "cork")) scr_stream_cork(stream); + else if (!strcmp(method, "uncork")) scr_stream_uncork(stream); + if (!result && !scr_exc_pending()) result = scr_dyn_this_get(); + scr_str_release(name); + scr_stream_release(stream); + return result; +} + +ScrDyn *scr_stream_dyn_view(ScrStream *stream) { + ScrDyn *view = scr_dyn_new_obj(); + ScrDyn *prototype = scr_dyn_new_obj(); + static const char *const methods[] = { + "on", "once", "addListener", "prependListener", "prependOnceListener", + "off", "removeListener", "pipe", "unpipe", "read", "pause", "resume", "isPaused", + "setEncoding", "write", "end", "destroy", "cork", "uncork", + }; + for (size_t i = 0; i < sizeof methods / sizeof *methods; i++) { + ScrClosure *cb = scr_closure_new((void *)scr_stream_view_call, 2); + cb->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); + cb->caps[1] = scr_box_new(SCR_BOX_STR); + scr_box_set_ref(cb->caps[0], scr_stream_retain(stream)); + scr_box_set_ref(cb->caps[1], scr_str_new(methods[i], strlen(methods[i]))); + scr_dyn_obj_set(prototype, methods[i], strlen(methods[i]), + scr_dyn_new_func(cb, scr_stream_view_call, 0, "native stream method", methods[i])); + } + static const char *const properties[] = { + "readable", "readableEnded", "readableLength", "readableHighWaterMark", + "writable", "writableEnded", "writableFinished", "writableLength", "writableHighWaterMark", + "writableNeedDrain", "destroyed", "closed", + }; + for (size_t i = 0; i < sizeof properties / sizeof *properties; i++) { + double value = scr_stream_prop(stream, properties[i]); + bool number = strstr(properties[i], "Length") || strstr(properties[i], "HighWaterMark"); + scr_dyn_obj_set(view, properties[i], strlen(properties[i]), + number ? scr_dyn_new_num(value) : scr_dyn_new_bool(value != 0)); + } + ScrDyn *attached = scr_dyn_set_prototype(view, prototype); + scr_dyn_release(attached); + scr_dyn_release(prototype); + return view; +} diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 62bcd4a1a..ee3c613f0 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1790,7 +1790,7 @@ static int scr_uri_hex_byte(const char *p, size_t rem) { * querystring unit's unescape needs exactly the try/catch shape Node's * qsUnescape wraps around decodeURIComponent (scr_qs.c), and the throwing * entry point below stays byte-identical by rethrowing over NULL. */ -ScrStr *scr_str_decode_uri_component_try(ScrStr *s) { +static ScrStr *scr_str_decode_uri_try(ScrStr *s, bool reserved) { /* Decoding only ever shrinks (%XX → 1 byte), so len is a safe cap. */ ScrStr *out = scr_str_alloc_raw(0, s->len); size_t w = 0; @@ -1805,6 +1805,11 @@ ScrStr *scr_str_decode_uri_component_try(ScrStr *s) { if (b0 < 0) goto malformed; i += 3; if (b0 < 0x80) { + if (reserved && b0 != 0 && strchr(";/?:@&=+$,#", b0)) { + memcpy(out->data + w, p + i - 3, 3); + w += 3; + continue; + } /* The empty component reserved set: every ASCII escape decodes. */ out->data[w++] = (char)b0; continue; @@ -1839,6 +1844,16 @@ ScrStr *scr_str_decode_uri_component_try(ScrStr *s) { return NULL; } +ScrStr *scr_str_decode_uri_component_try(ScrStr *s) { + return scr_str_decode_uri_try(s, false); +} + +ScrStr *scr_str_decode_uri(ScrStr *s) { + ScrStr *out = scr_str_decode_uri_try(s, true); + if (!out) scr_uri_malformed(); + return out; +} + ScrStr *scr_str_decode_uri_component(ScrStr *s) { ScrStr *out = scr_str_decode_uri_component_try(s); if (!out) { diff --git a/packages/runtime/src/scr_text_decoder_labels.h b/packages/runtime/src/scr_text_decoder_labels.h new file mode 100644 index 000000000..626e4ea50 --- /dev/null +++ b/packages/runtime/src/scr_text_decoder_labels.h @@ -0,0 +1,225 @@ +/* Generated by scripts/gen-text-decoder-labels.mts. Do not edit by hand. */ +static const struct { const char *label; int encoding; } scr_td_labels[] = { + {"unicode-1-1-utf-8", -1}, + {"unicode11utf8", -1}, + {"unicode20utf8", -1}, + {"utf-8", -1}, + {"utf8", -1}, + {"x-unicode20utf8", -1}, + {"866", 0}, + {"cp866", 0}, + {"csibm866", 0}, + {"ibm866", 0}, + {"csisolatin2", 1}, + {"iso-8859-2", 1}, + {"iso-ir-101", 1}, + {"iso8859-2", 1}, + {"iso88592", 1}, + {"iso_8859-2", 1}, + {"iso_8859-2:1987", 1}, + {"l2", 1}, + {"latin2", 1}, + {"csisolatin3", 2}, + {"iso-8859-3", 2}, + {"iso-ir-109", 2}, + {"iso8859-3", 2}, + {"iso88593", 2}, + {"iso_8859-3", 2}, + {"iso_8859-3:1988", 2}, + {"l3", 2}, + {"latin3", 2}, + {"csisolatin4", 3}, + {"iso-8859-4", 3}, + {"iso-ir-110", 3}, + {"iso8859-4", 3}, + {"iso88594", 3}, + {"iso_8859-4", 3}, + {"iso_8859-4:1988", 3}, + {"l4", 3}, + {"latin4", 3}, + {"csisolatincyrillic", 4}, + {"cyrillic", 4}, + {"iso-8859-5", 4}, + {"iso-ir-144", 4}, + {"iso8859-5", 4}, + {"iso88595", 4}, + {"iso_8859-5", 4}, + {"iso_8859-5:1988", 4}, + {"arabic", 5}, + {"asmo-708", 5}, + {"csiso88596e", 5}, + {"csiso88596i", 5}, + {"csisolatinarabic", 5}, + {"ecma-114", 5}, + {"iso-8859-6", 5}, + {"iso-8859-6-e", 5}, + {"iso-8859-6-i", 5}, + {"iso-ir-127", 5}, + {"iso8859-6", 5}, + {"iso88596", 5}, + {"iso_8859-6", 5}, + {"iso_8859-6:1987", 5}, + {"csisolatingreek", 6}, + {"ecma-118", 6}, + {"elot_928", 6}, + {"greek", 6}, + {"greek8", 6}, + {"iso-8859-7", 6}, + {"iso-ir-126", 6}, + {"iso8859-7", 6}, + {"iso88597", 6}, + {"iso_8859-7", 6}, + {"iso_8859-7:1987", 6}, + {"sun_eu_greek", 6}, + {"csiso88598e", 7}, + {"csiso88598i", 7}, + {"csisolatinhebrew", 7}, + {"hebrew", 7}, + {"iso-8859-8", 7}, + {"iso-8859-8-e", 7}, + {"iso-8859-8-i", 7}, + {"iso-ir-138", 7}, + {"iso8859-8", 7}, + {"iso88598", 7}, + {"iso_8859-8", 7}, + {"iso_8859-8:1988", 7}, + {"logical", 7}, + {"visual", 7}, + {"csisolatin6", 8}, + {"iso-8859-10", 8}, + {"iso-ir-157", 8}, + {"iso8859-10", 8}, + {"iso885910", 8}, + {"l6", 8}, + {"latin6", 8}, + {"iso-8859-13", 9}, + {"iso8859-13", 9}, + {"iso885913", 9}, + {"iso-8859-14", 10}, + {"iso8859-14", 10}, + {"iso885914", 10}, + {"csisolatin9", 11}, + {"iso-8859-15", 11}, + {"iso8859-15", 11}, + {"iso885915", 11}, + {"iso_8859-15", 11}, + {"l9", 11}, + {"iso-8859-16", 12}, + {"cskoi8r", 13}, + {"koi", 13}, + {"koi8", 13}, + {"koi8-r", 13}, + {"koi8_r", 13}, + {"koi8-ru", 14}, + {"koi8-u", 14}, + {"csmacintosh", 15}, + {"mac", 15}, + {"macintosh", 15}, + {"x-mac-roman", 15}, + {"dos-874", 16}, + {"iso-8859-11", 16}, + {"iso8859-11", 16}, + {"iso885911", 16}, + {"tis-620", 16}, + {"windows-874", 16}, + {"cp1250", 17}, + {"windows-1250", 17}, + {"x-cp1250", 17}, + {"cp1251", 18}, + {"windows-1251", 18}, + {"x-cp1251", 18}, + {"ansi_x3.4-1968", 19}, + {"ascii", 19}, + {"cp1252", 19}, + {"cp819", 19}, + {"csisolatin1", 19}, + {"ibm819", 19}, + {"iso-8859-1", 19}, + {"iso-ir-100", 19}, + {"iso8859-1", 19}, + {"iso88591", 19}, + {"iso_8859-1", 19}, + {"iso_8859-1:1987", 19}, + {"l1", 19}, + {"latin1", 19}, + {"us-ascii", 19}, + {"windows-1252", 19}, + {"x-cp1252", 19}, + {"cp1253", 20}, + {"windows-1253", 20}, + {"x-cp1253", 20}, + {"cp1254", 21}, + {"csisolatin5", 21}, + {"iso-8859-9", 21}, + {"iso-ir-148", 21}, + {"iso8859-9", 21}, + {"iso88599", 21}, + {"iso_8859-9", 21}, + {"iso_8859-9:1989", 21}, + {"l5", 21}, + {"latin5", 21}, + {"windows-1254", 21}, + {"x-cp1254", 21}, + {"cp1255", 22}, + {"windows-1255", 22}, + {"x-cp1255", 22}, + {"cp1256", 23}, + {"windows-1256", 23}, + {"x-cp1256", 23}, + {"cp1257", 24}, + {"windows-1257", 24}, + {"x-cp1257", 24}, + {"cp1258", 25}, + {"windows-1258", 25}, + {"x-cp1258", 25}, + {"x-mac-cyrillic", 26}, + {"x-mac-ukrainian", 26}, + {"x-user-defined", 27}, + {"csunicode", 28}, + {"iso-10646-ucs-2", 28}, + {"ucs-2", 28}, + {"unicode", 28}, + {"unicodefeff", 28}, + {"utf-16", 28}, + {"utf-16le", 28}, + {"unicodefffe", 29}, + {"utf-16be", 29}, + {"chinese", 30}, + {"csgb2312", 30}, + {"csiso58gb231280", 30}, + {"gb18030", 30}, + {"gb2312", 30}, + {"gb_2312", 30}, + {"gb_2312-80", 30}, + {"gbk", 30}, + {"iso-ir-58", 30}, + {"x-gbk", 30}, + {"big5", 31}, + {"big5-hkscs", 31}, + {"cn-big5", 31}, + {"csbig5", 31}, + {"x-x-big5", 31}, + {"cseucpkdfmtjapanese", 32}, + {"euc-jp", 32}, + {"x-euc-jp", 32}, + {"csiso2022jp", 33}, + {"iso-2022-jp", 33}, + {"csshiftjis", 34}, + {"ms932", 34}, + {"ms_kanji", 34}, + {"shift-jis", 34}, + {"shift_jis", 34}, + {"sjis", 34}, + {"windows-31j", 34}, + {"x-sjis", 34}, + {"cseuckr", 35}, + {"csksc56011987", 35}, + {"euc-kr", 35}, + {"iso-ir-149", 35}, + {"korean", 35}, + {"ks_c_5601-1987", 35}, + {"ks_c_5601-1989", 35}, + {"ksc5601", 35}, + {"ksc_5601", 35}, + {"windows-949", 35}, +}; diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c index 2926bbff4..b6d1fd73b 100644 --- a/packages/runtime/src/scr_url.c +++ b/packages/runtime/src/scr_url.c @@ -1167,9 +1167,10 @@ ScrStr *scr_url_hash(ScrUrl *u) { /* Checked native URL values preserve their handle across unknown storage. */ static ScrDyn *scr_native_url_get(void *ptr, const char *key, size_t len) { if (len == 12 && !memcmp(key, "searchParams", len)) { - static const char message[] = "Checked URL.searchParams has no native lowering"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); - return NULL; + ScrSearchParams *params = scr_url_search_params(ptr); + ScrDyn *result = scr_dyn_native_search_params(params); + scr_sp_release(params); + return result; } ScrStr *text = NULL; #define URL_GET(name) if (len == sizeof(#name) - 1 && memcmp(key, #name, len) == 0) text = scr_url_##name(ptr) @@ -1223,6 +1224,174 @@ ScrUrl *scr_dyn_native_url_check(const ScrDyn *value, const ScrDynPath *path) { return scr_url_retain(value->v.handle.ptr); } +static ScrDyn *scr_native_sp_get(void *ptr, const char *key, size_t len) { + if (len == 4 && !memcmp(key, "size", len)) return scr_dyn_new_num(scr_sp_size(ptr)); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *scr_native_sp_step(void *ptr, double *index, unsigned selection) { + bool done = *index >= scr_sp_size(ptr); + ScrDyn *value = scr_dyn_retain(scr_dyn_undefined()); + if (!done) { + scr_dyn_release(value); + ScrStr *key = scr_sp_key_at(ptr, *index), *text = scr_sp_val_at(ptr, *index); + (*index)++; + if (selection == 2) { + value = scr_dyn_new_arr(); + scr_dyn_arr_push(value, scr_dyn_new_str(key)); + scr_dyn_arr_push(value, scr_dyn_new_str(text)); + } else value = scr_dyn_new_str(selection == 1 ? key : text); + scr_str_release(key); scr_str_release(text); + } + 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); + return result; +} + +static ScrDyn *scr_native_sp_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + if (!strcmp(method, "entries") || !strcmp(method, "keys") || !strcmp(method, "values")) + return scr_dyn_native_handle_iterator(self, !strcmp(method, "entries") ? 2 : !strcmp(method, "keys") ? 1 : 0); + if (!strcmp(method, "toString")) { + ScrStr *text = scr_sp_to_string(ptr); + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; + } + if (!strcmp(method, "sort")) { scr_sp_sort(ptr); return scr_dyn_retain(scr_dyn_undefined()); } + if (!strcmp(method, "forEach")) { + if (!argc) { scr_dyn_arg_type_fail("callback", "of type function", scr_dyn_undefined()); return NULL; } + for (double i = 0; i < scr_sp_size(ptr); i++) { + ScrStr *key = scr_sp_key_at(ptr, i), *value = scr_sp_val_at(ptr, i); + ScrDyn *argv[] = {scr_dyn_new_str(value), scr_dyn_new_str(key), self}; + scr_str_release(key); scr_str_release(value); + scr_dyn_this_push_dyn(argc > 1 ? args[1] : NULL); + ScrDyn *called = scr_dyn_call(args[0], argv, 3, "callback"); + scr_dyn_this_pop(); + scr_dyn_release(called); scr_dyn_release(argv[0]); scr_dyn_release(argv[1]); + if (scr_exc_pending()) return NULL; + } + return scr_dyn_retain(scr_dyn_undefined()); + } + bool two = !strcmp(method, "append") || !strcmp(method, "set"); + bool one = !strcmp(method, "get") || !strcmp(method, "getAll") || !strcmp(method, "has") || !strcmp(method, "delete"); + if (!one && !two) { + scr_dyn_arg_type_fail(what, "of type function", scr_dyn_undefined()); + return NULL; + } + if (argc < (two ? 2 : 1)) { + static const char message[] = "Not enough arguments"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_MISSING_ARGS"); + return NULL; + } + ScrStr *key = scr_dyn_string_coerce_js(args[0]); + if (scr_exc_pending()) return NULL; + ScrStr *value = NULL; + if (two || argc > 1 && args[1]->kind != SCR_DYN_UNDEF) value = scr_dyn_string_coerce_js(args[1]); + if (scr_exc_pending()) { scr_str_release(key); return NULL; } + ScrDyn *result = NULL; + if (!strcmp(method, "get")) { + ScrStr *found = scr_sp_get(ptr, key); + result = found ? scr_dyn_new_str(found) : scr_dyn_new_null(); + scr_str_release(found); + } else if (!strcmp(method, "getAll")) { + ScrArr *found = scr_sp_get_all(ptr, key); + result = scr_dyn_new_arr(); + for (size_t i = 0; i < found->len; i++) { + ScrStr *text = scr_arr_get_ref(found, i); + scr_dyn_arr_push(result, scr_dyn_new_str(text)); + scr_str_release(text); + } + scr_arr_release(found); + } else if (!strcmp(method, "has")) result = scr_dyn_new_bool(value ? scr_sp_has_value(ptr, key, value) : scr_sp_has(ptr, key)); + else { + if (!strcmp(method, "append")) scr_sp_append(ptr, key, value); + else if (!strcmp(method, "set")) scr_sp_set(ptr, key, value); + else if (value) scr_sp_delete_value(ptr, key, value); + else scr_sp_delete(ptr, key); + result = scr_dyn_retain(scr_dyn_undefined()); + } + scr_str_release(key); scr_str_release(value); + return result; +} + +ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value) { + static const ScrDynHandleOps ops = { + "URLSearchParams", &scr_sp_retain_v, &scr_sp_release_v, &scr_native_sp_invoke, + &scr_native_sp_get, NULL, NULL, NULL, &scr_native_sp_step, + }; + scr_dyn_handle_install(SCR_DYNH_SEARCH_PARAMS, &ops); + return scr_dyn_new_handle(value, SCR_DYNH_SEARCH_PARAMS); +} + +bool scr_dyn_native_search_params_is(const ScrDyn *value) { + return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_SEARCH_PARAMS; +} + +ScrSearchParams *scr_dyn_native_search_params_check(const ScrDyn *value, const ScrDynPath *path) { + if (!scr_dyn_native_search_params_is(value)) { scr_dyn_check_fail(path, "URLSearchParams", value); return NULL; } + return scr_sp_retain(value->v.handle.ptr); +} + +ScrSearchParams *scr_sp_new_checked(const ScrDyn *value) { + if (!value || value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) return scr_sp_new(); + if (scr_dyn_native_search_params_is(value)) return scr_sp_copy(value->v.handle.ptr); + if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *iterator = scr_dyn_iterator_optional(value); + if (scr_exc_pending()) return NULL; + ScrSearchParams *result = scr_sp_new(); + if (iterator) { + for (;;) { + ScrStr *next_key = scr_str_new("next", 4); + ScrDyn *next = iterator->kind == SCR_DYN_HANDLE ? scr_dyn_handle_key_get(iterator, next_key) : scr_dyn_obj_read(iterator, "next", 4); + scr_str_release(next_key); + scr_dyn_this_push_dyn(iterator); + ScrDyn *step = scr_exc_pending() ? NULL : scr_dyn_call(next, NULL, 0, "iterator.next"); + scr_dyn_this_pop(); + scr_dyn_release(next); + if (scr_exc_pending()) break; + ScrDyn *done = scr_dyn_obj_read(step, "done", 4); + bool end = scr_dyn_truthy(done); + scr_dyn_release(done); + if (end) { scr_dyn_release(step); scr_dyn_release(iterator); return result; } + ScrDyn *row = scr_dyn_obj_read(step, "value", 5); + ScrDyn *pair = scr_dyn_iter_pack(row, NULL); + scr_dyn_release(row); scr_dyn_release(step); + if (scr_exc_pending()) { scr_dyn_release(pair); break; } + if (!pair || pair->kind != SCR_DYN_ARR || pair->v.arr.len != 2) { + static const char message[] = "Each query pair must be an iterable [name, value] tuple"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_TUPLE"); + scr_dyn_release(pair); break; + } + ScrStr *key = scr_dyn_string_coerce_js(pair->v.arr.items[0]); + ScrStr *text = scr_exc_pending() ? NULL : scr_dyn_string_coerce_js(pair->v.arr.items[1]); + if (!scr_exc_pending()) scr_sp_append(result, key, text); + scr_str_release(key); scr_str_release(text); scr_dyn_release(pair); + if (scr_exc_pending()) break; + } + scr_dyn_release(iterator); scr_sp_release(result); return NULL; + } + ScrDyn *keys = scr_dyn_obj_keys(value); + if (scr_exc_pending()) { scr_sp_release(result); return NULL; } + for (size_t i = 0; i < keys->v.arr.len; i++) { + ScrStr *key = keys->v.arr.items[i]->v.str; + ScrDyn *entry = scr_dyn_obj_read(value, key->data, key->len); + ScrStr *text = scr_exc_pending() ? NULL : scr_dyn_string_coerce_js(entry); + scr_dyn_release(entry); + if (!scr_exc_pending()) scr_sp_append(result, key, text); + scr_str_release(text); + if (scr_exc_pending()) { scr_dyn_release(keys); scr_sp_release(result); return NULL; } + } + scr_dyn_release(keys); return result; + } + ScrStr *text = scr_dyn_string_coerce_js(value); + if (scr_exc_pending()) return NULL; + ScrSearchParams *result = scr_sp_parse(text); + scr_str_release(text); + return result; +} + ScrStr *scr_url_checked_to_path(const ScrDyn *value) { if (scr_dyn_native_url_is(value)) return scr_url_to_path(value->v.handle.ptr); if (value->kind == SCR_DYN_STR) return scr_url_str_to_path(value->v.str); diff --git a/packages/runtime/test/bytes.test.ts b/packages/runtime/test/bytes.test.ts index ea9440a66..ead061dc1 100644 --- a/packages/runtime/test/bytes.test.ts +++ b/packages/runtime/test/bytes.test.ts @@ -46,6 +46,7 @@ beforeAll(async () => { join(testDir, "../src/scr_child.c"), join(testDir, "../src/scr_path.c"), join(testDir, "../src/scr_url.c"), + join(testDir, "../src/scr_url_params.c"), "-Wno-deprecated-declarations", "-lz", ...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []), diff --git a/packages/runtime/test/regex.test.ts b/packages/runtime/test/regex.test.ts index 1e54e9daf..c7a486f19 100644 --- a/packages/runtime/test/regex.test.ts +++ b/packages/runtime/test/regex.test.ts @@ -52,16 +52,3 @@ test("regex runtime: matching, substitutions, split, throws, RC accounting", asy const { stderr } = await execFileAsync(bin, []); expect(stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/); }); - -test("test() on a g-flagged regex aborts with the statefulness fence", async () => { - const err = await execFileAsync(bin, ["--crash-global-test"]).then( - () => { - throw new Error("expected scr_regex_test to abort"); - }, - (e: Error & { signal?: string; stderr?: string }) => e, - ); - expect(err.signal).toBe("SIGABRT"); - expect(err.stderr).toContain( - "scriptc: test() on a regex with the 'g' or 'y' flag is not supported", - ); -}); diff --git a/packages/runtime/test/test_regex.c b/packages/runtime/test/test_regex.c index 83e1a1b3f..cc3b99a2c 100644 --- a/packages/runtime/test/test_regex.c +++ b/packages/runtime/test/test_regex.c @@ -8,9 +8,6 @@ * without /g), capture-group splitting, the CESU-8 pattern re-encoding * for non-/u astral patterns, and the UTF-16 buffer round-trip. * - * Special mode: --crash-global-test calls test() on a /g regex and must - * abort() after printing the statefulness fence message (checked by - * regex.test.ts, like test_string.c's --crash-repeat). */ #include "../src/scr_runtime.h" @@ -272,12 +269,13 @@ static void test_split(void) { } int main(int argc, char **argv) { - if (argc > 1 && strcmp(argv[1], "--crash-global-test") == 0) { - ScrStr *s = S("aaa"); - scr_regex_test(&re_a_g, s); /* must abort with the fence message */ - fprintf(stderr, "UNREACHABLE: g-flagged test returned\n"); - return 0; - } + (void)argc; + (void)argv; + ScrStr *subject = S("aaa"); + check(scr_regex_test(&re_a_g, subject) && re_a_g.last_index == 1, "global test advances lastIndex"); + re_a_g.last_index = 3; + check(!scr_regex_test(&re_a_g, subject) && re_a_g.last_index == 0, "failed global test resets lastIndex"); + scr_str_release(subject); test_test(); test_source_flags(); diff --git a/packages/runtime/test/url.test.ts b/packages/runtime/test/url.test.ts index 64f8e7221..c0284be87 100644 --- a/packages/runtime/test/url.test.ts +++ b/packages/runtime/test/url.test.ts @@ -19,6 +19,7 @@ beforeAll(async () => { "-o", bin, join(testDir, "test_url.c"), join(testDir, "../src/scr_url.c"), + join(testDir, "../src/scr_url_params.c"), join(testDir, "../src/scr_path.c"), join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_number.c"), diff --git a/packages/runtime/vendor/v8-number-radix/LICENSE b/packages/runtime/vendor/v8-number-radix/LICENSE new file mode 100644 index 000000000..9898b2d48 --- /dev/null +++ b/packages/runtime/vendor/v8-number-radix/LICENSE @@ -0,0 +1,61 @@ +This license applies to all parts of V8 that are not externally +maintained libraries. The externally maintained libraries used by V8 +are: + + - PCRE test suite, located in + test/mjsunit/third_party/regexp-pcre/regexp-pcre.js. This is based on the + test suite from PCRE-7.3, which is copyrighted by the University + of Cambridge and Google, Inc. The copyright notice and license + are embedded in regexp-pcre.js. + + - Layout tests, located in test/mjsunit/third_party/object-keys. These are + based on layout tests from webkit.org which are copyrighted by + Apple Computer, Inc. and released under a 3-clause BSD license. + + - Strongtalk assembler, the basis of the files assembler-arm-inl.h, + assembler-arm.cc, assembler-arm.h, assembler-ia32-inl.h, + assembler-ia32.cc, assembler-ia32.h, assembler-x64-inl.h, + assembler-x64.cc, assembler-x64.h, assembler.cc and assembler.h. + This code is copyrighted by Sun Microsystems Inc. and released + under a 3-clause BSD license. + + - Valgrind client API header, located at third_party/valgrind/valgrind.h + This is released under the BSD license. + + - The Wasm C/C++ API headers, located at third_party/wasm-api/wasm.{h,hh} + This is released under the Apache license. The API's upstream prototype + implementation also formed the basis of V8's implementation in + src/wasm/c-api.cc. + +These libraries have their own licenses; we recommend you read them, +as their terms may differ from the terms below. + +Further license information can be found in LICENSE files located in +sub-directories. + +Copyright 2014, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4284ef410..bcc157daa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,6 +196,15 @@ importers: packages/runtime-win32-x64-msvc: {} + tests/fixtures/effect4: + dependencies: + '@effect/platform-node': + specifier: 4.0.0 + version: 4.0.0(effect@4.0.0)(redis@6.3.0) + effect: + specifier: 4.0.0 + version: 4.0.0 + packages: '@bytecodealliance/preview2-shim@0.17.6': @@ -221,6 +230,19 @@ packages: resolution: {integrity: sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==} engines: {node: '>=16'} + '@effect/platform-node-shared@4.0.0': + resolution: {integrity: sha512-VBXHJU9UXVZPdQGGDRjGXpo+TpOzRwZuMrdKAtIxi1qwcRftmacZ1zTgIrzBa0Z2RA2KVkoOecQUsTlt/nJZhw==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0 + + '@effect/platform-node@4.0.0': + resolution: {integrity: sha512-/L+4MUbl8FejPKLHLROYSOebvHnfixkP8KiblSD/DE5uH5xENZBHaux4nH6XMnRP+lr8ajpEJFS8sUrmmxFJeQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0 + redis: '>=5.0.0 <7.0.0' + '@emnapi/core@2.0.0-alpha.3': resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} @@ -853,6 +875,42 @@ packages: cpu: [x64] os: [win32] + '@redis/bloom@6.3.0': + resolution: {integrity: sha512-NQ5poYpZr0jv6zazN1cy6JY5kIipKFZ3R9GTL4kOUTX3a9YAfY29U8bsE4lWhVQEM+G4V0BXVC57mubIdDijeA==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.3.0 + + '@redis/client@6.3.0': + resolution: {integrity: sha512-fHrjwGCBvAANGi+TuRuwKi83sIOzgHr8eh+Uf48w3fSX7Hhrz174NrWqfJhAC2vnN0d/tfaZpdr6+o6JC3QHwg==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@node-rs/xxhash': ^1.1.0 + '@opentelemetry/api': '>=1 <2' + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + '@opentelemetry/api': + optional: true + + '@redis/json@6.3.0': + resolution: {integrity: sha512-yt6vRfPBXtW/3qm53cF81TSQpLMjWdW4/2E9WQ2GgbvsKkuCKCFQIcMiKlil1ZMnezK+IQuS795xKU5psbaegw==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.3.0 + + '@redis/search@6.3.0': + resolution: {integrity: sha512-RPXAZmKjZZG7Tm7m1w9keFdXzbL91PiSHQBfQRe9xwW40jNsTRbG0FwhIz4QlHPU4gKDAsCy8IQHj4yZl4H9rw==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.3.0 + + '@redis/time-series@6.3.0': + resolution: {integrity: sha512-+fJOB8mN1z5WRakCJNh5lVdz1KqvHGSOistsKeitD24QsCtPwyRQXWC0iq6T/i/ubLE/YcguZqPm8F598MKk4A==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.3.0 + '@renovatebot/pep440@4.2.1': resolution: {integrity: sha512-2FK1hF93Fuf1laSdfiEmJvSJPVIDHEUTz68D3Fi9s0IZrrpaEcj6pTFBTbYvsgC5du4ogrtf5re7yMMvrKNgkw==} engines: {node: ^20.9.0 || ^22.11.0 || ^24, pnpm: ^10.0.0} @@ -1122,6 +1180,9 @@ packages: '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@types/ws@8.18.2': + resolution: {integrity: sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==} + '@typescript-eslint/eslint-plugin@8.63.0': resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1657,6 +1718,9 @@ packages: effect@3.22.2: resolution: {integrity: sha512-fm1CQXCs7GTMwpadZxwU5rnAb2bN6mbQpknq/pXBGCzSKkosNYb9k0wsoL0TfB10rSa5xOEZJjo77u/KLXSANg==} + effect@4.0.0: + resolution: {integrity: sha512-ooc1TG5t+FfzgYnFz2ff6BBKyZ7EwBRVXC7c4RhQUAD6/TZ2gTXXMeb4WX7a19ozQo4J73/QW+S00YAIresoMQ==} + end-of-stream@1.1.0: resolution: {integrity: sha512-EoulkdKF/1xa92q25PbjuDcgJ9RDHYU2Rs3SCIvs2/dSQ3BpmxneNHmA/M7fe60M3PrV7nNGTTNbkK62l6vXiQ==} @@ -2266,6 +2330,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + redis@6.3.0: + resolution: {integrity: sha512-XFQbPie1lGpKeUZ8ySYY43yLnQy/iIQtHNvIr/1XdaABpb96rUSdm3Mfl+98FEVbv9ZJIzINMno5s0Nh8E5LEw==} + engines: {node: '>= 20.0.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2533,6 +2601,10 @@ packages: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} + undici@8.11.2: + resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} + engines: {node: '>=22.19.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2660,6 +2732,18 @@ packages: utf-8-validate: optional: true + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xdg-app-paths@5.1.0: resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==} engines: {node: '>=6'} @@ -2716,6 +2800,25 @@ snapshots: dependencies: '@edge-runtime/primitives': 4.1.0 + '@effect/platform-node-shared@4.0.0(effect@4.0.0)': + dependencies: + '@types/ws': 8.18.2 + effect: 4.0.0 + ws: 8.22.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform-node@4.0.0(effect@4.0.0)(redis@6.3.0)': + dependencies: + '@effect/platform-node-shared': 4.0.0(effect@4.0.0) + effect: 4.0.0 + redis: 6.3.0 + undici: 8.11.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@emnapi/core@2.0.0-alpha.3': dependencies: '@emnapi/wasi-threads': 2.0.1 @@ -3114,6 +3217,24 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.111.0': optional: true + '@redis/bloom@6.3.0(@redis/client@6.3.0)': + dependencies: + '@redis/client': 6.3.0 + + '@redis/client@6.3.0': {} + + '@redis/json@6.3.0(@redis/client@6.3.0)': + dependencies: + '@redis/client': 6.3.0 + + '@redis/search@6.3.0(@redis/client@6.3.0)': + dependencies: + '@redis/client': 6.3.0 + + '@redis/time-series@6.3.0(@redis/client@6.3.0)': + dependencies: + '@redis/client': 6.3.0 + '@renovatebot/pep440@4.2.1': {} '@rolldown/binding-android-arm64@1.0.0-rc.1': @@ -3282,6 +3403,10 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/ws@8.18.2': + dependencies: + '@types/node': 24.13.3 + '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3963,6 +4088,8 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 + effect@4.0.0: {} + end-of-stream@1.1.0: dependencies: once: 1.3.3 @@ -4590,6 +4717,17 @@ snapshots: readdirp@4.1.2: {} + redis@6.3.0: + dependencies: + '@redis/bloom': 6.3.0(@redis/client@6.3.0) + '@redis/client': 6.3.0 + '@redis/json': 6.3.0(@redis/client@6.3.0) + '@redis/search': 6.3.0(@redis/client@6.3.0) + '@redis/time-series': 6.3.0(@redis/client@6.3.0) + transitivePeerDependencies: + - '@node-rs/xxhash' + - '@opentelemetry/api' + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -4893,6 +5031,8 @@ snapshots: undici@7.29.0: {} + undici@8.11.2: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -5049,6 +5189,8 @@ snapshots: ws@8.21.1: {} + ws@8.22.0: {} + xdg-app-paths@5.1.0: dependencies: xdg-portable: 7.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 26f47b5b8..cf6325641 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,8 +1,12 @@ packages: - "packages/*" - "internal/*" + - "tests/fixtures/effect4" minimumReleaseAgeExclude: - "vercel@58.1.0" + - "effect@4.0.0" + - "@effect/platform-node@4.0.0" + - "@effect/platform-node-shared@4.0.0" allowBuilds: esbuild: true overrides: diff --git a/scripts/gen-text-decoder-labels.mts b/scripts/gen-text-decoder-labels.mts new file mode 100644 index 000000000..dea2cb1a1 --- /dev/null +++ b/scripts/gen-text-decoder-labels.mts @@ -0,0 +1,9 @@ +import { writeFileSync } from "node:fs"; +import { textDecoderLabels } from "../packages/compiler/src/frontend/lowering/text-decoder-encoding.js"; + +writeFileSync(new URL("../packages/runtime/src/scr_text_decoder_labels.h", import.meta.url), [ + "/* Generated by scripts/gen-text-decoder-labels.mts. Do not edit by hand. */", + "static const struct { const char *label; int encoding; } scr_td_labels[] = {", + ...textDecoderLabels.map(([label, encoding]) => ` {${JSON.stringify(label)}, ${encoding}},`), + "};", "", +].join("\n")); diff --git a/scripts/sandbox-test.mjs b/scripts/sandbox-test.mjs index 1b91988c3..5aa2f0a61 100644 --- a/scripts/sandbox-test.mjs +++ b/scripts/sandbox-test.mjs @@ -28,6 +28,7 @@ import { REMOTE_COMMAND_PENDING, sandboxCommand, sandboxStatusCommand, waitForSa const root = fileURLToPath(new URL("../", import.meta.url)); const laneCaseShardedFiles = [ "tests/harness/effect.test.ts", + "tests/harness/effect4.test.ts", "tests/harness/differential.test.ts", "tests/harness/llvm-differential.test.ts", "tests/harness/npm.test.ts", @@ -626,10 +627,10 @@ async function cleanup() { if (values.keep || created.size === 0) return; if (!cleanupPromise) { cleanupPromise = (async () => { - console.log(`\nRemoving ${created.size} disposable sandbox${created.size === 1 ? "" : "es"}...`); + console.log(`\nStopping ${created.size} disposable sandbox${created.size === 1 ? "" : "es"}...`); const results = await Promise.allSettled( [...created].map((name) => - vercel(["sandbox", "remove", name], { + vercel(["sandbox", "stop", name], { label: name, quiet: true, timeoutMs: 60_000, @@ -638,7 +639,7 @@ async function cleanup() { ); const failures = results.filter((result) => result.status === "rejected"); if (failures.length) { - console.error(`Failed to remove ${failures.length} sandbox${failures.length === 1 ? "" : "es"}.`); + console.error(`Failed to stop ${failures.length} sandbox${failures.length === 1 ? "" : "es"}.`); } })(); } @@ -651,10 +652,10 @@ for (const signal of ["SIGINT", "SIGTERM"]) { handlingSignal = true; for (const child of children) child.kill("SIGTERM"); if (!values.keep && created.size) { - console.log(`\nRemoving ${created.size} disposable sandbox${created.size === 1 ? "" : "es"}...`); + console.log(`\nStopping ${created.size} disposable sandbox${created.size === 1 ? "" : "es"}...`); spawnSync( "vercel", - ["sandbox", "remove", ...scopeArgs, ...created], + ["sandbox", "stop", ...scopeArgs, ...created], { cwd: root, env: { ...vercelProcessEnv, NO_UPDATE_NOTIFIER: "1" }, diff --git a/tests/corpus/110-number-radix.ts b/tests/corpus/110-number-radix.ts new file mode 100644 index 000000000..4bb7f0604 --- /dev/null +++ b/tests/corpus/110-number-radix.ts @@ -0,0 +1,8 @@ +const values = [0, -0, 1, -1, 255, 0.1, 0.5, 1.0000000000000002, Number.MIN_VALUE, Number.MAX_VALUE, 9007199254740991, 9007199254740992, 1e30, Infinity, -Infinity, NaN] +for (const value of values) { + for (const radix of [2, 3, 8, 10, 16, 36]) console.log(value.toString(radix)) +} +for (const radix of [0, 1, 37, NaN, Infinity]) { + try { console.log((42).toString(radix)) } catch (error) { console.log(error instanceof RangeError, (error as Error).message) } +} +console.log((42).toString(undefined), (42).toString(16.9)) diff --git a/tests/corpus/111-record-entries.ts b/tests/corpus/111-record-entries.ts new file mode 100644 index 000000000..7a462adb0 --- /dev/null +++ b/tests/corpus/111-record-entries.ts @@ -0,0 +1,26 @@ +const defaults = { strict: true }; +const forced = { target: 99, lib: ["lib.es2025.d.ts"], types: [] as string[], allowJs: true }; +function serialize(options: Record): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(options)) { + if (value === undefined) continue; + out[key] = value; + } + return out; +} +const options = { ...defaults, ...forced }; +console.log(JSON.stringify(serialize(options))); +console.log(JSON.stringify(serialize({ ...forced }))); +const base = { value: 1 }; +const boxed: unknown = base; +(boxed as { value: number }).value = 2; +console.log(base.value); +const writable: any = base; +writable.value = 3; +console.log(base.value, writable === base); +const extra: any = { value: 4 }; +extra.added = { value: 5 }; +console.log(extra.added.value, JSON.stringify(Object.keys(extra))); +extra.value = 7; +console.log(extra.value, extra.added.value); +console.log(JSON.stringify(Object.assign({}, ...[{ left: 1 }, { right: 2 }], { end: 3 }))); diff --git a/tests/corpus/112-class-descriptor-key.js b/tests/corpus/112-class-descriptor-key.js new file mode 100644 index 000000000..48009dbfa --- /dev/null +++ b/tests/corpus/112-class-descriptor-key.js @@ -0,0 +1,4 @@ +class Bag { constructor() { this.x = 1; } } +const bag = new Bag(); +Object.defineProperty(bag, String("data"), { value: 2 }); +console.log(bag.x, bag.data); diff --git a/tests/corpus/113-promise-checked-exits.ts b/tests/corpus/113-promise-checked-exits.ts new file mode 100644 index 000000000..a611c08d8 --- /dev/null +++ b/tests/corpus/113-promise-checked-exits.ts @@ -0,0 +1,19 @@ +async function main() { + const numberSource = Promise.resolve(42); + const numberValue: unknown = numberSource; + const first = numberValue as Promise; + const second = numberValue as Promise; + console.log(first === second, first === numberSource, first !== Promise.resolve(42)); + console.log(await first); + const flag: unknown = Promise.resolve(true); + const text: unknown = Promise.resolve("hello"); + const record: unknown = Promise.resolve({ value: 7 }); + const empty: unknown = Promise.resolve(); + console.log(await (flag as Promise), await (text as Promise), (await (record as Promise<{ value: number }>)).value); + await (empty as Promise); + console.log(undefined); + const failed: unknown = Promise.reject(new Error("original")); + try { await (failed as Promise); } + catch (error) { console.log((error as Error).message); } +} +main(); diff --git a/tests/corpus/114-live-record-delete.ts b/tests/corpus/114-live-record-delete.ts new file mode 100644 index 000000000..a156ee228 --- /dev/null +++ b/tests/corpus/114-live-record-delete.ts @@ -0,0 +1,8 @@ +const data: Record = JSON.parse('{"types":"index.d.ts","main":"index.js"}'); +const view = data as Record; +delete view.types; +console.log(JSON.stringify(data), JSON.stringify(Object.keys(view))); +const exports: Record = JSON.parse('{"types":"index.d.ts","node":"index.js"}'); +const alias: unknown = exports; +delete (alias as Record).types; +console.log(JSON.stringify(exports), "types" in exports); diff --git a/tests/corpus/115-array-compound.js b/tests/corpus/115-array-compound.js new file mode 100644 index 000000000..053e69f0d --- /dev/null +++ b/tests/corpus/115-array-compound.js @@ -0,0 +1,13 @@ +const values = new Array(4).fill(0); +let reads = 0; +function index() { reads++; return 1; } +const sum = values[index()] += 7; +const shifted = values[1] <<= 2; +const text = values[2] += "x"; +console.log(JSON.stringify(values), sum, shifted, text, reads); + +const object = { value: 2 }; +let order = ""; +const key = { toString() { order += "k"; return "value"; } }; +const result = object[key] += (() => { order += "r"; return 3; })(); +console.log(result, object.value, order); diff --git a/tests/corpus/116-text-decoder-runtime.js b/tests/corpus/116-text-decoder-runtime.js new file mode 100644 index 000000000..e553c89e5 --- /dev/null +++ b/tests/corpus/116-text-decoder-runtime.js @@ -0,0 +1,11 @@ +function decode(label) { + return new TextDecoder(label).decode(new Uint8Array([0x68, 0x69])); +} +console.log(decode(), decode(" \tUTF-8\r\n"), decode("latin1")); +for (const label of ["bad", "", "utf-8\u00a0"]) { + try { + decode(label); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} diff --git a/tests/corpus/117-weak-cache-cycle.js b/tests/corpus/117-weak-cache-cycle.js new file mode 100644 index 000000000..39760e1e1 --- /dev/null +++ b/tests/corpus/117-weak-cache-cycle.js @@ -0,0 +1,9 @@ +'use strict'; +function build() { + const cache = new WeakMap(); + const value = { read: () => cache }; + const key = {}; + cache.set(key, value); + console.log(cache.get(key) === value); +} +build(); diff --git a/tests/corpus/118-fs-stored-callbacks.cjs b/tests/corpus/118-fs-stored-callbacks.cjs new file mode 100644 index 000000000..732c93f01 --- /dev/null +++ b/tests/corpus/118-fs-stored-callbacks.cjs @@ -0,0 +1,33 @@ +'use strict'; +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const call = (fn, ...args) => new Promise((resolve, reject) => fn(...args, (error, value) => error ? reject(error) : resolve(value))); +async function main() { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-callback-')); + const file = path.join(directory, 'data.txt'); + try { + await call(fs.writeFile, file, 'one', { flag: 'wx', mode: 0o600 }); + await call(fs.appendFile, file, 'two'); + const controller = new AbortController(); + controller.abort(); + try { await call(fs.writeFile, file, 'cancelled', { signal: controller.signal }); } catch (error) { console.log(error.name, error.code); } + console.log(await call(fs.readFile, file, 'utf8')); + try { await call(fs.writeFile, file, 'lost', { flag: 'wx' }); } catch (error) { console.log(error.code); } + const info = await call(fs.stat, file); + console.log(info.isFile(), info.size); + const copy = path.join(directory, 'copy.txt'); + await call(fs.copyFile, file, copy, fs.constants.COPYFILE_EXCL); + console.log(await call(fs.readFile, copy, 'utf8')); + const source = path.join(directory, 'source'); + const destination = path.join(directory, 'destination'); + await call(fs.mkdir, source); + await call(fs.writeFile, path.join(source, 'a.txt'), 'copied'); + await call(fs.cp, source, destination, { recursive: true }); + console.log(await call(fs.readFile, path.join(destination, 'a.txt'), 'utf8')); + try { await call(fs.stat, path.join(directory, 'missing')); } catch (error) { console.log(error.code); } + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +} +main(); diff --git a/tests/corpus/119-native-patterns.js b/tests/corpus/119-native-patterns.js new file mode 100644 index 000000000..351f33182 --- /dev/null +++ b/tests/corpus/119-native-patterns.js @@ -0,0 +1,24 @@ +'use strict'; +const collect = function (items) { + const iterator = items[Symbol.iterator](); + const result = []; + for (;;) { + const next = iterator.next(); + if (next.done) return result; + result.push(next.value); + } +}; +console.log(JSON.stringify(collect([1, 2, 3]))); +console.log(JSON.stringify([1, 2].flatMap((value) => [value, value + 10]))); +console.log(JSON.stringify(JSON.parse("[1,2]").flatMap((value) => [value, value + 10]))); +const adapt = ([value]) => value; +const stored = adapt; +console.log(stored(['kept', 'surplus'], 'extra')); +function errorBase() { + return class extends Error { + constructor(value) { super(value.message); this.reason = value.reason; } + }; +} +class Failure extends errorBase() {} +const failure = new Failure({ message: 'broken', reason: 'test' }); +console.log(failure.message, failure.reason, failure instanceof Error); diff --git a/tests/corpus/1614-js-fence-dodges.cjs b/tests/corpus/1614-js-fence-dodges.cjs index aeb6366f9..e0b412d05 100644 --- a/tests/corpus/1614-js-fence-dodges.cjs +++ b/tests/corpus/1614-js-fence-dodges.cjs @@ -1,7 +1,6 @@ -// @deferred-fences: 1 // Unsupported JS statements remain deferred behind untaken branches. // Both bigint and number table arms now have native checked storage; -// the absent-global Proxy expression remains the deferred fence. +// the Proxy expression also has native checked storage. 'use strict'; function platformScale(ms) { diff --git a/tests/corpus/2597-dgram-send-ladders.cjs b/tests/corpus/2597-dgram-send-ladders.cjs index c7449bbd5..d85371a41 100644 --- a/tests/corpus/2597-dgram-send-ladders.cjs +++ b/tests/corpus/2597-dgram-send-ladders.cjs @@ -27,11 +27,13 @@ show(() => sock.send('hello', 3, 4, 12345, host)); show(() => sock.send(new Uint8Array([1, 2, 3, 4, 5]).subarray(0, 5), 6, 0, 12345, host)); show(() => sock.send(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]).subarray(2, 7), 0, 6, 12345, host)); show(() => sock.send(new DataView(new ArrayBuffer(7), 1, 5), 3, 4, 12345, host)); -sock.connect(12345, () => { +const peer = dgram.createSocket('udp4'); +peer.bind(0, host, () => sock.connect(peer.address().port, host, () => { show(() => sock.send(buf, 1, 1, -1, host)); show(() => sock.send(buf, 1234, '127.0.0.1', () => {})); show(() => sock.send('hello', 6, 0)); show(() => sock.send('hello', 0, 6)); show(() => sock.send(23, 12345, host)); sock.close(); -}); + peer.close(); +})); diff --git a/tests/corpus/2856-dynamic-any-local-operators.ts b/tests/corpus/2856-dynamic-any-local-operators.ts index cd2df8859..40ed945e1 100644 --- a/tests/corpus/2856-dynamic-any-local-operators.ts +++ b/tests/corpus/2856-dynamic-any-local-operators.ts @@ -1,7 +1,7 @@ // @dynamic // Checked-dynamic producers stay in dyn storage under an `any` spelling to -// preserve aliases, but JS-coercive operators cross their operands into the -// island. Strict equality remains native so object identity is not copied. +// preserve aliases. Static builds support coercive operators natively; +// the dynamic tier retains its island conversions. Strict equality preserves identity. function answer(): number { const value: any = JSON.parse("41"); return value + 1; diff --git a/tests/corpus/3125-regex-global-match.ts b/tests/corpus/3125-regex-global-match.ts new file mode 100644 index 000000000..ec7480df2 --- /dev/null +++ b/tests/corpus/3125-regex-global-match.ts @@ -0,0 +1,23 @@ +function matches(subject: string, pattern: RegExp): void { + console.log(JSON.stringify(subject.match(pattern))); +} + +matches("hello world hello", /(hello|world)/g); +matches("no matches", /xyz/g); +matches("", /x/g); +matches("", /(?:)/g); +matches("ab", /(?:)/g); +matches("😀x", /(?:)/gu); +matches("😀x", /(?:)/g); +matches("aa ba", /a/gy); +matches("ba", /a/gy); +matches("aaa", /(?:)/gy); +const repeated = /a/g; +matches("aba", repeated); +matches("aba", repeated); +matches("HELLO\nhello", /^hello/gim); +matches("one two", /(?\w+)/g); +console.log(JSON.stringify("one two".match(/\w+/g))); +const globalMatch = "one two".match(/(?\w+)/g)!; +console.log(globalMatch.groups === undefined, globalMatch.index === undefined, globalMatch.input === undefined); +console.log(JSON.stringify(/(one) (two)/.exec("one two"))); diff --git a/tests/corpus/3126-js-number-radix/format.js b/tests/corpus/3126-js-number-radix/format.js new file mode 100644 index 000000000..9bce34133 --- /dev/null +++ b/tests/corpus/3126-js-number-radix/format.js @@ -0,0 +1,16 @@ +export function format(values, radix) { + let result = ""; + for (let i = 0; i < values.length; i++) { + if (i > 0) result += ","; + result += values[i].toString(radix).padStart(2, "0"); + } + return result; +} + +export function missing(values) { + try { + return values[values.length].toString(16); + } catch (error) { + return error instanceof TypeError; + } +} diff --git a/tests/corpus/3126-js-number-radix/main.ts b/tests/corpus/3126-js-number-radix/main.ts new file mode 100644 index 000000000..3f0f991df --- /dev/null +++ b/tests/corpus/3126-js-number-radix/main.ts @@ -0,0 +1,11 @@ +import { format, missing } from "./format.js"; + +console.log(format(new Uint8Array([0, 1, 15, 16, 128, 255]), 16)); +console.log(format(new Uint8Array([2, 8, 32]), 2)); +console.log(format(new Float64Array([0.5, -42, Infinity, NaN]), 16)); +try { + format(new Uint8Array([1]), 1); +} catch (error) { + console.log(error instanceof RangeError, (error as Error).message); +} +console.log(missing(new Uint8Array([1]))); diff --git a/tests/corpus/3127-spawn-runtime-options.ts b/tests/corpus/3127-spawn-runtime-options.ts new file mode 100644 index 000000000..cead946d2 --- /dev/null +++ b/tests/corpus/3127-spawn-runtime-options.ts @@ -0,0 +1,41 @@ +import { spawn } from "node:child_process"; +import type { SpawnOptions } from "node:child_process"; + +async function capture(command: string, args: string[], options: SpawnOptions | undefined): Promise { + const child = spawn(command, args, options); + let spawned = false; + child.on("spawn", () => { spawned = true; }); + child.unref(); + child.ref(); + let stdout = ""; + let stderr = ""; + child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); }); + child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); }); + await new Promise((resolve) => { + child.on("close", (code) => { + console.log(spawned, code, JSON.stringify(stdout), JSON.stringify(stderr)); + resolve(); + }); + }); +} + +await capture("/bin/sh", ["-c", "printf default; printf error >&2"], undefined); +await capture("/bin/sh", ["-c", "printf '%s:%s' \"$PWD\" \"$GREETING\"; printf error >&2"], { + stdio: ["ignore", "pipe", "pipe"], cwd: "/", detached: false, + env: { GREETING: "hello", UNUSED: undefined }, shell: false, windowsHide: true, +}); +await capture("/bin/sh", ["-c", "printf hidden"], { stdio: "ignore" }); +await capture("printf shell", [], { stdio: "pipe", shell: true }); +await new Promise((resolve) => { + const options: SpawnOptions = { stdio: "ignore" }; + spawn("/bin/echo", options).on("close", (code) => { console.log("options only", code); resolve(); }); +}); +const invalid: unknown[] = [null, 1, { detached: 1 }, { windowsHide: 1 }, { cwd: 1 }]; +for (const options of invalid) { + try { + spawn("/bin/sh", [], options as SpawnOptions); + } catch (error) { + const failure = error as NodeJS.ErrnoException; + console.log(failure.code, failure.message); + } +} diff --git a/tests/corpus/3128-regex-last-index.ts b/tests/corpus/3128-regex-last-index.ts new file mode 100644 index 000000000..e10f77370 --- /dev/null +++ b/tests/corpus/3128-regex-last-index.ts @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; + +const global = /a/g; +console.log(global.lastIndex, global.test("aba"), global.lastIndex); +console.log(global.test("aba"), global.lastIndex, global.test("aba"), global.lastIndex); +global.lastIndex = 1; +console.log(JSON.stringify(global.exec("aba")), global.lastIndex); +global.lastIndex = 100; +console.log(global.exec("aba"), global.lastIndex); +global.lastIndex = -2; +console.log(global.test("aba"), global.lastIndex); +global.lastIndex = NaN; +console.log(global.test("aba"), global.lastIndex); +const sticky = /a/y; +sticky.lastIndex = 1; +console.log(JSON.stringify("ba".match(sticky)), sticky.lastIndex); +console.log(sticky.exec("ba"), sticky.lastIndex); +const regular = /a/; +regular.lastIndex = 5; +console.log(regular.test("a"), regular.lastIndex); +function fresh(): RegExp { return /a/g; } +const first = fresh(); +const second = fresh(); +console.log(first.test("aba"), first.lastIndex, second.lastIndex, second.test("aba")); +const empty = /(?:)/g; +console.log(JSON.stringify(empty.exec("a")), empty.lastIndex); +empty.lastIndex = 1; +console.log(JSON.stringify(empty.exec("a")), empty.lastIndex); +global.lastIndex = 2; +console.log(JSON.stringify(Array.from("aba".matchAll(global))), global.lastIndex); +console.log("aba".replace(global, "x"), global.lastIndex); +sticky.lastIndex = 1; +console.log("ba".replace(sticky, "x"), sticky.lastIndex); +sticky.lastIndex = 1; +console.log("ba".search(sticky), sticky.lastIndex); +console.log(JSON.stringify("ba".split(sticky)), sticky.lastIndex); +const assertion = /a/g; +assert.match("aba", assertion); +console.log("assertion", assertion.lastIndex); +assert.match("aba", assertion); +console.log("assertion", assertion.lastIndex); +assert.doesNotMatch("aba", assertion); +console.log("assertion", assertion.lastIndex); diff --git a/tests/corpus/3129-js-token-union/lexer.js b/tests/corpus/3129-js-token-union/lexer.js new file mode 100644 index 000000000..8ead4d113 --- /dev/null +++ b/tests/corpus/3129-js-token-union/lexer.js @@ -0,0 +1,26 @@ +// Inferred JavaScript variants have widened tags and optional padded fields. +export function lex(args) { + const tokens = []; + for (const arg of args) { + if (!arg.startsWith("-") || /^-\d+$/.test(arg)) { + tokens.push({ _tag: "Value", value: arg }); + } else if (arg.startsWith("--")) { + const equalIndex = arg.indexOf("="); + if (equalIndex !== -1) { + tokens.push({ _tag: "LongOption", name: arg.slice(2, equalIndex), raw: arg, value: arg.slice(equalIndex + 1) }); + } else { + tokens.push({ _tag: "LongOption", name: arg.slice(2), raw: arg }); + } + } else { + const flags = arg.slice(1); + const equalIndex = flags.indexOf("="); + if (equalIndex !== -1) { + const flag = flags.slice(0, equalIndex); + tokens.push({ _tag: "ShortOption", flag, raw: `-${flag}`, value: flags.slice(equalIndex + 1) }); + } else { + for (const flag of flags) tokens.push({ _tag: "ShortOption", flag, raw: `-${flag}` }); + } + } + } + return tokens; +} diff --git a/tests/corpus/3129-js-token-union/main.ts b/tests/corpus/3129-js-token-union/main.ts new file mode 100644 index 000000000..4277de976 --- /dev/null +++ b/tests/corpus/3129-js-token-union/main.ts @@ -0,0 +1,3 @@ +import { lex } from "./lexer.js"; + +console.log(JSON.stringify(lex(["world", "--loud", "--name=value", "-ab", "-c=value", "-2"]))); diff --git a/tests/corpus/3130-child-generic-streams.ts b/tests/corpus/3130-child-generic-streams.ts new file mode 100644 index 000000000..db2093ba9 --- /dev/null +++ b/tests/corpus/3130-child-generic-streams.ts @@ -0,0 +1,24 @@ +import { spawn } from "node:child_process"; +import { PassThrough } from "node:stream"; + +// Effect-style generic storage carries the child through an untyped slot. +const child: any = await Promise.resolve(spawn("/bin/cat", [], { stdio: "pipe" })); +const output = new PassThrough(); +let text = ""; +output.on("data", (chunk) => { text += chunk.toString(); }); +const finished = new Promise((resolve) => { output.on("end", resolve); }); +let spawned = false; +let exitCode: number | null = null; +let exitSignal: string | null = null; +child.on("spawn", () => { spawned = typeof child.pid === "number"; }); +const removed = () => { console.log("removed"); }; +child.on("exit", removed); +child.removeListener("exit", removed); +child.on("exit", (code: number | null, signal: string | null) => { exitCode = code; exitSignal = signal; }); +const closed = new Promise((resolve) => { child.on("close", resolve); }); +console.log(child.stdout === child.stdout, child.stdin === child.stdin); +child.stdout.pipe(output); +child.stdin.end("hello"); +await finished; +await closed; +console.log(spawned, exitCode, exitSignal, JSON.stringify(text)); diff --git a/tests/corpus/3131-child-rest-listeners.js b/tests/corpus/3131-child-rest-listeners.js new file mode 100644 index 000000000..8e2608540 --- /dev/null +++ b/tests/corpus/3131-child-rest-listeners.js @@ -0,0 +1,11 @@ +import { spawn } from "node:child_process"; + +const child = spawn("/bin/sh", ["-c", "exit 0"], { stdio: "ignore" }); +child.on("spawn", (...args) => { console.log("spawn", JSON.stringify(args)); }); +child.once("exit", (...args) => { console.log("exit", JSON.stringify(args)); }); +await new Promise((resolve) => { + child.on("close", (code, ...args) => { console.log("close", code, JSON.stringify(args)); resolve(undefined); }); +}); + +const failed = spawn("/scriptc-command-that-does-not-exist", [], { stdio: "ignore" }); +failed.on("error", (...args) => { console.log("error", args.length, args[0].code); }); diff --git a/tests/corpus/3132-child-generic-stream-completion.ts b/tests/corpus/3132-child-generic-stream-completion.ts new file mode 100644 index 000000000..1e28720f3 --- /dev/null +++ b/tests/corpus/3132-child-generic-stream-completion.ts @@ -0,0 +1,25 @@ +import { spawn } from "node:child_process"; + +const child: any = await Promise.resolve(spawn("/bin/cat", [], { stdio: "pipe" })); +const input = child.stdin; +const output = child.stdout; +console.log(input.writable, input.writableEnded, input.writableFinished); +let length = 0; +output.on("data", (chunk: Buffer) => { length += chunk.length; }); +const ended = new Promise((resolve) => { output.on("end", resolve); }); +const closed = new Promise((resolve) => { child.on("close", resolve); }); +const finished = new Promise((resolve) => { input.on("finish", resolve); }); +input.end("x".repeat(512 * 1024)); +await finished; +await ended; +await closed; +console.log(length, input.writable, input.writableEnded, input.writableFinished, output.readableEnded); + +const broken: any = await Promise.resolve(spawn("/bin/sh", ["-c", "exec 0<&-; printf ready; sleep 1"], { stdio: "pipe" })); +const brokenClosed = new Promise((resolve) => { broken.on("close", resolve); }); +const error = new Promise((resolve) => { + broken.stdin.on("error", (failure: NodeJS.ErrnoException) => { console.log(failure.code); resolve(); }); +}); +broken.stdout.on("data", () => { broken.stdin.write("hello"); }); +await error; +await brokenClosed; diff --git a/tests/corpus/3133-text-decoder-streaming.ts b/tests/corpus/3133-text-decoder-streaming.ts new file mode 100644 index 000000000..83b48f817 --- /dev/null +++ b/tests/corpus/3133-text-decoder-streaming.ts @@ -0,0 +1,29 @@ +const decoder = new TextDecoder(); +const alias = decoder; +console.log(JSON.stringify(decoder.decode(Uint8Array.from([0xef]), { stream: true }))); +console.log(JSON.stringify(alias.decode(Uint8Array.from([0xbb, 0xbf, 0xf0, 0x9f]), { stream: true }))); +console.log(JSON.stringify(decoder.decode(Uint8Array.from([0x98, 0x80, 0xef, 0xbb, 0xbf]), { stream: true }))); +console.log(JSON.stringify(alias.decode())); +console.log(JSON.stringify(decoder.decode(Uint8Array.from([0xef, 0xbb, 0xbf, 65])))); +console.log(JSON.stringify(decoder.decode(Uint8Array.from([0xe0, 0x80]), { stream: true }))); +console.log(JSON.stringify(decoder.decode())); +console.log(JSON.stringify(decoder.decode(Uint8Array.from([0xe2, 0x82]), { stream: true }))); +console.log(JSON.stringify(decoder.decode())); + +for (const label of ["utf-16le", "utf-16be"]) { + const unicode = new TextDecoder(label); + const bytes = label === "utf-16le" ? [0xff, 0xfe, 0x3d, 0xd8, 0x00, 0xde] : [0xfe, 0xff, 0xd8, 0x3d, 0xde, 0x00]; + let value = ""; + for (const byte of bytes) value += unicode.decode(Uint8Array.from([byte]), { stream: true }); + value += unicode.decode(); + console.log(label, JSON.stringify(value)); +} + +const latin = new TextDecoder("windows-1252"); +console.log(JSON.stringify(latin.decode(Uint8Array.from([0x80]), { stream: true }) + latin.decode(Uint8Array.from([0xe9])))); +const preserving = new TextDecoder("utf8", { ignoreBOM: true }); +console.log(JSON.stringify(preserving.decode(Uint8Array.from([0xef, 0xbb]), { stream: true }) + preserving.decode(Uint8Array.from([0xbf])))); +const strict = new TextDecoder("utf8", { fatal: true }); +console.log(JSON.stringify(strict.decode(Uint8Array.from([0xf0, 0x9f]), { stream: true }))); +try { strict.decode(); } catch (error) { console.log((error as NodeJS.ErrnoException).code); } +console.log(JSON.stringify(strict.decode(Uint8Array.from([65])))); diff --git a/tests/diagnostics/any.ts b/tests/diagnostics/any.ts index 56ef1858a..cbb5c5c96 100644 --- a/tests/diagnostics/any.ts +++ b/tests/diagnostics/any.ts @@ -6,5 +6,5 @@ const n: any = 41; const sum = n + 1; // ToNumber/ToPrimitive coercion — engine only var { x } = 0; // destructuring a non-object 'any' — engine only -for (const v of n as any) console.log(v); // iterating an 'any' value +for (const v of n as any) console.log(v); // native checked iteration reports non-iterables at runtime export const marker: number = 1; diff --git a/tests/diagnostics/dynamic-representable-types.ts b/tests/diagnostics/dynamic-representable-types.ts index a4b0d1291..fac6d1af6 100644 --- a/tests/diagnostics/dynamic-representable-types.ts +++ b/tests/diagnostics/dynamic-representable-types.ts @@ -1,11 +1,4 @@ -/* COMPOSITE types the dynamic engine represents but the static surface - * does not — `any[]`, records and functions with `any`-typed members, the - * evolving `const arr = []`: each site reports the SC2011 dynamic-family - * choice ("runs in the embedded dynamic engine"), proved by re-running the - * type mapping with the engine enabled — never the generic supported-types - * recitation. Bare `any` keeps its own SC2011 arm (the stronger stay-static - * hint: 'unknown' + a checked cast); types with NO dynamic representation - * (an optional-element tuple) keep the SC2001 recitation. */ +// Checked arrays and functions with any pieces compile natively; optional tuple elements retain their diagnostic. const arr: any[] = [1, "x"]; var evolving = []; // never used — the tsc-clean evolving-array shape const rec = { workItem: {} as any, width: "10px" }; diff --git a/tests/diagnostics/fetch-static.ts b/tests/diagnostics/fetch-static.ts index 85ae37272..40f3f0a44 100644 --- a/tests/diagnostics/fetch-static.ts +++ b/tests/diagnostics/fetch-static.ts @@ -28,4 +28,4 @@ async function bracketArrayBufferRead(url: string): Promise { } bracketArrayBufferRead("http://localhost/e"); const headers = new Headers(); -// The Headers constructor remains fenced even though response.headers is native. +// Headers construction is native; the detached Response method above remains fenced. diff --git a/tests/diagnostics/inheritance.ts b/tests/diagnostics/inheritance.ts index 39fa6e411..6b397c052 100644 --- a/tests/diagnostics/inheritance.ts +++ b/tests/diagnostics/inheritance.ts @@ -57,7 +57,7 @@ class Bivariant extends Animal { } } -// super() must be a top-level constructor statement +// Conditional super() is supported; both branches initialize the receiver. class Conditional extends Animal { constructor(flag: boolean) { if (flag) { @@ -68,7 +68,7 @@ class Conditional extends Animal { } } -// instanceof needs a class instance on the left... +// A record cannot be an instance of this native class and answers false. const rec = { name: "r" }; console.log(rec instanceof Animal); diff --git a/tests/diagnostics/maps-surface.ts b/tests/diagnostics/maps-surface.ts index a034d4794..3b2e44043 100644 --- a/tests/diagnostics/maps-surface.ts +++ b/tests/diagnostics/maps-surface.ts @@ -19,7 +19,7 @@ const es = m.entries(); // lowered iteration forms. const spread = [...m]; -// set() is declared void (not the JS `this`), so chaining is a type error. +// set() returns the same Map, so chained mutations compile. m.set("a", 1).set("b", 2); // The forEach callback receives (value, key) — no third map parameter. diff --git a/tests/diagnostics/regex.ts b/tests/diagnostics/regex.ts index 26f8e481a..ec29bc70e 100644 --- a/tests/diagnostics/regex.ts +++ b/tests/diagnostics/regex.ts @@ -1,11 +1,6 @@ -// The regex slice fences. test() on a g/y-flagged literal is the -// statefulness fence (lastIndex is not modeled); the d/v flags are -// outside the slice; .groups needs a statically-known regex (named -// capture groups themselves compile — corpus 2604); method-as-value has -// no value form; regexes stay out of union arms (ARRAYS of regexes -// compile — corpus 2448). -const g = /ab/g.test("abab"); -const y = /ab/y.test("abab"); +// The d/v flags are outside the native slice; .groups needs a +// statically-known regex. Named capture groups compile (corpus 2604). +// Method values and regex union arms remain unsupported; arrays compile. const indices = /cat/d; const sets = /[\p{L}]/v; const asValue = /x/.test; diff --git a/tests/diagnostics/text-codec-value.ts b/tests/diagnostics/text-codec-value.ts index 36a01e6b8..95cf0dd47 100644 --- a/tests/diagnostics/text-codec-value.ts +++ b/tests/diagnostics/text-codec-value.ts @@ -6,7 +6,6 @@ function encodeCaptured(s: string): Uint8Array { } const decoder = new TextDecoder(); -decoder.decode(new Uint8Array([0xc3]), { stream: true }); new TextDecoder("utf-8", { fatal: true }); encoder.encodeInto("hello", new Uint8Array(10)); const forged = JSON.parse("{}") as TextDecoder; diff --git a/tests/fixtures/effect4/ai-chat.ts b/tests/fixtures/effect4/ai-chat.ts new file mode 100644 index 000000000..c5cb2ab05 --- /dev/null +++ b/tests/fixtures/effect4/ai-chat.ts @@ -0,0 +1,7 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +import * as Layer from "effect/Layer" +import * as Ref from "effect/Ref" +import * as LanguageModel from "effect/ai/LanguageModel" +import * as Chat from "effect/ai/Chat" +const model=Layer.effect(LanguageModel.LanguageModel,LanguageModel.make({generateText:()=>Effect.succeed([{type:"text",text:"hello"},{type:"finish",reason:"stop",usage:{inputTokens:{total:1},outputTokens:{total:1}}}]),streamText:()=>Stream.empty}));Effect.runPromise((Effect.gen(function*(){const c=yield* Chat.empty;const a=yield* c.generateText({prompt:"Hello"});const b=yield* c.generateText({prompt:"Again"});const h=yield* Ref.get(c.history);return [a.text,b.text,h.content.length]}).pipe(Effect.provide(model))).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/ai-model.ts b/tests/fixtures/effect4/ai-model.ts new file mode 100644 index 000000000..bea43879a --- /dev/null +++ b/tests/fixtures/effect4/ai-model.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Stream from "effect/Stream";import * as LanguageModel from "effect/ai/LanguageModel";const model=Layer.effect(LanguageModel.LanguageModel,LanguageModel.make({generateText:()=>Effect.succeed([{type:"text",text:"hello"},{type:"finish",reason:"stop",usage:{inputTokens:{total:1},outputTokens:{total:1}}}]),streamText:()=>Stream.empty}));Effect.runPromise((LanguageModel.generateText({prompt:"Hello"}).pipe(Effect.map(r=>r.text),Effect.provide(model))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/ai-prompt.ts b/tests/fixtures/effect4/ai-prompt.ts new file mode 100644 index 000000000..4b816dad2 --- /dev/null +++ b/tests/fixtures/effect4/ai-prompt.ts @@ -0,0 +1,2 @@ +import * as Prompt from "effect/ai/Prompt" +console.log(JSON.stringify(Prompt.make("Hello world"))) diff --git a/tests/fixtures/effect4/ai-tool-execute.ts b/tests/fixtures/effect4/ai-tool-execute.ts new file mode 100644 index 000000000..e0985ae36 --- /dev/null +++ b/tests/fixtures/effect4/ai-tool-execute.ts @@ -0,0 +1,6 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +import * as Schema from "effect/Schema" +import * as Tool from "effect/ai/Tool" +import * as Toolkit from "effect/ai/Toolkit" +const kit=Toolkit.make(Tool.make("double",{description:"double",parameters:Schema.Struct({n:Schema.Number}),success:Schema.Number}));const layer=kit.toLayer({double:({n})=>Effect.succeed(n*2)});Effect.runPromise((Effect.gen(function*(){const handlers=yield* kit;const stream=yield* handlers.handle("double",{n:21},"call-1");return yield* Stream.runCollect(stream)}).pipe(Effect.provide(layer))).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/ai-tool.ts b/tests/fixtures/effect4/ai-tool.ts new file mode 100644 index 000000000..9a49b5d51 --- /dev/null +++ b/tests/fixtures/effect4/ai-tool.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import * as Tool from "effect/ai/Tool" +import * as Toolkit from "effect/ai/Toolkit" +const t=Tool.make("double",{description:"Double a number",parameters:Schema.Struct({n:Schema.Number}),success:Schema.Number}); const kit=Toolkit.make(t); console.log(Object.keys(kit.tools)) diff --git a/tests/fixtures/effect4/arbitrary.ts b/tests/fixtures/effect4/arbitrary.ts new file mode 100644 index 000000000..8458f86b0 --- /dev/null +++ b/tests/fixtures/effect4/arbitrary.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Arbitrary from "effect/Arbitrary" +import * as Schema from "effect/Schema" +Effect.runPromise(Arbitrary.sampleEffect(Arbitrary.schema(Schema.Literal(42)),{seed:1,count:3}).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/async-abort.ts b/tests/fixtures/effect4/async-abort.ts new file mode 100644 index 000000000..262a1ec96 --- /dev/null +++ b/tests/fixtures/effect4/async-abort.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Cause from "effect/Cause";const controller=new AbortController();const p=Effect.runPromiseExit(Effect.never,{signal:controller.signal});controller.abort();p.then((x:Exit.Exit)=>console.log(Exit.isFailure(x)&&Cause.hasInterrupts(x.cause))) diff --git a/tests/fixtures/effect4/async-callback.ts b/tests/fixtures/effect4/async-callback.ts new file mode 100644 index 000000000..5c94bc2b8 --- /dev/null +++ b/tests/fixtures/effect4/async-callback.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.callback(resume=> { setTimeout(()=>resume(Effect.succeed(42)),1) })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/async-promise.ts b/tests/fixtures/effect4/async-promise.ts new file mode 100644 index 000000000..231aa79e3 --- /dev/null +++ b/tests/fixtures/effect4/async-promise.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.promise(() => Promise.resolve(42))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/async-sleep.ts b/tests/fixtures/effect4/async-sleep.ts new file mode 100644 index 000000000..b2be659c1 --- /dev/null +++ b/tests/fixtures/effect4/async-sleep.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.gen(function* () { yield* Effect.sleep("1 millis"); yield* Effect.yieldNow; return yield* Effect.succeed(42).pipe(Effect.delay("1 millis")); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/async-timeout.ts b/tests/fixtures/effect4/async-timeout.ts new file mode 100644 index 000000000..d1487b062 --- /dev/null +++ b/tests/fixtures/effect4/async-timeout.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.never.pipe(Effect.timeout("1 millis"),Effect.catch(e=>Effect.succeed(e._tag)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/async-try-promise.ts b/tests/fixtures/effect4/async-try-promise.ts new file mode 100644 index 000000000..6429aaaba --- /dev/null +++ b/tests/fixtures/effect4/async-try-promise.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.tryPromise({try:()=>Promise.reject("bad"),catch:e=>String(e)}).pipe(Effect.catch(e=>Effect.succeed(e)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/cache-cached.ts b/tests/fixtures/effect4/cache-cached.ts new file mode 100644 index 000000000..b36b92c72 --- /dev/null +++ b/tests/fixtures/effect4/cache-cached.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.gen(function* () { let calls=0;const get=yield* Effect.cached(Effect.sync(()=>++calls));return [yield* get,yield* get,calls]; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/cache.ts b/tests/fixtures/effect4/cache.ts new file mode 100644 index 000000000..976c7fb46 --- /dev/null +++ b/tests/fixtures/effect4/cache.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Cache from "effect/Cache" +Effect.runPromise((Effect.gen(function* () { let calls=0;const c=yield* Cache.make({capacity:10,timeToLive:"1 minute",lookup:(key:string)=>Effect.sync(()=>{calls++;return key.length})});const a=yield* Cache.get(c,"world");const b=yield* Cache.get(c,"world");yield* Cache.invalidate(c,"world");const d=yield* Cache.get(c,"world");return [a,b,d,calls]; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/channel.ts b/tests/fixtures/effect4/channel.ts new file mode 100644 index 000000000..4389333b1 --- /dev/null +++ b/tests/fixtures/effect4/channel.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Channel from "effect/Channel" +Effect.runPromise((Channel.runCollect(Channel.fromIterable([1,2,3]))).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/cli-flags.ts b/tests/fixtures/effect4/cli-flags.ts new file mode 100644 index 000000000..db9578e3b --- /dev/null +++ b/tests/fixtures/effect4/cli-flags.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Command from "effect/cli/Command";import * as Flag from "effect/cli/Flag";import * as Argument from "effect/cli/Argument";import * as NodeServices from "@effect/platform-node/NodeServices";const c=Command.make("greet",{name:Argument.String("name"),loud:Flag.Boolean("loud")},({name,loud})=>Effect.sync(()=>console.log(loud?name.toUpperCase():name)));Effect.runPromise(Command.runWith(c,{version:"1.0.0"})(["world","--loud"]).pipe(Effect.provide(NodeServices.layer))) diff --git a/tests/fixtures/effect4/cli-subcommands.ts b/tests/fixtures/effect4/cli-subcommands.ts new file mode 100644 index 000000000..650d0bc78 --- /dev/null +++ b/tests/fixtures/effect4/cli-subcommands.ts @@ -0,0 +1,9 @@ +import * as Effect from "effect/Effect"; +import * as Command from "effect/cli/Command"; +import * as Flag from "effect/cli/Flag"; +import * as Argument from "effect/cli/Argument"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +const root=Command.make("tasks").pipe(Command.withSharedFlags({verbose:Flag.Boolean("verbose")})); +const create=Command.make("create",{name:Argument.String("name")},({name})=>Effect.gen(function*(){const options=yield* root;console.log(name,options.verbose)})); +const command=root.pipe(Command.withSubcommands([create])); +Effect.runPromise(Command.runWith(command,{version:"1.0.0"})(["--verbose","create","world"]).pipe(Effect.provide(NodeServices.layer))); diff --git a/tests/fixtures/effect4/cluster-definition.ts b/tests/fixtures/effect4/cluster-definition.ts new file mode 100644 index 000000000..25422c0f2 --- /dev/null +++ b/tests/fixtures/effect4/cluster-definition.ts @@ -0,0 +1 @@ +import * as Entity from "effect/cluster/Entity";import * as Rpc from "effect/rpc/Rpc";import * as Schema from "effect/Schema";const C=Entity.make("C",[Rpc.make("Inc",{success:Schema.Int})]);console.log(C.type) diff --git a/tests/fixtures/effect4/cluster-entity.ts b/tests/fixtures/effect4/cluster-entity.ts new file mode 100644 index 000000000..375df729d --- /dev/null +++ b/tests/fixtures/effect4/cluster-entity.ts @@ -0,0 +1,8 @@ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Ref from "effect/Ref" +import * as Schema from "effect/Schema" +import * as Entity from "effect/cluster/Entity" +import * as TestRunner from "effect/cluster/TestRunner" +import * as Rpc from "effect/rpc/Rpc" +const C=Entity.make("Counter",[Rpc.make("Inc",{payload:{amount:Schema.Int},success:Schema.Int})]);const handlers=C.toLayer(Effect.gen(function*(){const n=yield* Ref.make(0);return C.of({Inc:({payload})=>Ref.updateAndGet(n,v=>v+payload.amount)})}));Effect.runPromise((Effect.gen(function*(){const client=yield* C.client;const c=client("counter-1");return [yield* c.Inc({amount:1}),yield* c.Inc({amount:2})]}).pipe(Effect.provide(handlers.pipe(Layer.provideMerge(TestRunner.layer))))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/cluster-handlers.ts b/tests/fixtures/effect4/cluster-handlers.ts new file mode 100644 index 000000000..cb37b97f2 --- /dev/null +++ b/tests/fixtures/effect4/cluster-handlers.ts @@ -0,0 +1 @@ +import * as Entity from "effect/cluster/Entity";import * as Rpc from "effect/rpc/Rpc";import * as Schema from "effect/Schema";import * as Effect from "effect/Effect";const C=Entity.make("C",[Rpc.make("Inc",{success:Schema.Int})]);const handlers=C.toLayer(Effect.succeed(C.of({Inc:()=>Effect.succeed(42)})));console.log(typeof handlers) diff --git a/tests/fixtures/effect4/concurrency-all.ts b/tests/fixtures/effect4/concurrency-all.ts new file mode 100644 index 000000000..a611e35fd --- /dev/null +++ b/tests/fixtures/effect4/concurrency-all.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.all([1,2,3].map(n=>Effect.succeed(n).pipe(Effect.delay("1 millis"))),{concurrency:2})).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/concurrency-race.ts b/tests/fixtures/effect4/concurrency-race.ts new file mode 100644 index 000000000..4bb57f662 --- /dev/null +++ b/tests/fixtures/effect4/concurrency-race.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.race(Effect.succeed("first"),Effect.never)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/config-default-redacted.ts b/tests/fixtures/effect4/config-default-redacted.ts new file mode 100644 index 000000000..7059246b5 --- /dev/null +++ b/tests/fixtures/effect4/config-default-redacted.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import * as Config from "effect/Config" +import * as ConfigProvider from "effect/ConfigProvider" +import * as Redacted from "effect/Redacted" +Effect.runPromise((Effect.gen(function*(){const n=yield* Config.String("MISSING").pipe(Config.withDefault("default")); const secret=yield* Config.Redacted("TOKEN");return [n,Redacted.value(secret)]}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({TOKEN:"secret"}))))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/config-dotenv.ts b/tests/fixtures/effect4/config-dotenv.ts new file mode 100644 index 000000000..e18e62f77 --- /dev/null +++ b/tests/fixtures/effect4/config-dotenv.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Config from "effect/Config" +import * as ConfigProvider from "effect/ConfigProvider" +Effect.runPromise((Config.String("NAME").pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromDotEnvContents("NAME=world\n"))))).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/config-provider.ts b/tests/fixtures/effect4/config-provider.ts new file mode 100644 index 000000000..bf00157ed --- /dev/null +++ b/tests/fixtures/effect4/config-provider.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Config from "effect/Config" +import * as ConfigProvider from "effect/ConfigProvider" +Effect.runPromise((Effect.gen(function* () { const p=ConfigProvider.fromUnknown({NAME:"world",PORT:"42"});const a=yield* Config.String("NAME").pipe(Effect.provide(ConfigProvider.layer(p)));const b=yield* Config.Int("PORT").pipe(Effect.provide(ConfigProvider.layer(p)));return [a,b]; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/coordination-deferred.ts b/tests/fixtures/effect4/coordination-deferred.ts new file mode 100644 index 000000000..1e6234b80 --- /dev/null +++ b/tests/fixtures/effect4/coordination-deferred.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Deferred from "effect/Deferred" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function* () { const d=yield* Deferred.make(); const f=yield* Effect.forkChild(Deferred.await(d)); yield* Deferred.succeed(d,42); return yield* Fiber.join(f); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/core-all-record.ts b/tests/fixtures/effect4/core-all-record.ts new file mode 100644 index 000000000..50dd15b25 --- /dev/null +++ b/tests/fixtures/effect4/core-all-record.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.all({one:Effect.succeed(1),two:Effect.succeed("two")})).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/core-all-tuple.ts b/tests/fixtures/effect4/core-all-tuple.ts new file mode 100644 index 000000000..7ed5376dd --- /dev/null +++ b/tests/fixtures/effect4/core-all-tuple.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.all([Effect.succeed(1),Effect.succeed("two")])).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/core-do.ts b/tests/fixtures/effect4/core-do.ts new file mode 100644 index 000000000..710bc2153 --- /dev/null +++ b/tests/fixtures/effect4/core-do.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.Do.pipe(Effect.bind("n", () => Effect.succeed(21)), Effect.let("doubled", ({n}) => n*2))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/core-fn.ts b/tests/fixtures/effect4/core-fn.ts new file mode 100644 index 000000000..28c24679b --- /dev/null +++ b/tests/fixtures/effect4/core-fn.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +const double=Effect.fn("double")(function* (n:number) { return yield* Effect.succeed(n*2) }); +Effect.runPromise((double(21)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/core-for-each.ts b/tests/fixtures/effect4/core-for-each.ts new file mode 100644 index 000000000..fc6f00763 --- /dev/null +++ b/tests/fixtures/effect4/core-for-each.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.forEach([1,2,3], n => Effect.succeed(n*2))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/core-gen.ts b/tests/fixtures/effect4/core-gen.ts new file mode 100644 index 000000000..288fe2af5 --- /dev/null +++ b/tests/fixtures/effect4/core-gen.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect" +const program = Effect.gen(function* () { const name = yield* Effect.succeed("world"); return "Hello, " + name + "!" }) +Effect.runPromise(program).then((x: string) => console.log(x)) diff --git a/tests/fixtures/effect4/core-pipe.ts b/tests/fixtures/effect4/core-pipe.ts new file mode 100644 index 000000000..0625dabad --- /dev/null +++ b/tests/fixtures/effect4/core-pipe.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Function from "effect/Function" +Effect.runPromise((Function.pipe(Effect.succeed(20), Effect.map(n => n+1), Effect.flatMap(n => Effect.succeed(n*2)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/core-subpath.ts b/tests/fixtures/effect4/core-subpath.ts new file mode 100644 index 000000000..62446a840 --- /dev/null +++ b/tests/fixtures/effect4/core-subpath.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise(Effect.succeed("world")).then((x: string) => console.log(x)) diff --git a/tests/fixtures/effect4/core-suspend.ts b/tests/fixtures/effect4/core-suspend.ts new file mode 100644 index 000000000..fd9bbae6c --- /dev/null +++ b/tests/fixtures/effect4/core-suspend.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.suspend(() => Effect.succeed(42))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/crypto-service.ts b/tests/fixtures/effect4/crypto-service.ts new file mode 100644 index 000000000..56d0ca585 --- /dev/null +++ b/tests/fixtures/effect4/crypto-service.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Crypto from "effect/Crypto" +const c=Crypto.make({randomBytes:n=>new Uint8Array(n),digest:(_,data)=>Effect.succeed(data)});Effect.runPromise((c.randomBytes(3).pipe(Effect.map(b=>Array.from(b)))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/data-array.ts b/tests/fixtures/effect4/data-array.ts new file mode 100644 index 000000000..6cb8939cb --- /dev/null +++ b/tests/fixtures/effect4/data-array.ts @@ -0,0 +1,3 @@ +import { pipe } from "effect/Function" +import * as Array from "effect/Array" +console.log(JSON.stringify(pipe(Array.range(1,4),Array.map(n=>n*2),Array.filter(n=>n>4)))) diff --git a/tests/fixtures/effect4/data-bytesize.ts b/tests/fixtures/effect4/data-bytesize.ts new file mode 100644 index 000000000..857a26060 --- /dev/null +++ b/tests/fixtures/effect4/data-bytesize.ts @@ -0,0 +1,2 @@ +import * as ByteSize from "effect/ByteSize" +console.log(ByteSize.toBigInt(ByteSize.kibibytes(2)).toString()) diff --git a/tests/fixtures/effect4/data-chunk.ts b/tests/fixtures/effect4/data-chunk.ts new file mode 100644 index 000000000..d759ddfd0 --- /dev/null +++ b/tests/fixtures/effect4/data-chunk.ts @@ -0,0 +1,2 @@ +import * as Chunk from "effect/Chunk" +console.log(JSON.stringify(Chunk.toReadonlyArray(Chunk.map(Chunk.make(1,2,3),n=>n*2)))) diff --git a/tests/fixtures/effect4/data-datetime.ts b/tests/fixtures/effect4/data-datetime.ts new file mode 100644 index 000000000..595205f0f --- /dev/null +++ b/tests/fixtures/effect4/data-datetime.ts @@ -0,0 +1,2 @@ +import * as DateTime from "effect/DateTime" +console.log(DateTime.formatIso(DateTime.makeUnsafe("2026-01-01T00:00:00Z"))) diff --git a/tests/fixtures/effect4/data-duration.ts b/tests/fixtures/effect4/data-duration.ts new file mode 100644 index 000000000..8213b7bbb --- /dev/null +++ b/tests/fixtures/effect4/data-duration.ts @@ -0,0 +1,2 @@ +import * as Duration from "effect/Duration" +console.log(Duration.toMillis(Duration.sum(Duration.millis(40),Duration.millis(2)))) diff --git a/tests/fixtures/effect4/data-equal-arrays.ts b/tests/fixtures/effect4/data-equal-arrays.ts new file mode 100644 index 000000000..ede752199 --- /dev/null +++ b/tests/fixtures/effect4/data-equal-arrays.ts @@ -0,0 +1 @@ +import * as Equal from "effect/Equal";console.log(Equal.equals([1,2],[1,2])) diff --git a/tests/fixtures/effect4/data-equal-by-reference.ts b/tests/fixtures/effect4/data-equal-by-reference.ts new file mode 100644 index 000000000..82d99b440 --- /dev/null +++ b/tests/fixtures/effect4/data-equal-by-reference.ts @@ -0,0 +1 @@ +import * as Equal from "effect/Equal";const a=Equal.byReference({n:42});const b=Equal.byReference({n:42});console.log(Equal.equals(a,a),Equal.equals(a,b)) diff --git a/tests/fixtures/effect4/data-equal-objects.ts b/tests/fixtures/effect4/data-equal-objects.ts new file mode 100644 index 000000000..c9d11d6f5 --- /dev/null +++ b/tests/fixtures/effect4/data-equal-objects.ts @@ -0,0 +1 @@ +import * as Equal from "effect/Equal";console.log(Equal.equals({n:42},{n:42})) diff --git a/tests/fixtures/effect4/data-equal-primitives.ts b/tests/fixtures/effect4/data-equal-primitives.ts new file mode 100644 index 000000000..cbb5a0c76 --- /dev/null +++ b/tests/fixtures/effect4/data-equal-primitives.ts @@ -0,0 +1 @@ +import * as Equal from "effect/Equal";console.log(Equal.equals(42,42),Equal.equals("a","b"),Equal.equals(NaN,NaN)) diff --git a/tests/fixtures/effect4/data-equal.ts b/tests/fixtures/effect4/data-equal.ts new file mode 100644 index 000000000..93547173a --- /dev/null +++ b/tests/fixtures/effect4/data-equal.ts @@ -0,0 +1,3 @@ +import * as Equal from "effect/Equal" +import * as Hash from "effect/Hash" +console.log(Equal.equals({a:[1,2]},{a:[1,2]}),Hash.hash({a:1})===Hash.hash({a:1})) diff --git a/tests/fixtures/effect4/data-function.ts b/tests/fixtures/effect4/data-function.ts new file mode 100644 index 000000000..084fc43d6 --- /dev/null +++ b/tests/fixtures/effect4/data-function.ts @@ -0,0 +1,2 @@ +import * as Function from "effect/Function" +console.log(Function.pipe(21,Function.identity,n=>n*2)) diff --git a/tests/fixtures/effect4/data-hash-objects.ts b/tests/fixtures/effect4/data-hash-objects.ts new file mode 100644 index 000000000..9de6474b9 --- /dev/null +++ b/tests/fixtures/effect4/data-hash-objects.ts @@ -0,0 +1 @@ +import * as Hash from "effect/Hash";console.log(Hash.hash({n:42})===Hash.hash({n:42})) diff --git a/tests/fixtures/effect4/data-hash-primitives.ts b/tests/fixtures/effect4/data-hash-primitives.ts new file mode 100644 index 000000000..21fd0f157 --- /dev/null +++ b/tests/fixtures/effect4/data-hash-primitives.ts @@ -0,0 +1 @@ +import * as Hash from "effect/Hash";console.log(Hash.hash(42)===Hash.hash(42),Hash.hash("x")===Hash.hash("x")) diff --git a/tests/fixtures/effect4/data-hashmap.ts b/tests/fixtures/effect4/data-hashmap.ts new file mode 100644 index 000000000..5e7efb7d3 --- /dev/null +++ b/tests/fixtures/effect4/data-hashmap.ts @@ -0,0 +1,3 @@ +import * as HashMap from "effect/HashMap" +import * as Option from "effect/Option" +console.log(Option.getOrElse(HashMap.get(HashMap.make(["x",42]),"x"),()=>0)) diff --git a/tests/fixtures/effect4/data-hashset.ts b/tests/fixtures/effect4/data-hashset.ts new file mode 100644 index 000000000..1ae51a736 --- /dev/null +++ b/tests/fixtures/effect4/data-hashset.ts @@ -0,0 +1,2 @@ +import * as HashSet from "effect/HashSet" +console.log(HashSet.has(HashSet.make(1,2,3),2)) diff --git a/tests/fixtures/effect4/data-match.ts b/tests/fixtures/effect4/data-match.ts new file mode 100644 index 000000000..7c63e30de --- /dev/null +++ b/tests/fixtures/effect4/data-match.ts @@ -0,0 +1,2 @@ +import * as Match from "effect/Match" +const value={_tag:"Ok" as const,n:42}; console.log(Match.value(value).pipe(Match.tag("Ok",x=>x.n),Match.exhaustive)) diff --git a/tests/fixtures/effect4/data-option.ts b/tests/fixtures/effect4/data-option.ts new file mode 100644 index 000000000..dcd56c5d0 --- /dev/null +++ b/tests/fixtures/effect4/data-option.ts @@ -0,0 +1,2 @@ +import * as Option from "effect/Option" +console.log(Option.getOrElse(Option.map(Option.some(2), n => n + 1), () => 0)) diff --git a/tests/fixtures/effect4/data-predicate.ts b/tests/fixtures/effect4/data-predicate.ts new file mode 100644 index 000000000..ceee1f977 --- /dev/null +++ b/tests/fixtures/effect4/data-predicate.ts @@ -0,0 +1,2 @@ +import * as Predicate from "effect/Predicate" +console.log(Predicate.isString("world"),Predicate.isNumber(42),Predicate.isNumber("42")) diff --git a/tests/fixtures/effect4/data-result.ts b/tests/fixtures/effect4/data-result.ts new file mode 100644 index 000000000..10c6f4de1 --- /dev/null +++ b/tests/fixtures/effect4/data-result.ts @@ -0,0 +1,2 @@ +import * as Result from "effect/Result" +console.log(Result.match(Result.map(Result.succeed(21),n=>n*2),{onFailure:()=>0,onSuccess:n=>n})) diff --git a/tests/fixtures/effect4/encoding-sse.ts b/tests/fixtures/effect4/encoding-sse.ts new file mode 100644 index 000000000..87fd408ab --- /dev/null +++ b/tests/fixtures/effect4/encoding-sse.ts @@ -0,0 +1,2 @@ +import * as Sse from "effect/encoding/Sse" +const values:unknown[]=[];const parser=Sse.makeParser(e=>values.push(e));parser.feed("event: message\ndata: hello\n\n");console.log(JSON.stringify(values)) diff --git a/tests/fixtures/effect4/error-catch-filter.ts b/tests/fixtures/effect4/error-catch-filter.ts new file mode 100644 index 000000000..79af9daa3 --- /dev/null +++ b/tests/fixtures/effect4/error-catch-filter.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Filter from "effect/Filter" +Effect.runPromise((Effect.fail(42).pipe(Effect.catchFilter(Filter.fromPredicate((n:number)=>n===42), n=>Effect.succeed(n+1)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-catch-reason.ts b/tests/fixtures/effect4/error-catch-reason.ts new file mode 100644 index 000000000..05612eaad --- /dev/null +++ b/tests/fixtures/effect4/error-catch-reason.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.fail({_tag:"ApiError" as const,reason:{_tag:"RateLimit" as const,seconds:3}}).pipe(Effect.catchReason("ApiError","RateLimit",r=>Effect.succeed(r.seconds)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-catch-tag.ts b/tests/fixtures/effect4/error-catch-tag.ts new file mode 100644 index 000000000..0a8a331c3 --- /dev/null +++ b/tests/fixtures/effect4/error-catch-tag.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Data from "effect/Data" +class Missing extends Data.TaggedError("Missing")<{readonly id:number}> {} +Effect.runPromise((Effect.fail(new Missing({id:1})).pipe(Effect.catchTag("Missing", e => Effect.succeed(e.id)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-catch-tags.ts b/tests/fixtures/effect4/error-catch-tags.ts new file mode 100644 index 000000000..e7da0e431 --- /dev/null +++ b/tests/fixtures/effect4/error-catch-tags.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.fail({_tag:"Missing" as const,id:1}).pipe(Effect.catchTags({Missing:e=>Effect.succeed(e.id)}))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-catch.ts b/tests/fixtures/effect4/error-catch.ts new file mode 100644 index 000000000..8d29e3809 --- /dev/null +++ b/tests/fixtures/effect4/error-catch.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.fail("oops").pipe(Effect.catch(e => Effect.succeed(e+" recovered")))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-cause.ts b/tests/fixtures/effect4/error-cause.ts new file mode 100644 index 000000000..3cb1dd152 --- /dev/null +++ b/tests/fixtures/effect4/error-cause.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Cause from "effect/Cause" +Effect.runPromise((Effect.fail("oops").pipe(Effect.catchCause(c => Effect.succeed([Cause.hasFails(c),c.reasons.map(r=>r._tag)])))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-defect.ts b/tests/fixtures/effect4/error-defect.ts new file mode 100644 index 000000000..3a63c349b --- /dev/null +++ b/tests/fixtures/effect4/error-defect.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.die("broken").pipe(Effect.catchDefect(() => Effect.succeed("recovered")))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-match.ts b/tests/fixtures/effect4/error-match.ts new file mode 100644 index 000000000..1c72b9a18 --- /dev/null +++ b/tests/fixtures/effect4/error-match.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.fail("oops").pipe(Effect.match({onFailure:e=>e,onSuccess:()=>"ok"}))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-reporter.ts b/tests/fixtures/effect4/error-reporter.ts new file mode 100644 index 000000000..26cbac8f1 --- /dev/null +++ b/tests/fixtures/effect4/error-reporter.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as ErrorReporter from "effect/ErrorReporter" +import * as Cause from "effect/Cause" +const r=ErrorReporter.make(o=>console.log(o.error.message));Effect.runPromise(ErrorReporter.report(Cause.fail(new Error("broken"))).pipe(Effect.provide(ErrorReporter.layer([r])))) diff --git a/tests/fixtures/effect4/error-result.ts b/tests/fixtures/effect4/error-result.ts new file mode 100644 index 000000000..1d935c83c --- /dev/null +++ b/tests/fixtures/effect4/error-result.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Result from "effect/Result" +Effect.runPromise((Effect.fail("oops").pipe(Effect.result, Effect.map(r => Result.match(r,{onFailure:e=>"failure:"+e,onSuccess:()=>"success"})))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/error-try.ts b/tests/fixtures/effect4/error-try.ts new file mode 100644 index 000000000..ef1e405e8 --- /dev/null +++ b/tests/fixtures/effect4/error-try.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.try({try:()=>{throw "bad"},catch:e=>String(e)}).pipe(Effect.mapError(e=>"mapped:"+e), Effect.catch(e=>Effect.succeed(e)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/execution-plan.ts b/tests/fixtures/effect4/execution-plan.ts new file mode 100644 index 000000000..35d97ec20 --- /dev/null +++ b/tests/fixtures/effect4/execution-plan.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import * as ExecutionPlan from "effect/ExecutionPlan" +import * as Context from "effect/Context" +import * as Layer from "effect/Layer" +const S=Context.Service("S");const plan=ExecutionPlan.make({provide:Layer.succeed(S,"one"),attempts:1},{provide:Layer.succeed(S,"two"),attempts:1});Effect.runPromise((S.pipe(Effect.flatMap(s=>s==="one"?Effect.fail("retry"):Effect.succeed(s)),Effect.withExecutionPlan(plan))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/fiber-detach.ts b/tests/fixtures/effect4/fiber-detach.ts new file mode 100644 index 000000000..4b5c220f9 --- /dev/null +++ b/tests/fixtures/effect4/fiber-detach.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function* () { const f=yield* Effect.forkDetach(Effect.succeed(42)); return yield* Fiber.join(f); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/fiber-fork-join.ts b/tests/fixtures/effect4/fiber-fork-join.ts new file mode 100644 index 000000000..5ecbd42e5 --- /dev/null +++ b/tests/fixtures/effect4/fiber-fork-join.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function* () { const f=yield* Effect.forkChild(Effect.succeed(42)); return yield* Fiber.join(f); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/fiber-immediate.ts b/tests/fixtures/effect4/fiber-immediate.ts new file mode 100644 index 000000000..a4cbe7661 --- /dev/null +++ b/tests/fixtures/effect4/fiber-immediate.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function* () { const events:string[]=[]; const f=yield* Effect.forkChild(Effect.sync(()=>{events.push("child")}),{startImmediately:true}); events.push("parent"); yield* Fiber.join(f); return events; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/fiber-map.ts b/tests/fixtures/effect4/fiber-map.ts new file mode 100644 index 000000000..e18916e28 --- /dev/null +++ b/tests/fixtures/effect4/fiber-map.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as FiberMap from "effect/FiberMap" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function*(){const m=yield* FiberMap.make();const f=yield* FiberMap.run(m,"job",Effect.succeed(42));return yield* Fiber.join(f)}).pipe(Effect.scoped)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/fiber-parent-cleanup.ts b/tests/fixtures/effect4/fiber-parent-cleanup.ts new file mode 100644 index 000000000..f0828bc2f --- /dev/null +++ b/tests/fixtures/effect4/fiber-parent-cleanup.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";let cleaned=false;const p=Effect.gen(function*(){yield* Effect.forkChild(Effect.never.pipe(Effect.onInterrupt(()=>Effect.sync(()=>{cleaned=true}))),{startImmediately:true});return 42});Effect.runPromise(p).then((n:number)=>console.log(n,cleaned)) diff --git a/tests/fixtures/effect4/fiber-run-fork.ts b/tests/fixtures/effect4/fiber-run-fork.ts new file mode 100644 index 000000000..314966c86 --- /dev/null +++ b/tests/fixtures/effect4/fiber-run-fork.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +const f=Effect.runFork(Effect.succeed(42)); Effect.runPromise((Fiber.join(f)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/fiber-scoped.ts b/tests/fixtures/effect4/fiber-scoped.ts new file mode 100644 index 000000000..952bd1929 --- /dev/null +++ b/tests/fixtures/effect4/fiber-scoped.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function*(){ let stopped=false; const f=yield* Effect.forkScoped(Effect.never.pipe(Effect.onInterrupt(()=>Effect.sync(()=>{stopped=true}))),{startImmediately:true}); yield* Fiber.interrupt(f); return stopped; }).pipe(Effect.scoped)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/fiber-set.ts b/tests/fixtures/effect4/fiber-set.ts new file mode 100644 index 000000000..d9c67105f --- /dev/null +++ b/tests/fixtures/effect4/fiber-set.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as FiberSet from "effect/FiberSet" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function*(){const s=yield* FiberSet.make();const f=yield* FiberSet.run(s,Effect.succeed(42));return yield* Fiber.join(f)}).pipe(Effect.scoped)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/file-system-input.txt b/tests/fixtures/effect4/file-system-input.txt new file mode 100644 index 000000000..0a76eec1b --- /dev/null +++ b/tests/fixtures/effect4/file-system-input.txt @@ -0,0 +1 @@ +Hello from Effect 4! diff --git a/tests/fixtures/effect4/gen-annotated.ts b/tests/fixtures/effect4/gen-annotated.ts new file mode 100644 index 000000000..70ff9983e --- /dev/null +++ b/tests/fixtures/effect4/gen-annotated.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";const p:Effect.Effect=Effect.gen(function*(){return yield* Effect.succeed(42)});Effect.runPromise(p).then((n:number)=>console.log(n)) diff --git a/tests/fixtures/effect4/http-api.ts b/tests/fixtures/effect4/http-api.ts new file mode 100644 index 000000000..f7b4881b0 --- /dev/null +++ b/tests/fixtures/effect4/http-api.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Schema from "effect/Schema";import * as Layer from "effect/Layer";import * as HttpApi from "effect/http-api/HttpApi";import * as HttpApiGroup from "effect/http-api/HttpApiGroup";import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint";import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";import * as HttpApiTest from "effect/http-api/HttpApiTest";import * as HttpServer from "effect/http/HttpServer";const Api=HttpApi.make("Api").add(HttpApiGroup.make("hello").add(HttpApiEndpoint.get("greet","/hello",{success:Schema.String})));const handlers=HttpApiBuilder.group(Api,"hello",h=>h.handle("greet",()=>Effect.succeed("world")));Effect.runPromise((Effect.gen(function*(){const c=yield* HttpApiTest.groups(Api,["hello"]);return yield* c.hello.greet()}).pipe(Effect.scoped,Effect.provide(Layer.mergeAll(handlers,HttpServer.layerServices)))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/http-client-fetch.ts b/tests/fixtures/effect4/http-client-fetch.ts new file mode 100644 index 000000000..372dab263 --- /dev/null +++ b/tests/fixtures/effect4/http-client-fetch.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Schema from "effect/Schema";import * as FetchHttpClient from "effect/http/FetchHttpClient";import * as HttpClient from "effect/http/HttpClient";import * as HttpClientResponse from "effect/http/HttpClientResponse";const S=Schema.Struct({n:Schema.Int});Effect.runPromise((HttpClient.get("data:application/json,%7B%22n%22%3A42%7D").pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(S)),Effect.provide(FetchHttpClient.layer))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/http-cookies.ts b/tests/fixtures/effect4/http-cookies.ts new file mode 100644 index 000000000..5a7bd66cb --- /dev/null +++ b/tests/fixtures/effect4/http-cookies.ts @@ -0,0 +1,2 @@ +import * as Cookies from "effect/http/Cookies" +const c=Cookies.set(Cookies.empty,"name","world");console.log(JSON.stringify(c)) diff --git a/tests/fixtures/effect4/http-request.ts b/tests/fixtures/effect4/http-request.ts new file mode 100644 index 000000000..e4042c84f --- /dev/null +++ b/tests/fixtures/effect4/http-request.ts @@ -0,0 +1,2 @@ +import * as HttpClientRequest from "effect/http/HttpClientRequest" +const r=HttpClientRequest.get("https://example.com/path").pipe(HttpClientRequest.setHeader("x-test","yes"),HttpClientRequest.setUrlParams({n:"42"})); console.log(r.method,r.url,r.headers["x-test"]) diff --git a/tests/fixtures/effect4/http-response.ts b/tests/fixtures/effect4/http-response.ts new file mode 100644 index 000000000..aafe9fa8f --- /dev/null +++ b/tests/fixtures/effect4/http-response.ts @@ -0,0 +1,2 @@ +import * as HttpServerResponse from "effect/http/HttpServerResponse" +console.log(JSON.stringify(HttpServerResponse.jsonUnsafe({message:"world"}))) diff --git a/tests/fixtures/effect4/http-router.ts b/tests/fixtures/effect4/http-router.ts new file mode 100644 index 000000000..39adb0799 --- /dev/null +++ b/tests/fixtures/effect4/http-router.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as HttpRouter from "effect/http/HttpRouter";import * as HttpServerResponse from "effect/http/HttpServerResponse";import * as Layer from "effect/Layer";const {handler,dispose}=HttpRouter.toWebHandler(HttpRouter.add("GET","/hello",Effect.succeed(HttpServerResponse.text("hello"))),{disableLogger:true});handler(new Request("http://localhost/hello")).then(async(response:Response)=>{console.log(response.status,await response.text());await dispose()}) diff --git a/tests/fixtures/effect4/latch.ts b/tests/fixtures/effect4/latch.ts new file mode 100644 index 000000000..8261cd5fd --- /dev/null +++ b/tests/fixtures/effect4/latch.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Latch from "effect/Latch" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function* () { const l=yield* Latch.make(); const f=yield* Effect.forkChild(Latch.whenOpen(l,Effect.succeed(42))); yield* Latch.open(l); return yield* Fiber.join(f); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/layer-compose.ts b/tests/fixtures/effect4/layer-compose.ts new file mode 100644 index 000000000..ce2c382f6 --- /dev/null +++ b/tests/fixtures/effect4/layer-compose.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Layer from "effect/Layer" +const A=Context.Service("A"); const B=Context.Service("B"); const a=Layer.succeed(A,20); const b=Layer.effect(B,A.useSync(n=>n+2)).pipe(Layer.provide(a)); Effect.runPromise((Effect.all([A,B]).pipe(Effect.provide(Layer.mergeAll(a,b)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/layer-fresh.ts b/tests/fixtures/effect4/layer-fresh.ts new file mode 100644 index 000000000..1fa4378eb --- /dev/null +++ b/tests/fixtures/effect4/layer-fresh.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Context from "effect/Context";import * as Layer from "effect/Layer";const C=Context.Service("C");let n=0;const layer=Layer.effect(C,Effect.sync(()=>++n));const p=Effect.gen(function*(){const a=yield* C.pipe(Effect.provide(layer));const b=yield* C.pipe(Effect.provide(Layer.fresh(layer)));const c=yield* C.pipe(Effect.provide(layer,{local:true}));return [a,b,c]});Effect.runPromise(p.pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/layer-map.ts b/tests/fixtures/effect4/layer-map.ts new file mode 100644 index 000000000..aa645ba1d --- /dev/null +++ b/tests/fixtures/effect4/layer-map.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import * as LayerMap from "effect/LayerMap" +import * as Layer from "effect/Layer" +import * as Context from "effect/Context" +const C=Context.Service("C");Effect.runPromise((Effect.gen(function*(){const m=yield* LayerMap.fromRecord({one:Layer.succeed(C,1),two:Layer.succeed(C,2)});const a=yield* C.pipe(Effect.provide(m.get("one")));const b=yield* C.pipe(Effect.provide(m.get("two")));return [a,b]}).pipe(Effect.scoped)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/layer-memoization.ts b/tests/fixtures/effect4/layer-memoization.ts new file mode 100644 index 000000000..7352e9816 --- /dev/null +++ b/tests/fixtures/effect4/layer-memoization.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Layer from "effect/Layer" +const C=Context.Service("C"); let builds=0; const layer=Layer.effect(C,Effect.sync(()=>++builds)); Effect.runPromise((Effect.gen(function* () { const a=yield* C.pipe(Effect.provide(layer)); const b=yield* C.pipe(Effect.provide(layer)); return [a,b,builds]; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/layer-ref.ts b/tests/fixtures/effect4/layer-ref.ts new file mode 100644 index 000000000..5a170d652 --- /dev/null +++ b/tests/fixtures/effect4/layer-ref.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as LayerRef from "effect/LayerRef" +import * as Context from "effect/Context" +const S=Context.Service("S");let builds=0;Effect.runPromise((Effect.gen(function*(){const r=yield* LayerRef.make(Layer.effect(S,Effect.sync(()=>++builds)));const a=yield* S.pipe(Effect.provide(r.get));yield* r.refresh;const b=yield* S.pipe(Effect.provide(r.get));return [a,b,builds]}).pipe(Effect.scoped)).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/layer-succeed.ts b/tests/fixtures/effect4/layer-succeed.ts new file mode 100644 index 000000000..c56c62724 --- /dev/null +++ b/tests/fixtures/effect4/layer-succeed.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Layer from "effect/Layer" +const C=Context.Service("C"); Effect.runPromise((C.pipe(Effect.provide(Layer.succeed(C,42)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/layer-unwrap.ts b/tests/fixtures/effect4/layer-unwrap.ts new file mode 100644 index 000000000..0c52abfac --- /dev/null +++ b/tests/fixtures/effect4/layer-unwrap.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Layer from "effect/Layer" +const C=Context.Service("C"); Effect.runPromise((C.pipe(Effect.provide(Layer.unwrap(Effect.succeed(Layer.succeed(C,42)))))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/logging.ts b/tests/fixtures/effect4/logging.ts new file mode 100644 index 000000000..263f17b13 --- /dev/null +++ b/tests/fixtures/effect4/logging.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Logger from "effect/Logger" +Effect.runPromise((Effect.log("Hello world").pipe(Effect.annotateLogs({requestId:"r1"}),Effect.provide(Logger.layer([Logger.make(options=>console.log(JSON.stringify({message:options.message,annotations:options.logLevel})))])))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/metrics.ts b/tests/fixtures/effect4/metrics.ts new file mode 100644 index 000000000..a6da00e3c --- /dev/null +++ b/tests/fixtures/effect4/metrics.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Metric from "effect/Metric" +Effect.runPromise((Effect.gen(function* () { const c=Metric.counter("requests");yield* Metric.update(c,2);yield* Metric.update(c,3);return yield* Metric.value(c); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/node-fs-operations.ts b/tests/fixtures/effect4/node-fs-operations.ts new file mode 100644 index 000000000..556f6966a --- /dev/null +++ b/tests/fixtures/effect4/node-fs-operations.ts @@ -0,0 +1,19 @@ +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"; + +const program = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const directory = yield* fs.makeTempDirectoryScoped(); + const original = directory + "/original.txt"; + yield* fs.writeFileString(original, "hello"); + yield* fs.access(original, { readable: true }); + yield* fs.copyFile(original, directory + "/copy.txt"); + yield* fs.rename(directory + "/copy.txt", directory + "/renamed.txt"); + const info = yield* fs.stat(original); + const contents = yield* fs.readFileString(directory + "/renamed.txt"); + const missing = yield* fs.exists(directory + "/missing.txt"); + console.log(info.type, Number(info.size), contents, missing); +}).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)); + +Effect.runPromise(program); diff --git a/tests/fixtures/effect4/node-fs-scoped.ts b/tests/fixtures/effect4/node-fs-scoped.ts new file mode 100644 index 000000000..b2e274750 --- /dev/null +++ b/tests/fixtures/effect4/node-fs-scoped.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"; +const program=Effect.gen(function*(){const fs=yield* FileSystem.FileSystem;const dir=yield* fs.makeTempDirectoryScoped();const path=dir+"/hello.txt";yield* fs.writeFileString(path,"hello");return yield* fs.readFileString(path)}).pipe(Effect.scoped,Effect.provide(NodeFileSystem.layer),Effect.tap(v=>Effect.sync(()=>console.log(v)))); +Effect.runPromise(program); diff --git a/tests/fixtures/effect4/node-fs.ts b/tests/fixtures/effect4/node-fs.ts new file mode 100644 index 000000000..decc04392 --- /dev/null +++ b/tests/fixtures/effect4/node-fs.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";Effect.runPromise((Effect.gen(function*(){const fs=yield* FileSystem.FileSystem;return yield* fs.readFileString("tests/fixtures/effect4/file-system-input.txt")}).pipe(Effect.provide(NodeFileSystem.layer))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/node-path.ts b/tests/fixtures/effect4/node-path.ts new file mode 100644 index 000000000..468fe1a31 --- /dev/null +++ b/tests/fixtures/effect4/node-path.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Path from "effect/Path";import * as NodePath from "@effect/platform-node/NodePath";Effect.runPromise((Effect.gen(function*(){const path=yield* Path.Path;return path.join("a","b","c")}).pipe(Effect.provide(NodePath.layer))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/node-process.ts b/tests/fixtures/effect4/node-process.ts new file mode 100644 index 000000000..bba2e1179 --- /dev/null +++ b/tests/fixtures/effect4/node-process.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as ChildProcess from "effect/process/ChildProcess";import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";import * as NodeServices from "@effect/platform-node/NodeServices";Effect.runPromise((Effect.gen(function*(){const spawner=yield* ChildProcessSpawner.ChildProcessSpawner;return yield* spawner.string(ChildProcess.make("/usr/bin/printf",["hello"]))}).pipe(Effect.provide(NodeServices.layer))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/node-runtime.ts b/tests/fixtures/effect4/node-runtime.ts new file mode 100644 index 000000000..d6facf6ee --- /dev/null +++ b/tests/fixtures/effect4/node-runtime.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as NodeRuntime from "@effect/platform-node/NodeRuntime";NodeRuntime.runMain(Effect.sync(()=>console.log("hello"))) diff --git a/tests/fixtures/effect4/node-services.ts b/tests/fixtures/effect4/node-services.ts new file mode 100644 index 000000000..1556f8caf --- /dev/null +++ b/tests/fixtures/effect4/node-services.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Path from "effect/Path";import * as NodeServices from "@effect/platform-node/NodeServices";Effect.runPromise((Effect.gen(function*(){const path=yield* Path.Path;return path.join("a","b")}).pipe(Effect.provide(NodeServices.layer))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/node-stream.ts b/tests/fixtures/effect4/node-stream.ts new file mode 100644 index 000000000..a2eb12bdf --- /dev/null +++ b/tests/fixtures/effect4/node-stream.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Stream from "effect/Stream";import * as NodeStream from "@effect/platform-node/NodeStream";import {Readable} from "node:stream";Effect.runPromise((NodeStream.fromReadable({evaluate:()=>Readable.from(["a","b"]),onError:String}).pipe(Stream.runCollect)).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/observability-otlp.ts b/tests/fixtures/effect4/observability-otlp.ts new file mode 100644 index 000000000..cb6849298 --- /dev/null +++ b/tests/fixtures/effect4/observability-otlp.ts @@ -0,0 +1,10 @@ +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as OtlpTracer from "effect/observability/OtlpTracer"; +import * as OtlpSerialization from "effect/observability/OtlpSerialization"; +import * as HttpClient from "effect/http/HttpClient"; +import * as HttpClientResponse from "effect/http/HttpClientResponse"; +const urls:string[]=[]; +const client=HttpClient.make(request=>Effect.sync(()=>{urls.push(request.url);return HttpClientResponse.fromWeb(request,new Response("",{status:200}))})); +const layer=OtlpTracer.layer({url:"http://local/v1/traces",resource:{serviceName:"test"}}).pipe(Layer.provide(OtlpSerialization.layerJson),Layer.provide(Layer.succeed(HttpClient.HttpClient,client))); +Effect.runPromise(Effect.void.pipe(Effect.withSpan("hello"),Effect.provide(layer))).then(()=>console.log(JSON.stringify(urls))); diff --git a/tests/fixtures/effect4/original-hello.ts b/tests/fixtures/effect4/original-hello.ts new file mode 100644 index 000000000..4e17dddbf --- /dev/null +++ b/tests/fixtures/effect4/original-hello.ts @@ -0,0 +1,10 @@ +import { Effect, Logger } from "effect" + +const program = Effect.gen(function* () { + const name = yield* Effect.succeed("world") + yield* Effect.log("Hello, " + name + "!") +}) + +Effect.runPromise(program.pipe(Effect.provide(Logger.layer([ + Logger.make(({ message }) => console.log(message)) +])))) diff --git a/tests/fixtures/effect4/package.json b/tests/fixtures/effect4/package.json new file mode 100644 index 000000000..b8b47d2e9 --- /dev/null +++ b/tests/fixtures/effect4/package.json @@ -0,0 +1,9 @@ +{ + "name": "@internal/effect4-corpus", + "private": true, + "type": "module", + "dependencies": { + "effect": "4.0.0", + "@effect/platform-node": "4.0.0" + } +} diff --git a/tests/fixtures/effect4/persistence-memory.ts b/tests/fixtures/effect4/persistence-memory.ts new file mode 100644 index 000000000..6d18ede63 --- /dev/null +++ b/tests/fixtures/effect4/persistence-memory.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as KeyValueStore from "effect/persistence/KeyValueStore" +import * as Option from "effect/Option" +Effect.runPromise((Effect.gen(function*(){const kv=yield* KeyValueStore.KeyValueStore;yield* kv.set("key","world");return (yield* kv.get("key")) ?? "missing"}).pipe(Effect.provide(KeyValueStore.layerMemory))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/pool.ts b/tests/fixtures/effect4/pool.ts new file mode 100644 index 000000000..3f0e6ae3b --- /dev/null +++ b/tests/fixtures/effect4/pool.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Pool from "effect/Pool" +Effect.runPromise((Effect.gen(function*(){const pool=yield* Pool.make({acquire:Effect.succeed(42),size:1});return yield* Pool.get(pool)}).pipe(Effect.scoped)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/promise-annotated.ts b/tests/fixtures/effect4/promise-annotated.ts new file mode 100644 index 000000000..48373b443 --- /dev/null +++ b/tests/fixtures/effect4/promise-annotated.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";const p:Promise=Effect.runPromise(Effect.succeed(42));p.then((n:number)=>console.log(n)) diff --git a/tests/fixtures/effect4/pubsub.ts b/tests/fixtures/effect4/pubsub.ts new file mode 100644 index 000000000..11e82eaa9 --- /dev/null +++ b/tests/fixtures/effect4/pubsub.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as PubSub from "effect/PubSub" +import * as Queue from "effect/Queue" +Effect.runPromise((Effect.gen(function*(){const p=yield* PubSub.unbounded();const q=yield* PubSub.subscribe(p);yield* PubSub.publish(p,42);return yield* PubSub.take(q)}).pipe(Effect.scoped)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/queue-backpressure.ts b/tests/fixtures/effect4/queue-backpressure.ts new file mode 100644 index 000000000..f61cb0454 --- /dev/null +++ b/tests/fixtures/effect4/queue-backpressure.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Queue from "effect/Queue" +import * as Fiber from "effect/Fiber" +Effect.runPromise((Effect.gen(function* () { const q=yield* Queue.bounded(1); yield* Queue.offer(q,1); const f=yield* Effect.forkChild(Queue.offer(q,2),{startImmediately:true}); const a=yield* Queue.take(q); yield* Fiber.join(f); const b=yield* Queue.take(q); return [a,b]; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/queue-completion.ts b/tests/fixtures/effect4/queue-completion.ts new file mode 100644 index 000000000..43fd2e3b9 --- /dev/null +++ b/tests/fixtures/effect4/queue-completion.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Queue from "effect/Queue" +import * as Cause from "effect/Cause" +Effect.runPromise((Effect.gen(function* () { const q=yield* Queue.unbounded(); yield* Queue.offerAll(q,[1,2]); yield* Queue.end(q); return yield* Queue.collect(q); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/queue-unbounded.ts b/tests/fixtures/effect4/queue-unbounded.ts new file mode 100644 index 000000000..72a86009f --- /dev/null +++ b/tests/fixtures/effect4/queue-unbounded.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Queue from "effect/Queue" +Effect.runPromise((Effect.gen(function* () { const q=yield* Queue.unbounded(); yield* Queue.offer(q,42); const n=yield* Queue.take(q); yield* Queue.shutdown(q); return n; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/random-seeded.ts b/tests/fixtures/effect4/random-seeded.ts new file mode 100644 index 000000000..7eb9e593a --- /dev/null +++ b/tests/fixtures/effect4/random-seeded.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Random from "effect/Random" +Effect.runPromise((Effect.all([Random.nextIntBetween(1,100),Random.nextIntBetween(1,100)]).pipe(Random.withSeed("fixed"))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/rate-limiter.ts b/tests/fixtures/effect4/rate-limiter.ts new file mode 100644 index 000000000..991a6ee89 --- /dev/null +++ b/tests/fixtures/effect4/rate-limiter.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as RateLimiter from "effect/persistence/RateLimiter" +Effect.runPromise((Effect.gen(function*(){const l=yield* RateLimiter.make;const opts={key:"one",limit:1,window:"1 minute" as const,onExceeded:"fail" as const};const a=yield* l.consume(opts);const b=yield* l.consume(opts).pipe(Effect.catch(e=>Effect.succeed(e.reason._tag)));return [a.remaining,b]}).pipe(Effect.provide(RateLimiter.layerStoreMemory))).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/request-batching.ts b/tests/fixtures/effect4/request-batching.ts new file mode 100644 index 000000000..f163bc05d --- /dev/null +++ b/tests/fixtures/effect4/request-batching.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import * as Request from "effect/Request" +import * as RequestResolver from "effect/RequestResolver" +import * as Exit from "effect/Exit" +class Get extends Request.Class<{readonly id:number},number> {} const resolver=RequestResolver.make(entries=>Effect.sync(()=>{for(const entry of entries)entry.completeUnsafe(Exit.succeed(entry.request.id*2))})); Effect.runPromise((Effect.forEach([1,2,3],id=>Effect.request(new Get({id}),resolver),{concurrency:"unbounded"})).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/resource-ensuring.ts b/tests/fixtures/effect4/resource-ensuring.ts new file mode 100644 index 000000000..8ca10d7ba --- /dev/null +++ b/tests/fixtures/effect4/resource-ensuring.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +const events:string[]=[]; Effect.runPromise((Effect.succeed(42).pipe(Effect.ensuring(Effect.sync(()=>{events.push("finalized")})),Effect.map(n=>[n,events]))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/resource-finalizer.ts b/tests/fixtures/effect4/resource-finalizer.ts new file mode 100644 index 000000000..5d23c0843 --- /dev/null +++ b/tests/fixtures/effect4/resource-finalizer.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +const events:string[]=[]; Effect.runPromise((Effect.gen(function*(){yield* Effect.addFinalizer(()=>Effect.sync(()=>{events.push("finalized")}));return 42}).pipe(Effect.scoped,Effect.map(n=>[n,events]))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/resource-rc-map.ts b/tests/fixtures/effect4/resource-rc-map.ts new file mode 100644 index 000000000..3d792bba8 --- /dev/null +++ b/tests/fixtures/effect4/resource-rc-map.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as RcMap from "effect/RcMap" +Effect.runPromise((Effect.gen(function*(){const m=yield* RcMap.make({lookup:(key:string)=>Effect.succeed(key.length),idleTimeToLive:"1 minute"});return yield* RcMap.get(m,"world")}).pipe(Effect.scoped)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/resource-rc-ref.ts b/tests/fixtures/effect4/resource-rc-ref.ts new file mode 100644 index 000000000..d0d4b9556 --- /dev/null +++ b/tests/fixtures/effect4/resource-rc-ref.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as RcRef from "effect/RcRef" +Effect.runPromise((Effect.gen(function*(){const r=yield* RcRef.make({acquire:Effect.succeed(42),idleTimeToLive:"1 minute"});return yield* RcRef.get(r)}).pipe(Effect.scoped)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/resource-scope.ts b/tests/fixtures/effect4/resource-scope.ts new file mode 100644 index 000000000..7d6eb78a0 --- /dev/null +++ b/tests/fixtures/effect4/resource-scope.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Scope from "effect/Scope" +import * as Exit from "effect/Exit" +Effect.runPromise((Effect.gen(function* () { const scope=yield* Scope.make(); let n=0; yield* Scope.addFinalizer(scope,Effect.sync(()=>{n=42})); yield* Scope.close(scope,Exit.void); return n; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/resource-scoped.ts b/tests/fixtures/effect4/resource-scoped.ts new file mode 100644 index 000000000..ff10a69b7 --- /dev/null +++ b/tests/fixtures/effect4/resource-scoped.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +const events:string[]=[]; const p=Effect.gen(function*(){ const r=yield* Effect.acquireRelease(Effect.succeed(42),()=>Effect.sync(()=>{events.push("released")})); return r }).pipe(Effect.scoped,Effect.map(n=>[n,events])); Effect.runPromise((p).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/rpc-inmemory.ts b/tests/fixtures/effect4/rpc-inmemory.ts new file mode 100644 index 000000000..ad5d5f38f --- /dev/null +++ b/tests/fixtures/effect4/rpc-inmemory.ts @@ -0,0 +1,6 @@ +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import * as Rpc from "effect/rpc/Rpc" +import * as RpcGroup from "effect/rpc/RpcGroup" +import * as RpcTest from "effect/rpc/RpcTest" +const Group=RpcGroup.make(Rpc.make("double",{payload:{n:Schema.Number},success:Schema.Number})); const handlers=Group.toLayer({double:({n})=>Effect.succeed(n*2)}); Effect.runPromise((Effect.gen(function*(){const c=yield* RpcTest.makeClient(Group);return yield* c.double({n:21})}).pipe(Effect.scoped,Effect.provide(handlers))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/runner-context.ts b/tests/fixtures/effect4/runner-context.ts new file mode 100644 index 000000000..4c223a0c9 --- /dev/null +++ b/tests/fixtures/effect4/runner-context.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +const Count=Context.Service("Count"); const context=Context.make(Count,42); Effect.runPromiseWith(context)(Count).then((n:number) => console.log(n)) diff --git a/tests/fixtures/effect4/runner-promise-exit.ts b/tests/fixtures/effect4/runner-promise-exit.ts new file mode 100644 index 000000000..cc475b2b2 --- /dev/null +++ b/tests/fixtures/effect4/runner-promise-exit.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +Effect.runPromiseExit(Effect.fail("oops")).then((x:Exit.Exit) => console.log(Exit.isFailure(x) ? JSON.stringify(x.cause) : "success")) diff --git a/tests/fixtures/effect4/runner-sync-exit.ts b/tests/fixtures/effect4/runner-sync-exit.ts new file mode 100644 index 000000000..69fa7690a --- /dev/null +++ b/tests/fixtures/effect4/runner-sync-exit.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +const x=Effect.runSyncExit(Effect.fail("oops")); console.log(Exit.isFailure(x) ? x.cause.reasons[0]?._tag : "success") diff --git a/tests/fixtures/effect4/runner-sync.ts b/tests/fixtures/effect4/runner-sync.ts new file mode 100644 index 000000000..1d22c905f --- /dev/null +++ b/tests/fixtures/effect4/runner-sync.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +console.log(Effect.runSync(Effect.succeed(42))) diff --git a/tests/fixtures/effect4/runtime-managed.ts b/tests/fixtures/effect4/runtime-managed.ts new file mode 100644 index 000000000..8b697090c --- /dev/null +++ b/tests/fixtures/effect4/runtime-managed.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Layer from "effect/Layer" +import * as ManagedRuntime from "effect/ManagedRuntime" +const C=Context.Service("C"); const r=ManagedRuntime.make(Layer.succeed(C,42)); r.runPromise(C).then((n:number)=>{console.log(n); return r.dispose()}).then(()=>console.log("disposed")) diff --git a/tests/fixtures/effect4/schedule-backoff.ts b/tests/fixtures/effect4/schedule-backoff.ts new file mode 100644 index 000000000..f914a1533 --- /dev/null +++ b/tests/fixtures/effect4/schedule-backoff.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Schedule from "effect/Schedule" +let n=0; Effect.runPromise((Effect.suspend(()=>++n<3?Effect.fail("again"):Effect.succeed(n)).pipe(Effect.retry(Schedule.max([Schedule.exponential("1 millis"),Schedule.recurs(2)])))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/schedule-repeat.ts b/tests/fixtures/effect4/schedule-repeat.ts new file mode 100644 index 000000000..58a8fd33c --- /dev/null +++ b/tests/fixtures/effect4/schedule-repeat.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Schedule from "effect/Schedule" +let n=0; Effect.runPromise((Effect.sync(()=>++n).pipe(Effect.repeat(Schedule.recurs(2)),Effect.map(()=>n))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/schedule-retry.ts b/tests/fixtures/effect4/schedule-retry.ts new file mode 100644 index 000000000..d84549675 --- /dev/null +++ b/tests/fixtures/effect4/schedule-retry.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Schedule from "effect/Schedule" +let n=0; Effect.runPromise((Effect.suspend(()=>++n<3?Effect.fail("again"):Effect.succeed(n)).pipe(Effect.retry(Schedule.recurs(3)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/schema-aot-generate.ts b/tests/fixtures/effect4/schema-aot-generate.ts new file mode 100644 index 000000000..f627c28c9 --- /dev/null +++ b/tests/fixtures/effect4/schema-aot-generate.ts @@ -0,0 +1 @@ +import * as Schema from "effect/Schema";import * as SchemaAOTCompiler from "effect/schema/SchemaAOTCompiler";const source=SchemaAOTCompiler.compile([{ast:Schema.Struct({n:Schema.Int}).ast,operations:["decode","is","make"]}]);console.log(source.includes("install"),source.includes("globalThis.Function")) diff --git a/tests/fixtures/effect4/schema-aot-generated.d.ts b/tests/fixtures/effect4/schema-aot-generated.d.ts new file mode 100644 index 000000000..14ae42bab --- /dev/null +++ b/tests/fixtures/effect4/schema-aot-generated.d.ts @@ -0,0 +1,2 @@ +import type * as SchemaAST from "effect/SchemaAST"; +export declare function install(asts: ReadonlyArray): void; diff --git a/tests/fixtures/effect4/schema-aot-generated.js b/tests/fixtures/effect4/schema-aot-generated.js new file mode 100644 index 000000000..3a3682933 --- /dev/null +++ b/tests/fixtures/effect4/schema-aot-generated.js @@ -0,0 +1,11 @@ +// Generated by SchemaAOTCompiler. Regenerate after schema or Effect changes. +import { runtime } from "effect/schema/SchemaCompiler/runtime"; +function d0(ast,resolve,operation){const R=runtime;switch(operation){case "is":{return (function(C,R){"use strict";const {getCheckIssues:K,matchesTemplateLiteral:T,getCandidateIndex:U,getIndexSignatureKeys:G,defaultParseOptions:D,hasExcessProperties:E}=R;;;return function(i,o){if(typeof i!=="object"||i===null||Array.isArray(i))return false;if(o!==D&&o.onExcessProperty==="error"&&E(C[0],i,void 0))return false;const v0=i["n"];if(typeof v0!=="number")return false;const v1=v0;if(K(C[1],v1,0,o))return false;return true}})([ast,ast.propertySignatures[0].type],R)}case "decode":{return (function(C,R){"use strict";const {invalid:I,getCheckIssues:K,matchesTemplateLiteral:T,getCandidateIndex:U,getIndexSignatureKeys:G,defaultParseOptions:D,hasExcessProperties:E}=R;;;return function(i,o){if(typeof i!=="object"||i===null||Array.isArray(i))return I;if(o!==D&&o.onExcessProperty==="error"&&E(C[0],i,void 0))return I;const v1=i["n"];if(typeof v1!=="number")return I;const v2=v1;if(K(C[1],v2,0,o))return I;const v0={"n":v2};return v0}})([ast,ast.propertySignatures[0].type],R)}case "make":{if(ast.propertySignatures.some(p=>resolve(p.type).compiled!==void 0))return;return (function(C,R){"use strict";const {invalid:I,getCheckIssues:K,matchesTemplateLiteral:T,getCandidateIndex:U,getIndexSignatureKeys:G,defaultParseOptions:D,hasExcessProperties:E}=R;;;return function(i,o){if(typeof i!=="object"||i===null||Array.isArray(i))return I;if(o!==D&&o.onExcessProperty==="error"&&E(C[0],i,void 0))return I;const v1=i["n"];if(typeof v1!=="number")return I;const v2=v1;if(K(C[1],v2,0,o))return I;const v0={"n":v2};return v0}})([ast,ast.propertySignatures[0].type],R)}case "decodeEffect":{return R.decode(ast,resolve,function({ast,getProperties,fallback,resume,step}){;return function(i,o){try{if(i===R.missing)return R.missingExit;if(o.errors==="all"||o.onExcessProperty!==void 0||(o.concurrency!==void 0&&o.concurrency!==1))return fallback(i,o);if(typeof i!=="object"||i===null||Array.isArray(i))return R.invalidType(ast,i,o);const properties=getProperties(),out={};const state={ast,input:i,out,options:o,issues:void 0};let r,t,value;const p0=properties[0],h0="n" in i,v0=h0?i["n"]:R.missing;r=p0.parser(v0,o);if(r===R.sameExit){if(h0){out["n"]=v0}}else{if(!R.effectIsExit(r))return resume(state,0,r);if(r._tag==="Success"&&(value=r[R.args])!==R.missing){out["n"]=value}else{t=step(state,p0,r);if(t)return t}};return R.succeed(out)}catch(e){return R.die(e)}}},true,undefined,undefined)}case "makeEffect":{return R.make(ast,resolve,function({ast,getProperties,fallback,resume,step}){;return function(i,o){try{if(i===R.missing)return R.missingExit;if(o.errors==="all"||o.onExcessProperty!==void 0||(o.concurrency!==void 0&&o.concurrency!==1))return fallback(i,o);if(typeof i!=="object"||i===null||Array.isArray(i))return R.invalidType(ast,i,o);const properties=getProperties(),out={};const state={ast,input:i,out,options:o,issues:void 0};let r,t,value;const p0=properties[0],h0="n" in i,v0=h0?i["n"]:R.missing;r=p0.parser(v0,o);if(r===R.sameExit){if(h0){out["n"]=v0}}else{if(!R.effectIsExit(r))return resume(state,0,r);if(r._tag==="Success"&&(value=r[R.args])!==R.missing){out["n"]=value}else{t=step(state,p0,r);if(t)return t}};return R.succeed(out)}catch(e){return R.die(e)}}},undefined)}}} +function d1(ast,resolve,operation){const R=runtime;switch(operation){case "decodeEffect":{return R.decode(ast,resolve,undefined,true,undefined,undefined)}case "makeEffect":{return R.make(ast,resolve,undefined,undefined)}}} +/** @param {ReadonlyArray} asts */ +export function install(asts){ +const a0=asts[0]; +const a1=a0.propertySignatures[0].type; +runtime.setCompiler(a0,d0); +runtime.setCompiler(a1,d1); +} diff --git a/tests/fixtures/effect4/schema-aot-installed.ts b/tests/fixtures/effect4/schema-aot-installed.ts new file mode 100644 index 000000000..b14ae96a3 --- /dev/null +++ b/tests/fixtures/effect4/schema-aot-installed.ts @@ -0,0 +1,5 @@ +import * as Schema from "effect/Schema"; +import {install} from "./schema-aot-generated.js"; +const S=Schema.Struct({n:Schema.Int}); +install([S.ast]); +console.log(JSON.stringify(Schema.decodeUnknownSync(S)({n:42})),Schema.is(S)({n:42}),Schema.is(S)({n:"bad"})); diff --git a/tests/fixtures/effect4/schema-binary.ts b/tests/fixtures/effect4/schema-binary.ts new file mode 100644 index 000000000..be322ab0a --- /dev/null +++ b/tests/fixtures/effect4/schema-binary.ts @@ -0,0 +1 @@ +import * as Schema from "effect/Schema";import * as SchemaBinary from "effect/encoding/SchemaBinary";const S=SchemaBinary.toCodec(Schema.Struct({n:Schema.Int}));const bytes=Schema.encodeSync(S)({n:42});console.log(JSON.stringify(Schema.decodeSync(S)(bytes))) diff --git a/tests/fixtures/effect4/schema-class.ts b/tests/fixtures/effect4/schema-class.ts new file mode 100644 index 000000000..ebff34111 --- /dev/null +++ b/tests/fixtures/effect4/schema-class.ts @@ -0,0 +1,2 @@ +import * as Schema from "effect/Schema" +class User extends Schema.Class("User")({id:Schema.Int,name:Schema.NonEmptyString}) {} const u=User.make({id:42,name:"world"}); console.log(u instanceof User,JSON.stringify(Schema.encodeSync(User)(u))) diff --git a/tests/fixtures/effect4/schema-effect.ts b/tests/fixtures/effect4/schema-effect.ts new file mode 100644 index 000000000..722992095 --- /dev/null +++ b/tests/fixtures/effect4/schema-effect.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +Effect.runPromise((Schema.decodeUnknownEffect(Schema.Struct({name:Schema.String}))({name:"world"}).pipe(Effect.flatMap(value=>Schema.encodeEffect(Schema.Struct({name:Schema.String}))(value)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/schema-invalid.ts b/tests/fixtures/effect4/schema-invalid.ts new file mode 100644 index 000000000..faf126ce7 --- /dev/null +++ b/tests/fixtures/effect4/schema-invalid.ts @@ -0,0 +1,3 @@ +import * as Schema from "effect/Schema" +import * as Exit from "effect/Exit" +const S=Schema.Struct({age:Schema.Int}); const x=Schema.decodeUnknownExit(S)({age:"bad"}); console.log(Exit.isFailure(x)) diff --git a/tests/fixtures/effect4/schema-jit.ts b/tests/fixtures/effect4/schema-jit.ts new file mode 100644 index 000000000..9e1a7c625 --- /dev/null +++ b/tests/fixtures/effect4/schema-jit.ts @@ -0,0 +1 @@ +import * as Schema from "effect/Schema";import * as SchemaJITCompiler from "effect/schema/SchemaJITCompiler";const S=Schema.Struct({n:Schema.Int});SchemaJITCompiler.enable(S.ast);console.log(JSON.stringify(Schema.decodeSync(S)({n:42}))) diff --git a/tests/fixtures/effect4/schema-json-import.ts b/tests/fixtures/effect4/schema-json-import.ts new file mode 100644 index 000000000..e20264a6c --- /dev/null +++ b/tests/fixtures/effect4/schema-json-import.ts @@ -0,0 +1 @@ +import * as Schema from "effect/Schema";import * as JsonSchema from "effect/JsonSchema";import * as SchemaRepresentation from "effect/SchemaRepresentation";const S=SchemaRepresentation.fromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({type:"string"}));console.log(Schema.decodeUnknownSync(S as Schema.Codec)("world")) diff --git a/tests/fixtures/effect4/schema-json-schema.ts b/tests/fixtures/effect4/schema-json-schema.ts new file mode 100644 index 000000000..59b898c9b --- /dev/null +++ b/tests/fixtures/effect4/schema-json-schema.ts @@ -0,0 +1,2 @@ +import * as Schema from "effect/Schema" +const d=Schema.toJsonSchemaDocument(Schema.Struct({name:Schema.String}));console.log(JSON.stringify(d)) diff --git a/tests/fixtures/effect4/schema-json.ts b/tests/fixtures/effect4/schema-json.ts new file mode 100644 index 000000000..3b90125f1 --- /dev/null +++ b/tests/fixtures/effect4/schema-json.ts @@ -0,0 +1,2 @@ +import * as Schema from "effect/Schema" +const S=Schema.fromJsonString(Schema.Struct({n:Schema.Int})); const d=Schema.decodeSync(S)('{"n":42}'); console.log(Schema.encodeSync(S)(d)) diff --git a/tests/fixtures/effect4/schema-optional.ts b/tests/fixtures/effect4/schema-optional.ts new file mode 100644 index 000000000..98f98e556 --- /dev/null +++ b/tests/fixtures/effect4/schema-optional.ts @@ -0,0 +1,2 @@ +import * as Schema from "effect/Schema" +const S=Schema.Struct({name:Schema.String,age:Schema.optionalKey(Schema.Int)});console.log(JSON.stringify(Schema.decodeUnknownSync(S)({name:"world"}))) diff --git a/tests/fixtures/effect4/schema-recursive.ts b/tests/fixtures/effect4/schema-recursive.ts new file mode 100644 index 000000000..d120640b5 --- /dev/null +++ b/tests/fixtures/effect4/schema-recursive.ts @@ -0,0 +1,2 @@ +import * as Schema from "effect/Schema" +type Tree={readonly value:number;readonly children:ReadonlyArray};const Tree:Schema.Codec=Schema.Struct({value:Schema.Int,children:Schema.Array(Schema.suspend(()=>Tree))});console.log(JSON.stringify(Schema.decodeUnknownSync(Tree)({value:1,children:[{value:2,children:[]}]}))) diff --git a/tests/fixtures/effect4/schema-representation.ts b/tests/fixtures/effect4/schema-representation.ts new file mode 100644 index 000000000..b4d445ea3 --- /dev/null +++ b/tests/fixtures/effect4/schema-representation.ts @@ -0,0 +1,3 @@ +import * as Schema from "effect/Schema" +import * as SchemaRepresentation from "effect/SchemaRepresentation" +const r=SchemaRepresentation.toRepresentation(Schema.Struct({name:Schema.String}).ast);console.log(JSON.stringify(SchemaRepresentation.toJson(r))) diff --git a/tests/fixtures/effect4/schema-service-transform.ts b/tests/fixtures/effect4/schema-service-transform.ts new file mode 100644 index 000000000..679eb7df7 --- /dev/null +++ b/tests/fixtures/effect4/schema-service-transform.ts @@ -0,0 +1 @@ +import * as Schema from "effect/Schema";import * as SchemaGetter from "effect/SchemaGetter";import * as Effect from "effect/Effect";import * as Context from "effect/Context";const Offset=Context.Service("Offset");const S=Schema.Number.pipe(Schema.decodeTo(Schema.Number,{decode:SchemaGetter.transformEffect(n=>Effect.map(Offset,x=>n+x)),encode:SchemaGetter.transform(n=>n)}));Effect.runPromise((Schema.decodeUnknownEffect(S)(40).pipe(Effect.provideService(Offset,2))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/schema-standard.ts b/tests/fixtures/effect4/schema-standard.ts new file mode 100644 index 000000000..980621fdf --- /dev/null +++ b/tests/fixtures/effect4/schema-standard.ts @@ -0,0 +1,2 @@ +import * as Schema from "effect/Schema" +const S=Schema.toStandardSchemaV1(Schema.Struct({n:Schema.Int}));console.log(JSON.stringify(S["~standard"].validate({n:42}))) diff --git a/tests/fixtures/effect4/schema-struct.ts b/tests/fixtures/effect4/schema-struct.ts new file mode 100644 index 000000000..f12f05e47 --- /dev/null +++ b/tests/fixtures/effect4/schema-struct.ts @@ -0,0 +1,3 @@ +import { Schema } from "effect" +const S = Schema.Struct({name: Schema.String, age: Schema.Int}) +console.log(JSON.stringify(Schema.decodeUnknownSync(S)({name:"world", age:42}))) diff --git a/tests/fixtures/effect4/schema-tagged-error.ts b/tests/fixtures/effect4/schema-tagged-error.ts new file mode 100644 index 000000000..0bf8c5744 --- /dev/null +++ b/tests/fixtures/effect4/schema-tagged-error.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +class Missing extends Schema.TaggedError()("Missing",{id:Schema.Int}) {} Effect.runPromise((Effect.fail(new Missing({id:42})).pipe(Effect.catchTag("Missing",e=>Effect.succeed(e.id)))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/schema-transform.ts b/tests/fixtures/effect4/schema-transform.ts new file mode 100644 index 000000000..df5170d7b --- /dev/null +++ b/tests/fixtures/effect4/schema-transform.ts @@ -0,0 +1 @@ +import * as Schema from "effect/Schema";import * as SchemaTransformation from "effect/SchemaTransformation";const S=Schema.String.pipe(Schema.decodeTo(Schema.Number,SchemaTransformation.transform({decode:s=>Number(s),encode:n=>String(n)})));console.log(Schema.decodeSync(S)("42"),Schema.encodeSync(S)(42)) diff --git a/tests/fixtures/effect4/schema-union-check.ts b/tests/fixtures/effect4/schema-union-check.ts new file mode 100644 index 000000000..57b34b93b --- /dev/null +++ b/tests/fixtures/effect4/schema-union-check.ts @@ -0,0 +1,2 @@ +import * as Schema from "effect/Schema" +const S=Schema.Union([Schema.Literals(["admin","member"]),Schema.Number.check(Schema.isGreaterThan(0))]); console.log(Schema.decodeUnknownSync(S)(42)) diff --git a/tests/fixtures/effect4/semaphore.ts b/tests/fixtures/effect4/semaphore.ts new file mode 100644 index 000000000..94e40139e --- /dev/null +++ b/tests/fixtures/effect4/semaphore.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Semaphore from "effect/Semaphore" +Effect.runPromise((Effect.gen(function* () { const s=yield* Semaphore.make(1); return yield* Effect.all([1,2,3].map(n=>Semaphore.withPermit(s,Effect.succeed(n))),{concurrency:3}); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/service-class.ts b/tests/fixtures/effect4/service-class.ts new file mode 100644 index 000000000..6441dd54a --- /dev/null +++ b/tests/fixtures/effect4/service-class.ts @@ -0,0 +1,4 @@ +import { Context, Effect } from "effect" +class Name extends Context.Service()("Name") {} +const p = Effect.gen(function* () { return (yield* Name).value }) +Effect.runPromise(p.pipe(Effect.provideService(Name, {value:"world"}))).then((x: string) => console.log(x)) diff --git a/tests/fixtures/effect4/service-function.ts b/tests/fixtures/effect4/service-function.ts new file mode 100644 index 000000000..012616b12 --- /dev/null +++ b/tests/fixtures/effect4/service-function.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +const Count=Context.Service("Count"); Effect.runPromise((Count.pipe(Effect.provideService(Count,42))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/service-make.ts b/tests/fixtures/effect4/service-make.ts new file mode 100644 index 000000000..fc8f7294d --- /dev/null +++ b/tests/fixtures/effect4/service-make.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Layer from "effect/Layer" +class Counter extends Context.Service()("Counter", {make:Effect.succeed({count:42})}) { static layer=Layer.effect(this,this.make) } Effect.runPromise((Counter.useSync(c=>c.count).pipe(Effect.provide(Counter.layer))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/service-reference.ts b/tests/fixtures/effect4/service-reference.ts new file mode 100644 index 000000000..88dac00fc --- /dev/null +++ b/tests/fixtures/effect4/service-reference.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +const Flag=Context.Reference("Flag",{defaultValue:()=>false}); Effect.runPromise((Effect.gen(function* () { const a=yield* Flag; const b=yield* Flag.pipe(Effect.provideService(Flag,true)); const c=yield* Flag; return [a,b,c]; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/service-use.ts b/tests/fixtures/effect4/service-use.ts new file mode 100644 index 000000000..14d1cdcb6 --- /dev/null +++ b/tests/fixtures/effect4/service-use.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +const S=Context.Service<{value:number}>("S"); Effect.runPromise((S.useSync(s=>s.value).pipe(Effect.provideService(S,{value:42}))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/sql-query.ts b/tests/fixtures/effect4/sql-query.ts new file mode 100644 index 000000000..71347df32 --- /dev/null +++ b/tests/fixtures/effect4/sql-query.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Stream from "effect/Stream";import * as SqlClient from "effect/sql/SqlClient";import * as Statement from "effect/sql/Statement";import * as Reactivity from "effect/reactivity/Reactivity";const conn={execute:(sql:string,params:ReadonlyArray)=>Effect.succeed([{sql,params}]),executeRaw:(sql:string,params:ReadonlyArray)=>Effect.succeed([{sql,params}]),executeValuesUnprepared:()=>Effect.succeed([]),executeValues:()=>Effect.succeed([]),executeUnprepared:(sql:string,params:ReadonlyArray)=>Effect.succeed([{sql,params}]),executeStream:()=>Stream.empty};Effect.runPromise((Effect.gen(function*(){const sql=yield* SqlClient.make({acquirer:Effect.succeed(conn),compiler:Statement.makeCompilerSqlite(),spanAttributes:[]});return yield* sql.withTransaction(sql`select ${42} as answer`)}).pipe(Effect.scoped,Effect.provide(Reactivity.layer))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/state-ref.ts b/tests/fixtures/effect4/state-ref.ts new file mode 100644 index 000000000..784395b02 --- /dev/null +++ b/tests/fixtures/effect4/state-ref.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Ref from "effect/Ref" +Effect.runPromise((Effect.gen(function* () { const r=yield* Ref.make(40); yield* Ref.update(r,n=>n+2); return yield* Ref.get(r); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/state-subscription-ref.ts b/tests/fixtures/effect4/state-subscription-ref.ts new file mode 100644 index 000000000..6d5c53bea --- /dev/null +++ b/tests/fixtures/effect4/state-subscription-ref.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as SubscriptionRef from "effect/SubscriptionRef" +import * as Stream from "effect/Stream" +Effect.runPromise((Effect.gen(function* () { const r=yield* SubscriptionRef.make(42); return yield* Stream.runCollect(SubscriptionRef.changes(r).pipe(Stream.take(1))); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/state-sync-ref.ts b/tests/fixtures/effect4/state-sync-ref.ts new file mode 100644 index 000000000..3f6747e12 --- /dev/null +++ b/tests/fixtures/effect4/state-sync-ref.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as SynchronizedRef from "effect/SynchronizedRef" +Effect.runPromise((Effect.gen(function* () { const r=yield* SynchronizedRef.make(40); yield* SynchronizedRef.updateEffect(r,n=>Effect.succeed(n+2)); return yield* SynchronizedRef.get(r); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/stream-async-iterable.ts b/tests/fixtures/effect4/stream-async-iterable.ts new file mode 100644 index 000000000..b5845b3dc --- /dev/null +++ b/tests/fixtures/effect4/stream-async-iterable.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +async function* values(){yield 1;yield 2} Effect.runPromise((Stream.fromAsyncIterable(values(),String).pipe(Stream.runCollect)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/stream-callback.ts b/tests/fixtures/effect4/stream-callback.ts new file mode 100644 index 000000000..144035247 --- /dev/null +++ b/tests/fixtures/effect4/stream-callback.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +import * as Queue from "effect/Queue" +Effect.runPromise((Stream.callback(q=>Effect.gen(function*(){yield* Queue.offer(q,42);yield* Queue.end(q)})).pipe(Stream.runCollect)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/stream-concurrent.ts b/tests/fixtures/effect4/stream-concurrent.ts new file mode 100644 index 000000000..f07498fd4 --- /dev/null +++ b/tests/fixtures/effect4/stream-concurrent.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +Effect.runPromise((Stream.make(1,2,3).pipe(Stream.mapEffect(n=>Effect.succeed(n*2).pipe(Effect.delay("1 millis")),{concurrency:2}),Stream.runCollect)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/stream-effect.ts b/tests/fixtures/effect4/stream-effect.ts new file mode 100644 index 000000000..827ac8708 --- /dev/null +++ b/tests/fixtures/effect4/stream-effect.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +Effect.runPromise((Stream.fromEffect(Effect.succeed(21)).pipe(Stream.mapEffect(n=>Effect.succeed(n*2)),Stream.runCollect)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/stream-error.ts b/tests/fixtures/effect4/stream-error.ts new file mode 100644 index 000000000..817090ad0 --- /dev/null +++ b/tests/fixtures/effect4/stream-error.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +Effect.runPromise((Stream.fail("oops").pipe(Stream.catch(e=>Stream.succeed(e)),Stream.runCollect)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/stream-map.ts b/tests/fixtures/effect4/stream-map.ts new file mode 100644 index 000000000..af5897d7d --- /dev/null +++ b/tests/fixtures/effect4/stream-map.ts @@ -0,0 +1,2 @@ +import { Effect, Stream } from "effect" +Effect.runPromise(Stream.fromIterable([1,2,3]).pipe(Stream.map(n => n*2), Stream.runCollect)).then((x: ReadonlyArray) => console.log(JSON.stringify(x))) diff --git a/tests/fixtures/effect4/stream-merge.ts b/tests/fixtures/effect4/stream-merge.ts new file mode 100644 index 000000000..26cff4c59 --- /dev/null +++ b/tests/fixtures/effect4/stream-merge.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +Effect.runPromise((Stream.merge(Stream.make(1,3),Stream.make(2,4)).pipe(Stream.runCollect,Effect.map(ns=>[...ns].sort()))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/stream-ndjson.ts b/tests/fixtures/effect4/stream-ndjson.ts new file mode 100644 index 000000000..9fa351d74 --- /dev/null +++ b/tests/fixtures/effect4/stream-ndjson.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +import * as Schema from "effect/Schema" +import * as Ndjson from "effect/encoding/Ndjson" +Effect.runPromise((Stream.make('{"n":1}\n{"n":2}\n').pipe(Stream.pipeThroughChannel(Ndjson.decodeSchemaString(Schema.Struct({n:Schema.Int}))()),Stream.map(v=>({n:v.n*2})),Stream.pipeThroughChannel(Ndjson.encodeSchemaString(Schema.Struct({n:Schema.Int}))()),Stream.runCollect)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/stream-sink.ts b/tests/fixtures/effect4/stream-sink.ts new file mode 100644 index 000000000..ae1bbca08 --- /dev/null +++ b/tests/fixtures/effect4/stream-sink.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +import * as Sink from "effect/Sink" +Effect.runPromise((Stream.make(1,2,3).pipe(Stream.run(Sink.sum))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/stream-take.ts b/tests/fixtures/effect4/stream-take.ts new file mode 100644 index 000000000..81bd32e69 --- /dev/null +++ b/tests/fixtures/effect4/stream-take.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +Effect.runPromise((Stream.iterate(1,n=>n+1).pipe(Stream.take(3),Stream.runCollect)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/test-clock.ts b/tests/fixtures/effect4/test-clock.ts new file mode 100644 index 000000000..55d20247c --- /dev/null +++ b/tests/fixtures/effect4/test-clock.ts @@ -0,0 +1 @@ +import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as TestClock from "effect/testing/TestClock";Effect.runPromise((Effect.gen(function*(){const f=yield* Effect.forkChild(Effect.succeed(42).pipe(Effect.delay("10 seconds")),{startImmediately:true});yield* TestClock.adjust("10 seconds");return yield* Fiber.join(f)}).pipe(Effect.provide(TestClock.layer()))).pipe(Effect.tap(value=>Effect.sync(()=>console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/tracing.ts b/tests/fixtures/effect4/tracing.ts new file mode 100644 index 000000000..6bf7046e0 --- /dev/null +++ b/tests/fixtures/effect4/tracing.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect" +Effect.runPromise((Effect.gen(function*(){yield* Effect.annotateCurrentSpan({id:1});const s=yield* Effect.currentSpan;return [s.name,s.attributes.get("id")] }).pipe(Effect.withSpan("test"))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/tx-deferred.ts b/tests/fixtures/effect4/tx-deferred.ts new file mode 100644 index 000000000..12aba10fa --- /dev/null +++ b/tests/fixtures/effect4/tx-deferred.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as TxDeferred from "effect/TxDeferred" +Effect.runPromise((Effect.gen(function*(){const d=yield* TxDeferred.make();yield* TxDeferred.succeed(d,42);return yield* TxDeferred.await(d)}).pipe(Effect.tx)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/tx-hashmap.ts b/tests/fixtures/effect4/tx-hashmap.ts new file mode 100644 index 000000000..cfe528fe7 --- /dev/null +++ b/tests/fixtures/effect4/tx-hashmap.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as TxHashMap from "effect/TxHashMap" +import * as Option from "effect/Option" +Effect.runPromise((Effect.gen(function*(){const m=yield* TxHashMap.make(["key",42]);const v=yield* TxHashMap.get(m,"key");return Option.getOrElse(v,()=>0)}).pipe(Effect.tx)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/tx-pubsub.ts b/tests/fixtures/effect4/tx-pubsub.ts new file mode 100644 index 000000000..0bbd471b9 --- /dev/null +++ b/tests/fixtures/effect4/tx-pubsub.ts @@ -0,0 +1,4 @@ +import * as Effect from "effect/Effect" +import * as TxPubSub from "effect/TxPubSub" +import * as TxQueue from "effect/TxQueue" +Effect.runPromise((Effect.gen(function*(){const p=yield* TxPubSub.unbounded();const q=yield* TxPubSub.subscribe(p);yield* TxPubSub.publish(p,42);return yield* TxQueue.take(q)}).pipe(Effect.tx,Effect.scoped)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/tx-queue.ts b/tests/fixtures/effect4/tx-queue.ts new file mode 100644 index 000000000..4ff51f170 --- /dev/null +++ b/tests/fixtures/effect4/tx-queue.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as TxQueue from "effect/TxQueue" +Effect.runPromise((Effect.gen(function*(){const q=yield* TxQueue.unbounded();yield* TxQueue.offer(q,42);return yield* TxQueue.take(q)}).pipe(Effect.tx)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/tx-ref.ts b/tests/fixtures/effect4/tx-ref.ts new file mode 100644 index 000000000..82c12d52d --- /dev/null +++ b/tests/fixtures/effect4/tx-ref.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as TxRef from "effect/TxRef" +Effect.runPromise((Effect.gen(function*(){const r=yield* TxRef.make(40);yield* TxRef.update(r,n=>n+2);return yield* TxRef.get(r)}).pipe(Effect.tx)).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/tx-rollback.ts b/tests/fixtures/effect4/tx-rollback.ts new file mode 100644 index 000000000..517e8ada3 --- /dev/null +++ b/tests/fixtures/effect4/tx-rollback.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as TxRef from "effect/TxRef" +Effect.runPromise((Effect.gen(function* () { const r=yield* TxRef.make(1); yield* Effect.gen(function*(){yield* TxRef.set(r,2);return yield* Effect.fail("rollback")}).pipe(Effect.tx,Effect.catch(()=>Effect.void)); return yield* TxRef.get(r); })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/tx-semaphore.ts b/tests/fixtures/effect4/tx-semaphore.ts new file mode 100644 index 000000000..08098b479 --- /dev/null +++ b/tests/fixtures/effect4/tx-semaphore.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as TxSemaphore from "effect/TxSemaphore" +Effect.runPromise((Effect.gen(function*(){const s=yield* TxSemaphore.make(2);yield* TxSemaphore.acquire(s);const a=yield* TxSemaphore.available(s);yield* TxSemaphore.release(s);return [a,yield* TxSemaphore.available(s)]}).pipe(Effect.tx)).pipe(Effect.tap(v=>Effect.sync(()=>console.log(JSON.stringify(v)))))) diff --git a/tests/fixtures/effect4/workflow-memory.ts b/tests/fixtures/effect4/workflow-memory.ts new file mode 100644 index 000000000..cffe95944 --- /dev/null +++ b/tests/fixtures/effect4/workflow-memory.ts @@ -0,0 +1,6 @@ +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import * as Workflow from "effect/workflow/Workflow" +import * as WorkflowEngine from "effect/workflow/WorkflowEngine" +import * as Layer from "effect/Layer" +const W=Workflow.make("Hello",{payload:{name:Schema.String},success:Schema.String,idempotencyKey:p=>p.name}); const layer=W.toLayer(p=>Effect.succeed("Hello "+p.name)); Effect.runPromise((W.execute({name:"world"}).pipe(Effect.provide(layer),Effect.provide(WorkflowEngine.layerMemory))).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/yieldable-option.ts b/tests/fixtures/effect4/yieldable-option.ts new file mode 100644 index 000000000..19a407949 --- /dev/null +++ b/tests/fixtures/effect4/yieldable-option.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +Effect.runPromise((Effect.gen(function* () { const a=yield* Effect.fromOption(Option.some(40)); const b=yield* Effect.fromOption(Option.some(2)); return a+b; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/effect4/yieldable-result.ts b/tests/fixtures/effect4/yieldable-result.ts new file mode 100644 index 000000000..f7a7c000d --- /dev/null +++ b/tests/fixtures/effect4/yieldable-result.ts @@ -0,0 +1,3 @@ +import * as Effect from "effect/Effect" +import * as Result from "effect/Result" +Effect.runPromise((Effect.gen(function* () { const a=yield* Effect.fromResult(Result.succeed(42)); return a; })).pipe(Effect.tap(value => Effect.sync(() => console.log(JSON.stringify(value)))))) diff --git a/tests/fixtures/fetch/static-request/main.mts b/tests/fixtures/fetch/static-request/main.mts new file mode 100644 index 000000000..52c97db24 --- /dev/null +++ b/tests/fixtures/fetch/static-request/main.mts @@ -0,0 +1,26 @@ +const headers = new Headers({ "x-test": "one" }); +headers.append("x-test", "two"); +const copied = new Headers(headers); +console.log(copied.get("x-test"), copied === headers); +const controller = new AbortController(); +const request = new Request("http://localhost/path", { + method: "POST", headers: copied, body: "hello", redirect: "manual", signal: controller.signal, +}); +console.log(request.url, request.method, request.headers.get("x-test"), request.redirect); +console.log(request.destination, request.referrer, request.referrerPolicy, request.mode, request.credentials, request.cache, request.integrity); +console.log(request.keepalive, request.isReloadNavigation, request.isHistoryNavigation, request.duplex); +console.log(request.body !== null, request.bodyUsed, request.signal === controller.signal, request.signal.aborted); +controller.abort("stopped"); +console.log(request.signal.aborted, request.signal.reason); +console.log(await request.text(), request.bodyUsed); +try { await request.text(); } catch (error) { console.log((error as Error).name); } +const json = new Request("http://localhost/json", { method: "POST", body: '{"value":42}' }); +console.log(JSON.stringify(await json.json())); +const bytes = new Request("http://localhost/bytes", { method: "POST", body: new Uint8Array([65, 66]) }); +console.log(JSON.stringify(Array.from(await bytes.bytes()))); +const buffer = new Request("http://localhost/buffer", { method: "POST", body: "AB" }); +console.log(JSON.stringify(Array.from(new Uint8Array(await buffer.arrayBuffer())))); +for (const method of ["GET", "HEAD"]) { + try { new Request("http://localhost/invalid", { method, body: "no" }); } + catch (error) { console.log((error as Error).name); } +} diff --git a/tests/fixtures/node-types/child-stdin-fences.ts b/tests/fixtures/node-types/child-stdin-fences.ts index eeabaac6c..0859d8513 100644 --- a/tests/fixtures/node-types/child-stdin-fences.ts +++ b/tests/fixtures/node-types/child-stdin-fences.ts @@ -1,9 +1,5 @@ import { spawn } from "node:child_process"; -const options = { stdio: "pipe" as const, detached: true }; -spawn("node", options); -spawn("node", [], options); - const child = spawn("node", []); child.stdin.write("x", "utf8"); child.stdin.end("x"); diff --git a/tests/fixtures/npm-static/effect4-shape-match-metadata.ts b/tests/fixtures/npm-static/effect4-shape-match-metadata.ts new file mode 100644 index 000000000..ca65b9dfc --- /dev/null +++ b/tests/fixtures/npm-static/effect4-shape-match-metadata.ts @@ -0,0 +1,9 @@ +const match = "😀prefix/hello".match(/(\/hello)/); +if (match) console.log(match.index, match.input, JSON.stringify(match)); +const exec = /(world)/.exec("hello world"); +if (exec) console.log(exec.index, exec.input, exec[1]); +console.log("no match".match(/missing/) === null); +const owner = new ArrayBuffer(16); +const view = new Uint8Array(owner, 4, 8); +view[0] = 42; +console.log(view instanceof Uint8Array, new DataView(owner) instanceof Uint8Array, new Uint8Array(owner)[4]); diff --git a/tests/fixtures/npm-static/effect4-shape-prototype-factory.ts b/tests/fixtures/npm-static/effect4-shape-prototype-factory.ts new file mode 100644 index 000000000..c0754ad2f --- /dev/null +++ b/tests/fixtures/npm-static/effect4-shape-prototype-factory.ts @@ -0,0 +1 @@ +import {make} from "effect4-shapes/prototype";console.log(make(42).value()) diff --git a/tests/fixtures/npm-static/effect4-shape-semantics.ts b/tests/fixtures/npm-static/effect4-shape-semantics.ts new file mode 100644 index 000000000..ccfc2718a --- /dev/null +++ b/tests/fixtures/npm-static/effect4-shape-semantics.ts @@ -0,0 +1,78 @@ +import * as Shapes from "effect4-shapes/semantics"; + +console.log(JSON.stringify(Shapes.openLocal((resume) => resume({ value: 42 })))); +console.log(JSON.stringify(Shapes.callbackResult(() => ({ value: 42 })))); +console.log(Shapes.callbackResult() === Symbol.for("shape/pending")); +console.log(Shapes.defaultObject(), Shapes.defaultObject({ value: 42 })); +console.log(JSON.stringify(Shapes.restArguments("first", "second"))); +console.log(JSON.stringify(Shapes.openRecord())); +console.log(JSON.stringify(Shapes.advance(new Shapes.State()))); +console.log(JSON.stringify(Shapes.copyProperties({ value: 42 }))); +class Missing extends Shapes.Tagged("Missing") {} +const missing = new Missing({ message: "gone", id: 1 }); +console.log(missing.name, missing._tag, missing.id, JSON.stringify(Object.keys(missing))); +console.log(JSON.stringify(Shapes.cached("value"))); +console.log(JSON.stringify(Shapes.cached(Symbol.for("value")))); + +class Nested extends Shapes.nestedFactory("a")("bc") {} +const nested = new Nested(39); +console.log(nested.value, nested.read()); +class Receiver { + value = 42; + child = 1; + method() { return this.value; } + add(...args: number[]) { return this.value + args[0]! + args[1]!; } +} +const key = Symbol.for("shape/key"); +console.log(JSON.stringify(Shapes.optionalReads(null, key))); +console.log(JSON.stringify(Shapes.optionalReads({ [key]: new Receiver() }, key))); +console.log(JSON.stringify(Shapes.missingProperties({ value: 42 }))); +console.log(JSON.stringify([Shapes.dynamicIndex([1, 2], 1)])); +console.log(JSON.stringify([Shapes.dynamicIndex([1, 2], "length")])); +console.log(JSON.stringify(Shapes.callableProperties())); +const cached = new Shapes.Cached(); +console.log(JSON.stringify(Shapes.readCached(cached))); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cached, "result"))); +console.log(JSON.stringify(Shapes.spreadMethod(new Receiver(), [1, 1]))); + +console.log(JSON.stringify(Shapes.templateProperties())); +console.log(JSON.stringify(Shapes.freezeValues())); +console.log(JSON.stringify(Shapes.prototypeRecord())); +console.log(JSON.stringify(Shapes.evolvingFields())); +const restKey = Symbol.for("rest/key"); +const restSource = { omitted: 1, included: 2, [restKey]: 3 }; +Object.defineProperty(restSource, "hidden", { value: 4 }); +const rest = Shapes.objectRest(restSource); +console.log(JSON.stringify(rest)); +console.log(JSON.stringify(Shapes.nativeSymbols(new Headers()))); +console.log(JSON.stringify(Shapes.uriDecoders("%2f%3F%23%20%E2%82%AC"))); +console.log(Shapes.clearStoredTimer()); + +console.log(JSON.stringify(Shapes.webConstructors())); +console.log(JSON.stringify(Shapes.stringPositions())); +console.log(Shapes.readDestructuredExports()); +const yes = new Shapes.ConditionalDerived(true); +const no = new Shapes.ConditionalDerived(false); +console.log(yes.value, yes.initialized, no.value, no.initialized); + +console.log(JSON.stringify(Shapes.collectionCopies())); + +const backing = new Uint8Array([0, 65, 66, 0]).buffer; +console.log(JSON.stringify(Shapes.decodeViews(new DataView(backing, 1, 2)))); +console.log(JSON.stringify(Shapes.decodeViews(new Uint16Array(backing, 0, 2)))); +console.log(JSON.stringify(Shapes.decodeViews(backing))); + +console.log(JSON.stringify(Shapes.mappedCollections())); + +console.log(JSON.stringify(Shapes.spreadPush())); +console.log(JSON.stringify(Shapes.regexReset())); + +console.log(JSON.stringify(Shapes.readOwnStatic())); +console.log(JSON.stringify(Shapes.arrayBufferBrand(backing))); +console.log(JSON.stringify(Shapes.arrayBufferBrand({}))); + +console.log(JSON.stringify(Shapes.surplusCallbacks())); +console.log(Shapes.ownStaticReceiver()); +console.log(Shapes.inheritedStaticReceiver(), Shapes.detachedStaticReceiver()); +console.log(JSON.stringify(Shapes.methodEvaluationOrder())); +console.log(JSON.stringify(Shapes.surplusNativeCallback())); diff --git a/tests/fixtures/npm-static/effect4-shape-string-constructor.ts b/tests/fixtures/npm-static/effect4-shape-string-constructor.ts new file mode 100644 index 000000000..5dadd7f8a --- /dev/null +++ b/tests/fixtures/npm-static/effect4-shape-string-constructor.ts @@ -0,0 +1,3 @@ +import { CauseLike } from "effect4-shapes/constructor"; +const value=new CauseLike(["Fail"]); +console.log(value.reasons.join(","),value["~effect/Cause"]); diff --git a/tests/fixtures/npm-static/node_modules/effect4-shapes/constructor.d.ts b/tests/fixtures/npm-static/node_modules/effect4-shapes/constructor.d.ts new file mode 100644 index 000000000..541bf5de0 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/effect4-shapes/constructor.d.ts @@ -0,0 +1,5 @@ +export declare class CauseLike { + readonly "~effect/Cause": "~effect/Cause"; + readonly reasons: ReadonlyArray; + constructor(reasons: ReadonlyArray); +} diff --git a/tests/fixtures/npm-static/node_modules/effect4-shapes/constructor.js b/tests/fixtures/npm-static/node_modules/effect4-shapes/constructor.js new file mode 100644 index 000000000..5f22eb036 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/effect4-shapes/constructor.js @@ -0,0 +1,8 @@ +// Same computed-string constructor assignment shape as Effect 4 internal/core.js CauseImpl. +const TypeId = "~effect/Cause"; +export class CauseLike { + constructor(reasons) { + this[TypeId] = TypeId; + this.reasons = reasons; + } +} diff --git a/tests/fixtures/npm-static/node_modules/effect4-shapes/package.json b/tests/fixtures/npm-static/node_modules/effect4-shapes/package.json new file mode 100644 index 000000000..5b0c9ee06 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/effect4-shapes/package.json @@ -0,0 +1,20 @@ +{ + "name": "effect4-shapes", + "version": "0.0.0", + "type": "module", + "sideEffects": false, + "exports": { + "./constructor": { + "types": "./constructor.d.ts", + "default": "./constructor.js" + }, + "./prototype": { + "types": "./prototype.d.ts", + "default": "./prototype.js" + }, + "./semantics": { + "types": "./semantics.d.ts", + "default": "./semantics.js" + } + } +} diff --git a/tests/fixtures/npm-static/node_modules/effect4-shapes/prototype.d.ts b/tests/fixtures/npm-static/node_modules/effect4-shapes/prototype.d.ts new file mode 100644 index 000000000..d74313c63 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/effect4-shapes/prototype.d.ts @@ -0,0 +1 @@ +export declare function make(value: number): {readonly "~effect/Effect": string;value(): number}; diff --git a/tests/fixtures/npm-static/node_modules/effect4-shapes/prototype.js b/tests/fixtures/npm-static/node_modules/effect4-shapes/prototype.js new file mode 100644 index 000000000..2abd43ece --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/effect4-shapes/prototype.js @@ -0,0 +1,13 @@ +// Reduced from Effect 4 internal/effect.js fromIteratorUnsafe/IteratorImpl. +export const make = function () { + const Proto = { + value() { return 42; } + }; + const IteratorImpl = function (initial) { + this.initial = initial; + }; + IteratorImpl.prototype = Proto; + return function (initial) { + return new IteratorImpl(initial); + }; +}(); diff --git a/tests/fixtures/npm-static/node_modules/effect4-shapes/semantics.d.ts b/tests/fixtures/npm-static/node_modules/effect4-shapes/semantics.d.ts new file mode 100644 index 000000000..9c04f79b8 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/effect4-shapes/semantics.d.ts @@ -0,0 +1,51 @@ +export declare function openLocal(register: (resume: (value: unknown) => void) => void): unknown; +export declare function callbackResult(register?: () => unknown): unknown; +export declare function defaultObject(options?: { value: number }): number; +export declare function restArguments(...values: unknown[]): unknown[]; +export declare function openRecord(): unknown[]; +export declare class State { state: { _tag: string; value?: number }; } +export declare function advance(value: State): unknown[]; +export declare function copyProperties(source: object): Record; +export declare const Tagged: (tag: string) => new (properties: { message: string; id: number }) => Error & { _tag: string; id: number }; +export declare function cached(key: PropertyKey): unknown[]; + +export declare const nestedFactory: (label: string) => (suffix: string) => new (value: number) => { value: number; read(): number }; +export declare function optionalReads(value: unknown, key: PropertyKey): unknown[]; +export declare function missingProperties(value: object): unknown[]; +export declare function dynamicIndex(value: unknown[], index: unknown): unknown; +export declare function callableProperties(): unknown[]; +export declare class Cached { calls: number; readonly result: { value: number }; } +export declare function readCached(value: Cached): unknown[]; +export declare function spreadMethod(value: { value: number; add(...args: number[]): number }, args: number[]): unknown; +export declare function templateProperties(): unknown; +export declare function freezeValues(): boolean[]; +export declare function prototypeRecord(): unknown; +export declare function objectRest(source: object): unknown; +export declare function evolvingFields(): unknown; +export declare function nativeSymbols(value: object): unknown; +export declare function uriDecoders(value: string): string[]; +export declare function clearStoredTimer(): string; + +export declare function webConstructors(): unknown; +export declare function stringPositions(): unknown; +export declare function readDestructuredExports(): number; +export declare class ConditionalDerived { constructor(flag: boolean); value: number; initialized: number; } +export declare class RepeatedSuper { constructor(); } + +export declare function collectionCopies(): unknown; + +export declare function decodeViews(input: unknown): string[]; + +export declare function mappedCollections(): unknown; + +export declare function spreadPush(): unknown; +export declare function regexReset(): boolean[]; + +export declare function readOwnStatic(): unknown; +export declare function arrayBufferBrand(value: unknown): boolean[]; +export function surplusCallbacks(): [number, string[]]; +export function ownStaticReceiver(): number; +export function inheritedStaticReceiver(): number; +export function detachedStaticReceiver(): boolean; +export function methodEvaluationOrder(): unknown; +export function surplusNativeCallback(): [number, number]; diff --git a/tests/fixtures/npm-static/node_modules/effect4-shapes/semantics.js b/tests/fixtures/npm-static/node_modules/effect4-shapes/semantics.js new file mode 100644 index 000000000..12a7808b5 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/effect4-shapes/semantics.js @@ -0,0 +1,248 @@ +export function openLocal(register) { + let result = false; + register((value) => { result = value; }); + return result; +} + +export function callbackResult(register) { + if (register) return register(); + return Symbol.for("shape/pending"); +} + +export function defaultObject(options = { value: 1 }) { + return options.value; +} + +export function restArguments(...values) { + const later = () => arguments[1]; + values[0] = "changed"; + return [values[0], arguments[0], arguments.length, later()]; +} + +export function openRecord() { + const value = { next: undefined, count: 0 }; + const alias = value; + alias.next = { value: 42 }; + value.count++; + return [alias.next.value, alias.count]; +} + +export class State { + state = { _tag: "Empty" }; +} + +export function advance(value) { + value.state = { _tag: "Ready", value: 42 }; + return [value.state._tag, value.state.value]; +} + +export function copyProperties(source) { + const target = {}; + for (const key of Reflect.ownKeys(source)) { + if (Object.prototype.propertyIsEnumerable.call(source, key)) target[key] = source[key]; + } + return target; +} + +export const Tagged = (tag) => { + class Base extends Error { + _tag = tag; + constructor(properties) { + super(properties.message); + Object.assign(this, properties); + } + } + Base.prototype.name = tag; + return Base; +}; + +export function cached(key) { + const values = {}; + let calls = 0; + const make = () => ++calls; + values[key] ??= make(); + values[key] ??= make(); + return [values[key], calls]; +} + + +export const nestedFactory = (label) => (suffix) => class { + static label = label + suffix; + constructor(value) { this.value = value; } + read() { return this.value + label.length + suffix.length; } +}; + +export function optionalReads(value, key) { + let calls = 0; + const select = () => { calls++; return key; }; + const first = value?.[select()]; + const tail = value?.[key].child; + const call = value?.[key].method?.(); + return [first, tail, call, calls]; +} + +export function missingProperties(value) { + const { missing, count = 7 } = value; + return [missing, count]; +} + +export function dynamicIndex(value, index) { return value[index]; } + +export function callableProperties() { + const fn = Object.assign(function () { return this.value; }, { + value: 42, + tools: { double: 2 }, + [Symbol.iterator]: function* () { return yield 1; } + }); + return [Object.keys(fn.tools), Reflect.apply(fn, fn, []), fn.call(fn), [...fn]]; +} + +export class Cached { + calls = 0; + save(key, value) { Object.defineProperty(this, key, { value }); return value; } + get result() { this.calls++; return this.save("result", { value: 42 }); } +} + +export function readCached(value) { return [value.result, value.result, value.calls]; } +export function spreadMethod(value, args) { return value.add(...args); } + +export function checkedTag(strings) { + return [strings[0], strings.raw[0], Object.isFrozen(strings), Object.isFrozen(strings.raw), + Object.getOwnPropertyDescriptor(strings, "raw"), strings]; +} +export function templateProperties() { + const first = () => checkedTag`line\n`; + const a = first(), b = first(); + return [a.slice(0, 5), a[5] === b[5], a[5] === checkedTag`line\n`[5]]; +} +export function freezeValues() { + const values = ["one", "two"]; + values.forEach(Object.freeze); + return values.map(value => Object.isFrozen(value)); +} +export function prototypeRecord() { + const record = { value: 42 }; + const identity = Object.setPrototypeOf(record, { read() { return this.value; } }); + return [identity === record, record.read(), Object.getPrototypeOf(record).read.call(record)]; +} + +export function objectRest(source) { + const { omitted, ...remaining } = source; + return [remaining, remaining[Symbol.for("rest/key")], Object.getPrototypeOf(remaining) === Object.prototype]; +} + +export function evolvingFields() { + const builder = { statements: [], bindings: [], next: 0 }; + builder.statements.push("hello"); + builder.bindings.push({ reference: "world" }); + return { statements: builder.statements, bindings: builder.bindings }; +} + +export function nativeSymbols(value) { + const key = Symbol.for("shape/native"); + const before = key in value; + value[key] = 42; + return [before, value[key], key in value, Object.getOwnPropertySymbols(value).includes(key)]; +} + +export function uriDecoders(value) { + return [decodeURI(value), decodeURIComponent(value)]; +} + +export function clearStoredTimer() { + const timers = new Map(); + timers.set("pending", setTimeout(() => console.log("unexpected"), 50)); + clearTimeout(timers.get("pending")); + clearTimeout({}); + return "cleared"; +} + +export function webConstructors() { + const headers = new globalThis.Headers({ "x-test": "yes" }); + const request = new globalThis.Request("http://localhost/hello", { headers }); + const response = new globalThis.Response("hello", { headers }); + const keys = new WeakMap(); + keys.set(request, "request"); keys.set(response, "response"); + return [request.url, headers.get("x-test"), response.status, request instanceof Request, response instanceof Response, headers instanceof Headers, ({}) instanceof Response, keys.get(request), keys.get(response)]; +} +export function stringPositions() { + const url = "http://localhost/hello"; + return [url.indexOf("/", url.indexOf("//") + 2), url.includes("http", 1), url.lastIndexOf("/", 6)]; +} +const exportedFactory = () => ({ left: () => 21, right: () => 21 }); +export const { left, right } = exportedFactory(); +export function readDestructuredExports() { return left() + right(); } +class ConditionalBase { + constructor(value) { this.value = value; } +} +export class ConditionalDerived extends ConditionalBase { + initialized = this.value + 1; + constructor(flag) { if (flag) super(1); else super(2); } +} +export class RepeatedSuper extends ConditionalBase { + constructor() { super(1); super(2); } +} + +export function collectionCopies() { + const original = new Set([1, 2]); + const copied = new Set(original); + copied.add(3); + const map = new Map([["one", 1], ["two", 2]]); + const copiedMap = new Map(map); + return [[...original], [...copied], [...copiedMap]]; +} + +export function decodeViews(input) { + const stored = new TextDecoder(); + return [new TextDecoder().decode(input), stored.decode(input)]; +} + +export function mappedCollections() { + return [Array.from(new Set([1, 2]), value => value.toString()), Array.from(new Map([["one", 1]]), ([key, value]) => key + value)]; +} + +export function spreadPush() { const routes = []; routes.push(...[1, 2]); return routes; } +export function regexReset() { const re = new globalThis.RegExp(/^[+-]?\d+$/); re.lastIndex = 0; return [re.test("42"), re.test("wrong")]; } + +export const staticFactory = () => class { static inherited = "base"; }; +export class OwnStatic extends staticFactory() { static transform = value => value + 1; } +export function readOwnStatic() { return [OwnStatic.inherited, OwnStatic.transform(41), [OwnStatic.transform].map(fn => fn(41))]; } +export function arrayBufferBrand(value) { const ctor = globalThis.ArrayBuffer; return [value instanceof ctor, ctor === ArrayBuffer]; } + +export function surplusCallbacks() { + const trace = []; + const object = { get callback() { trace.push("callee"); return () => 42; } }; + const result = object.callback((trace.push("argument"), 1)); + return [result, trace]; +} +export class StaticReceiver { + static value = 42; + static read = function () { return this.value; }; +} +export function ownStaticReceiver() { return StaticReceiver.read(); } +export class InheritedReceiver extends StaticReceiver { static value = 43; } +export function inheritedStaticReceiver() { return InheritedReceiver.read(); } +export function detachedStaticReceiver() { + const read = StaticReceiver.read; + try { return read(); } catch (error) { return error instanceof TypeError; } +} +export function methodEvaluationOrder() { + const trace = []; + const target = { callback: () => 1 }; + const old = target.callback((target.callback = () => 2, trace.push("argument"))); + const throwing = { get callback() { trace.push("getter"); throw new Error("stop"); } }; + try { throwing.callback(trace.push("unexpected")); } catch { trace.push("caught"); } + const noncallable = { get callback() { trace.push("noncallable"); return undefined; } }; + try { noncallable.callback(trace.push("evaluated")); } catch { trace.push("type-error"); } + const absent = {}; + try { absent.callback((absent.callback = () => 3, trace.push("added"))); } catch { trace.push("still-absent"); } + const missing = null; + try { missing.callback(trace.push("unexpected-null")); } catch { trace.push("null-error"); } + return [old, target.callback(), trace]; +} +export function surplusNativeCallback() { + let counter = 0; + const options = { callback: () => 42 }; + const result = options.callback(++counter); + return [result, counter]; +} diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/braced/a.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/braced/a.js new file mode 100644 index 000000000..f3291186e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/braced/a.js @@ -0,0 +1,2 @@ +console.log("braced"); +export const ready = true; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/bridge.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/bridge.js new file mode 100644 index 000000000..cb0c483a3 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/bridge.js @@ -0,0 +1,2 @@ +import "./effects/start.js"; +export const ready = true; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/effects/start.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/effects/start.js new file mode 100644 index 000000000..2d9e970c2 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/effects/start.js @@ -0,0 +1,2 @@ +console.log("startup"); +export const ready = true; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/feature.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/feature.js new file mode 100644 index 000000000..46d3ca8c6 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/feature.js @@ -0,0 +1 @@ +export const value = 42; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/index.d.ts b/tests/fixtures/npm-static/node_modules/selectivebarrel/index.d.ts new file mode 100644 index 000000000..cf017ddf9 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/index.d.ts @@ -0,0 +1 @@ +export * as Feature from "./feature.js"; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/index.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/index.js new file mode 100644 index 000000000..9ae35a1a7 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/index.js @@ -0,0 +1,7 @@ +export * as Feature from "./feature.js"; +export * as Spare from "./spare.js"; +export * as Startup from "./effects/start.js"; +export * as Bridge from "./bridge.js"; +export * as Loop from "./loop-a.js"; +export * as Init from "./nested/custom.init.js"; +export * as Braced from "./braced/a.js"; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/loop-a.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/loop-a.js new file mode 100644 index 000000000..4cfe5f97f --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/loop-a.js @@ -0,0 +1,2 @@ +import "./loop-b.js"; +export const a = 1; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/loop-b.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/loop-b.js new file mode 100644 index 000000000..e57c29eb2 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/loop-b.js @@ -0,0 +1,3 @@ +import "./loop-a.js"; +import "./effects/start.js"; +export const b = 2; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/nested/custom.init.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/nested/custom.init.js new file mode 100644 index 000000000..8a513c1b7 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/nested/custom.init.js @@ -0,0 +1,2 @@ +console.log("initializer"); +export const ready = true; diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/package.json b/tests/fixtures/npm-static/node_modules/selectivebarrel/package.json new file mode 100644 index 000000000..50ed46912 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/package.json @@ -0,0 +1,15 @@ +{ + "name": "selectivebarrel", + "type": "module", + "sideEffects": [ + "./effects/**/*.js", + "*.init.js", + "./braced/{a,b}.js" + ], + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js" + } + } +} diff --git a/tests/fixtures/npm-static/node_modules/selectivebarrel/spare.js b/tests/fixtures/npm-static/node_modules/selectivebarrel/spare.js new file mode 100644 index 000000000..7c1e0ae70 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/selectivebarrel/spare.js @@ -0,0 +1 @@ +export * from "picocolors"; diff --git a/tests/fixtures/npm-static/selectivebarrel-cli.ts b/tests/fixtures/npm-static/selectivebarrel-cli.ts new file mode 100644 index 000000000..37c24f480 --- /dev/null +++ b/tests/fixtures/npm-static/selectivebarrel-cli.ts @@ -0,0 +1,2 @@ +import { Feature } from "selectivebarrel"; +console.log(Feature.value); diff --git a/tests/harness/__snapshots__/any.ts.txt b/tests/harness/__snapshots__/any.ts.txt index 55fd8a755..27e24a7bf 100644 --- a/tests/harness/__snapshots__/any.ts.txt +++ b/tests/harness/__snapshots__/any.ts.txt @@ -3,15 +3,6 @@ any.ts:8:5 - error SC2011: destructuring on 'any'-typed values runs in the embed 7 | const sum = n + 1; // ToNumber/ToPrimitive coercion — engine only 8 | var { x } = 0; // destructuring a non-object 'any' — engine only | ^~~~~~~~~~~~~~ - 9 | for (const v of n as any) console.log(v); // iterating an 'any' value - - hint: build with --dynamic to run 'any'-typed code in the embedded engine (adds ~620KB to the binary), or stay static with 'unknown' and a checked cast ('x as T') - -any.ts:9:22 - error SC2011: values of type 'any' run in the embedded dynamic engine, which this build does not include - - 8 | var { x } = 0; // destructuring a non-object 'any' — engine only - 9 | for (const v of n as any) console.log(v); // iterating an 'any' value - | ^~~ - 10 | export const marker: number = 1; + 9 | for (const v of n as any) console.log(v); // native checked iteration reports non-iterables at runtime hint: build with --dynamic to run 'any'-typed code in the embedded engine (adds ~620KB to the binary), or stay static with 'unknown' and a checked cast ('x as T') \ No newline at end of file diff --git a/tests/harness/__snapshots__/child-process.ts.txt b/tests/harness/__snapshots__/child-process.ts.txt index b43b96bf1..2d83132f5 100644 --- a/tests/harness/__snapshots__/child-process.ts.txt +++ b/tests/harness/__snapshots__/child-process.ts.txt @@ -1,12 +1,3 @@ -child-process.ts:19:19 - error SC2020: 'spawn with a non-literal options argument' is part of the standard library types but has no scriptc lowering yet - - 18 | const options: { stdio: "ignore"; detached: boolean } = { stdio: "ignore", detached: true }; - 19 | spawn("true", [], options); - | ^~~~~~~ - 20 | spawn("printf", ["unsafe"], { stdio: "ignore", shell: true }); - - hint: pass stdio, detached, env, cwd, windowsHide, and shell in an inline object literal - child-process.ts:20:17 - error SC2020: 'spawn with shell enabled and separate arguments' is part of the standard library types but has no scriptc lowering yet 19 | spawn("true", [], options); @@ -23,7 +14,7 @@ child-process.ts:27:14 - error SC1090: listeners returning a value (make the cal | ^~~~~~~~~~~~~~~ 28 | // Methods have no bound-value form — call on directly. -child-process.ts:29:7 - error SC2007: values of type '{ (event: "exit", listener: (code: number | null, signal: string | null) => void): void; (event: "close", listener: (code: number | null, signal: string | null) => void): void; (event: "error", listener: (err: Error) => void): void; (event: "message", listener: (message: any) => void): void; (event: "disconnect", li...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature +child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): void; (event: "exit", listener: (code: number | null, signal: string | null) => void): void; (event: "close", listener: (code: number | null, signal: string | null) => void): void; (event: "error", listener: (err: Error) => void): void; (event: "message", listener: (message:...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature 28 | // Methods have no bound-value form — call on directly. 29 | const f = c.on; diff --git a/tests/harness/__snapshots__/create-require.ts.txt b/tests/harness/__snapshots__/create-require.ts.txt index c39b19e42..a6d156b3f 100644 --- a/tests/harness/__snapshots__/create-require.ts.txt +++ b/tests/harness/__snapshots__/create-require.ts.txt @@ -1,12 +1,3 @@ -create-require.ts:13:7 - error SC2011: values of type 'ScriptcRequire' have no static representation but run in the embedded dynamic engine, which this build does not include - - 12 | // The base names some OTHER file: unrecognized — the member fence. - 13 | const req = createRequire("/tmp/parent.js"); - | ^~~ - 14 | console.log(String(req("./config.cjs"))); - - hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary) - create-require.ts:13:13 - error SC2020: 'module.createRequire' is part of the standard library types but has no scriptc lowering yet 12 | // The base names some OTHER file: unrecognized — the member fence. @@ -16,15 +7,6 @@ create-require.ts:13:13 - error SC2020: 'module.createRequire' is part of the st hint: the lowered shape is a const binding (or a proven stable top-level let/var) over createRequire(import.meta.url) (or __filename) whose require calls take STATIC string literals — builtins, project modules (including #imports), relative .json documents, --npm-static packages, and installed npm packages under --dynamic resolve at build time; dynamic specifiers cannot exist in a compiled binary's fixed module graph -create-require.ts:14:20 - error SC2004: uses of 'req' inherit the blocker on its declaration - - 13 | const req = createRequire("/tmp/parent.js"); - 14 | console.log(String(req("./config.cjs"))); - | ^~~ - 15 | // The supported base still fences dynamic requests and unresolved modules. - - hint: the declaration of 'req' did not compile — fix the diagnostic reported there and these sites clear with it - create-require.ts:20:1 - error SC2020: 'createRequire's require with this argument shape' is part of the standard library types but has no scriptc lowering yet 19 | const which = "left" + "-pad"; diff --git a/tests/harness/__snapshots__/dynamic-representable-types.ts.txt b/tests/harness/__snapshots__/dynamic-representable-types.ts.txt index 6e4884a2b..b7606cf79 100644 --- a/tests/harness/__snapshots__/dynamic-representable-types.ts.txt +++ b/tests/harness/__snapshots__/dynamic-representable-types.ts.txt @@ -1,35 +1,17 @@ -dynamic-representable-types.ts:9:7 - error SC2011: values of type 'any[]' have no static representation but run in the embedded dynamic engine, which this build does not include +dynamic-representable-types.ts:7:7 - error SC2011: values of type '[number, (string | undefined)?]' have no static representation but run in the embedded dynamic engine, which this build does not include - 8 | * (an optional-element tuple) keep the SC2001 recitation. */ - 9 | const arr: any[] = [1, "x"]; - | ^~~ - 10 | var evolving = []; // never used — the tsc-clean evolving-array shape + 6 | const bare: any = 41; + 7 | const optTuple: [number, string?] = [1]; + | ^~~~~~~~ + 8 | console.log(arr.length, rec.width, fn(1), bare, optTuple.length); hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary) -dynamic-representable-types.ts:10:5 - error SC2011: values of type 'any[]' have no static representation but run in the embedded dynamic engine, which this build does not include +dynamic-representable-types.ts:8:49 - error SC2004: uses of 'optTuple' inherit the blocker on its declaration - 9 | const arr: any[] = [1, "x"]; - 10 | var evolving = []; // never used — the tsc-clean evolving-array shape - | ^~~~~~~~ - 11 | const rec = { workItem: {} as any, width: "10px" }; - - hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary) - -dynamic-representable-types.ts:14:7 - error SC2011: values of type '[number, (string | undefined)?]' have no static representation but run in the embedded dynamic engine, which this build does not include - - 13 | const bare: any = 41; - 14 | const optTuple: [number, string?] = [1]; - | ^~~~~~~~ - 15 | console.log(arr.length, rec.width, fn(1), bare, optTuple.length); - - hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary) - -dynamic-representable-types.ts:15:49 - error SC2004: uses of 'optTuple' inherit the blocker on its declaration - - 14 | const optTuple: [number, string?] = [1]; - 15 | console.log(arr.length, rec.width, fn(1), bare, optTuple.length); - | ^~~~~~~~~~~~~~~ - 16 | export const marker: number = 1; + 7 | const optTuple: [number, string?] = [1]; + 8 | console.log(arr.length, rec.width, fn(1), bare, optTuple.length); + | ^~~~~~~~~~~~~~~ + 9 | export const marker: number = 1; hint: the declaration of 'optTuple' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/__snapshots__/fetch-static.ts.txt b/tests/harness/__snapshots__/fetch-static.ts.txt index 0570d39c5..1d397170a 100644 --- a/tests/harness/__snapshots__/fetch-static.ts.txt +++ b/tests/harness/__snapshots__/fetch-static.ts.txt @@ -5,13 +5,4 @@ fetch-static.ts:27:23 - error SC2020: 'Response.arrayBuffer in a static build' i | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | } - hint: the native static Response surface is status/ok/statusText/url/redirected/headers/body/bodyUsed plus json(), text(), bytes(), and arrayBuffer(); use --dynamic for the wider Web API - -fetch-static.ts:30:17 - error SC2020: 'new Headers' is part of the standard library types but has no scriptc lowering yet - - 29 | bracketArrayBufferRead("http://localhost/e"); - 30 | const headers = new Headers(); - | ^~~~~~~~~~~~~ - 31 | // The Headers constructor remains fenced even though response.headers is native. - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file + hint: the native static Response surface is status/ok/statusText/url/redirected/headers/body/bodyUsed plus json(), text(), bytes(), and arrayBuffer(); use --dynamic for the wider Web API \ No newline at end of file diff --git a/tests/harness/__snapshots__/generics.ts.txt b/tests/harness/__snapshots__/generics.ts.txt index 4c21bfb88..2be6e8ae5 100644 --- a/tests/harness/__snapshots__/generics.ts.txt +++ b/tests/harness/__snapshots__/generics.ts.txt @@ -45,11 +45,4 @@ generics.ts:66:1 - error SC1090: constructing through a class value whose class 65 | new Box(1); 66 | new Chained(2); | ^~~~~~~~~~~~~~ - 67 | new ExprFamily(); - -generics.ts:67:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet - - 66 | new Chained(2); - 67 | new ExprFamily(); - | ^~~~~~~~~~~~~~~~~~~~~~~~ - 68 | \ No newline at end of file + 67 | new ExprFamily(); \ No newline at end of file diff --git a/tests/harness/__snapshots__/inheritance.ts.txt b/tests/harness/__snapshots__/inheritance.ts.txt index 7bdfb5287..2264a4600 100644 --- a/tests/harness/__snapshots__/inheritance.ts.txt +++ b/tests/harness/__snapshots__/inheritance.ts.txt @@ -33,34 +33,6 @@ inheritance.ts:55:3 - error SC1090: overriding a method with a different signatu | ^~~~ 56 | return other.name; -inheritance.ts:62:3 - error SC1090: super() calls anywhere but as a top-level constructor statement is not supported yet - - 61 | class Conditional extends Animal { - 62 | constructor(flag: boolean) { - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 63 | if (flag) { - -inheritance.ts:64:7 - error SC1090: super() calls anywhere but as a top-level constructor statement is not supported yet - - 63 | if (flag) { - 64 | super(); - | ^~~~~~~ - 65 | } else { - -inheritance.ts:66:7 - error SC1090: super() calls anywhere but as a top-level constructor statement is not supported yet - - 65 | } else { - 66 | super(); - | ^~~~~~~ - 67 | } - -inheritance.ts:73:13 - error SC1090: 'instanceof' on values other than class instances (narrow union-typed values first) are not supported yet - - 72 | const rec = { name: "r" }; - 73 | console.log(rec instanceof Animal); - | ^~~~~~~~~~~~~~~~~~~~~ - 74 | - inheritance.ts:80:13 - error SC1090: statically-decided 'instanceof' on computed operands (bind the value to a variable first) are not supported yet 79 | } diff --git a/tests/harness/__snapshots__/maps-surface.ts.txt b/tests/harness/__snapshots__/maps-surface.ts.txt index e0d2aebe8..3362ab155 100644 --- a/tests/harness/__snapshots__/maps-surface.ts.txt +++ b/tests/harness/__snapshots__/maps-surface.ts.txt @@ -25,15 +25,6 @@ maps-surface.ts:15:12 - error SC2020: '.entries() outside an immediate array spr hint: iterator objects have no lowering — drain it into an array where it is made: [...m.entries()] -maps-surface.ts:23:1 - error SC2020: 'chained Map method calls' is part of the standard library types but has no scriptc lowering yet - - 22 | // set() is declared void (not the JS `this`), so chaining is a type error. - 23 | m.set("a", 1).set("b", 2); - | ^~~~~~~~~~~~~ - 24 | - - hint: the lowered set() produces no value — call each set(k, v) as its own statement - maps-surface.ts:26:11 - error SC2011: values of type '(v: number, k: string, theMap: Map) => void' have no static representation but run in the embedded dynamic engine, which this build does not include 25 | // The forEach callback receives (value, key) — no third map parameter. diff --git a/tests/harness/__snapshots__/regex.ts.txt b/tests/harness/__snapshots__/regex.ts.txt index 0c409dcec..2db17d9c8 100644 --- a/tests/harness/__snapshots__/regex.ts.txt +++ b/tests/harness/__snapshots__/regex.ts.txt @@ -1,51 +1,33 @@ -regex.ts:7:11 - error SC1121: '.test()' on a regex with the 'g' or 'y' flag is not supported yet +regex.ts:4:17 - error SC1120: the regex 'd' flag (match indices) is not supported yet - 6 | // compile — corpus 2448). - 7 | const g = /ab/g.test("abab"); - | ^~~~~~~~~~~~~~~~~~ - 8 | const y = /ab/y.test("abab"); - - hint: g/y regexes carry mutable lastIndex state between calls, which is not modeled; drop the flag for a plain match test, or use replace/replaceAll/split (their iteration is internal) - -regex.ts:8:11 - error SC1121: '.test()' on a regex with the 'g' or 'y' flag is not supported yet - - 7 | const g = /ab/g.test("abab"); - 8 | const y = /ab/y.test("abab"); - | ^~~~~~~~~~~~~~~~~~ - 9 | const indices = /cat/d; - - hint: g/y regexes carry mutable lastIndex state between calls, which is not modeled; drop the flag for a plain match test, or use replace/replaceAll/split (their iteration is internal) - -regex.ts:9:17 - error SC1120: the regex 'd' flag (match indices) is not supported yet - - 8 | const y = /ab/y.test("abab"); - 9 | const indices = /cat/d; - | ^~~~~~ - 10 | const sets = /[\p{L}]/v; + 3 | // Method values and regex union arms remain unsupported; arrays compile. + 4 | const indices = /cat/d; + | ^~~~~~ + 5 | const sets = /[\p{L}]/v; hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \k), .source/.flags, and string replace/replaceAll/split with string replacement templates -regex.ts:10:14 - error SC1120: the regex 'v' flag (unicode sets) is not supported yet +regex.ts:5:14 - error SC1120: the regex 'v' flag (unicode sets) is not supported yet - 9 | const indices = /cat/d; - 10 | const sets = /[\p{L}]/v; - | ^~~~~~~~~~ - 11 | const asValue = /x/.test; + 4 | const indices = /cat/d; + 5 | const sets = /[\p{L}]/v; + | ^~~~~~~~~~ + 6 | const asValue = /x/.test; hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \k), .source/.flags, and string replace/replaceAll/split with string replacement templates -regex.ts:11:17 - error SC1090: regex methods as values (call 'test' directly) are not supported yet +regex.ts:6:17 - error SC1090: regex methods as values (call 'test' directly) are not supported yet - 10 | const sets = /[\p{L}]/v; - 11 | const asValue = /x/.test; - | ^~~~~~~~ - 12 | const maybe: RegExp | undefined = /a/; + 5 | const sets = /[\p{L}]/v; + 6 | const asValue = /x/.test; + | ^~~~~~~~ + 7 | const maybe: RegExp | undefined = /a/; -regex.ts:15:22 - error SC2020: 'RegExpExecArray.groups' is part of the standard library types but has no scriptc lowering yet +regex.ts:10:22 - error SC2020: 'RegExpExecArray.groups' is part of the standard library types but has no scriptc lowering yet - 14 | const m = re.exec("2024-07"); - 15 | if (m) console.log(m.groups); + 9 | const m = re.exec("2024-07"); + 10 | if (m) console.log(m.groups); | ^~~~~~~~ - 16 | } + 11 | } hint: '.groups' lowers when the match's regex is STATICALLY known — a regex literal, or a const initialized with one (the group-name table is built at compile time) — and the read is not an optional-chain step: narrow the match instead (if (m) { m.groups } or m!.groups) \ No newline at end of file diff --git a/tests/harness/__snapshots__/text-codec-value.ts.txt b/tests/harness/__snapshots__/text-codec-value.ts.txt index e64d6996b..2d4c0f2e2 100644 --- a/tests/harness/__snapshots__/text-codec-value.ts.txt +++ b/tests/harness/__snapshots__/text-codec-value.ts.txt @@ -5,59 +5,41 @@ text-codec-value.ts:2:13 - error SC1090: console.log of 'TextEncoder' values (Te | ^~~~~~~ 3 | -text-codec-value.ts:9:1 - error SC2020: 'decode with a stream option' is part of the standard library types but has no scriptc lowering yet +text-codec-value.ts:10:1 - error SC2020: 'TextEncoder.encodeInto' is part of the standard library types but has no scriptc lowering yet - 8 | const decoder = new TextDecoder(); - 9 | decoder.decode(new Uint8Array([0xc3]), { stream: true }); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 10 | new TextDecoder("utf-8", { fatal: true }); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -text-codec-value.ts:10:1 - error SC2020: 'new TextDecoder with runtime-valued options or an unknown label' is part of the standard library types but has no scriptc lowering yet - - 9 | decoder.decode(new Uint8Array([0xc3]), { stream: true }); - 10 | new TextDecoder("utf-8", { fatal: true }); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 11 | encoder.encodeInto("hello", new Uint8Array(10)); - - hint: a recognized literal WHATWG label with default options compiles - -text-codec-value.ts:11:1 - error SC2020: 'TextEncoder.encodeInto' is part of the standard library types but has no scriptc lowering yet - - 10 | new TextDecoder("utf-8", { fatal: true }); - 11 | encoder.encodeInto("hello", new Uint8Array(10)); + 9 | new TextDecoder("utf-8", { fatal: true }); + 10 | encoder.encodeInto("hello", new Uint8Array(10)); | ^~~~~~~~~~~~~~~~~~ - 12 | const forged = JSON.parse("{}") as TextDecoder; + 11 | const forged = JSON.parse("{}") as TextDecoder; hint: TextEncoder instances can be stored, passed, and captured; encode(string) and encode() compile, while encodeInto and other members have no static lowering -text-codec-value.ts:18:5 - error SC2020: 'uninitialized lexical codec bindings' is part of the standard library types but has no scriptc lowering yet +text-codec-value.ts:17:5 - error SC2020: 'uninitialized lexical codec bindings' is part of the standard library types but has no scriptc lowering yet - 17 | var varEncoder = new TextEncoder(); - 18 | let uninitializedDecoder: TextDecoder; + 16 | var varEncoder = new TextEncoder(); + 17 | let uninitializedDecoder: TextDecoder; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 19 | function earlyVarCapture(): Uint8Array { + 18 | function earlyVarCapture(): Uint8Array { hint: initialize let or const TextEncoder/TextDecoder bindings at their declaration -text-codec-value.ts:20:21 - error SC1030: the reference to 'localEncoder' above its 'var' declaration (a read there would be 'undefined', which the binding's type cannot hold — annotate it '| undefined' or move the declaration up) is not supported yet +text-codec-value.ts:19:21 - error SC1030: the reference to 'localEncoder' above its 'var' declaration (a read there would be 'undefined', which the binding's type cannot hold — annotate it '| undefined' or move the declaration up) is not supported yet - 19 | function earlyVarCapture(): Uint8Array { - 20 | const use = () => localEncoder.encode("x"); + 18 | function earlyVarCapture(): Uint8Array { + 19 | const use = () => localEncoder.encode("x"); | ^~~~~~~~~~~~ - 21 | const bytes = use(); + 20 | const bytes = use(); -text-codec-value.ts:34:15 - error SC1090: the reference to 'caseEncoder' (a binding form with no lowering) is not supported yet +text-codec-value.ts:33:15 - error SC1090: the reference to 'caseEncoder' (a binding form with no lowering) is not supported yet - 33 | const Encoded = class { - 34 | bytes = caseEncoder.encode("should fence"); + 32 | const Encoded = class { + 33 | bytes = caseEncoder.encode("should fence"); | ^~~~~~~~~~~ - 35 | }; + 34 | }; -text-codec-value.ts:40:17 - error SC1090: codec bindings read from another switch clause before initialization is proven is not supported yet +text-codec-value.ts:39:17 - error SC1090: codec bindings read from another switch clause before initialization is proven is not supported yet - 39 | // @ts-expect-error -- dispatch can enter here while caseEncoder is in its TDZ - 40 | console.log(caseEncoder.encode("should fence directly").length); + 38 | // @ts-expect-error -- dispatch can enter here while caseEncoder is in its TDZ + 39 | console.log(caseEncoder.encode("should fence directly").length); | ^~~~~~~~~~~ - 41 | break; \ No newline at end of file + 40 | break; \ No newline at end of file diff --git a/tests/harness/__snapshots__/text-decoder-label.ts.txt b/tests/harness/__snapshots__/text-decoder-label.ts.txt index f7ac2a42b..27183345f 100644 --- a/tests/harness/__snapshots__/text-decoder-label.ts.txt +++ b/tests/harness/__snapshots__/text-decoder-label.ts.txt @@ -1,30 +1,3 @@ -text-decoder-label.ts:4:13 - error SC2020: 'new TextDecoder with runtime-valued options or an unknown label' is part of the standard library types but has no scriptc lowering yet - - 3 | const runtimeLabel: string = process.argv[2] ?? "utf-8"; - 4 | console.log(new TextDecoder(runtimeLabel).decode(bytes)); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 5 | - - hint: a recognized literal WHATWG label with default options compiles - -text-decoder-label.ts:7:13 - error SC2020: 'new TextDecoder with runtime-valued options or an unknown label' is part of the standard library types but has no scriptc lowering yet - - 6 | // WHATWG's replacement labels deliberately fail TextDecoder construction. - 7 | console.log(new TextDecoder("replacement").decode(bytes)); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 8 | - - hint: a recognized literal WHATWG label with default options compiles - -text-decoder-label.ts:10:13 - error SC2020: 'new TextDecoder with runtime-valued options or an unknown label' is part of the standard library types but has no scriptc lowering yet - - 9 | // Vertical tab is not WHATWG ASCII whitespace and must not be trimmed. - 10 | console.log(new TextDecoder("\vutf-8").decode(bytes)); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~ - 11 | - - hint: a recognized literal WHATWG label with default options compiles - text-decoder-label.ts:16:31 - error SC1090: the reference to 'forwardLabel' (a binding form with no lowering) is not supported yet 15 | // @ts-expect-error Deliberately retain the runtime TDZ shape. diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 63659204a..895c0969c 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -120,11 +120,12 @@ test("JavaScript builtin aliases keep their deferred value fence", async () => { ); }); -test("any-typed checked-dynamic locals honor the --dynamic coverage promise", () => { +test("any-typed checked-dynamic operators compile in both tiers", () => { const file = join(repoRoot, "tests/corpus/2856-dynamic-any-local-operators.ts"); const staticCoverage = analyze(file).coverage; - expect(staticCoverage.diagnostics.length).toBeGreaterThan(0); - expect(new Set(staticCoverage.diagnostics.map((d) => d.code))).toEqual(new Set(["SC2011"])); + expect(staticCoverage.diagnostics).toEqual([]); + expect(staticCoverage.stats.statementsFailed).toBe(0); + expect(staticCoverage.stats.statementsIsland).toBe(0); const dynamicCoverage = analyze(file, { dynamic: true }).coverage; expect(dynamicCoverage.diagnostics).toEqual([]); diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index 2ce301ba3..e55103b25 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -85,6 +85,25 @@ describe(`dynamic-boundary checks (scriptc-only${sanitize ? ", sanitized" : ""}) expect(r.stderr).toBe(""); }); + test("typed Promise exits validate fulfillment and preserve rejection", async () => { + const result = await compileAndRun("promise-exit-payload", ` + async function main() { + const value: unknown = Promise.resolve("wrong"); + try { await (value as Promise); } + catch (error) { console.log(error instanceof TypeError, (error as Error).message); } + const rejected: unknown = Promise.reject(new Error("original")); + try { await (rejected as Promise); } + catch (error) { console.log((error as Error).message); } + } + main(); + `); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe("true expected number at $, got string\noriginal\n"); + expect(result.stderr.split("\n").filter((line) => + !sanitize || !/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext/.test(line), + ).join("\n")).toBe(""); + }); + test("wrong-typed field throws with the path", async () => { const r = await compileAndRun( "wrong-type", @@ -612,9 +631,8 @@ console.log("recovered"); expect(r.stdout).toBe("assigned 2\nnever: boom\nrecovered\n"); }); - test("a NAMED record into an `any` slot is a deep copy — dyn writes never alias the original", async () => { - // dynFrom's aliasing stance (documented for 'unknown' slots) applies - // to any-typed storage identically: JS would alias, the dyn copies. + test("a named record retains identity and mutations through an `any` slot", async () => { + // Checked storage keeps the live native record shared with its source. const r = await compileAndRun( "any-record-copy", `const base = { a: 1 }; @@ -624,7 +642,7 @@ console.log(base.a, boxed.a); `, ); expect(r.exitCode).toBe(0); - expect(r.stdout).toBe("1 2\n"); + expect(r.stdout).toBe("2 2\n"); }); test("an uninitialized `any` binding is the dyn undefined, not a trap", async () => { diff --git a/tests/harness/effect4.test.ts b/tests/harness/effect4.test.ts new file mode 100644 index 000000000..f6c5fe87f --- /dev/null +++ b/tests/harness/effect4.test.ts @@ -0,0 +1,57 @@ +import { execFile } from "node:child_process"; +import { globSync } from "node:fs"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { basename, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import type { CompileResult } from "@scriptc/compiler"; +import { shardSelect } from "./shard.js"; + +const exec = promisify(execFile); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; +const fixtures = globSync(join(import.meta.dirname, "../fixtures/effect4/*.ts")).filter((file) => !file.endsWith(".d.ts")).sort(); +const cases = shardSelect(fixtures, (file) => basename(file)); + +async function run(command: string, args: string[]) { + try { + const { stdout, stderr } = await exec(command, args, { encoding: "utf8", timeout: 60_000 }); + return { stdout, stderr, status: 0 }; + } catch (error) { + const result = error as { code?: unknown; stdout?: string; stderr?: string }; + if (typeof result.code !== "number") throw error; + return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", status: result.code }; + } +} + +test.for(cases)("published Effect 4 %s matches Node statically", async (entry) => { + const dir = await mkdtemp("/tmp/scriptc-effect4-"); + try { + const reference = await run(process.execPath, ["--no-warnings", entry]); + expect(reference.status).toBe(0); + expect(reference.stdout.trim().length).toBeGreaterThan(0); + const options = { + outDir: dir, outPath: join(dir, "program"), backend: "llvm", optimization: "dev", dynamic: false, + npmStatic: ["effect", "@effect/platform-node", "@effect/platform-node-shared", "undici"], sanitize, + }; + // Published cluster graphs need more heap than Node's default. Compile + // each fixture in an isolated process so successive graphs release all + // checker state and the worker's memory stays bounded. + const source = new URL("../../packages/compiler/src/index.ts", import.meta.url).href; + const child = `import { compile } from ${JSON.stringify(source)}; + import { writeFileSync } from "node:fs"; + const result = await compile(process.argv[1], JSON.parse(process.argv[2])); + writeFileSync(process.argv[3], JSON.stringify(result));`; + const resultPath = join(dir, "compile.json"); + await exec(process.execPath, ["--max-old-space-size=8192", "--import", "tsx", "--input-type=module", "--eval", child, entry, JSON.stringify(options), resultPath], { timeout: 540_000 }); + const result = JSON.parse(await readFile(resultPath, "utf8")) as CompileResult; + if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const actual = await run(result.binaryPath, []); + if (sanitize) actual.stderr = actual.stderr.split("\n").filter((line) => + !line.startsWith("scriptc RC audit skipped:") && + !/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext/.test(line), + ).join("\n"); + expect(actual).toEqual(reference); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 600_000); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c0e468c58..27d4e7ab5 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -375,7 +375,7 @@ subtle.digest('SHA-256'); expect(r.exitCode).toBe(1); expect(r.stdout).toBe("bound true\n"); expect(r.stderr).toMatch( - /^Uncaught Error: method calls like 'subtle\.digest' is not supported yet \[SC1090 at .*destructure-opaque-global\.cjs:4\]\n$/, + /^Uncaught TypeError: subtle\.digest is not a function\n$/, ); }); diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index a731d91fc..ed0cc4d6c 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -188,6 +188,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { const staticCases = [ "static", + "static-request", "static-coercion", "static-controller", "static-stream", diff --git a/tests/harness/native-weak-collections.test.ts b/tests/harness/native-weak-collections.test.ts index 4dc85d6e9..8fff8b906 100644 --- a/tests/harness/native-weak-collections.test.ts +++ b/tests/harness/native-weak-collections.test.ts @@ -37,7 +37,7 @@ console.log(String(map.delete(key)), String(map.has(key)), String(set.delete(oth } }); - test(`weak collections refuse keys without native identity (${backend})`, async () => { + test(`weak collections preserve native record, array, tuple and class identity (${backend})`, async () => { const outDir = await mkdtemp(join(tmpdir(), "scriptc-weak-boundaries-")); try { const entry = join(outDir, "main.ts"); @@ -50,14 +50,25 @@ const tuple: [string, number] = ["value", 1]; class Key { value: number; constructor() { this.value = 1; } } const instance = new Key(); function check(value: any): void { - try { map.set(value, 1); } catch (error) { if (error instanceof Error) console.log(error.message); else throw error; } - try { set.add(value); } catch (error) { if (error instanceof Error) console.log(error.message); else throw error; } + console.log(map.set(value, 1) === map, map.get(value), map.has(value)); + console.log(set.add(value) === set, set.has(value), map.delete(value), set.delete(value)); } check(record); check(array); check(tuple); console.log(map.set(instance, 3) === map, map.get(instance)); console.log(set.add(instance) === set, set.has(instance)); +// Keep enough live keys to grow the identity table, then re-box the same +// native records after rehashing and verify their WeakMap entries survive. +const keys: { value: unknown }[] = []; +let total = 0; +for (let i = 0; i < 4096; i++) { + const key: { value: unknown } = { value: i }; + keys.push(key); + map.set(key, i); +} +for (const key of keys) total += map.get(key)!; +console.log(total, keys.length); console.log("after"); `); const result = await compile(entry, { @@ -66,13 +77,9 @@ console.log("after"); }); expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); if (!result.ok) return; - const child = await exec(result.binaryPath); - expect(child.stdout).toBe([ - ...Array(6).fill("Weak collection keys of this native reference type have no weak lifetime lowering"), - "true 3", "true true", - "after", "", - ].join("\n")); - expect(child.stderr).toBe(""); + const [reference, child] = await Promise.all([exec(process.execPath, [entry]), exec(result.binaryPath)]); + expect(child.stdout).toBe(reference.stdout); + expect(child.stderr).toBe(reference.stderr); } finally { await rm(outDir, { recursive: true, force: true }); } diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index bffea47a3..cdb943738 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -99,6 +99,22 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise } describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { + test.each([ + ["effect4-shape-string-constructor.ts", "effect4-shapes"], + ["effect4-shape-prototype-factory.ts", "effect4-shapes"], + ["effect4-shape-semantics.ts", "effect4-shapes"], + ["effect4-shape-match-metadata.ts", "effect4-shapes"], + ["selectivebarrel-cli.ts", "selectivebarrel"], + ])("%s preserves native object layout and module initialization", async (name, pkg) => { + const entry = join(pilotRoot, name); + const reference = await runBinary(process.execPath, [entry]); + const binary = await buildStatic(entry, [pkg]); + const actual = await runBinary(binary, []); + actual.stderr = comparableStderr(actual.stderr); + expect(actual).toEqual(reference); + expect(actual.exitCode).toBe(0); + }); + test("conditional exports keep distinct import and require behavior", async () => { const dir = mkdtempSync("/tmp/scriptc-conditional-exports-"); try { diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 3234b21e8..522132106 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -146,7 +146,7 @@ test("node-types: callback execFile uses the typed error-first overload", async expect(stdout).toBe("typed-execfile true\n"); }); -test("node-types: unsupported child stdin and spawn options forms remain named fences", async () => { +test("node-types: unsupported child stdin forms remain named fences", async () => { const dir = outDirFor("node-child-stdin-fences"); const entry = join(nodeTypesDir, "child-stdin-fences.ts"); const result = await compile(entry, { @@ -156,10 +156,8 @@ test("node-types: unsupported child stdin and spawn options forms remain named f }); expect(result.ok).toBe(false); if (result.ok) return; - expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(["SC2020", "SC2020", "SC2020", "SC2020", "SC2020"]); + expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(["SC2020", "SC2020", "SC2020"]); expect(result.diagnostics.map((diagnostic) => diagnostic.message)).toEqual([ - expect.stringContaining("spawn with a non-literal options argument"), - expect.stringContaining("spawn with a non-literal options argument"), expect.stringContaining("child stdin write with 2 arguments"), expect.stringContaining("Writable.end"), expect.stringContaining("Writable.destroy"), diff --git a/tests/harness/regex.test.ts b/tests/harness/regex.test.ts index fdcdf3d3f..f495a0b15 100644 --- a/tests/harness/regex.test.ts +++ b/tests/harness/regex.test.ts @@ -1,9 +1,6 @@ /* Regex behavior that is scriptc-only by nature — deliberately NOT in the * differential corpus (tests/corpus/1200.. holds everything Node-comparable): * - * - The statefulness fence at runtime: test() on a g/y regex that flowed - * through a variable (the frontend only sees literal receivers) must - * abort with the fence message, never silently model lastIndex. * - The two size/stability pins: a regex-free program must not reference * the regex runtime at all (byte-identical link line — its C names no * ScrRegex symbol), and a regex-USING static binary pays libregexp @@ -57,31 +54,6 @@ async function build( } describe("regex (scriptc-only behavior)", () => { - test("test() on a g-flagged regex reaching runtime aborts with the fence message", async () => { - // The g flag is invisible to the frontend here: the regex flows through - // a function parameter, so the RUNTIME fence must catch it. - const r = await build( - "g-test-runtime", - `function check(re: RegExp, s: string): boolean { - return re.test(s); -} -console.log("before"); -console.log(check(/a/g, "abc")); -`, - ); - const err = await execFileAsync(r.binaryPath, []).then( - () => { - throw new Error("expected the g-flagged test() to abort"); - }, - (e: Error & { signal?: string; stdout?: string; stderr?: string }) => e, - ); - expect(err.signal).toBe("SIGABRT"); - expect(err.stdout).toContain("before"); - expect(err.stderr).toContain( - "test() on a regex with the 'g' or 'y' flag is not supported", - ); - }); - test("a pattern the engine rejects aborts at first use with a SyntaxError message", async () => { // tsc's parser (and V8) accept 300 capture groups; libregexp caps // captures at 255, so the engine rejects the pattern at its LAZY diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 53e368f6a..34fe2a657 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -51,7 +51,7 @@ for (const backend of ["llvm"] as const) { schema: "scriptc.runtime-pack.v1", format: 1, package: target.runtimePackPackage, version: "1.2.3", target: { name: target.name, llvm_triple: target.llvmTriple, architecture: target.architecture, object_format: target.objectFormat, minimum_os: target.minimumOs }, - runtime_abi: { version: 5, marker: "scr_runtime_abi_v5" }, + runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { release: { optimization: "-O2", runtime_units: units }, dev: { optimization: "-O0", runtime_units: units } }, diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index 95bcdbe54..7e752faf8 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -224,7 +224,6 @@ const PROBES: Probe[] = [ // Class-instance field/getter destructures graduated (corpus 2429); the // METHOD-extraction refusal carries the sample now. { id: "diagnostic.sc1031", source: "class C {\n f = 1;\n m(): number {\n return this.f;\n }\n}\nconst { m } = new C();\nconsole.log(m());\n" }, - { id: "diagnostic.sc1121", source: 'console.log(/ab/g.test("abab"));\n' }, { id: "node-builtin.zlib.brotliCompressSync", source: 'import { brotliCompressSync } from "node:zlib";\nbrotliCompressSync(Buffer.from("data"));\nconsole.log(0);\n',