mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
init
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# arm64 macOS: same clang/libc the differential + number-formatting
|
||||
# oracles were verified against.
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- name: Tests (plain lane)
|
||||
# Capped workers: the shared runner's cores oversubscribe under the
|
||||
# corpus compile fan-out, and vitest's worker RPC times out ("Timeout
|
||||
# calling onTaskUpdate") with every test green — the same contention
|
||||
# artifact the harness README documents for local parallel suites.
|
||||
run: SCRIPTC_TEST_WORKERS=4 pnpm test
|
||||
- name: Tests (ASan + RC audit lane)
|
||||
run: SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
node_modules/
|
||||
# test-fixture packages are TEST DATA, committed on purpose
|
||||
!tests/diagnostics/npm-imports/node_modules/
|
||||
!tests/fixtures/commander-calc/node_modules/
|
||||
!tests/fixtures/npm/node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/fixtures/npm/store/*/node_modules/
|
||||
!tests/fixtures/npm/cases/*/node_modules/
|
||||
!tests/fixtures/provenance/node_modules/
|
||||
!tests/fixtures/fetch/node_modules/
|
||||
!tests/fixtures/gateway-e2e/node_modules/
|
||||
!tests/fixtures/node-types/node_modules/
|
||||
dist/
|
||||
!tests/fixtures/fetch/node_modules/eventsource-parser/dist/
|
||||
!tests/fixtures/npm/node_modules/*/dist/
|
||||
!tests/fixtures/npm/workspace/*/dist/
|
||||
!tests/fixtures/provenance/node_modules/*/dist/
|
||||
.scriptc/
|
||||
*.log
|
||||
.DS_Store
|
||||
packages/runtime/test/build/
|
||||
# lazily built engine archives for --dynamic (see packages/runtime/vendor/README.md)
|
||||
packages/runtime/vendor/.cache/
|
||||
# fs corpus scratch directories (created in the test cwd, removed on clean runs)
|
||||
tmp-*/
|
||||
.claude/
|
||||
@@ -0,0 +1 @@
|
||||
24.15.0
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,111 @@
|
||||
# scriptc
|
||||
|
||||
**Zero-runtime TypeScript.** scriptc compiles ordinary TypeScript into small, fast native executables — no Node, no V8, no JavaScript engine in the binary.
|
||||
|
||||
```console
|
||||
$ cat fib.ts
|
||||
function fib(n: number): number {
|
||||
return n < 2 ? n : fib(n - 1) + fib(n - 2);
|
||||
}
|
||||
console.log(fib(30));
|
||||
|
||||
$ scriptc run fib.ts
|
||||
832040
|
||||
|
||||
$ scriptc build fib.ts && ls -la fib
|
||||
-rwxr-xr-x 178K fib # a self-contained native binary, ~2ms startup
|
||||
```
|
||||
|
||||
No changes to your code. No annotations, no dialect — the same TypeScript you run on Node, type-checked by the real TypeScript compiler and compiled to native. What compiles behaves byte-for-byte like Node.
|
||||
|
||||
## Install
|
||||
|
||||
```console
|
||||
$ npm install -g scriptc
|
||||
```
|
||||
|
||||
Requires clang (preinstalled with Xcode Command Line Tools). macOS arm64 is the primary platform; Linux and Windows binaries build by cross-compilation, each verified by its own differential test lane.
|
||||
|
||||
## The idea: staticness you can see
|
||||
|
||||
Most TypeScript is far more static than the ecosystem assumes. scriptc decides, construct by construct, what can compile to native code — and tells you:
|
||||
|
||||
```console
|
||||
$ scriptc coverage app.ts
|
||||
|
||||
statements analyzed 4481
|
||||
compile statically 4451 (99%)
|
||||
|
||||
blockers:
|
||||
×2 functions with optional parameters as values SC1090
|
||||
×1 Promise.reject SC2020
|
||||
```
|
||||
|
||||
Three tiers, always explicit:
|
||||
|
||||
1. **Compiled statically** — native code, no engine. The default, and the only mode unless you opt out.
|
||||
2. **Runs dynamically** (`--dynamic`) — an embedded JavaScript engine ([quickjs-ng](https://github.com/quickjs-ng/quickjs), ~620KB) executes what can't be static: npm dependencies' shipped JS, `any`-typed code. Every value crossing back into static code is validated at runtime — a lying type throws a catchable `TypeError` instead of corrupting memory.
|
||||
3. **Rejected** — everything else fails with a specific error code, a code frame, and usually a rewrite hint. Nothing is ever silently miscompiled.
|
||||
|
||||
## What compiles
|
||||
|
||||
The static surface covers the language and the standard library real programs use:
|
||||
|
||||
- **The language** — classes with single inheritance and true dynamic dispatch (devirtualized when provably safe), closures with JS capture semantics, generics (monomorphized), discriminated unions as tagged values driven by TypeScript's own narrowing, `async`/`await` on stackful fibers with JS-exact scheduling, exceptions with `finally`, destructuring, spread, optional/default/rest parameters, getters/setters, iterators over strings/arrays/Maps/Sets, template literals, regular expressions (the engine is the same ECMAScript-exact bytecode interpreter QuickJS uses, linked only into regex-using binaries).
|
||||
- **The standard library** — strings with UTF-16-exact semantics, arrays/Maps/Sets with JS-exact ordering and identity, `JSON` with runtime-validated casts, `Math`, typed arrays and `Buffer`, `Error` hierarchies with typed `catch`.
|
||||
- **Node's API surface** — `fs` (sync and promises), `path` (byte-exact port), `process`, `child_process` with piped streams, `os`, `crypto`, `url`/`URL`, `zlib`, timers and signal handlers on a dependency-free event loop — and the server stack: **`net`, `http`, `https`, `tls`** (vendored mbedTLS), `dgram`, `dns`, `fs.watch`, `readline`. Real proxy servers compile.
|
||||
- **`fetch`** and the WHATWG web subset (streams, `Headers`, `AbortSignal`) over the same native net/TLS stack — redirects, gzip, `AbortSignal.timeout`, Node-shaped error causes; no libcurl, no system HTTP dependency.
|
||||
- **npm dependencies** (with `--dynamic`) — packages resolve with Node's own algorithm, typecheck against their shipped `.d.ts`, and their JS is embedded into the binary at build time. Binaries never read `node_modules` at runtime.
|
||||
|
||||
Programs typecheck against TypeScript's real `es2025` lib (plus `@types/node` when your project has it), and your `tsconfig.json` governs checker strictness. Anything reached that has no lowering is a precise diagnostic, never a surprise.
|
||||
|
||||
## Correctness
|
||||
|
||||
Two enforcement mechanisms run on every change:
|
||||
|
||||
- **Differential testing** — every corpus program (800+ tests) runs under Node *and* as a native binary; stdout, stderr, and exit codes must match byte-for-byte. Number formatting is JS-exact (shortest-roundtrip, fuzz-verified against Node on a million doubles). Servers are tested with live client drivers against both implementations.
|
||||
- **Memory-safety lane** — the entire corpus re-runs under AddressSanitizer with a reference-count audit; leaks and use-after-free are build failures.
|
||||
|
||||
The deliberate divergences from Node (there are a few dozen, mostly around timing internals and error-object properties) are documented and numbered; nothing diverges silently.
|
||||
|
||||
## Performance
|
||||
|
||||
Measured on Apple M-series against the same workloads in Node, Go, Rust, and Zig (all byte-identical output, verified):
|
||||
|
||||
| dimension | scriptc | context |
|
||||
|---|---|---|
|
||||
| startup | ~2.4ms | Node: ~47ms; on par with Zig, ahead of Go/Rust |
|
||||
| binary size | 170–200KB static, ~3MB with `--dynamic` + embedded deps | Go: ~2MB; Node SEA: 60–100MB |
|
||||
| memory (RSS) | 1–4MB typical | Node: 67–116MB |
|
||||
| runtime | JS-faithful f64 semantics; competitive with the systems languages on most workloads | integer inference and ownership analysis are on the roadmap |
|
||||
|
||||
## Escape hatches, honestly priced
|
||||
|
||||
- **`comptime(() => ...)`** runs TypeScript at build time (in an isolated VM inside the compiler) and bakes the result into the binary as a literal.
|
||||
- **`--dynamic`** embeds the engine for npm deps and `any` code. `scriptc coverage --dynamic` reports exactly which statements run where and what the remaining blockers are. Static stays the default: a binary never silently grows an engine.
|
||||
- **Checked casts** — `JSON.parse(...) as Config` inserts a runtime validation that throws a catchable error naming the offending path (`expected number at $.port, got string`). TypeScript's `as` is a promise; scriptc verifies it.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
TS[TypeScript] -->|tsc: parse + typecheck| L[lowering]
|
||||
L --> IR[typed IR]
|
||||
IR --> C[C]
|
||||
C -->|clang| BIN[native executable]
|
||||
```
|
||||
|
||||
- `packages/compiler` — frontend (tsc API → IR), the IR with validator/serializer, the LLVM and C backends. The IR is the only interface between the ends; LLVM is the default code generator (with a transparent fallback for programs outside its tier), and C is the reference backend forever (readable, source-line-annotated output via `--backend c`).
|
||||
- `packages/runtime` — the C runtime: refcounted values with a cycle collector, stackful fibers and the event loop (kqueue), the server stack, JS-exact number formatting. Feature units are link-gated: binaries pay only for what they use.
|
||||
- `packages/cli` — `scriptc build | run | coverage`.
|
||||
|
||||
## Development
|
||||
|
||||
```console
|
||||
$ pnpm install && pnpm build
|
||||
$ pnpm test # differential corpus + diagnostics snapshots
|
||||
$ SCRIPTC_SAN=1 pnpm test # the same corpus under ASan + RC audit
|
||||
$ pnpm scriptc build x.ts --emit-ir # keep .scriptc/x.c and x.ir.json
|
||||
```
|
||||
|
||||
Every feature lands with differential tests; both lanes green is the merge bar.
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.next/
|
||||
.next-check/
|
||||
next-env.d.ts
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -0,0 +1,52 @@
|
||||
# Docs Site Conventions
|
||||
|
||||
This is the scriptc docs site: Next.js App Router + MDX, one topic per `src/app/<topic>/page.mdx`, standalone from the repo's pnpm workspace (it has its own lockfile; `pnpm install && pnpm check` runs entirely inside `docs/`).
|
||||
|
||||
## Naming
|
||||
|
||||
The project is **scriptc**, lowercase, everywhere — page titles, prose, code samples. All name-bearing strings (site name, tagline, GitHub URL, canonical origin `https://scriptc.dev`) live in `src/lib/site.ts`; copy elsewhere is written name-neutral and pulls from there.
|
||||
|
||||
## Content rules
|
||||
|
||||
- Coverage numbers on a page are only ever the output of `scriptc coverage` on a specific program shown in that same block — never aggregate statistics.
|
||||
- Every shell command shown in a fence must have been run successfully against the current build before it lands on a page. Output blocks are real output (trimmed is fine, invented is not).
|
||||
- Limitations are documented plainly on their own page, not scattered as fine print.
|
||||
|
||||
## MDX Tables
|
||||
|
||||
Always use HTML `<table>` syntax in MDX pages, never markdown pipe tables. This ensures consistent styling and avoids MDX parsing edge cases.
|
||||
|
||||
```html
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>field</code></td>
|
||||
<td>What it does</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
## Code fences
|
||||
|
||||
The fence info string carries the language, and optionally a filename after a colon (` ```ts:src/main.ts `) — that is the only fence metadata that survives the MDX pipeline. `console` is the register for shell sessions ( `$ command` then output); `diff` fences get +/- coloring.
|
||||
|
||||
## Definition lists
|
||||
|
||||
Flag and subcommand references use HTML `<dl>/<dt>/<dd>` (styled via `article dl` rules in `globals.css`).
|
||||
|
||||
## Adding a page
|
||||
|
||||
1. `src/app/<topic>/page.mdx` for the content and `src/app/<topic>/layout.tsx` exporting `pageMetadata("<topic>")`.
|
||||
2. Add the slug to `PAGE_TITLES` in `src/lib/page-titles.ts` (drives metadata and OG images).
|
||||
3. Add a nav entry in `src/lib/docs-navigation.ts` (drives the sidebar, mobile nav, and sitemap).
|
||||
|
||||
## Verification
|
||||
|
||||
`pnpm check` (typecheck + production build) must pass before committing. Run it with `NEXT_DIST_DIR=.next-check` if a dev server may be running.
|
||||
@@ -0,0 +1,33 @@
|
||||
import createMDX from "@next/mdx";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
// Resolve the plugin to an absolute path (still a string, so the config
|
||||
// stays serializable for Turbopack). A bare "remark-gfm" is require()d
|
||||
// from the MDX loader's own package context, which under pnpm's strict
|
||||
// module isolation cannot see this app's dependencies — production
|
||||
// builds resolved it, the Turbopack dev server did not.
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const withMDX = createMDX({
|
||||
options: {
|
||||
// GFM is what gives .mdx pages autolinks and strikethrough. Tables are
|
||||
// authored as literal HTML per AGENTS.md, but the plugin stays so a
|
||||
// stray pipe table degrades gracefully instead of rendering as a
|
||||
// paragraph of pipes.
|
||||
remarkPlugins: [[require.resolve("remark-gfm")]],
|
||||
},
|
||||
});
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ["ts", "tsx", "md", "mdx"],
|
||||
// CI-style builds set NEXT_DIST_DIR so `pnpm check` never shares .next
|
||||
// with a running dev server (a shared dist dir corrupts the dev cache).
|
||||
distDir: process.env.NEXT_DIST_DIR || ".next",
|
||||
// Keep check builds from retriggering a running dev server's watcher.
|
||||
watchOptions: {
|
||||
ignored: ["**/.next-check/**"],
|
||||
},
|
||||
};
|
||||
|
||||
export default withMDX(nextConfig);
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@scriptc/docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.23.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check": "pnpm typecheck && pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/loader": "^3",
|
||||
"@mdx-js/react": "^3",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.7.0",
|
||||
"next": "^16.2.9",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^4.0.2",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/mdx": "^2",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
Generated
+4106
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
# This file makes docs/ its own pnpm root: installs here stay standalone
|
||||
# instead of attaching to the repo workspace (whose globs are packages/*).
|
||||
packages: []
|
||||
|
||||
overrides:
|
||||
"postcss@<8.5.10": ">=8.5.10"
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("cli");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
# CLI Reference
|
||||
|
||||
The CLI has three commands. `scriptc --help` prints this same surface.
|
||||
|
||||
```console
|
||||
$ scriptc --help
|
||||
scriptc — TypeScript/JavaScript to native executables (experimental)
|
||||
|
||||
Usage:
|
||||
scriptc build <file.ts|.js> [options] compile to a native executable
|
||||
scriptc run <file.ts|.js> [options] compile and run
|
||||
scriptc coverage <file.ts|.js> how much compiles statically, and why not
|
||||
scriptc coverage <file.ts|.js> --dynamic what a --dynamic build compiles, and what still blocks it
|
||||
```
|
||||
|
||||
## scriptc build
|
||||
|
||||
Compiles a TypeScript (or JavaScript) entry file to a native executable. The program is type-checked first — by the real TypeScript compiler, honoring the nearest `tsconfig.json` — and any construct without a lowering is a compile error with an `SC`-prefixed code, a code frame, and usually a rewrite hint.
|
||||
|
||||
```console
|
||||
$ scriptc build fib.ts -o fib
|
||||
$ ./fib
|
||||
832040
|
||||
```
|
||||
|
||||
Without `-o`, the executable lands in `.scriptc/<name>` next to the input, and the generated C translation unit is kept beside it (see `--keep-c`):
|
||||
|
||||
```console
|
||||
$ scriptc build fib.ts --emit-ir
|
||||
$ ls .scriptc/
|
||||
fib
|
||||
fib.c
|
||||
fib.ir.json
|
||||
```
|
||||
|
||||
## scriptc run
|
||||
|
||||
`build` followed by executing the binary, with stdio inherited.
|
||||
|
||||
```console
|
||||
$ scriptc run fib.ts
|
||||
832040
|
||||
```
|
||||
|
||||
Note: `run` does not forward extra command-line arguments to the program. To pass arguments, `build` the executable and invoke it directly.
|
||||
|
||||
## scriptc coverage
|
||||
|
||||
Analyzes the program without producing a binary and reports, statement by statement, what compiles statically, what needs the embedded dynamic engine, and what blocks the rest. With `--dynamic` it answers a different question: what would a `--dynamic` build compile, and what still blocks it. Both forms are covered in depth in [Coverage Reports](/coverage).
|
||||
|
||||
## Options
|
||||
|
||||
<dl>
|
||||
<dt><code>-o, --out <path></code></dt>
|
||||
<dd>Output executable path. Default: <code>.scriptc/<name></code> next to the input file.</dd>
|
||||
|
||||
<dt><code>--dynamic</code></dt>
|
||||
<dd>Embed the dynamic engine (~620KB) so npm dependencies and <code>any</code>-typed code can run. Static stays the default — without this flag, dynamic-tier sites are per-site compile errors. See <a href="/dependencies">npm Dependencies</a>.</dd>
|
||||
|
||||
<dt><code>--backend <c|llvm></code></dt>
|
||||
<dd>Code generator: <code>llvm</code> (default — emits LLVM IR text, compiled by the same clang) or <code>c</code> (the readable reference backend). Unset, the build tries LLVM and falls back to the C backend when the program is outside the LLVM tier — a one-line stderr note says so, and program behavior is identical either way. An explicit <code>--backend llvm</code> fails with a diagnostic instead of falling back (the debugging/CI pin); <code>--backend c</code> pins the reference backend.</dd>
|
||||
|
||||
<dt><code>--npm-static <pkg[,pkg…]|auto></code></dt>
|
||||
<dd>EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; <code>auto</code> opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See <a href="/dependencies">npm Dependencies</a> for maturity notes.</dd>
|
||||
|
||||
<dt><code>--provenance-sources</code></dt>
|
||||
<dd>EXPERIMENTAL. Compile npm dependencies from their provenance-attested source, fetched at the attested commit, as static program modules; packages without a usable attestation keep the engine path (a note, never a failure).</dd>
|
||||
|
||||
<dt><code>--sanitize</code></dt>
|
||||
<dd>Build with AddressSanitizer plus the runtime reference-count audit — the same lane the compiler's own test corpus runs under. Useful when a program misbehaves and you want leaks or memory errors to be loud.</dd>
|
||||
|
||||
<dt><code>--emit-ir</code></dt>
|
||||
<dd>Also write the typed IR as JSON (<code><name>.ir.json</code>) next to the executable.</dd>
|
||||
|
||||
<dt><code>--keep-c</code> / <code>--no-keep-c</code></dt>
|
||||
<dd>Keep (default) or delete the generated program translation unit next to the executable — the <code>.ll</code> file under the default LLVM backend, or the <code>.c</code> under <code>--backend c</code> (and when the default build fell back). The generated C is readable and annotated with source lines.</dd>
|
||||
|
||||
<dt><code>--from-c</code></dt>
|
||||
<dd>Treat the input as a C (or <code>.ll</code>) file. Toolchain plumbing and debugging.</dd>
|
||||
|
||||
<dt><code>-h, --help</code></dt>
|
||||
<dd>Show usage.</dd>
|
||||
</dl>
|
||||
|
||||
## Environment variables
|
||||
|
||||
<dl>
|
||||
<dt><code>SCRIPTC_CC</code></dt>
|
||||
<dd>The C compiler to invoke. <code>zigcc</code> selects zig's bundled clang, which enables cross-compilation with its bundled sysroots.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_TARGET</code></dt>
|
||||
<dd>Target triple for cross-compilation, e.g. <code>aarch64-linux-gnu.2.36</code> or <code>x86_64-windows-gnu</code>. See <a href="/platforms">Platform Support</a>.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux
|
||||
$ file fib-linux
|
||||
fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped
|
||||
```
|
||||
|
||||
## Backends
|
||||
|
||||
The default backend emits textual LLVM IR, compiled by the same clang that links the runtime. It covers most of the supported surface; a program outside that tier is never miscompiled — the build falls back to the C backend transparently and says so in one stderr line:
|
||||
|
||||
```console
|
||||
$ scriptc build cli-tool.ts --dynamic -o cli-tool
|
||||
scriptc: backend c (llvm refused: npmEmbedding)
|
||||
```
|
||||
|
||||
The C backend is the reference, forever: deliberately readable, source-line-annotated output, and byte-identical program behavior wherever the two backends overlap — every release verifies that. Pin it when you want to read what your program became:
|
||||
|
||||
```console
|
||||
$ scriptc build fib.ts --backend c -o fib
|
||||
$ ./fib
|
||||
832040
|
||||
$ head -1 fib.c
|
||||
/* Generated by scriptc from fib.ts. Do not edit. */
|
||||
```
|
||||
|
||||
An explicit <code>--backend llvm</code> pins the LLVM backend and fails with diagnostic SC3001, naming the unsupported construct, instead of falling back — useful for CI lanes that must notice tier changes.
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("coverage");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# Coverage Reports
|
||||
|
||||
`scriptc coverage` answers "how much of my program compiles to native code?" It analyzes the program without building it and assigns every statement to a tier. No global scoreboard, no hand-waving: the numbers are for *your* program, and every non-static site is named with an error code.
|
||||
|
||||
## A fully static program
|
||||
|
||||
```console
|
||||
$ scriptc coverage app.ts
|
||||
|
||||
statements analyzed 10
|
||||
compile statically 10 (100%)
|
||||
|
||||
fully static — this program has no dynamic remainder.
|
||||
```
|
||||
|
||||
This is the common case for typed application code: the whole program becomes native code, and a `scriptc build` binary carries no engine.
|
||||
|
||||
## A program with a dynamic remainder
|
||||
|
||||
Import an npm package and the report changes shape:
|
||||
|
||||
```console
|
||||
$ scriptc coverage cli.ts
|
||||
|
||||
statements analyzed 4
|
||||
compile statically 3 (75%)
|
||||
|
||||
runs with --dynamic 2 sites (embeds a JS engine, ~620KB — static stays the default)
|
||||
×1 importing 'picocolors' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there SC2013
|
||||
×1 values from the 'picocolors' package run in the embedded dynamic engine, which this build does not include SC2013
|
||||
```
|
||||
|
||||
Reading it line by line:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Line</th>
|
||||
<th>Meaning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>statements analyzed</code></td>
|
||||
<td>Every executable statement in your program (not in dependencies).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>compile statically</code></td>
|
||||
<td>Statements that become native code — no engine involved.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runs with --dynamic</code></td>
|
||||
<td>Sites that <em>would</em> run in the embedded engine if you rebuilt with <code>--dynamic</code>. Without the flag, each is a compile error — this build does not include an engine, and never will silently.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>blockers</code></td>
|
||||
<td>Statements that compile in <em>no</em> tier yet, each with a count, a one-line reason, and its <code>SC</code> code. These are the rejected tier: rebuilding with <code>--dynamic</code> does not make them go away.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## The --dynamic view
|
||||
|
||||
`scriptc coverage --dynamic` answers a different question: **what would a `--dynamic` build compile, and what still blocks it?**
|
||||
|
||||
```console
|
||||
$ scriptc coverage cli.ts --dynamic
|
||||
|
||||
statements analyzed 4
|
||||
compile statically 3 (75%)
|
||||
compile dynamically 1 (25%) (island sites — the embedded engine runs them)
|
||||
|
||||
builds with --dynamic — no remaining blockers (the island sites above run in the embedded engine).
|
||||
```
|
||||
|
||||
The closing line is the actionable verdict: this program builds with `--dynamic`. When it doesn't, the remaining blockers are listed exactly like the static view's.
|
||||
|
||||
## The embedded-builtins report
|
||||
|
||||
When embedded npm code imports Node builtin modules, the `--dynamic` report says so — each builtin the embedded dependency graph reaches, whether it's shimmed inside the engine, and which package wanted it:
|
||||
|
||||
```console
|
||||
$ scriptc coverage tool.ts --dynamic
|
||||
|
||||
statements analyzed 5
|
||||
compile statically 2 (40%)
|
||||
compile dynamically 3 (60%) (island sites — the embedded engine runs them)
|
||||
|
||||
embedded npm code imports Node builtins:
|
||||
node:child_process shimmed (commander)
|
||||
node:events shimmed (commander)
|
||||
node:fs shimmed (commander)
|
||||
node:path shimmed (commander)
|
||||
node:process shimmed (commander)
|
||||
node:util shimmed (commander)
|
||||
|
||||
builds with --dynamic — no remaining blockers (the island sites above run in the embedded engine).
|
||||
```
|
||||
|
||||
The shims are reimplementations inside the engine, not Node itself — see [npm Dependencies](/dependencies) for what that means.
|
||||
|
||||
## What the fences mean
|
||||
|
||||
Blocker lines carry `SC` codes — the same codes `scriptc build` errors with, so the coverage report and the compiler never disagree. A few common shapes:
|
||||
|
||||
- `'X' is part of the standard library types but has no scriptc lowering yet (SC2020)` — the type checker sees the full standard library, but only the supported surface compiles. The build error at that site includes the supported-alternatives hint.
|
||||
- `importing 'pkg' requires the embedded dynamic engine (SC2013)` — npm package implementations run in the engine; add `--dynamic` or drop the dependency.
|
||||
- `'fetch' runs in the embedded dynamic engine (SC2012)` — island-backed ambient API: typed, callable from your code, executed by the engine.
|
||||
- `passing 'unknown' values where 'any' is expected (SC1100)` — the `any`/`unknown` boundary rules; narrow or cast first.
|
||||
|
||||
## Type errors gate the analysis
|
||||
|
||||
Coverage only analyzes programs that typecheck. A program with TypeScript errors reports them instead of numbers:
|
||||
|
||||
```console
|
||||
$ scriptc coverage broken.ts
|
||||
|
||||
not analyzable: 1 TypeScript error — fix type errors first (scriptc only analyzes programs that typecheck)
|
||||
```
|
||||
|
||||
This is deliberate: tier assignment is driven by types, so numbers computed over an ill-typed program would be fiction.
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("dependencies");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# npm Dependencies
|
||||
|
||||
npm packages are the dynamic frontier: their shipped JavaScript is untyped, minified, and written against V8. scriptc's answer is the **dynamic island** — an embedded JavaScript engine ([quickjs-ng](https://github.com/quickjs-ng/quickjs), ~620KB) that executes dependency code inside your binary, opted in with `--dynamic`, with every value validated as it crosses back into static code.
|
||||
|
||||
## The island story
|
||||
|
||||
```ts:tool.ts
|
||||
import { Command } from "commander";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("greet")
|
||||
.argument("<name>", "who to greet")
|
||||
.option("-u, --upper", "shout it")
|
||||
.action((name: string, opts: { upper?: boolean }) => {
|
||||
const text = `hello, ${name}`;
|
||||
console.log(opts.upper ? text.toUpperCase() : text);
|
||||
});
|
||||
|
||||
program.parse();
|
||||
```
|
||||
|
||||
```console
|
||||
$ npm install commander
|
||||
$ scriptc build tool.ts --dynamic -o greet
|
||||
$ ./greet ada --upper
|
||||
HELLO, ADA
|
||||
```
|
||||
|
||||
What happened, step by step:
|
||||
|
||||
1. **Resolution** — `commander` resolves with Node's own resolution algorithm from your `node_modules`, honoring package.json `exports` conditions.
|
||||
2. **Types** — the package's shipped `.d.ts` is the type surface your code checks against, exactly as in a Node project.
|
||||
3. **Embedding** — the package's JavaScript (and everything it imports, resolved the same way) is embedded into the binary **at build time**. The executable never reads `node_modules` at runtime — it runs from any directory on any machine of the same platform.
|
||||
4. **Execution** — embedded code runs in the engine with full JavaScript semantics: the real `commander`, unmodified.
|
||||
5. **The boundary** — values cross by copy, never by reference. Your callback's typed parameters (`name: string`) are validated at call time: if the package passes something that isn't a string, that's a catchable `TypeError`, not memory corruption.
|
||||
|
||||
Your own code — the `.action` callback body, the functions it calls — still compiles statically. The engine runs only what must be dynamic, and [`scriptc coverage --dynamic`](/coverage) shows the exact split, including which Node builtins the embedded packages import and whether each is shimmed.
|
||||
|
||||
## What the island is
|
||||
|
||||
- **It's quickjs-ng, not V8.** Embedded dependency code runs correctly but slower than under Node for CPU-bound work. The win is startup, size, memory, and deployment shape — not raw dependency throughput.
|
||||
- **Shims, not Node.** Embedded package code that requires builtins (`node:events`, `node:path`, `node:process`, ...) gets faithful in-island implementations, several of which bridge to the same native runtime functions static code uses. The coverage report names every builtin the embedded graph reaches; an unshimmed one is reported, never silently stubbed.
|
||||
- **The boundary copies.** A static value flowing into dependency code marshals by value; mutations made by dynamic code are invisible to the static original, and vice versa. Where JS would alias, scriptc copies — a documented [divergence](/limitations).
|
||||
- **`any`-typed code runs there too.** Under `--dynamic`, expressions the checker types as `any` execute in the engine with full JS semantics, and every `any` → static edge is a validated exit.
|
||||
- **One engine per process**, created lazily on first use. An island-free `--dynamic` build emits the same code as a static one.
|
||||
|
||||
## --npm-static (experimental)
|
||||
|
||||
`--npm-static <pkg[,pkg…]|auto>` asks the compiler to take the named packages **out** of the island: their shipped JavaScript is compiled statically as program modules, type-informed by their own `.d.ts`.
|
||||
|
||||
This is experimental. Real packages compile statically at high but partial coverage. Sites the static compiler can't take are *deferred* — the build succeeds and the report names every deferred site, but reaching one at runtime is an error naming the exact unsupported operation. Some packages' code hits the static frontier outright and fails to build statically today; a package the preflight refuses falls back to the island with a coverage note. If a package matters to you statically, try it and read the report — the answer is specific, per package.
|
||||
|
||||
## --provenance-sources (experimental)
|
||||
|
||||
`--provenance-sources` goes one step further: for packages published with npm provenance attestations, the compiler fetches the package's **source** at the attested commit and compiles that — TypeScript compiled as TypeScript, instead of the shipped JS. Packages without a usable attestation keep the engine path, with a note, never a failure. Same maturity caveats as `--npm-static`.
|
||||
|
||||
## Where this scales
|
||||
|
||||
The design goal is unmodified real-world packages. The measuring stick used during development is the published [Vercel CLI](https://www.npmjs.com/package/vercel) — straight off the registry, compiled with `--dynamic` into a single self-contained executable that runs its real workflows, replacing a ~120MB Node runtime plus 181MB of `node_modules`.
|
||||
|
||||
## Current limits
|
||||
|
||||
- **Cross-compiled `--dynamic` binaries are not there yet** — the engine archive is built per target; today `--dynamic` is host-native. See [Platform Support](/platforms).
|
||||
- Packages without shipped or installed type declarations fail the typecheck gate (the standard `Could not find a declaration file` error) — add `@types/<pkg>` or a local declaration, as you would in any strict TypeScript project.
|
||||
- Class instances and promises can't flow *into* `any` slots (no island representation); closures cross as host functions in specific shapes. Each refusal is a compile error naming the fix.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,496 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "tailwindcss-animate";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
--font-mono:
|
||||
var(--font-geist-mono), ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo,
|
||||
Consolas, monospace;
|
||||
|
||||
/* Geist scales, exposed as Tailwind color utilities. The underlying custom
|
||||
properties flip per theme, so one class covers light and dark. */
|
||||
--color-background-100: var(--ds-background-100);
|
||||
--color-background-200: var(--ds-background-200);
|
||||
|
||||
--color-gray-100: var(--ds-gray-100);
|
||||
--color-gray-200: var(--ds-gray-200);
|
||||
--color-gray-300: var(--ds-gray-300);
|
||||
--color-gray-400: var(--ds-gray-400);
|
||||
--color-gray-500: var(--ds-gray-500);
|
||||
--color-gray-600: var(--ds-gray-600);
|
||||
--color-gray-700: var(--ds-gray-700);
|
||||
--color-gray-800: var(--ds-gray-800);
|
||||
--color-gray-900: var(--ds-gray-900);
|
||||
--color-gray-1000: var(--ds-gray-1000);
|
||||
|
||||
--color-gray-alpha-100: var(--ds-gray-alpha-100);
|
||||
--color-gray-alpha-200: var(--ds-gray-alpha-200);
|
||||
--color-gray-alpha-300: var(--ds-gray-alpha-300);
|
||||
--color-gray-alpha-400: var(--ds-gray-alpha-400);
|
||||
--color-gray-alpha-500: var(--ds-gray-alpha-500);
|
||||
--color-gray-alpha-600: var(--ds-gray-alpha-600);
|
||||
--color-gray-alpha-700: var(--ds-gray-alpha-700);
|
||||
--color-gray-alpha-800: var(--ds-gray-alpha-800);
|
||||
--color-gray-alpha-900: var(--ds-gray-alpha-900);
|
||||
--color-gray-alpha-1000: var(--ds-gray-alpha-1000);
|
||||
|
||||
--color-blue-100: var(--ds-blue-100);
|
||||
--color-blue-200: var(--ds-blue-200);
|
||||
--color-blue-300: var(--ds-blue-300);
|
||||
--color-blue-400: var(--ds-blue-400);
|
||||
--color-blue-500: var(--ds-blue-500);
|
||||
--color-blue-600: var(--ds-blue-600);
|
||||
--color-blue-700: var(--ds-blue-700);
|
||||
--color-blue-800: var(--ds-blue-800);
|
||||
--color-blue-900: var(--ds-blue-900);
|
||||
--color-blue-1000: var(--ds-blue-1000);
|
||||
|
||||
--color-red-100: var(--ds-red-100);
|
||||
--color-red-200: var(--ds-red-200);
|
||||
--color-red-300: var(--ds-red-300);
|
||||
--color-red-400: var(--ds-red-400);
|
||||
--color-red-500: var(--ds-red-500);
|
||||
--color-red-600: var(--ds-red-600);
|
||||
--color-red-700: var(--ds-red-700);
|
||||
--color-red-800: var(--ds-red-800);
|
||||
--color-red-900: var(--ds-red-900);
|
||||
--color-red-1000: var(--ds-red-1000);
|
||||
|
||||
--color-amber-100: var(--ds-amber-100);
|
||||
--color-amber-200: var(--ds-amber-200);
|
||||
--color-amber-300: var(--ds-amber-300);
|
||||
--color-amber-400: var(--ds-amber-400);
|
||||
--color-amber-500: var(--ds-amber-500);
|
||||
--color-amber-600: var(--ds-amber-600);
|
||||
--color-amber-700: var(--ds-amber-700);
|
||||
--color-amber-800: var(--ds-amber-800);
|
||||
--color-amber-900: var(--ds-amber-900);
|
||||
--color-amber-1000: var(--ds-amber-1000);
|
||||
|
||||
--color-green-100: var(--ds-green-100);
|
||||
--color-green-200: var(--ds-green-200);
|
||||
--color-green-300: var(--ds-green-300);
|
||||
--color-green-400: var(--ds-green-400);
|
||||
--color-green-500: var(--ds-green-500);
|
||||
--color-green-600: var(--ds-green-600);
|
||||
--color-green-700: var(--ds-green-700);
|
||||
--color-green-800: var(--ds-green-800);
|
||||
--color-green-900: var(--ds-green-900);
|
||||
--color-green-1000: var(--ds-green-1000);
|
||||
|
||||
/* Semantic aliases used by shared components. */
|
||||
--color-background: var(--ds-background-100);
|
||||
--color-foreground: var(--ds-gray-1000);
|
||||
--color-border: var(--ds-gray-alpha-400);
|
||||
--color-muted: var(--ds-gray-100);
|
||||
--color-muted-foreground: var(--ds-gray-900);
|
||||
--color-primary: var(--ds-gray-1000);
|
||||
--color-primary-foreground: var(--ds-background-100);
|
||||
--color-sidebar: var(--ds-background-200);
|
||||
|
||||
/* Geist elevation: tonal surfaces + borders first, shadows soft. */
|
||||
--shadow-card: var(--ds-shadow-card);
|
||||
--shadow-popover:
|
||||
0 1px 1px rgba(0, 0, 0, 0.02), 0 4px 8px -4px rgba(0, 0, 0, 0.04),
|
||||
0 16px 24px -8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-modal:
|
||||
0 1px 1px rgba(0, 0, 0, 0.02), 0 8px 16px -4px rgba(0, 0, 0, 0.04),
|
||||
0 24px 32px -8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* Geist easing for state transitions. */
|
||||
--ease-geist: cubic-bezier(0.175, 0.885, 0.32, 1.1);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ tokens
|
||||
Geist color tokens (vercel.com/design.md). Step semantics: 100/200/300
|
||||
default/hover/active backgrounds, 400/500/600 borders, 700/800 solid
|
||||
fills, 900/1000 secondary/primary text. */
|
||||
|
||||
:root {
|
||||
--ds-background-100: #ffffff;
|
||||
--ds-background-200: #fafafa;
|
||||
|
||||
--ds-gray-100: #f2f2f2;
|
||||
--ds-gray-200: #ebebeb;
|
||||
--ds-gray-300: #e6e6e6;
|
||||
--ds-gray-400: #eaeaea;
|
||||
--ds-gray-500: #c9c9c9;
|
||||
--ds-gray-600: #a8a8a8;
|
||||
--ds-gray-700: #8f8f8f;
|
||||
--ds-gray-800: #7d7d7d;
|
||||
--ds-gray-900: #4d4d4d;
|
||||
--ds-gray-1000: #171717;
|
||||
|
||||
--ds-gray-alpha-100: #0000000d;
|
||||
--ds-gray-alpha-200: #00000015;
|
||||
--ds-gray-alpha-300: #0000001a;
|
||||
--ds-gray-alpha-400: #00000014;
|
||||
--ds-gray-alpha-500: #00000036;
|
||||
--ds-gray-alpha-600: #0000003d;
|
||||
--ds-gray-alpha-700: #00000070;
|
||||
--ds-gray-alpha-800: #00000082;
|
||||
--ds-gray-alpha-900: #000000b3;
|
||||
--ds-gray-alpha-1000: #000000e8;
|
||||
|
||||
--ds-blue-100: #f0f7ff;
|
||||
--ds-blue-200: #e9f4ff;
|
||||
--ds-blue-300: #dfefff;
|
||||
--ds-blue-400: #cae7ff;
|
||||
--ds-blue-500: #94ccff;
|
||||
--ds-blue-600: #48aeff;
|
||||
--ds-blue-700: #006bff;
|
||||
--ds-blue-800: #0059ec;
|
||||
--ds-blue-900: #005ff2;
|
||||
--ds-blue-1000: #002359;
|
||||
|
||||
--ds-red-100: #ffeeef;
|
||||
--ds-red-200: #ffe8ea;
|
||||
--ds-red-300: #ffe3e4;
|
||||
--ds-red-400: #ffd7d6;
|
||||
--ds-red-500: #ffb1b3;
|
||||
--ds-red-600: #ff676d;
|
||||
--ds-red-700: #fc0035;
|
||||
--ds-red-800: #ea001d;
|
||||
--ds-red-900: #d8001b;
|
||||
--ds-red-1000: #47000c;
|
||||
|
||||
--ds-amber-100: #fff6de;
|
||||
--ds-amber-200: #fff4cf;
|
||||
--ds-amber-300: #fff1c1;
|
||||
--ds-amber-400: #ffdc73;
|
||||
--ds-amber-500: #ffc543;
|
||||
--ds-amber-600: #ffa600;
|
||||
--ds-amber-700: #ffae00;
|
||||
--ds-amber-800: #ff9300;
|
||||
--ds-amber-900: #aa4d00;
|
||||
--ds-amber-1000: #561900;
|
||||
|
||||
--ds-green-100: #ecfdec;
|
||||
--ds-green-200: #e5fce7;
|
||||
--ds-green-300: #d3fad1;
|
||||
--ds-green-400: #b9f5bc;
|
||||
--ds-green-500: #82eb8d;
|
||||
--ds-green-600: #4ce15e;
|
||||
--ds-green-700: #28a948;
|
||||
--ds-green-800: #279141;
|
||||
--ds-green-900: #107d32;
|
||||
--ds-green-1000: #003a00;
|
||||
|
||||
/* Focus: two-layer ring — a surface-colored gap, then blue. */
|
||||
--ds-focus-color: #006bff;
|
||||
|
||||
--ds-shadow-card: 0 2px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--ds-background-100: #000000;
|
||||
--ds-background-200: #000000;
|
||||
|
||||
--ds-gray-100: #1a1a1a;
|
||||
--ds-gray-200: #1f1f1f;
|
||||
--ds-gray-300: #292929;
|
||||
--ds-gray-400: #2e2e2e;
|
||||
--ds-gray-500: #454545;
|
||||
--ds-gray-600: #878787;
|
||||
--ds-gray-700: #8f8f8f;
|
||||
--ds-gray-800: #7d7d7d;
|
||||
--ds-gray-900: #a0a0a0;
|
||||
--ds-gray-1000: #ededed;
|
||||
|
||||
--ds-gray-alpha-100: #ffffff12;
|
||||
--ds-gray-alpha-200: #ffffff17;
|
||||
--ds-gray-alpha-300: #ffffff21;
|
||||
--ds-gray-alpha-400: #ffffff24;
|
||||
--ds-gray-alpha-500: #ffffff3d;
|
||||
--ds-gray-alpha-600: #ffffff82;
|
||||
--ds-gray-alpha-700: #ffffff8a;
|
||||
--ds-gray-alpha-800: #ffffff78;
|
||||
--ds-gray-alpha-900: #ffffff9c;
|
||||
--ds-gray-alpha-1000: #ffffffeb;
|
||||
|
||||
--ds-blue-100: #06193a;
|
||||
--ds-blue-200: #022248;
|
||||
--ds-blue-300: #002f62;
|
||||
--ds-blue-400: #003674;
|
||||
--ds-blue-500: #00418b;
|
||||
--ds-blue-600: #0090ff;
|
||||
--ds-blue-700: #006efe;
|
||||
--ds-blue-800: #005be7;
|
||||
--ds-blue-900: #47a8ff;
|
||||
--ds-blue-1000: #eaf6ff;
|
||||
|
||||
--ds-red-100: #330a11;
|
||||
--ds-red-200: #440d13;
|
||||
--ds-red-300: #5d0e17;
|
||||
--ds-red-400: #6f101b;
|
||||
--ds-red-500: #88151f;
|
||||
--ds-red-600: #f32e40;
|
||||
--ds-red-700: #f13242;
|
||||
--ds-red-800: #e2162a;
|
||||
--ds-red-900: #ff565f;
|
||||
--ds-red-1000: #ffe9ed;
|
||||
|
||||
--ds-amber-100: #2a1700;
|
||||
--ds-amber-200: #361900;
|
||||
--ds-amber-300: #502800;
|
||||
--ds-amber-400: #5b3000;
|
||||
--ds-amber-500: #703e00;
|
||||
--ds-amber-600: #ed9a00;
|
||||
--ds-amber-700: #ffae00;
|
||||
--ds-amber-800: #ff9300;
|
||||
--ds-amber-900: #ff9300;
|
||||
--ds-amber-1000: #fff3d5;
|
||||
|
||||
--ds-green-100: #002608;
|
||||
--ds-green-200: #00320b;
|
||||
--ds-green-300: #003a0e;
|
||||
--ds-green-400: #004615;
|
||||
--ds-green-500: #006717;
|
||||
--ds-green-600: #00952d;
|
||||
--ds-green-700: #00ac3a;
|
||||
--ds-green-800: #009432;
|
||||
--ds-green-900: #00ca50;
|
||||
--ds-green-1000: #d8ffe4;
|
||||
|
||||
--ds-focus-color: #47a8ff;
|
||||
|
||||
--ds-shadow-card: 0 1px 2px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- typography
|
||||
Geist type ramp. Headings are weight 600 with tracking that tightens as
|
||||
size grows; copy is weight 400 with taller line-height; buttons and
|
||||
labels are single-line. */
|
||||
|
||||
@utility heading-72 {
|
||||
font-size: 72px;
|
||||
line-height: 72px;
|
||||
letter-spacing: -4.32px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-64 {
|
||||
font-size: 64px;
|
||||
line-height: 64px;
|
||||
letter-spacing: -3.84px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-56 {
|
||||
font-size: 56px;
|
||||
line-height: 56px;
|
||||
letter-spacing: -3.36px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-48 {
|
||||
font-size: 48px;
|
||||
line-height: 56px;
|
||||
letter-spacing: -2.88px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-40 {
|
||||
font-size: 40px;
|
||||
line-height: 48px;
|
||||
letter-spacing: -2.4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-32 {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
letter-spacing: -1.28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-24 {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
letter-spacing: -0.96px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-20 {
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
letter-spacing: -0.4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-16 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.32px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility heading-14 {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@utility copy-18 {
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@utility copy-16 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@utility copy-14 {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@utility copy-13 {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@utility label-14 {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@utility label-13 {
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@utility label-12 {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@utility button-16 {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@utility button-14 {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@utility button-12 {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------- base */
|
||||
|
||||
::selection {
|
||||
background-color: var(--ds-gray-1000);
|
||||
color: var(--ds-background-100);
|
||||
}
|
||||
|
||||
/* Anchor targets (heading deep links) land clear of the sticky header. */
|
||||
html {
|
||||
scroll-padding-top: 5rem;
|
||||
}
|
||||
|
||||
/* Geist focus ring: a 2px surface-colored gap plus a 2px blue ring. Never
|
||||
removed — only ever replaced by a component's own visible treatment. */
|
||||
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ds-background-100),
|
||||
0 0 0 4px var(--ds-focus-color);
|
||||
}
|
||||
|
||||
article table {
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
article th {
|
||||
border-bottom: 1px solid var(--ds-gray-alpha-400);
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--ds-gray-900);
|
||||
}
|
||||
|
||||
article td {
|
||||
border-bottom: 1px solid var(--ds-gray-alpha-200);
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--ds-gray-900);
|
||||
}
|
||||
|
||||
/* Definition lists (flag/subcommand references on the CLI page and
|
||||
friends). Literal HTML in MDX bypasses the mdx-components mapping, so
|
||||
these live next to the article table rules above. */
|
||||
article dl {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
article dt {
|
||||
margin-top: 0.75rem;
|
||||
color: var(--ds-gray-1000);
|
||||
}
|
||||
|
||||
article dt:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
article dd {
|
||||
margin-top: 0.25rem;
|
||||
padding-left: 1.25rem;
|
||||
line-height: 1.625;
|
||||
color: var(--ds-gray-900);
|
||||
}
|
||||
|
||||
article dl code {
|
||||
font-size: 13px;
|
||||
font-family: var(--font-mono);
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--ds-gray-alpha-100);
|
||||
padding: 0.125rem 0.375rem;
|
||||
}
|
||||
|
||||
.diff-add {
|
||||
color: var(--ds-green-700);
|
||||
}
|
||||
|
||||
.diff-remove {
|
||||
color: var(--ds-red-700);
|
||||
}
|
||||
|
||||
.shiki,
|
||||
.shiki span {
|
||||
color: var(--shiki-light) !important;
|
||||
background-color: var(--shiki-light-bg) !important;
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("how-it-works");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
# How It Works
|
||||
|
||||
## The pipeline
|
||||
|
||||
```
|
||||
TypeScript ──tsc: parse + typecheck──▶ lowering ──▶ typed IR ──▶ LLVM IR ──clang──▶ native executable
|
||||
└─────▶ C ────┘
|
||||
```
|
||||
|
||||
1. **Frontend** — the real TypeScript compiler parses and type-checks your program against `es2025` (plus `@types/node` when your project has it), honoring your `tsconfig.json` for checker strictness. The frontend then lowers the checked AST into a typed intermediate representation, using tsc's own type and narrowing answers to drive every decision. A construct with no lowering is a precise diagnostic at this stage — never a miscompile later.
|
||||
2. **Typed IR** — the only interface between the ends: a validated, serializable representation (`--emit-ir` writes it as JSON). Types are concrete here; generics have been monomorphized, unions are tagged values, closures have explicit captures.
|
||||
3. **Backends** — the LLVM backend is the default: it emits textual LLVM IR for most of the supported surface, and a program outside its tier falls back to the C backend transparently (one stderr note; `--backend llvm` pins it and fails with a diagnostic instead). The C backend is the reference, forever: deliberately readable, source-line-annotated output (`--backend c`), kept next to the binary by default (`--keep-c`). Both are compiled by the same clang, and where they overlap they must produce byte-identical program output.
|
||||
4. **Link** — the runtime is a C library of link-gated feature units: binaries pay only for what they use. A hello-world links nothing but libSystem; a regex-using program links the regex engine; an `http` server links the net stack.
|
||||
|
||||
Inspect any stage yourself:
|
||||
|
||||
```console
|
||||
$ scriptc build fib.ts --emit-ir
|
||||
$ ls .scriptc/
|
||||
fib
|
||||
fib.ir.json
|
||||
fib.ll
|
||||
$ scriptc build fib.ts --backend c --emit-ir
|
||||
$ ls .scriptc/
|
||||
fib
|
||||
fib.c
|
||||
fib.ir.json
|
||||
```
|
||||
|
||||
## The runtime
|
||||
|
||||
- **Memory** — values are reference-counted; an acyclic value is freed the moment its last reference drops. Reference cycles are collected at deterministic points by a cycle collector, not a concurrent GC. There are no GC pauses and no tracing heap.
|
||||
- **Concurrency** — `async`/`await` runs on stackful fibers with JS-exact scheduling: microtasks drain in the same order Node's do, timers fire in the same order, and the event loop (kqueue on macOS, epoll on Linux) has no external dependencies.
|
||||
- **The server stack** — `net`, `http`, `https`, `tls` (vendored mbedTLS), `dgram`, `dns` are native implementations on that same loop.
|
||||
- **Numbers** — JS-exact f64 semantics, including shortest-roundtrip number-to-string formatting fuzz-verified against Node's output.
|
||||
- **Regular expressions** — the same ECMAScript-exact bytecode interpreter QuickJS uses, linked only into regex-using binaries.
|
||||
|
||||
## The correctness story: differential testing
|
||||
|
||||
scriptc's correctness claim is not "we implemented the spec" — it is "we ran your semantics against Node's and they matched." Two lanes enforce it on every change:
|
||||
|
||||
- **The differential corpus** — every corpus program runs under Node *and* as a compiled binary; stdout, stderr, and exit codes must match **byte-for-byte**. Servers are exercised by live client drivers against both implementations. Number formatting was additionally fuzz-verified against Node's `String(x)` on a million random doubles.
|
||||
- **The memory-safety lane** — the entire corpus re-runs under AddressSanitizer with a reference-count audit at exit; a leak or use-after-free anywhere is a build failure. The same lane is available for your programs as `scriptc build --sanitize`.
|
||||
|
||||
Where matching Node byte-for-byte is impossible or deliberately not the goal (timing internals, error-object internals, aliasing at the dynamic boundary), the divergence is **documented and numbered** — see [Limitations](/limitations). Empty divergence space is load-bearing: the verified-identical list records what has been verified, not merely assumed.
|
||||
|
||||
## The dynamic island
|
||||
|
||||
`--dynamic` embeds quickjs-ng for [npm dependencies](/dependencies) and `any`-typed code. Architecturally, the island is a second world with its own heap and its own microtask queue; the boundary between the worlds copies values and validates every dynamic → static crossing at runtime. Static code never trusts the island — a lying type is a catchable `TypeError`, not corruption.
|
||||
|
||||
## Repository layout
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>What it is</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>packages/compiler</code></td>
|
||||
<td>The frontend (tsc API → IR), the typed IR with validator and serializer, the C and LLVM backends, the coverage analyzer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>packages/runtime</code></td>
|
||||
<td>The C runtime: refcounted values with a cycle collector, fibers and the event loop, the server stack, JS-exact number formatting, the island glue.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>packages/cli</code></td>
|
||||
<td><code>scriptc build | run | coverage</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The repository README covers the development workflow.
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("introduction");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
# Introduction
|
||||
|
||||
**scriptc compiles ordinary TypeScript into small, fast native executables** — no Node, no V8, no JavaScript engine in the binary. No changes to your code: no annotations, no dialect, no special standard library. The same TypeScript you run on Node, type-checked by the real TypeScript compiler, compiled to native code. What compiles behaves byte-for-byte like Node.
|
||||
|
||||
```console
|
||||
$ cat fib.ts
|
||||
function fib(n: number): number {
|
||||
return n < 2 ? n : fib(n - 1) + fib(n - 2);
|
||||
}
|
||||
console.log(fib(30));
|
||||
|
||||
$ scriptc run fib.ts
|
||||
832040
|
||||
|
||||
$ scriptc build fib.ts -o fib && ./fib
|
||||
832040
|
||||
```
|
||||
|
||||
The result is a self-contained executable in the ~320KB size class that starts in a few milliseconds and links against nothing but the system C library:
|
||||
|
||||
```console
|
||||
$ otool -L fib
|
||||
fib:
|
||||
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)
|
||||
```
|
||||
|
||||
## The idea: staticness you can see
|
||||
|
||||
Most TypeScript is far more static than the ecosystem assumes. scriptc decides, construct by construct, what can compile to native code — and tells you. Every construct in your program lands in exactly one of three tiers, and the tier is the promise:
|
||||
|
||||
1. **Compiled statically** — native code, no engine. The default, and the only mode unless you opt out. A program that stays in this tier produces byte-identical stdout and the same exit code as running the same file under Node, apart from a short, numbered list of documented divergences.
|
||||
2. **Runs dynamically** (with `--dynamic`) — an embedded JavaScript engine ([quickjs-ng](https://github.com/quickjs-ng/quickjs), ~620KB) executes what can't be static: npm dependencies' shipped JS and `any`-typed code. Every value crossing back into static code is validated at runtime — a lying type throws a catchable `TypeError` instead of corrupting memory.
|
||||
3. **Rejected** — everything else fails at compile time with a specific error code, a code frame, and usually a rewrite hint. Nothing is ever silently miscompiled.
|
||||
|
||||
The [coverage report](/coverage) makes the tiers visible for your program:
|
||||
|
||||
```console
|
||||
$ scriptc coverage cli.ts
|
||||
|
||||
statements analyzed 4
|
||||
compile statically 3 (75%)
|
||||
|
||||
runs with --dynamic 2 sites (embeds a JS engine, ~620KB — static stays the default)
|
||||
×1 importing 'picocolors' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there SC2013
|
||||
×1 values from the 'picocolors' package run in the embedded dynamic engine, which this build does not include SC2013
|
||||
```
|
||||
|
||||
## What compiles statically
|
||||
|
||||
The static surface covers the language and the standard library real programs use:
|
||||
|
||||
- **The language** — classes with single inheritance and dynamic dispatch, closures with JS capture semantics, generic function declarations (monomorphized), discriminated unions driven by TypeScript's own narrowing, `async`/`await` with JS-exact scheduling, exceptions with `finally`, destructuring, spread, optional/default/rest parameters, getters and setters, iterators, template literals, bitwise operators with JS-exact ToInt32 semantics, and the static slice of regular expressions.
|
||||
- **The standard library** — strings with UTF-16-exact surface semantics, arrays, `Map` and `Set` with JS-exact ordering, `JSON` with runtime-validated casts, `Math`, typed arrays and `Buffer`, `Error` hierarchies with typed `catch`.
|
||||
- **Node's API surface** — `fs` (sync and promises), `path`, `process`, `child_process`, `os`, `crypto`, `url`/`URL`, `zlib`, timers and signal handlers on a dependency-free event loop, and the server stack: `net`, `http`, `https`, `tls`, `dgram`, `dns`, `readline`. Real servers compile:
|
||||
|
||||
```ts:server.ts
|
||||
import { createServer } from "node:http";
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.setHeader("content-type", "application/json");
|
||||
res.end(JSON.stringify({ path: req.url, pid: process.pid }));
|
||||
});
|
||||
|
||||
server.listen(8080, () => {
|
||||
console.log("listening on http://localhost:8080");
|
||||
});
|
||||
```
|
||||
|
||||
```console
|
||||
$ scriptc build server.ts -o server && ./server &
|
||||
listening on http://localhost:8080
|
||||
$ curl -s http://localhost:8080/status
|
||||
{"path":"/status","pid":90126}
|
||||
```
|
||||
|
||||
Programs typecheck against TypeScript's real `es2025` lib (plus `@types/node` when your project has it), and your `tsconfig.json` governs checker strictness. Anything reached that has no lowering is a precise diagnostic, never a surprise. The [limitations page](/limitations) lists what doesn't compile and what diverges by design, in plain language.
|
||||
|
||||
## Escape hatches and their costs
|
||||
|
||||
- **`--dynamic`** embeds the engine for [npm dependencies](/dependencies) and `any`-typed code. `scriptc coverage --dynamic` reports exactly which statements run where. Static stays the default: a binary never silently grows an engine.
|
||||
- **Checked casts** — `JSON.parse(...) as Config` inserts a runtime validation that throws a catchable error naming the offending path. TypeScript's `as` is a promise; scriptc verifies it:
|
||||
|
||||
```console
|
||||
$ cat cast.ts
|
||||
type Config = { port: number };
|
||||
try {
|
||||
const cfg = JSON.parse('{"port": "eighty"}') as Config;
|
||||
console.log(cfg.port);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.log(`caught: ${e.message}`);
|
||||
}
|
||||
|
||||
$ scriptc run cast.ts
|
||||
caught: expected number at $.port, got string
|
||||
```
|
||||
|
||||
- **`comptime(() => ...)`** runs TypeScript at build time (in an isolated VM inside the compiler) and bakes the result into the binary as a literal:
|
||||
|
||||
```console
|
||||
$ cat banner.ts
|
||||
const build = comptime(() => `built ${new Date().toISOString().slice(0, 10)}`);
|
||||
console.log(build);
|
||||
|
||||
$ scriptc run banner.ts
|
||||
built 2026-07-22
|
||||
```
|
||||
|
||||
## Correctness as methodology
|
||||
|
||||
Two enforcement mechanisms run on every change:
|
||||
|
||||
- **Differential testing** — every corpus program runs under Node *and* as a native binary; stdout, stderr, and exit codes must match byte-for-byte. Number formatting is JS-exact (shortest-roundtrip, fuzz-verified against Node). Servers are tested with live client drivers against both implementations.
|
||||
- **Memory-safety lane** — the entire corpus re-runs under AddressSanitizer with a reference-count audit; leaks and use-after-free are build failures.
|
||||
|
||||
The deliberate divergences from Node — mostly around timing internals and error-object properties — are documented and numbered; nothing diverges silently. [How It Works](/how-it-works) covers the architecture behind this.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Quickstart](/quickstart) — build the compiler and your first binary.
|
||||
- [CLI Reference](/cli) — every command and flag.
|
||||
- [Coverage Reports](/coverage) — reading `scriptc coverage` output.
|
||||
- [npm Dependencies](/dependencies) — the embedded-engine story.
|
||||
- [Limitations](/limitations) — what doesn't compile, and why.
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistPixelSquare } from "geist/font/pixel";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { HeaderNav, HeaderMobileMenu } from "@/components/header-nav";
|
||||
import { DocsMobileNav } from "@/components/docs-mobile-nav";
|
||||
import { DocsNav } from "@/components/docs-nav";
|
||||
import { getStarCount } from "@/lib/github";
|
||||
import { siteName, siteUrl, tagline, description, githubUrl } from "@/lib/site";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: {
|
||||
default: `${siteName} | ${tagline}`,
|
||||
template: `%s | ${siteName}`,
|
||||
},
|
||||
description,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: siteUrl,
|
||||
siteName,
|
||||
title: `${siteName} | ${tagline}`,
|
||||
description,
|
||||
images: [{ url: "/og", width: 1200, height: 630, alt: siteName }],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: `${siteName} | ${tagline}`,
|
||||
description,
|
||||
images: ["/og"],
|
||||
},
|
||||
};
|
||||
|
||||
function Header({ stars }: { stars?: string }) {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-gray-alpha-400 bg-background-100/80 backdrop-blur-sm">
|
||||
<div className="flex h-16 items-center justify-between px-4 gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="https://vercel.com" title="Made with love by Vercel">
|
||||
<svg
|
||||
data-testid="geist-icon"
|
||||
height="18"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="18"
|
||||
style={{ color: "currentcolor" }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 1L16 15H0L8 1Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<span className="text-gray-500">
|
||||
<svg
|
||||
data-testid="geist-icon"
|
||||
height="16"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
style={{ color: "currentcolor" }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<Link href="/">
|
||||
<span className={`${GeistPixelSquare.className} text-lg`}>{siteName}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<HeaderNav />
|
||||
<a
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 label-14 text-gray-900 hover:text-gray-1000 transition-colors"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
{stars && <span>{stars}</span>}
|
||||
</a>
|
||||
<div className="hidden md:block">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<HeaderMobileMenu />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const stars = await getStarCount();
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
suppressHydrationWarning
|
||||
// overscroll-none on the root scroller: the document never
|
||||
// rubber-bands past its edges.
|
||||
className={`${GeistSans.variable} ${GeistMono.variable} overscroll-none`}
|
||||
>
|
||||
<body className="overscroll-none bg-background-100 text-gray-1000 antialiased">
|
||||
<ThemeProvider>
|
||||
<Header stars={stars} />
|
||||
<DocsMobileNav />
|
||||
<DocsNav>{children}</DocsNav>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("limitations");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# Limitations
|
||||
|
||||
Honesty is the product. The static surface is large but not total, and a few behaviors diverge from Node **by design**. This page is the plain-language version; `scriptc coverage` on your program is the real answer for your code, and every blocker it reports is specific and coded. Nothing on this page is silent: everything here is either a compile error or a documented, numbered divergence.
|
||||
|
||||
## What doesn't compile (yet)
|
||||
|
||||
These are rejected at compile time with an `SC` code, a code frame, and usually a rewrite hint. A non-exhaustive tour of the ones you're most likely to meet:
|
||||
|
||||
**Language edges**
|
||||
|
||||
- `var` declarations, and loose equality `==`/`!=` (they need dynamic coercion semantics; use `let`/`const` and `===`).
|
||||
- Labeled `break`/`continue`, and jumps crossing a `finally` block (`return` through `finally` is supported).
|
||||
- Top-level `await` — wrap it in an `async function main()` and call it.
|
||||
- Generic classes, generic methods, and generic arrows — only top-level generic function declarations monomorphize.
|
||||
- Generic functions and stdlib methods used as *values* (`const f = id`, `const g = Math.floor`) — call them directly.
|
||||
|
||||
**Types and shapes**
|
||||
|
||||
- **Record shapes are exact structs.** Passing `{a, b}` where `{a}` is expected is SC2002. Where the compiler does accept a strict field-subset flow, it copies the record — see divergences below.
|
||||
- Union edges: unions used whole where a per-arm answer is needed (`console.log(u)` on a union — narrow first), union-into-union widening, function arms beside data arms.
|
||||
- Watch for **tuple inference**: `Promise.all([work(1), work(2)])` infers a tuple type, and tuple edges (like `.join` on a tuple) are fenced. Type the array first:
|
||||
|
||||
```ts
|
||||
const jobs: Promise<number>[] = [work(1), work(2), work(3)];
|
||||
const results = await Promise.all(jobs); // number[] — compiles
|
||||
```
|
||||
|
||||
- `undefined`/`null` exist only as union arms, not as standalone values; optional *class* fields are fenced (optional record fields and optional/default/rest parameters compile).
|
||||
|
||||
**Standard library**
|
||||
|
||||
- The type checker sees the full standard library; only the supported surface compiles. Reaching declared-but-unlowered surface is SC2020 with the supported alternatives in the hint — e.g. parts of the regex API (`re.exec`), `Symbol`, `globalThis`, array/Map/Set methods beyond the lowered sets.
|
||||
- Map keys and Set elements are strings and numbers; other key types are fenced.
|
||||
|
||||
**The `any`/`unknown` boundary**
|
||||
|
||||
- `any` without `--dynamic` is a compile error (SC2011) — use `unknown` and a checked cast, or opt into the engine.
|
||||
- `any` and `unknown` ride locals, parameters, and returns — never class fields, array elements, or union arms (record and tuple fields hold `unknown`).
|
||||
- Operations on `unknown` beyond the supported surface (truthiness, `typeof` narrowing, property access, `+`, `switch`, `throw`) need a checked cast first.
|
||||
|
||||
Run `scriptc build` on a file to get the full, current list for that program — the compiler is always more up to date than this page.
|
||||
|
||||
## What diverges by design
|
||||
|
||||
A static-tier program otherwise produces byte-identical stdout and the same exit code as Node. Every known divergence is deliberate and pinned by the differential test suite; these are the ones with real consequences:
|
||||
|
||||
**Arrays are dense — invalid indices trap.** scriptc arrays have no holes and no `undefined` elements. Where JS would produce `undefined` (out-of-bounds read, `pop()` on empty), the runtime prints a `RangeError` message and aborts. This catches real bugs — but it means `process.argv[2]` with no third argument is a trap, not `undefined`:
|
||||
|
||||
```ts
|
||||
// JS habit — traps when argv[2] doesn't exist:
|
||||
const who = process.argv[2] ?? "world";
|
||||
// scriptc-safe (the length check is explicit instead of implied):
|
||||
const who = process.argv.length > 2 ? process.argv[2] : "world";
|
||||
```
|
||||
|
||||
**Runtime traps are not catchable.** User `throw` is fully catchable, and runtime failures Node models as exceptions (JSON parse errors, checked-cast failures, fs errors, regex errors) throw real error objects — but the hard-trap set (array index violations and friends) aborts the process rather than throwing.
|
||||
|
||||
**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. Same stance at the dynamic boundary: values cross by copy, never 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).
|
||||
|
||||
**Strings are stored as UTF-8.** Invisible through `.length` and the string methods (which compute UTF-16 semantics) — except relational comparison (`<`, `>`), which uses code-point order, and surrogate-splitting operations, which produce U+FFFD.
|
||||
|
||||
**Memory is reference-counted.** Acyclic values free deterministically; reference cycles are collected at deterministic collection points, not by a concurrent GC. Cycles that cross the static/island boundary are uncollectable by either side.
|
||||
|
||||
**Process shape** — `process.argv[0]` is `"scriptc"` and `argv[1]` is the binary's path (positions line up with Node; `argv[2]` onward are your args). The uncaught-exception stderr line reads `Uncaught <value>` instead of Node's stack-trace block (exit code and pre-throw stdout are identical). Runtime errors carry `message` and Node's `code`, but not `errno`/`syscall`/`path`.
|
||||
|
||||
**Comparator call sequences differ in `sort`** (stable insertion sort here, TimSort in V8 — sorted results are byte-identical for consistent comparators), and **`localeCompare` compares code units**, not ICU collation.
|
||||
|
||||
## Dynamic-tier limits
|
||||
|
||||
- **The island is quickjs-ng, not V8** — correct, but slower for CPU-bound dependency code. The win is startup, size, memory, deployment shape.
|
||||
- **The island's Node builtins are shims** — reimplementations, reported per-builtin in the coverage report, not the real modules.
|
||||
- **Island microtask interleaving**: static fibers drain first, then the engine's jobs at loop quiescence — a static `await` racing a package promise resolves in a documented, deterministic order that can differ from Node's interleaving.
|
||||
- **`--npm-static` and `--provenance-sources` are experimental** — see [npm Dependencies](/dependencies) for the maturity notes.
|
||||
- **Cross-compiled `--dynamic` binaries are not supported yet** — the engine archive is host-native today.
|
||||
|
||||
## Tooling gaps
|
||||
|
||||
- `scriptc run` does not forward extra CLI arguments to the program — `build` and invoke the binary directly.
|
||||
- Numbers are JS-exact f64 everywhere. Integer inference and ownership analysis — the systems-language performance ceiling — are roadmap, not shipped.
|
||||
- No Windows servers or `child_process` yet — see [Platform Support](/platforms).
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPageTitle, renderOgImage } from "../og-image";
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string[] }> }) {
|
||||
const { slug } = await params;
|
||||
const title = getPageTitle(slug.join("/"));
|
||||
|
||||
if (!title) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return renderOgImage(title);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { siteName } from "@/lib/site";
|
||||
|
||||
export { getPageTitle } from "@/lib/page-titles";
|
||||
|
||||
let fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null = null;
|
||||
|
||||
async function loadFonts() {
|
||||
if (fontCache) return fontCache;
|
||||
const [geistRegular, geistPixelSquare] = await Promise.all([
|
||||
readFile(join(process.cwd(), "public/Geist-Regular.ttf")),
|
||||
readFile(join(process.cwd(), "public/GeistPixel-Square.ttf")),
|
||||
]);
|
||||
fontCache = { geistRegular, geistPixelSquare };
|
||||
return fontCache;
|
||||
}
|
||||
|
||||
export async function renderOgImage(title: string) {
|
||||
const { geistRegular, geistPixelSquare } = await loadFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "black",
|
||||
padding: "60px 80px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
}}
|
||||
>
|
||||
<svg width="36" height="36" viewBox="0 0 16 16" fill="white">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8 1L16 15H0L8 1Z" />
|
||||
</svg>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 36,
|
||||
color: "#666",
|
||||
fontFamily: "Geist",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontFamily: "GeistPixelSquare",
|
||||
fontWeight: 400,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
{siteName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{title.split("\n").map((line, i) => (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontFamily: "Geist",
|
||||
fontWeight: 400,
|
||||
color: "white",
|
||||
letterSpacing: "-0.02em",
|
||||
textAlign: "center",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Geist",
|
||||
data: geistRegular.buffer as ArrayBuffer,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: "GeistPixelSquare",
|
||||
data: geistPixelSquare.buffer as ArrayBuffer,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { getPageTitle, renderOgImage } from "./og-image";
|
||||
|
||||
export async function GET() {
|
||||
const title = getPageTitle("")!;
|
||||
return renderOgImage(title);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import Link from "next/link";
|
||||
import { Code } from "@/components/code";
|
||||
import { siteName, githubUrl } from "@/lib/site";
|
||||
|
||||
const HERO_DEMO = `$ cat fib.ts
|
||||
function fib(n: number): number {
|
||||
return n < 2 ? n : fib(n - 1) + fib(n - 2);
|
||||
}
|
||||
console.log(fib(30));
|
||||
|
||||
$ scriptc run fib.ts
|
||||
832040
|
||||
|
||||
$ scriptc build fib.ts -o fib && ./fib
|
||||
832040`;
|
||||
|
||||
const COVERAGE_DEMO = `$ scriptc coverage cli.ts
|
||||
|
||||
statements analyzed 4
|
||||
compile statically 3 (75%)
|
||||
|
||||
runs with --dynamic 2 sites (embeds a JS engine, ~620KB — static stays the default)
|
||||
×1 importing 'picocolors' requires the embedded dynamic engine — the package's implementation runs there SC2013`;
|
||||
|
||||
const tiers = [
|
||||
{
|
||||
title: "Compiled statically",
|
||||
body: "The default. Ordinary TypeScript — classes, closures, async/await, the stdlib, Node's fs/path/process/http surface — becomes native code with no engine in the binary. What compiles behaves byte-for-byte like Node.",
|
||||
},
|
||||
{
|
||||
title: "Runs dynamically",
|
||||
body: "Opt in with --dynamic: an embedded JavaScript engine (~620KB) executes what can't be static — npm dependencies' shipped JS, any-typed code. Every value crossing back into static code is validated at runtime.",
|
||||
},
|
||||
{
|
||||
title: "Rejected at compile time",
|
||||
body: "Everything else fails the build with a specific error code, a code frame, and usually a rewrite hint. Nothing is ever silently miscompiled.",
|
||||
},
|
||||
];
|
||||
|
||||
const points = [
|
||||
{
|
||||
title: "No code changes",
|
||||
body: "No annotations, no dialect, no special stdlib. The same TypeScript you run on Node, type-checked by the real TypeScript compiler.",
|
||||
},
|
||||
{
|
||||
title: "Small and fast",
|
||||
body: "A hello-world binary is ~320KB, starts in about 4ms, and links against nothing but libSystem. Node needs a ~120MB runtime and ~35ms to print the same line.",
|
||||
},
|
||||
{
|
||||
title: "Measured coverage",
|
||||
body: "scriptc coverage tells you, statement by statement, what compiles statically, what needs the dynamic engine, and exactly what blocks the rest.",
|
||||
},
|
||||
{
|
||||
title: "Differentially tested",
|
||||
body: "Every corpus program runs under Node and as a native binary; stdout, stderr, and exit codes must match byte-for-byte. The whole corpus re-runs under AddressSanitizer.",
|
||||
},
|
||||
];
|
||||
|
||||
function TerminalPane({ title, code }: { title: string; code: string }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md border border-gray-alpha-400 bg-background-100 text-left shadow-card">
|
||||
<div className="flex items-center gap-1.5 border-b border-gray-alpha-400 bg-background-200 px-4 py-2.5 dark:bg-gray-alpha-100">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-gray-500" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-gray-500" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-gray-500" />
|
||||
<span className="ml-3 font-mono label-12 text-gray-900">{title}</span>
|
||||
</div>
|
||||
<div className="[&>div]:my-0! [&>div]:rounded-none! [&>div]:border-none! [&>div]:bg-transparent!">
|
||||
<Code lang="console">{code}</Code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
{/* Hero */}
|
||||
<section className="relative overflow-hidden">
|
||||
<div className="relative mx-auto max-w-[1200px] px-6 pt-16 text-center sm:pt-24">
|
||||
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.18em] text-gray-900 sm:text-xs sm:tracking-[0.25em]">
|
||||
macOS · Linux · Windows
|
||||
</p>
|
||||
<h1 className="mx-auto mt-4 max-w-5xl heading-40 text-gray-1000 sm:heading-64 lg:heading-72">
|
||||
The native compiler <br className="hidden sm:block" />
|
||||
for TypeScript
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl copy-16 text-gray-900 sm:copy-18">
|
||||
Ordinary TypeScript becomes a small, fast native binary — no Node, no V8, no
|
||||
JavaScript engine required. What compiles behaves byte-for-byte like Node.
|
||||
</p>
|
||||
<div className="mt-8 flex items-center justify-center gap-3">
|
||||
<Link
|
||||
href="/quickstart"
|
||||
className="flex h-10 items-center rounded-md bg-gray-1000 px-5 button-14 text-background-100 transition-opacity hover:opacity-90"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
<a
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex h-10 items-center rounded-md border border-gray-alpha-400 px-5 button-14 text-gray-1000 transition-colors hover:border-gray-alpha-500"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mx-auto mt-8 max-w-3xl px-6 pb-16 sm:mt-10 sm:pb-24">
|
||||
<TerminalPane title="fib.ts — compiled with scriptc" code={HERO_DEMO} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Three tiers */}
|
||||
<section className="border-t border-gray-alpha-400">
|
||||
<div className="mx-auto max-w-[1200px] px-6 py-16">
|
||||
<h2 className="heading-24 text-gray-1000">Three tiers, always explicit</h2>
|
||||
<p className="mt-3 max-w-2xl copy-14 text-gray-900">
|
||||
Every construct in your program lands in exactly one tier, and the tier is the
|
||||
promise.
|
||||
</p>
|
||||
<div className="mt-8 grid gap-6 md:grid-cols-3">
|
||||
{tiers.map((tier, i) => (
|
||||
<div key={tier.title} className="rounded-lg border border-gray-alpha-400 p-6">
|
||||
<div className="label-12 font-medium uppercase tracking-wider text-gray-700">
|
||||
Tier {i + 1}
|
||||
</div>
|
||||
<h3 className="mt-2 heading-16 text-gray-1000">{tier.title}</h3>
|
||||
<p className="mt-2 copy-13 text-gray-900">{tier.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Coverage demo */}
|
||||
<section className="border-t border-gray-alpha-400">
|
||||
<div className="mx-auto max-w-[1200px] px-6 py-16">
|
||||
<div className="grid items-center gap-10 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="heading-24 text-gray-1000">Staticness you can see</h2>
|
||||
<p className="mt-3 copy-14 text-gray-900">
|
||||
Most TypeScript is far more static than the ecosystem assumes. {siteName}{" "}
|
||||
decides, construct by construct, what can compile to native code — and tells
|
||||
you. A binary never silently grows an engine: the dynamic tier is opt-in, and
|
||||
the coverage report names every site that needs it.
|
||||
</p>
|
||||
<Link
|
||||
href="/coverage"
|
||||
className="mt-4 inline-block label-14 text-gray-1000 underline underline-offset-4"
|
||||
>
|
||||
Reading coverage reports →
|
||||
</Link>
|
||||
</div>
|
||||
<Code lang="console">{COVERAGE_DEMO}</Code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Points */}
|
||||
<section className="border-t border-gray-alpha-400">
|
||||
<div className="mx-auto max-w-[1200px] px-6 py-16">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{points.map((point) => (
|
||||
<div key={point.title} className="rounded-lg border border-gray-alpha-400 p-6">
|
||||
<h3 className="heading-16 text-gray-1000">{point.title}</h3>
|
||||
<p className="mt-2 copy-13 text-gray-900">{point.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer CTA */}
|
||||
<section className="border-t border-gray-alpha-400">
|
||||
<div className="mx-auto max-w-[1200px] px-6 py-16 text-center">
|
||||
<h2 className="heading-24 text-gray-1000">Compile your first binary</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl copy-14 text-gray-900">
|
||||
Clone the repository, build the compiler, and turn a TypeScript file into a native
|
||||
executable in a couple of minutes.
|
||||
</p>
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<Link
|
||||
href="/quickstart"
|
||||
className="flex h-10 items-center rounded-md bg-gray-1000 px-5 button-14 text-background-100 transition-opacity hover:opacity-90"
|
||||
>
|
||||
Quickstart
|
||||
</Link>
|
||||
<Link
|
||||
href="/introduction"
|
||||
className="flex h-10 items-center rounded-md border border-gray-alpha-400 px-5 button-14 text-gray-1000 transition-colors hover:border-gray-alpha-500"
|
||||
>
|
||||
Introduction
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("platforms");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# Platform Support
|
||||
|
||||
## macOS (arm64)
|
||||
|
||||
The primary platform. clang — preinstalled with the Xcode Command Line Tools — is the only system dependency for producing binaries. The full surface is supported: the language, the stdlib, the Node API surface including the server stack, `--dynamic`, and the sanitizer lane.
|
||||
|
||||
## Cross-compilation via zig
|
||||
|
||||
scriptc cross-compiles from macOS using zig's bundled clang and sysroots. Install [zig](https://ziglang.org), then select it with two environment variables:
|
||||
|
||||
<dl>
|
||||
<dt><code>SCRIPTC_CC=zigcc</code></dt>
|
||||
<dd>Use zig's clang as the C compiler.</dd>
|
||||
<dt><code>SCRIPTC_TARGET=<triple></code></dt>
|
||||
<dd>The target triple, including a glibc version for Linux targets.</dd>
|
||||
</dl>
|
||||
|
||||
### Linux (arm64, x86_64)
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux
|
||||
$ file fib-linux
|
||||
fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped
|
||||
```
|
||||
|
||||
The runtime has native Linux backends throughout: the event loop is epoll (kqueue on macOS), the server stack and TLS (with distro CA-bundle probing) and `fs.watch` all have Linux implementations, verified against Linux Node in a container-based differential lane. `x86_64-linux-gnu.2.36` works the same way.
|
||||
|
||||
### Windows (x86_64)
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=x86_64-windows-gnu scriptc build fib.ts -o fib.exe
|
||||
$ file fib.exe
|
||||
fib.exe: PE32+ executable (console) x86-64, for MS Windows
|
||||
```
|
||||
|
||||
The language, stdlib, `fs`, and async surfaces pass a differential lane against Windows Node. **Not ported yet:** the socket/server stack (`net`/`http`/`https`/`tls`/`dgram`) and `child_process`.
|
||||
|
||||
## What doesn't cross-compile yet
|
||||
|
||||
- **`--dynamic` binaries are host-native only.** The embedded engine archive is built per target; cross-compiling a `--dynamic` build is not supported today. Static programs cross-compile fully.
|
||||
- `--sanitize` is a host-build lane.
|
||||
|
||||
## Summary
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<th>How</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>macOS arm64</td>
|
||||
<td>native</td>
|
||||
<td>Primary: full surface, <code>--dynamic</code>, sanitizer lane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux arm64 / x86_64</td>
|
||||
<td><code>SCRIPTC_CC=zigcc SCRIPTC_TARGET=<arch>-linux-gnu.2.36</code></td>
|
||||
<td>Static surface incl. servers, TLS, fs.watch; verified against Linux Node</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows x86_64</td>
|
||||
<td><code>SCRIPTC_CC=zigcc SCRIPTC_TARGET=x86_64-windows-gnu</code></td>
|
||||
<td>Language, stdlib, fs, async; no servers or child_process yet</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("quickstart");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# Quickstart
|
||||
|
||||
scriptc is installed from source today; an npm package (`npm install -g scriptc`) is coming. Building from a clone takes a couple of minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **macOS arm64** is the primary platform ([Linux and Windows](/platforms) are cross-compilation targets).
|
||||
- **clang** — preinstalled with the Xcode Command Line Tools.
|
||||
- **Node ≥ 24** and **pnpm** — for the compiler itself. The binaries it produces need neither.
|
||||
|
||||
## Install from source
|
||||
|
||||
```console
|
||||
$ git clone https://github.com/vercel-labs/scriptc && cd scriptc
|
||||
$ pnpm install && pnpm build
|
||||
```
|
||||
|
||||
The repo exposes the CLI as a package script, so `pnpm scriptc` works from the repo directory. For a shell-wide command, alias it:
|
||||
|
||||
```console
|
||||
$ alias scriptc="pnpm --dir $PWD scriptc"
|
||||
```
|
||||
|
||||
## Your first binary
|
||||
|
||||
Write ordinary TypeScript:
|
||||
|
||||
```ts:hello.ts
|
||||
const who: string = process.argv.length > 2 ? process.argv[2] : "world";
|
||||
console.log(`hello, ${who}`);
|
||||
```
|
||||
|
||||
Compile and run in one step:
|
||||
|
||||
```console
|
||||
$ scriptc run hello.ts
|
||||
hello, world
|
||||
```
|
||||
|
||||
Or produce the executable:
|
||||
|
||||
```console
|
||||
$ scriptc build hello.ts -o hello
|
||||
$ ./hello scriptc
|
||||
hello, scriptc
|
||||
$ ls -la hello
|
||||
-rwxr-xr-x 1 you staff 329752 hello
|
||||
```
|
||||
|
||||
That is a self-contained native binary — no Node, no node_modules, no JavaScript engine. It starts in about 4ms where Node takes ~35ms to print the same line.
|
||||
|
||||
Note the `process.argv.length` guard in `hello.ts`: scriptc arrays are dense, so an out-of-bounds read like `process.argv[2]` with no third argument is a runtime trap, not `undefined`. This is one of the documented [divergences](/limitations); the compiler and runtime tell you rather than silently differing.
|
||||
|
||||
## See what compiles
|
||||
|
||||
Ask the compiler how static your program is — and what, precisely, isn't:
|
||||
|
||||
```console
|
||||
$ scriptc coverage hello.ts
|
||||
|
||||
statements analyzed 2
|
||||
compile statically 2 (100%)
|
||||
|
||||
fully static — this program has no dynamic remainder.
|
||||
```
|
||||
|
||||
For programs that import npm packages or use `any`, the report names each dynamic site and each blocker with an error code. See [Coverage Reports](/coverage).
|
||||
|
||||
## Use an npm dependency
|
||||
|
||||
npm packages run in an embedded JavaScript engine, opted in with `--dynamic`:
|
||||
|
||||
```ts:cli.ts
|
||||
import pc from "picocolors";
|
||||
|
||||
function banner(text: string): string {
|
||||
return pc.bold(pc.green(text));
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
console.log(banner("scriptc demo"));
|
||||
console.log(`args: ${args.length}`);
|
||||
```
|
||||
|
||||
```console
|
||||
$ npm install picocolors
|
||||
$ scriptc build cli.ts --dynamic -o demo
|
||||
$ ./demo one two
|
||||
scriptc demo
|
||||
args: 2
|
||||
```
|
||||
|
||||
The package's JS is embedded into the binary at build time — the executable never reads `node_modules` and runs from anywhere. See [npm Dependencies](/dependencies) for how the boundary works.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [CLI Reference](/cli) — every command and flag, including `--emit-ir`, `--backend llvm`, and `--sanitize`.
|
||||
- [Platform Support](/platforms) — cross-compiling to Linux and Windows with zig.
|
||||
- [Limitations](/limitations) — what doesn't compile yet.
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { siteUrl } from "@/lib/site";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: { userAgent: "*", allow: "/" },
|
||||
sitemap: `${siteUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { allDocsPages } from "@/lib/docs-navigation";
|
||||
import { siteUrl } from "@/lib/site";
|
||||
import { statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
// The homepage lives in the site header, not the docs nav, so list it explicitly.
|
||||
const hrefs = ["/", ...allDocsPages.map((page) => page.href)];
|
||||
return hrefs.map((href) => ({
|
||||
url: `${siteUrl}${href}`,
|
||||
lastModified: lastModifiedFor(href),
|
||||
}));
|
||||
}
|
||||
|
||||
function lastModifiedFor(href: string): Date {
|
||||
const relative = href === "/" ? "page.tsx" : path.join(href.slice(1), "page.mdx");
|
||||
try {
|
||||
return statSync(path.join(process.cwd(), "src", "app", relative)).mtime;
|
||||
} catch {
|
||||
return new Date("2026-07-22T00:00:00.000Z");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { codeToHtml } from "shiki";
|
||||
|
||||
const vercelDarkTheme = {
|
||||
name: "vercel-dark",
|
||||
type: "dark" as const,
|
||||
colors: {
|
||||
"editor.background": "transparent",
|
||||
"editor.foreground": "#EDEDED",
|
||||
},
|
||||
settings: [
|
||||
{
|
||||
scope: ["punctuation.separator.prompt.shell-session"],
|
||||
settings: { foreground: "#A1A1A1" },
|
||||
},
|
||||
{
|
||||
scope: ["source.shell", "entity.name.command.shell", "meta.statement.shell"],
|
||||
settings: { foreground: "#EDEDED" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.command"],
|
||||
settings: { foreground: "#C472FB" },
|
||||
},
|
||||
{
|
||||
scope: ["constant.other.option", "string.other.option"],
|
||||
settings: { foreground: "#FF9300" },
|
||||
},
|
||||
{
|
||||
scope: ["punctuation.separator.statement.and.shell", "keyword.operator.pipe.shell"],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["meta.output.shell-session"],
|
||||
settings: { foreground: "#A1A1A1" },
|
||||
},
|
||||
|
||||
{
|
||||
scope: ["comment", "punctuation.definition.comment"],
|
||||
settings: { foreground: "#A1A1A1" },
|
||||
},
|
||||
{
|
||||
scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
|
||||
settings: { foreground: "#00CA50" },
|
||||
},
|
||||
{
|
||||
scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
|
||||
settings: { foreground: "#47A8FF" },
|
||||
},
|
||||
{
|
||||
scope: ["keyword", "storage.type", "storage.modifier"],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["keyword.operator", "keyword.control"],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.function", "support.function", "meta.function-call"],
|
||||
settings: { foreground: "#C472FB" },
|
||||
},
|
||||
{
|
||||
scope: ["variable", "variable.other"],
|
||||
settings: { foreground: "#EDEDED" },
|
||||
},
|
||||
{
|
||||
scope: ["variable.parameter"],
|
||||
settings: { foreground: "#FF9300" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["punctuation", "meta.brace", "meta.bracket"],
|
||||
settings: { foreground: "#EDEDED" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.type.property-name",
|
||||
"entity.name.tag.json",
|
||||
"meta.object-literal.key",
|
||||
"punctuation.support.type.property-name",
|
||||
],
|
||||
settings: { foreground: "#FF4D8D" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.other.attribute-name"],
|
||||
settings: { foreground: "#00CA50" },
|
||||
},
|
||||
{
|
||||
scope: ["support.type.primitive", "entity.name.type.primitive"],
|
||||
settings: { foreground: "#00CA50" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const vercelLightTheme = {
|
||||
name: "vercel-light",
|
||||
type: "light" as const,
|
||||
colors: {
|
||||
"editor.background": "transparent",
|
||||
"editor.foreground": "#171717",
|
||||
},
|
||||
settings: [
|
||||
{
|
||||
scope: ["punctuation.separator.prompt.shell-session"],
|
||||
settings: { foreground: "#666666" },
|
||||
},
|
||||
{
|
||||
scope: ["source.shell", "entity.name.command.shell", "meta.statement.shell"],
|
||||
settings: { foreground: "#171717" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.command"],
|
||||
settings: { foreground: "#7D00CC" },
|
||||
},
|
||||
{
|
||||
scope: ["constant.other.option", "string.other.option"],
|
||||
settings: { foreground: "#A35200" },
|
||||
},
|
||||
{
|
||||
scope: ["punctuation.separator.statement.and.shell", "keyword.operator.pipe.shell"],
|
||||
settings: { foreground: "#C41562" },
|
||||
},
|
||||
{
|
||||
scope: ["meta.output.shell-session"],
|
||||
settings: { foreground: "#666666" },
|
||||
},
|
||||
|
||||
{
|
||||
scope: ["comment", "punctuation.definition.comment"],
|
||||
settings: { foreground: "#6B7280" },
|
||||
},
|
||||
{
|
||||
scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
|
||||
settings: { foreground: "#067A6E" },
|
||||
},
|
||||
{
|
||||
scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
|
||||
settings: { foreground: "#0070C0" },
|
||||
},
|
||||
{
|
||||
scope: ["keyword", "storage.type", "storage.modifier"],
|
||||
settings: { foreground: "#D6409F" },
|
||||
},
|
||||
{
|
||||
scope: ["keyword.operator", "keyword.control"],
|
||||
settings: { foreground: "#D6409F" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.function", "support.function", "meta.function-call"],
|
||||
settings: { foreground: "#6E56CF" },
|
||||
},
|
||||
{
|
||||
scope: ["variable", "variable.other"],
|
||||
settings: { foreground: "#171717" },
|
||||
},
|
||||
{
|
||||
scope: ["variable.parameter"],
|
||||
settings: { foreground: "#B45309" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
|
||||
settings: { foreground: "#D6409F" },
|
||||
},
|
||||
{
|
||||
scope: ["punctuation", "meta.brace", "meta.bracket"],
|
||||
settings: { foreground: "#6B7280" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"support.type.property-name",
|
||||
"entity.name.tag.json",
|
||||
"meta.object-literal.key",
|
||||
"punctuation.support.type.property-name",
|
||||
],
|
||||
settings: { foreground: "#D6409F" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.other.attribute-name"],
|
||||
settings: { foreground: "#067A6E" },
|
||||
},
|
||||
{
|
||||
scope: ["support.type.primitive", "entity.name.type.primitive"],
|
||||
settings: { foreground: "#067A6E" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* The optional filename row atop a fence: a small file glyph (the site's
|
||||
* inline-SVG icon register — 16 viewBox, 1.5 stroke, h-3.5) and the path in
|
||||
* the mono register, quiet against the header band. Standalone fences render
|
||||
* it as their own top bar; inside a two-language CodeToggle the toggle's CSS
|
||||
* lifts the row into the shared header bar, opposite the segmented control
|
||||
* (see code-toggle.tsx). The h-9 height is the contract that makes that
|
||||
* overlay line up with the toggle bar — change one, change both.
|
||||
*/
|
||||
function FilenameRow({ filename }: { filename: string }) {
|
||||
return (
|
||||
<div
|
||||
data-code-filename=""
|
||||
className="flex h-9 items-center gap-2 border-b border-neutral-200 bg-neutral-100 px-4 text-xs text-neutral-500 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" className="h-3.5 w-3.5 shrink-0" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M9.5 1.5h-5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V4.5l-3-3Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M9.5 1.5v3h3" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span className="truncate">{filename}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffBlock({ children, filename }: { children: string; filename?: string }) {
|
||||
const lines = children.trim().split("\n");
|
||||
return (
|
||||
<div
|
||||
data-language="diff"
|
||||
className="my-4 rounded-lg border border-neutral-200 bg-neutral-50 text-[13px] font-mono overflow-hidden dark:border-neutral-800 dark:bg-neutral-900"
|
||||
>
|
||||
{filename ? <FilenameRow filename={filename} /> : null}
|
||||
<pre className="m-0 overflow-x-auto">
|
||||
<code>
|
||||
{lines.map((line, i) => {
|
||||
let cls = "block px-4";
|
||||
if (i === 0) cls += " pt-4";
|
||||
if (i === lines.length - 1) cls += " pb-4";
|
||||
if (line.startsWith("+")) cls += " diff-add";
|
||||
else if (line.startsWith("-")) cls += " diff-remove";
|
||||
return (
|
||||
<span key={i} className={cls}>
|
||||
{line}
|
||||
{"\n"}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CodeProps {
|
||||
children: string;
|
||||
lang?: string;
|
||||
/**
|
||||
* Optional filename shown in a header row above the code. Authored in the
|
||||
* fence info string as `lang:path` (e.g. ```ts:src/core.ts) — the one
|
||||
* fence-meta channel that survives the MDX pipeline as a className (see
|
||||
* mdx-components.tsx). No filename, no header row.
|
||||
*/
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export async function Code({ children, lang = "typescript", filename }: CodeProps) {
|
||||
if (lang === "diff") {
|
||||
return <DiffBlock filename={filename}>{children}</DiffBlock>;
|
||||
}
|
||||
|
||||
const html = await codeToHtml(children.trim(), {
|
||||
lang,
|
||||
themes: {
|
||||
light: vercelLightTheme,
|
||||
dark: vercelDarkTheme,
|
||||
},
|
||||
defaultColor: false,
|
||||
});
|
||||
|
||||
return (
|
||||
// data-language stamps the fence's language on the rendered wrapper.
|
||||
// CodeToggle's CSS keys on it to switch samples: its children cross the
|
||||
// RSC boundary as one opaque node, so the DOM attribute is the only
|
||||
// channel that survives to tell the TypeScript fence from the Zig one.
|
||||
<div
|
||||
data-language={lang}
|
||||
className="my-4 rounded-lg border border-neutral-200 bg-neutral-50 text-[13px] font-mono overflow-hidden dark:border-neutral-800 dark:bg-neutral-900"
|
||||
>
|
||||
{filename ? <FilenameRow filename={filename} /> : null}
|
||||
<div
|
||||
className="overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Sheet, SheetTrigger, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
||||
import { allDocsPages, navSections } from "@/lib/docs-navigation";
|
||||
|
||||
export function DocsMobileNav() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const currentPage = useMemo(() => {
|
||||
return allDocsPages.find((page) => page.href === pathname) ?? allDocsPages[0];
|
||||
}, [pathname]);
|
||||
|
||||
// The homepage is a full-width landing page without the docs chrome.
|
||||
if (pathname === "/") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger
|
||||
aria-label="Open table of contents"
|
||||
className="lg:hidden sticky top-16 z-40 w-full px-6 py-3 bg-background-100/80 backdrop-blur-sm border-b border-gray-alpha-400 flex items-center justify-between focus:outline-none"
|
||||
>
|
||||
<div className="label-14 font-medium text-gray-1000">
|
||||
{currentPage.name}
|
||||
</div>
|
||||
<div className="w-8 h-8 flex items-center justify-center">
|
||||
<svg
|
||||
className="h-4 w-4 text-gray-900"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<line x1="8" y1="6" x2="21" y2="6" />
|
||||
<line x1="8" y1="12" x2="21" y2="12" />
|
||||
<line x1="8" y1="18" x2="21" y2="18" />
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" />
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" />
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" />
|
||||
</svg>
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
{/* overscroll-contain: sheet edge-scroll must not chain to the page. */}
|
||||
<SheetContent side="left" className="overflow-y-auto overscroll-contain p-6" showCloseButton={false}>
|
||||
<SheetTitle className="mb-6">Table of Contents</SheetTitle>
|
||||
<nav className="space-y-6">
|
||||
{navSections.map((section) => (
|
||||
<div key={section.title}>
|
||||
<div className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-400 dark:text-neutral-500">
|
||||
{section.title}
|
||||
</div>
|
||||
<ul className="space-y-0.5">
|
||||
{section.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className={`text-sm block py-2 transition-colors ${
|
||||
pathname === item.href
|
||||
? "text-neutral-900 dark:text-neutral-100 font-medium"
|
||||
: "text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100"
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { navSections } from "@/lib/docs-navigation";
|
||||
import { DocsToc } from "@/components/docs-toc";
|
||||
|
||||
function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside className="hidden w-56 shrink-0 lg:block">
|
||||
{/* overscroll-contain: sidebar edge-scroll must not chain to the page. */}
|
||||
<nav className="fixed top-16 w-56 h-[calc(100vh-4rem)] overflow-y-auto overscroll-contain py-8 pr-4 space-y-6">
|
||||
{navSections.map((section) => (
|
||||
<div key={section.title}>
|
||||
<div className="mb-2 px-3 label-12 font-medium uppercase tracking-wider text-gray-900">
|
||||
{section.title}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{section.items.map(({ href, name }) => {
|
||||
const active = pathname === href;
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={`block rounded-md px-3 py-1.5 text-sm transition-colors ${
|
||||
active
|
||||
? "bg-gray-alpha-100 font-medium text-gray-1000"
|
||||
: "text-gray-900 hover:text-gray-1000"
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocsNav({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// The homepage is a full-width landing page without the docs sidebar.
|
||||
if (pathname === "/") {
|
||||
return <main>{children}</main>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl px-6 py-8 lg:py-12 xl:max-w-7xl">
|
||||
<div className="flex gap-12">
|
||||
<Sidebar />
|
||||
<main className="min-w-0 flex-1">
|
||||
<article className="max-w-none">{children}</article>
|
||||
</main>
|
||||
{/* The rail keeps its slot on every page so content width is stable
|
||||
across navigation, even when a page has no headings to list. */}
|
||||
<aside className="hidden w-52 shrink-0 xl:block">
|
||||
<div className="sticky top-24 max-h-[calc(100vh-8rem)] overflow-y-auto overscroll-contain">
|
||||
<DocsToc />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
type TocHeading = { id: string; text: string; level: 2 | 3 };
|
||||
|
||||
/**
|
||||
* The "On this page" rail: h2/h3 headings read from the rendered article,
|
||||
* so the list and the anchor ids can never disagree — HeadingLink stamps
|
||||
* the ids, and this component only reads them back out of the DOM.
|
||||
*/
|
||||
export function DocsToc() {
|
||||
const pathname = usePathname();
|
||||
const [headings, setHeadings] = useState<TocHeading[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const article = document.querySelector("article");
|
||||
if (!article) {
|
||||
setHeadings([]);
|
||||
return;
|
||||
}
|
||||
const found: TocHeading[] = [];
|
||||
for (const el of article.querySelectorAll<HTMLHeadingElement>("h2[id], h3[id]")) {
|
||||
found.push({
|
||||
id: el.id,
|
||||
text: el.textContent ?? "",
|
||||
level: el.tagName === "H3" ? 3 : 2,
|
||||
});
|
||||
}
|
||||
setHeadings(found);
|
||||
setActiveId(found[0]?.id ?? null);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (headings.length === 0) return;
|
||||
|
||||
function onScroll() {
|
||||
// Matches the root scroll-padding-top (5rem) with a little slack, so
|
||||
// the heading an anchor jump lands on is the one that lights up.
|
||||
const cutoff = 88;
|
||||
let current = headings[0].id;
|
||||
for (const heading of headings) {
|
||||
const el = document.getElementById(heading.id);
|
||||
if (el && el.getBoundingClientRect().top <= cutoff) current = heading.id;
|
||||
}
|
||||
setActiveId(current);
|
||||
}
|
||||
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, [headings]);
|
||||
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav aria-label="On this page">
|
||||
<p className="mb-2 label-12 font-medium uppercase tracking-wider text-gray-900">
|
||||
On this page
|
||||
</p>
|
||||
<ul className="space-y-0.5">
|
||||
{headings.map((heading) => (
|
||||
<li key={heading.id}>
|
||||
<a
|
||||
href={`#${heading.id}`}
|
||||
className={`block py-1 text-sm transition-colors ${
|
||||
heading.level === 3 ? "pl-3" : ""
|
||||
} ${
|
||||
activeId === heading.id
|
||||
? "font-medium text-gray-1000"
|
||||
: "text-gray-900 hover:text-gray-1000"
|
||||
}`}
|
||||
>
|
||||
{heading.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Sheet, SheetTrigger, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
||||
|
||||
const links = [
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Docs", href: "/introduction" },
|
||||
];
|
||||
|
||||
function isCurrent(href: string, pathname: string): boolean {
|
||||
if (href === "/") return pathname === "/";
|
||||
// Docs owns every other page.
|
||||
return pathname !== "/";
|
||||
}
|
||||
|
||||
export function HeaderNav() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav aria-label="Site" className="hidden md:flex items-center gap-4">
|
||||
{links.map(({ name, href }) => {
|
||||
const current = isCurrent(href, pathname);
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
aria-current={current ? "page" : undefined}
|
||||
className={`label-14 transition-colors ${
|
||||
current ? "text-gray-1000" : "text-gray-900 hover:text-gray-1000"
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeaderMobileMenu() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger
|
||||
aria-label="Open menu"
|
||||
className="md:hidden flex items-center text-gray-900 hover:text-gray-1000 transition-colors"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<line x1="4" y1="6" x2="20" y2="6" />
|
||||
<line x1="4" y1="12" x2="20" y2="12" />
|
||||
<line x1="4" y1="18" x2="20" y2="18" />
|
||||
</svg>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="overflow-y-auto overscroll-contain p-6">
|
||||
<SheetTitle className="sr-only">Site menu</SheetTitle>
|
||||
<nav aria-label="Site" className="mt-8">
|
||||
<ul className="space-y-0.5">
|
||||
{links.map(({ name, href }) => {
|
||||
const current = isCurrent(href, pathname);
|
||||
return (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
onClick={() => setOpen(false)}
|
||||
aria-current={current ? "page" : undefined}
|
||||
className={`text-sm block py-2 transition-colors ${
|
||||
current
|
||||
? "text-neutral-900 dark:text-neutral-100 font-medium"
|
||||
: "text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100"
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getTextContent(children: React.ReactNode): string {
|
||||
if (typeof children === "string") return children;
|
||||
if (typeof children === "number") return String(children);
|
||||
if (Array.isArray(children)) return children.map(getTextContent).join("");
|
||||
if (children && typeof children === "object" && "props" in children) {
|
||||
const el = children as { props?: { children?: React.ReactNode } };
|
||||
return getTextContent(el.props?.children);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function HeadingLink({
|
||||
as: Tag,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
as: "h1" | "h2" | "h3";
|
||||
className: string;
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLAttributes<HTMLHeadingElement>) {
|
||||
const text = getTextContent(children);
|
||||
const id = slugify(text);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
const url = `${window.location.origin}${window.location.pathname}#${id}`;
|
||||
navigator.clipboard.writeText(url);
|
||||
window.history.replaceState(null, "", `#${id}`);
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Tag id={id} className={`group relative ${className}`} {...props}>
|
||||
{children}
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="ml-2 inline-flex opacity-0 group-hover:opacity-100 transition-opacity text-neutral-300 hover:text-neutral-500 dark:text-neutral-700 dark:hover:text-neutral-400"
|
||||
aria-label={`Copy link to ${text}`}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
</button>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="w-8 h-8" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-md text-gray-900 hover:text-gray-1000 hover:bg-gray-alpha-100 transition-colors"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Dialog as SheetPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
overlayClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
showCloseButton?: boolean;
|
||||
overlayClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay className={overlayClassName} />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-white dark:bg-neutral-950 data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l border-neutral-200 dark:border-neutral-800 sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r border-neutral-200 dark:border-neutral-800 sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("font-semibold text-neutral-900 dark:text-neutral-100", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" className={cn(className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Sheet, SheetTrigger, SheetContent, SheetTitle };
|
||||
@@ -0,0 +1,37 @@
|
||||
export type NavItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type NavSection = {
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
export const navSections: NavSection[] = [
|
||||
{
|
||||
title: "Get Started",
|
||||
items: [
|
||||
{ name: "Introduction", href: "/introduction" },
|
||||
{ name: "Quickstart", href: "/quickstart" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Guides",
|
||||
items: [
|
||||
{ name: "Coverage Reports", href: "/coverage" },
|
||||
{ name: "npm Dependencies", href: "/dependencies" },
|
||||
{ name: "Platform Support", href: "/platforms" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Reference",
|
||||
items: [
|
||||
{ name: "CLI Reference", href: "/cli" },
|
||||
{ name: "How It Works", href: "/how-it-works" },
|
||||
{ name: "Limitations", href: "/limitations" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const allDocsPages: NavItem[] = navSections.flatMap((s) => s.items);
|
||||
@@ -0,0 +1,19 @@
|
||||
const REPO = "vercel-labs/scriptc";
|
||||
const REVALIDATE = 86400;
|
||||
|
||||
export async function getStarCount(): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
|
||||
headers: { Accept: "application/vnd.github.v3+json" },
|
||||
next: { revalidate: REVALIDATE },
|
||||
});
|
||||
if (!res.ok) return "";
|
||||
const data = await res.json();
|
||||
const count = data.stargazers_count;
|
||||
if (typeof count !== "number") return "";
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k`;
|
||||
return String(count);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PAGE_TITLES } from "./page-titles";
|
||||
import { description, siteName } from "./site";
|
||||
|
||||
export function pageMetadata(slug: string): Metadata {
|
||||
const title = PAGE_TITLES[slug];
|
||||
if (!title) return {};
|
||||
|
||||
const displayTitle = title.replace(/\n/g, " ");
|
||||
const fullTitle = `${displayTitle} | ${siteName}`;
|
||||
const ogImageUrl = slug ? `/og/${slug}` : "/og";
|
||||
|
||||
return {
|
||||
title: displayTitle,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName,
|
||||
title: fullTitle,
|
||||
description,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: `${displayTitle} - ${siteName}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: fullTitle,
|
||||
description,
|
||||
images: [ogImageUrl],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export const PAGE_TITLES: Record<string, string> = {
|
||||
"": "Zero-runtime\nTypeScript",
|
||||
introduction: "Introduction",
|
||||
quickstart: "Quickstart",
|
||||
cli: "CLI Reference",
|
||||
coverage: "Coverage Reports",
|
||||
dependencies: "npm Dependencies",
|
||||
platforms: "Platform Support",
|
||||
"how-it-works": "How It Works",
|
||||
limitations: "Limitations",
|
||||
};
|
||||
|
||||
export function getPageTitle(slug: string): string | null {
|
||||
return slug in PAGE_TITLES ? PAGE_TITLES[slug]! : null;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* The single source of truth for the project's public name and every
|
||||
* name-bearing string on the site. All copy elsewhere is written
|
||||
* name-neutral and pulls from here.
|
||||
*/
|
||||
export const siteName = "scriptc";
|
||||
|
||||
/** The public repository. */
|
||||
export const githubUrl = "https://github.com/vercel-labs/scriptc";
|
||||
|
||||
/** The canonical site origin (used for metadata + Open Graph). */
|
||||
export const siteUrl = "https://scriptc.dev";
|
||||
|
||||
/** The one-line tagline used in titles and Open Graph text. */
|
||||
export const tagline = "Zero-runtime TypeScript";
|
||||
|
||||
/** The one-sentence description used in metadata. */
|
||||
export const description =
|
||||
"scriptc compiles ordinary TypeScript into small, fast native executables — no Node, no V8, no JavaScript engine in the binary. Same code, checked by the real TypeScript compiler, compiled to native.";
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
import { Code } from "@/components/code";
|
||||
import { HeadingLink } from "@/components/heading-link";
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
h1: (props) => (
|
||||
<HeadingLink
|
||||
as="h1"
|
||||
className="mb-6 text-2xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-100"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h2: (props) => (
|
||||
<HeadingLink
|
||||
as="h2"
|
||||
className="mb-4 mt-12 text-lg font-semibold text-neutral-900 first:mt-0 dark:text-neutral-100"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h3: (props) => (
|
||||
<HeadingLink
|
||||
as="h3"
|
||||
className="mb-3 mt-8 text-base font-semibold text-neutral-900 dark:text-neutral-100"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
p: (props) => (
|
||||
<p
|
||||
className="mb-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: (props) => <ul className="mb-4 list-disc space-y-1 pl-5 text-sm" {...props} />,
|
||||
ol: (props) => <ol className="mb-4 list-decimal space-y-1 pl-5 text-sm" {...props} />,
|
||||
li: (props) => <li className="text-neutral-600 dark:text-neutral-400" {...props} />,
|
||||
a: (props) => (
|
||||
<a
|
||||
className="text-neutral-900 underline decoration-neutral-300 underline-offset-2 hover:decoration-neutral-900 dark:text-neutral-100 dark:decoration-neutral-700 dark:hover:decoration-neutral-100"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ children, className }: { children?: React.ReactNode; className?: string }) => {
|
||||
if (className) {
|
||||
return <code className={className}>{children}</code>;
|
||||
}
|
||||
return (
|
||||
<code className="rounded bg-neutral-100 px-1.5 py-0.5 text-[13px] dark:bg-neutral-800">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
pre: async ({ children }: { children?: React.ReactNode }) => {
|
||||
const codeElement = children as React.ReactElement<{
|
||||
className?: string;
|
||||
children?: string;
|
||||
}>;
|
||||
const className = codeElement?.props?.className || "";
|
||||
// The fence info string, e.g. ```ts:src/core.ts — MDX drops fence
|
||||
// meta (```ts title="…" never reaches this component), so the
|
||||
// colon-in-info-string convention is the one filename channel that
|
||||
// survives the pipeline: micromark treats the whole word as the
|
||||
// language, MDX forwards it as language-ts:src/core.ts, and this
|
||||
// split hands shiki the real language and Code the filename.
|
||||
const info = className.replace("language-", "") || "typescript";
|
||||
const colon = info.indexOf(":");
|
||||
const lang = (colon === -1 ? info : info.slice(0, colon)) || "typescript";
|
||||
const filename = colon === -1 ? undefined : info.slice(colon + 1) || undefined;
|
||||
const code = codeElement?.props?.children || "";
|
||||
|
||||
return (
|
||||
<Code lang={lang} filename={filename}>
|
||||
{typeof code === "string" ? code : String(code)}
|
||||
</Code>
|
||||
);
|
||||
},
|
||||
blockquote: (props) => (
|
||||
<blockquote
|
||||
className="mb-4 border-l-2 border-neutral-200 pl-4 text-sm text-neutral-500 dark:border-neutral-800 dark:text-neutral-500"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// Markdown pipe tables render as bare table elements inside a scroll
|
||||
// wrapper: the `article table` rules in globals.css style them, the
|
||||
// same register the literal HTML tables use.
|
||||
table: (props) => (
|
||||
<div className="overflow-x-auto">
|
||||
<table {...props} />
|
||||
</div>
|
||||
),
|
||||
hr: () => <hr className="my-8 border-neutral-200 dark:border-neutral-800" />,
|
||||
strong: (props) => (
|
||||
<strong className="font-medium text-neutral-900 dark:text-neutral-100" {...props} />
|
||||
),
|
||||
...components,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.mdx",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
".next-check/types/**/*.ts",
|
||||
".next-check/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", ".next", ".next-check"]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
/* The 5.9.3 parser islands: the ONLY files that may import typescript5
|
||||
* (the alias pinning the old compiler). Everything else lives in the
|
||||
* 7.0.2 world — the two worlds' enums are renumbered and their nodes are
|
||||
* not interchangeable, so a stray cross-import is a correctness bug, not
|
||||
* a style problem. */
|
||||
const TS5_ISLANDS = [
|
||||
"packages/compiler/src/frontend/npm.ts",
|
||||
"packages/compiler/src/frontend/cjs-lexer.ts",
|
||||
"packages/compiler/src/frontend/lowering/lower-comptime.ts",
|
||||
"packages/compiler/src/frontend/ts7/world-check.ts",
|
||||
// The provenance prescan parses files BEFORE any program world exists
|
||||
// (the bare-import walk that decides what to fetch) — a parser island
|
||||
// exactly like npm.ts's specifier scan.
|
||||
"packages/compiler/src/frontend/provenance.ts",
|
||||
];
|
||||
|
||||
const ts5Fence = {
|
||||
group: ["typescript5", "typescript5/*"],
|
||||
message: "typescript5 (the 5.9.3 island alias) is only for the parser islands (npm.ts, cjs-lexer.ts, lower-comptime.ts, ts7/world-check.ts) — everything else is the 7.0.2 world; the worlds never mix",
|
||||
};
|
||||
const frontendFence = {
|
||||
group: ["**/frontend/*", "**/frontend/**"],
|
||||
message: "backend must not import frontend — the IR is the only interface",
|
||||
};
|
||||
const backendFence = {
|
||||
group: ["**/backend/*", "**/backend/**"],
|
||||
message: "frontend must not import backend — the IR is the only interface",
|
||||
};
|
||||
|
||||
/* NOTE: no-restricted-imports does not MERGE across matching config
|
||||
* blocks — the last matching block replaces the whole rule config. Every
|
||||
* block below therefore carries its complete pattern set. */
|
||||
export default tseslint.config(
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
// The IR is the only interface between frontend and backend.
|
||||
files: ["packages/compiler/src/backend/**"],
|
||||
rules: {
|
||||
"no-restricted-imports": ["error", { patterns: [frontendFence, ts5Fence] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/compiler/src/frontend/**"],
|
||||
ignores: TS5_ISLANDS,
|
||||
rules: {
|
||||
"no-restricted-imports": ["error", { patterns: [backendFence, ts5Fence] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: TS5_ISLANDS,
|
||||
rules: {
|
||||
"no-restricted-imports": ["error", { patterns: [backendFence] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Everything in the compiler outside frontend/backend (ir, diagnostics,
|
||||
// index) is 7.0.2-world too.
|
||||
files: ["packages/compiler/src/**"],
|
||||
ignores: ["packages/compiler/src/frontend/**", "packages/compiler/src/backend/**"],
|
||||
rules: {
|
||||
"no-restricted-imports": ["error", { patterns: [ts5Fence] }],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "scriptc-monorepo",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm -r --filter './packages/*' run build",
|
||||
"build:fresh": "rm -rf packages/compiler/dist packages/cli/dist node_modules/.cache/scriptc-tsc && pnpm build",
|
||||
"test": "vitest run",
|
||||
"test:cache-identity": "node tests/harness/cache-identity.mjs",
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint packages/compiler/src packages/cli/src",
|
||||
"scriptc": "tsx packages/cli/src/main.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"eslint": "^9.20.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "^8.24.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,35 @@
|
||||
# scriptc
|
||||
|
||||
Compile ordinary TypeScript and JavaScript to small, fast native executables — no Node, no V8, no JavaScript engine in the binary. What compiles behaves byte-for-byte like Node.
|
||||
|
||||
```console
|
||||
$ cat fib.ts
|
||||
function fib(n: number): number {
|
||||
return n < 2 ? n : fib(n - 1) + fib(n - 2);
|
||||
}
|
||||
console.log(fib(30));
|
||||
|
||||
$ scriptc run fib.ts
|
||||
832040
|
||||
|
||||
$ scriptc build fib.ts -o fib && ./fib
|
||||
832040
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
```console
|
||||
$ npm install -g scriptc
|
||||
```
|
||||
|
||||
Requires clang on the PATH (Xcode Command Line Tools on macOS, `clang` package on Linux).
|
||||
|
||||
## Commands
|
||||
|
||||
- `scriptc build <file.ts>` — compile to a native executable
|
||||
- `scriptc run <file.ts>` — compile and run
|
||||
- `scriptc coverage <file.ts>` — what compiles statically, and why the rest doesn't
|
||||
|
||||
No annotations, no dialect, no special stdlib: the same TypeScript you run on Node, type-checked by the real TypeScript compiler. Programs outside the static tier can opt into `--dynamic`, which embeds a small JavaScript engine (~620KB) for the parts that can't be static; everything else fails the build with a specific error code and usually a rewrite hint.
|
||||
|
||||
Docs: [scriptc.dev](https://scriptc.dev)
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "scriptc",
|
||||
"version": "0.0.1",
|
||||
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables — no Node, no V8, no JavaScript engine in the binary",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/cli"
|
||||
},
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"scriptc": "dist/main.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scriptc/compiler": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import { rmSync } from "node:fs";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { analyze, compile, compileC, renderAll, renderCoverage, resolveProvenanceSources, setProvenanceSources } from "@scriptc/compiler";
|
||||
|
||||
const USAGE = `scriptc — TypeScript/JavaScript to native executables (experimental)
|
||||
|
||||
Usage:
|
||||
scriptc build <file.ts|.js> [options] compile to a native executable
|
||||
scriptc run <file.ts|.js> [options] compile and run
|
||||
scriptc coverage <file.ts|.js> how much compiles statically, and why not
|
||||
scriptc coverage <file.ts|.js> --dynamic what a --dynamic build compiles, and what still blocks it
|
||||
|
||||
Options:
|
||||
-o, --out <path> output executable path (default: .scriptc/<name>)
|
||||
--backend <b> code generator: llvm (default — emits LLVM IR text,
|
||||
compiled by the same clang) or c (the readable reference
|
||||
backend). The default falls back to the C backend when a
|
||||
program is outside the LLVM tier (a one-line stderr note
|
||||
says so; program behavior is identical); an explicit
|
||||
--backend llvm fails loudly instead of falling back
|
||||
--from-c treat input as a C (or .ll) file (toolchain plumbing/debugging)
|
||||
--keep-c keep the generated program TU next to the executable
|
||||
(default; the .ll — or the .c under --backend=c or
|
||||
when the build fell back)
|
||||
--no-keep-c delete the generated program TU after compiling
|
||||
--emit-ir also write the IR as JSON next to the executable
|
||||
--sanitize build with ASan + runtime RC audit
|
||||
--dynamic embed the dynamic engine (adds ~620KB; static stays the default)
|
||||
--npm-static <pkg[,pkg…]|auto>
|
||||
compile the named npm packages' shipped JS statically as
|
||||
program modules (repeatable; "auto" opts in every eligible
|
||||
direct import: own .d.ts, unminified JS, no build-transform
|
||||
markers). A package preflight refuses falls back to the
|
||||
island (--dynamic) with a coverage-report note — opt-in,
|
||||
experimental
|
||||
--provenance-sources
|
||||
EXPERIMENTAL: compile npm dependencies from their
|
||||
provenance-attested SOURCE (fetched at the attested
|
||||
commit) as static program modules; packages without a
|
||||
usable attestation keep the island path (a note, never
|
||||
a failure)
|
||||
-h, --help show this help
|
||||
`;
|
||||
|
||||
/* The exit discipline: NEVER process.exit() after writing output. stdout/
|
||||
* stderr to a PIPE are async streams — process.exit() drops whatever libuv
|
||||
* hasn't flushed yet, which truncates large diagnostic renders at the pipe
|
||||
* buffer (observed: 64KB cut mid-code-frame). Every path sets
|
||||
* process.exitCode and returns instead; Node exits naturally once the
|
||||
* streams drain. */
|
||||
class CliExit extends Error {
|
||||
constructor(readonly code: number) {
|
||||
super(`exit ${code}`);
|
||||
}
|
||||
}
|
||||
|
||||
function fail(msg: string): never {
|
||||
process.stderr.write(msg + "\n");
|
||||
throw new CliExit(1);
|
||||
}
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const { values, positionals } = parseArgs({
|
||||
options: {
|
||||
out: { type: "string", short: "o" },
|
||||
// No parseArgs default: unset means the compiler's default lane
|
||||
// (LLVM with the transparent C fallback); an explicit value pins.
|
||||
backend: { type: "string" },
|
||||
"from-c": { type: "boolean", default: false },
|
||||
"keep-c": { type: "boolean", default: true },
|
||||
"emit-ir": { type: "boolean", default: false },
|
||||
sanitize: { type: "boolean", default: false },
|
||||
dynamic: { type: "boolean", default: false },
|
||||
"npm-static": { type: "string", multiple: true },
|
||||
"provenance-sources": { type: "boolean", default: false },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
allowPositionals: true,
|
||||
allowNegative: true,
|
||||
});
|
||||
|
||||
if (values.help || positionals.length === 0) {
|
||||
process.stdout.write(USAGE);
|
||||
return values.help ? 0 : 1;
|
||||
}
|
||||
|
||||
const [command, inputArg] = positionals;
|
||||
if (command !== "build" && command !== "run" && command !== "coverage") {
|
||||
fail(`unknown command "${command}"\n\n${USAGE}`);
|
||||
}
|
||||
if (!inputArg) fail(`missing input file\n\n${USAGE}`);
|
||||
const input = resolve(inputArg);
|
||||
const backend = values.backend;
|
||||
if (backend !== undefined && backend !== "c" && backend !== "llvm") {
|
||||
fail(`unknown backend "${backend}" (supported: c, llvm)\n\n${USAGE}`);
|
||||
}
|
||||
|
||||
// --npm-static: repeatable and comma-splittable; the literal "auto"
|
||||
// switches to eligibility-based detection (mixing "auto" with names
|
||||
// is rejected — the shapes answer different questions).
|
||||
const npmStaticRaw = (values["npm-static"] ?? []).flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
||||
let npmStatic: string[] | "auto" | undefined;
|
||||
if (npmStaticRaw.includes("auto")) {
|
||||
if (npmStaticRaw.length > 1) fail(`--npm-static auto cannot be combined with package names\n\n${USAGE}`);
|
||||
npmStatic = "auto";
|
||||
} else if (npmStaticRaw.length > 0) {
|
||||
npmStatic = npmStaticRaw;
|
||||
}
|
||||
|
||||
// --provenance-sources resolves BEFORE the program loads (tsgo needs the
|
||||
// source "paths" at creation): attestations and source trees fetch (or
|
||||
// ride the content-addressed cache / the offline manifest), the registry
|
||||
// installs, and every fallback prints as a note — never a failure.
|
||||
const provenance = values["provenance-sources"] ? await resolveProvenanceSources(input) : null;
|
||||
if (provenance !== null) {
|
||||
setProvenanceSources(provenance);
|
||||
for (const pkg of provenance.packages) {
|
||||
process.stderr.write(
|
||||
`provenance: ${pkg.name}@${pkg.version} ← ${pkg.repo.replace(/^git\+/, "")} @ ${pkg.commit.slice(0, 12)} (source compiles statically)\n`,
|
||||
);
|
||||
}
|
||||
for (const note of provenance.notes) process.stderr.write(`provenance: ${note}\n`);
|
||||
}
|
||||
|
||||
if (command === "coverage") {
|
||||
const { coverage } = analyze(input, { dynamic: values.dynamic, ...(npmStatic !== undefined ? { npmStatic } : {}) });
|
||||
const color = process.stdout.isTTY ?? false;
|
||||
process.stdout.write(renderCoverage(coverage, { color }) + "\n");
|
||||
return coverage.preflightFailed ? 1 : 0;
|
||||
}
|
||||
|
||||
const outDir = values.out ? dirname(resolve(values.out)) : join(dirname(input), ".scriptc");
|
||||
const stem = basename(input).replace(/\.(ts|js|mjs|cjs|c|ll)$/, "");
|
||||
const outPath = values.out ? resolve(values.out) : join(outDir, stem);
|
||||
|
||||
const build = async (): Promise<string> => {
|
||||
if (values["from-c"]) {
|
||||
await compileC({ cPath: input, outPath, sanitize: values.sanitize, dynamic: values.dynamic });
|
||||
return outPath;
|
||||
}
|
||||
const result = await compile(input, {
|
||||
outPath,
|
||||
outDir,
|
||||
emitIr: values["emit-ir"],
|
||||
sanitize: values.sanitize,
|
||||
dynamic: values.dynamic,
|
||||
...(backend !== undefined ? { backend } : {}),
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
const color = process.stderr.isTTY ?? false;
|
||||
process.stderr.write(renderAll(result.diagnostics, result.sourceTexts, { color }) + "\n");
|
||||
const n = result.diagnostics.length;
|
||||
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
|
||||
throw new CliExit(1);
|
||||
}
|
||||
// The lane-change note: the ONLY case where silence would be dishonest
|
||||
// is the default lane quietly building through C — one stderr line
|
||||
// names the refusal. A successful LLVM build is the documented default
|
||||
// (and the kept .ll next to the binary is the durable record), and an
|
||||
// explicit --backend was the user's own choice — neither gets a line.
|
||||
if (result.llvmRefusal !== undefined) {
|
||||
process.stderr.write(`scriptc: backend c (llvm refused: ${result.llvmRefusal})\n`);
|
||||
}
|
||||
if (!values["keep-c"]) rmSync(result.cPath, { force: true });
|
||||
return result.binaryPath;
|
||||
};
|
||||
|
||||
const binary = await build();
|
||||
|
||||
if (command === "run") {
|
||||
return new Promise<number>((resolveExit) => {
|
||||
const child = spawn(binary, [], { stdio: "inherit" });
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.stderr.write(`scriptc: program killed by ${signal}\n`);
|
||||
resolveExit(1);
|
||||
} else {
|
||||
resolveExit(code ?? 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
process.stdout.write(`${binary}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
process.exitCode = await main();
|
||||
} catch (err) {
|
||||
if (err instanceof CliExit) process.exitCode = err.code;
|
||||
else throw err;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* The CLI's exit must WAIT for its output streams to drain. stdout/stderr
|
||||
* to a pipe are async; a process.exit() right after rendering a large
|
||||
* diagnostic set drops whatever libuv hasn't flushed — observed as a hard
|
||||
* 65536-byte (pipe buffer) truncation mid-code-frame, losing the error
|
||||
* count and everything after it. This pins the fixed behavior: a render
|
||||
* several times the pipe buffer arrives COMPLETE through a real pipe,
|
||||
* final "N errors." line included. */
|
||||
import { execFile } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const require = createRequire(import.meta.url);
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
|
||||
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
|
||||
|
||||
test("a >64KB diagnostic render survives a piped stderr intact", async () => {
|
||||
const dir = join(repoRoot, "node_modules/.cache/scriptc-tests/cli-flush");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const entry = join(dir, "many-diags.ts");
|
||||
// 1200 unsupported statements: each renders a code frame, so the full
|
||||
// report is several hundred KB — far past the 64KB pipe buffer where the
|
||||
// truncation bit.
|
||||
writeFileSync(entry, "debugger;\n".repeat(1200));
|
||||
|
||||
// execFile pipes stdio (the shape that truncated); tolerate the expected
|
||||
// exit 1 and keep the full stderr.
|
||||
const res = await execFileAsync(
|
||||
process.execPath,
|
||||
["--import", tsxLoader, cliEntry, "build", entry],
|
||||
{ maxBuffer: 16 * 1024 * 1024 },
|
||||
).then(
|
||||
() => {
|
||||
throw new Error("build of a broken program must exit nonzero");
|
||||
},
|
||||
(err: { code?: unknown; stderr?: string }) => err,
|
||||
);
|
||||
|
||||
expect(res.code).toBe(1);
|
||||
const stderr = res.stderr ?? "";
|
||||
expect(Buffer.byteLength(stderr)).toBeGreaterThan(64 * 1024);
|
||||
// The count line is written LAST — its presence proves the whole render
|
||||
// flushed before exit.
|
||||
expect(stderr).toMatch(/\n1200 errors\.\n$/);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "../../node_modules/.cache/scriptc-tsc/cli.tsbuildinfo"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
+3001
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
/* scriptc's divergence and precision overrides — interface merges that
|
||||
* re-type a handful of standard-library members to scriptc's documented
|
||||
* runtime behavior. Merged interfaces resolve overloads later-declaration-
|
||||
* first, so these win at call sites that match them; call forms only the
|
||||
* lib signatures accept still typecheck and are fenced by the lowerer.
|
||||
*
|
||||
* This file is part of the LOWERING program only. Preflight's second-chance
|
||||
* pass (checkPreflight) rebuilds the program WITHOUT it — the project's own
|
||||
* type world — so a program that typechecks under its own tsc is never
|
||||
* rejected over an override-manufactured error; the affected sites meet the
|
||||
* lowerer's honest fences instead (SC1100's checked-cast family for
|
||||
* JSON.parse results, the executor fences for Promise shapes). The
|
||||
* primitives every world needs (comptime, setTimeout, __island_eval) live
|
||||
* in scriptc.d.ts, which ships to BOTH programs. */
|
||||
|
||||
/* `pop(): T`, not the lib's `T | undefined`: popping an empty array traps
|
||||
* at runtime — a documented divergence (SEMANTICS.md) this override keeps
|
||||
* typecheckable. Check `.length` first, as real code must. */
|
||||
interface Array<T> {
|
||||
pop(): T;
|
||||
}
|
||||
|
||||
/* `at(): string`, not the lib's `string | undefined`: undefined is
|
||||
* unrepresentable, so an out-of-range at() THROWS a catchable TypeError at
|
||||
* the validated island exit instead of returning undefined (documented
|
||||
* divergence, SEMANTICS.md). */
|
||||
interface String {
|
||||
at(index: number): string;
|
||||
}
|
||||
|
||||
/* Object.values/entries with the actual field-value union, not the lib's
|
||||
* `any` (the lib's precise overloads only cover index-signature and
|
||||
* ArrayLike sources). A PRECISION override, not a divergence: the lowering
|
||||
* emits the field list statically and the values are exactly T[keyof T] —
|
||||
* without this the result would be `any`-typed and drag every downstream
|
||||
* use into the island. */
|
||||
interface ObjectConstructor {
|
||||
values<T extends object>(o: T): Array<T[keyof T]>;
|
||||
entries<T extends object>(o: T): Array<[string, T[keyof T]]>;
|
||||
}
|
||||
|
||||
/* `parse` returns `unknown`, not the lib's `any` — the dynamic boundary. A
|
||||
* checked cast (`JSON.parse(s) as Config`) validates the value against the
|
||||
* target type at runtime and THROWS on mismatch — the mechanism that makes
|
||||
* trusting TS types sound at data boundaries. The lib's any-returning
|
||||
* overload stays reachable only through the reviver form, which the
|
||||
* lowerer rejects (as it does stringify's replacer/space parameters).
|
||||
*
|
||||
* `stringify` takes `unknown`, not the lib's `any`: the same honest
|
||||
* surface, but the parameter must not CONTEXTUALLY TYPE literal arguments
|
||||
* as `any` (an any-typed slot is the island boundary; `JSON.stringify([1,
|
||||
* 2])` must keep building a static array and serializing it
|
||||
* type-directedly). */
|
||||
interface JSON {
|
||||
parse(text: string): unknown;
|
||||
stringify(value: unknown): string;
|
||||
}
|
||||
|
||||
/* The supported Promise construction shape: an executor whose resolve takes
|
||||
* the plain value. Preferred over the lib signature at inference time, so
|
||||
* `new Promise<T>((resolve) => ...)` types resolve as `(value: T) => void`
|
||||
* — no PromiseLike union. The reject parameter is a real closure rejecting
|
||||
* the promise; its reason is pinned to Error (the lib says `reason?: any`)
|
||||
* because rejection payloads ride the same representation as thrown values
|
||||
* and the idiomatic reason IS an Error — reject(nonError) and bare
|
||||
* reject() typecheck only in the project world and meet the lowerer's
|
||||
* honest coercion fences instead. First settle wins, exactly JS: a reject
|
||||
* after resolve (or a second reject) is a no-op, and an executor throw
|
||||
* after any settle is swallowed. Executors that use the lib signature's
|
||||
* other extras (resolving with a thenable) still typecheck and fail
|
||||
* lowering on their types. */
|
||||
interface PromiseConstructor {
|
||||
new <T>(
|
||||
executor: (resolve: (value: T) => void, reject: (reason: Error) => void) => void,
|
||||
): Promise<T>;
|
||||
/* ES2024's Promise.withResolvers, in the executor's own scriptc shape:
|
||||
* resolve takes the plain value (no PromiseLike union; the conditional
|
||||
* collapses Promise<void>'s resolve to () => void so the record's
|
||||
* field types map exactly), reject is the executor's Error-pinned
|
||||
* rejection closure. The lib's own declaration would otherwise be
|
||||
* unrepresentable field-by-field. */
|
||||
withResolvers<T>(): {
|
||||
promise: Promise<T>;
|
||||
// `0 extends 1 & T` detects any (untyped JS: Promise.withResolvers()
|
||||
// infers it) — its settled value rides the dyn arm like unknown's,
|
||||
// so resolve takes one unknown; a naked conditional over any would
|
||||
// otherwise union BOTH branches, which no field can map.
|
||||
resolve: 0 extends 1 & T
|
||||
? (value: unknown) => void
|
||||
: [T] extends [void]
|
||||
? () => void
|
||||
: (value: T) => void;
|
||||
reject: (reason: Error) => void;
|
||||
};
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/* scriptc's shipped declarations — the ALWAYS-SHIPPED CORE. Programs
|
||||
* compile against the REAL TypeScript standard library (lib.es2023, no
|
||||
* DOM, no @types): the checker sees the full standard surface, and the
|
||||
* LOWERER is the scope fence — any reached use of standard-library surface
|
||||
* without a lowering is a SC2020 diagnostic at its use site. This file
|
||||
* declares only what the es2023 lib does not:
|
||||
*
|
||||
* 1. scriptc's own primitives (comptime, __island_eval).
|
||||
* 2. setTimeout/clearTimeout and the Timeout handle (Node/DOM territory the
|
||||
* lib files don't cover; plain declarations, so with @types/node present
|
||||
* they MERGE as overloads instead of colliding — the Timeout handle maps
|
||||
* to the numeric timer id either way, and .unref()/.ref()/.hasRef() are
|
||||
* loop-liveness bookkeeping).
|
||||
*
|
||||
* This file ships to EVERY program scriptc builds — the lowering program
|
||||
* and preflight's project-world second-chance program alike (a project
|
||||
* using comptime must typecheck in both). The divergence/precision
|
||||
* overrides (JSON.parse(): unknown, pop(): T, the Promise executor shape,
|
||||
* ...) live in scriptc-overrides.d.ts, which joins the LOWERING program only.
|
||||
*
|
||||
* console, process, and the "node:fs" module live in scriptc-node-fallback.d.ts,
|
||||
* shipped ONLY when the target project has no @types/node — with it, the
|
||||
* project's real Node types are the type surface and the fallback stands
|
||||
* down (its `declare const`/`declare module` forms would collide). The
|
||||
* lowering tables recognize the same members either way, by name +
|
||||
* provenance. */
|
||||
|
||||
/* The timer handle — setTimeout's return, mapped to the numeric timer id.
|
||||
* unref() drops it from the event loop's keep-alive set (the process may
|
||||
* exit with the timer still armed — it never fires then, exactly Node);
|
||||
* ref() restores it; hasRef() reports the state. The methods return the
|
||||
* handle for chaining. Under @types/node this MERGES with NodeJS.Timeout
|
||||
* (both map to the same numeric handle). */
|
||||
interface Timeout {
|
||||
ref(): Timeout;
|
||||
unref(): Timeout;
|
||||
hasRef(): boolean;
|
||||
/* Re-arms the timer to fire at now + the original delay (Node's
|
||||
* Timeout.refresh). Chaining like ref/unref. */
|
||||
refresh(): Timeout;
|
||||
}
|
||||
/* The callback is invoked with NO arguments; the second overload admits a
|
||||
* callback declared with one parameter for the sleep idiom —
|
||||
* `setTimeout(resolve, ms)` with Promise<unknown>'s resolve — where the
|
||||
* zero-argument invocation delivers undefined (@types/node's generic
|
||||
* signature admits the same shape and MERGES with these). Promise<void>'s
|
||||
* resolve keeps matching the first overload (void params accept absence). */
|
||||
declare function setTimeout(callback: () => void, ms?: number): Timeout;
|
||||
declare function setTimeout(callback: (value?: unknown) => void, ms?: number): Timeout;
|
||||
/* The trailing-argument form: Node passes the extras to the callback
|
||||
* (`setTimeout(cb, 0, 'foo')` fires cb('foo')). The callback slot accepts
|
||||
* any function shape (never[] rest — parameter-contravariance's bottom);
|
||||
* the delivered call rides the checked-dynamic boundary, so each argument
|
||||
* is validated against the callback's real signature at fire time. */
|
||||
declare function setTimeout(callback: (...args: never[]) => void, ms?: number, ...args: unknown[]): Timeout;
|
||||
declare function clearTimeout(handle?: Timeout | number | null | undefined): void;
|
||||
|
||||
/* Compile-time evaluation. The callback must be an inline arrow/function
|
||||
* expression with NO references to outer bindings (it is extracted
|
||||
* source-textually and executed in an isolated node:vm context inside the
|
||||
* COMPILER's Node process); the returned value is baked into the binary as a
|
||||
* literal — numbers (finite), strings, booleans, arrays, and records,
|
||||
* nested. What type-checks runs with real JavaScript semantics, so the
|
||||
* baked result is exactly what Node would compute at runtime (the
|
||||
* differential harness verifies this with a `comptime = (f) => f()` shim). */
|
||||
declare function comptime<T>(compute: () => T): T;
|
||||
|
||||
/* INTERNAL TESTING HOOK — not part of the supported surface, deliberately
|
||||
* undocumented. Evaluates `code` in the embedded dynamic-island engine and
|
||||
* returns String(result); island exceptions arrive as catchable errors.
|
||||
* Requires --dynamic (a clean diagnostic otherwise). This exists to prove
|
||||
* the engine embed end-to-end in tests; user-facing constructs will lower
|
||||
* to the island through their own paths, not through this. */
|
||||
declare function __island_eval(code: string): string;
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@scriptc/compiler",
|
||||
"version": "0.0.1",
|
||||
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM and C backends",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/compiler"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./scriptc.d.ts": "./ambient/scriptc.d.ts",
|
||||
"./scriptc-overrides.d.ts": "./ambient/scriptc-overrides.d.ts",
|
||||
"./scriptc-node-fallback.d.ts": "./ambient/scriptc-node-fallback.d.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"ambient"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node node_modules/typescript5/bin/tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scriptc/runtime": "workspace:*",
|
||||
"typescript": "7.0.2",
|
||||
"typescript5": "npm:typescript@5.9.3"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,312 @@
|
||||
/* Island-boundary C emission owned pieces: the embedded npm module/edge
|
||||
* tables (--dynamic builds embed every reached npm source; the island's
|
||||
* module loader and require shim read them — binaries never touch
|
||||
* node_modules at runtime) and the interned host-call adapters that let the
|
||||
* engine invoke scriptc closures. */
|
||||
import { deflateRawSync } from "node:zlib";
|
||||
import type { CEmitter } from "./emitter.js";
|
||||
import { cDecl, cFnPtrCast, cStringLiteral, releaseCallC } from "./emit-types.js";
|
||||
import { IrType, islandCallbackRet, NPM_COMPRESS_MIN, typeKey } from "../../ir/nodes.js";
|
||||
|
||||
/** Embedded npm modules (--dynamic): every reached module's SOURCE as a
|
||||
* static string plus the resolution edges — the island's module loader
|
||||
* and require shim read these tables; binaries never touch node_modules
|
||||
* at runtime. Registered with the runtime at the top of main.
|
||||
*
|
||||
* Module text at least NPM_COMPRESS_MIN long stores as raw DEFLATE when
|
||||
* that shrinks it (JS deflates 3-4×; a big CLI graph is tens of MB of
|
||||
* text, THE dominant binary-size term). The table row carries the
|
||||
* inflated length beside the stored one, and the island's loader inflates
|
||||
* LAZILY at a module's first load through the inflater the emitted main
|
||||
* installs (scr_island_set_inflate ← scr_zlib_inflate_exact; index.ts
|
||||
* links scr_zlib.c/libz on the same moduleEmbedsCompressedNpm predicate),
|
||||
* so boot touches no compressed page a run never loads — cold start
|
||||
* SHRINKS with the binary. */
|
||||
export function emitNpmEmbedding(E: CEmitter, out: string[]): void {
|
||||
const embedded = E.mod.embedded;
|
||||
if (embedded && embedded.modules.length > 0) {
|
||||
const emitChunked = (name: string, bytes: Buffer, comment?: string): void => {
|
||||
out.push(`static const char ${name}[] = ${comment !== undefined ? `/* ${comment} */` : ""}`);
|
||||
for (let off = 0; off < bytes.length; off += 2048) {
|
||||
const last = off + 2048 >= bytes.length;
|
||||
out.push(` ${cStringLiteral(bytes.subarray(off, off + 2048))}${last ? ";" : ""}`);
|
||||
}
|
||||
if (bytes.length === 0) out.push(` "";`);
|
||||
};
|
||||
// text → {bytes to store, raw: 0 for plain | the inflated length}.
|
||||
// The candidate test is moduleEmbedsCompressedNpm's, by construction.
|
||||
const store = (text: string): { bytes: Buffer; raw: number } => {
|
||||
const plain = Buffer.from(text, "utf8");
|
||||
if (text.length < NPM_COMPRESS_MIN) return { bytes: plain, raw: 0 };
|
||||
const deflated = deflateRawSync(plain, { level: 9 });
|
||||
return deflated.length < plain.length
|
||||
? { bytes: deflated, raw: plain.length }
|
||||
: { bytes: plain, raw: 0 };
|
||||
};
|
||||
const stored = embedded.modules.map((m) => ({
|
||||
src: store(m.source),
|
||||
esm: m.esm !== undefined ? store(m.esm) : null,
|
||||
}));
|
||||
stored.forEach((s, i) => {
|
||||
const m = embedded.modules[i]!;
|
||||
emitChunked(`sc_npm_src_${i}`, s.src.bytes, m.key.split("/node_modules/").pop());
|
||||
// CJS modules carry their synthesized ESM facade (default plus the
|
||||
// export names LEXED at build time, matching Node's vendored CJS
|
||||
// lexer): the island loader evaluates it when an ES module imports
|
||||
// the file.
|
||||
if (s.esm !== null) emitChunked(`sc_npm_esm_${i}`, s.esm.bytes);
|
||||
});
|
||||
const fmt = { esm: 0, cjs: 1, json: 2 } as const;
|
||||
out.push(`static const ScrIslandModule sc_npm_modules[] = {`);
|
||||
stored.forEach((s, i) => {
|
||||
const m = embedded.modules[i]!;
|
||||
const facade =
|
||||
s.esm !== null
|
||||
? `sc_npm_esm_${i}, sizeof sc_npm_esm_${i} - 1, ${s.esm.raw}`
|
||||
: `NULL, 0, 0`;
|
||||
out.push(
|
||||
` { ${cStringLiteral(Buffer.from(m.key, "utf8"))}, sc_npm_src_${i}, ` +
|
||||
`sizeof sc_npm_src_${i} - 1, ${s.src.raw}, ${fmt[m.format]}, ${facade} },`,
|
||||
);
|
||||
});
|
||||
out.push(`};`);
|
||||
if (embedded.edges.length > 0) {
|
||||
// kind: 0 = any call form, 1 = import-resolved, 2 = require-resolved
|
||||
// (a dual package's condition split — the loader looks edges up with
|
||||
// its call form's kind).
|
||||
const kindOf = { any: 0, import: 1, require: 2 } as const;
|
||||
out.push(`static const ScrIslandEdge sc_npm_edges[] = {`);
|
||||
for (const e of embedded.edges) {
|
||||
const s = (t: string) => cStringLiteral(Buffer.from(t, "utf8"));
|
||||
out.push(` { ${s(e.from)}, ${s(e.specifier)}, ${s(e.to)}, ${kindOf[e.kind]} },`);
|
||||
}
|
||||
out.push(`};`);
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
}
|
||||
|
||||
/** The host-call adapter for closures of a given (arity, return kind) —
|
||||
* see islandAdapters. `argv` cells are BORROWED by the wrapper; the
|
||||
* closure ABI consumes (+1) each param, so the adapter retains them in.
|
||||
* A jsval-returning closure's +1 result passes straight through;
|
||||
* primitive returns marshal back by value; void closures return NULL
|
||||
* (the wrapper turns that into `undefined`). */
|
||||
export function islandAdapter(E: CEmitter, arity: number, retKind: "void" | "jsval" | "f64" | "bool" | "string"): string {
|
||||
const tag = { void: "v", jsval: "j", f64: "f", bool: "b", string: "s" }[retKind];
|
||||
const key = `${arity}:${tag}`;
|
||||
const existing = E.islandAdapters.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `sc_ia_${arity}${tag}`;
|
||||
E.islandAdapters.set(key, name);
|
||||
const cRet = { void: "void", jsval: "ScrJsval *", f64: "double", bool: "bool", string: "ScrStr *" }[retKind];
|
||||
const params = ["ScrClosure *"].concat(Array<string>(arity).fill("ScrJsval *"));
|
||||
const callArgs = ["c", ...Array.from({ length: arity }, (_, i) => `scr_jsval_retain(argv[${i}])`)];
|
||||
const call = `((Fn)c->fn)(${callArgs.join(", ")})`;
|
||||
const body =
|
||||
retKind === "void"
|
||||
? [` ${call};`, ` return NULL;`]
|
||||
: retKind === "jsval"
|
||||
? [` return ${call};`]
|
||||
: retKind === "f64"
|
||||
? [` return scr_jsval_from_f64(${call});`]
|
||||
: retKind === "bool"
|
||||
? [` return scr_jsval_from_bool(${call});`]
|
||||
: [
|
||||
// The closure's +1 string marshals in, then releases.
|
||||
// NULL is the throw-path dummy — the wrapper sees the
|
||||
// pending exception and reverse-bridges it.
|
||||
` ScrStr *s = ${call};`,
|
||||
` if (!s) return NULL;`,
|
||||
` ScrJsval *r = scr_jsval_from_str(s);`,
|
||||
` scr_str_release(s);`,
|
||||
` return r;`,
|
||||
];
|
||||
const sig = `static ScrJsval *${name}(ScrClosure *c, ScrJsval **argv)`;
|
||||
E.walkerProtos.push(
|
||||
`${sig}; /* island host-call adapter (${arity} arg${arity === 1 ? "" : "s"}, ${retKind}) */`,
|
||||
);
|
||||
E.walkerDefs.push(
|
||||
`${sig} {`,
|
||||
` typedef ${cRet} (*Fn)(${params.join(", ")});`,
|
||||
...(arity === 0 ? [` (void)argv;`] : []),
|
||||
...body,
|
||||
`}`,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The host-call adapter for a closure with TYPED parameters — one per
|
||||
* full signature (param typeKeys + return classification), interned like
|
||||
* the all-'any' adapters above. On invocation from the engine, each
|
||||
* BORROWED argv cell converts to its param's static type through the
|
||||
* EXISTING exit machinery: strict primitive exits, JSON round-trip
|
||||
* composites via the dynCheck walker (width-tolerant records, path-
|
||||
* annotated failures), a `T | undefined` param taking the interned
|
||||
* undefined arm when the argument is absent or undefined (the engine
|
||||
* wrapper pads missing args with undefined and drops surplus — JS call
|
||||
* semantics), and jsval params passing through as retained handles. A
|
||||
* conversion failure releases what was already built and returns NULL
|
||||
* with the exception pending — the wrapper reverse-bridges it into the
|
||||
* island as a TypeError, so a lying engine argument throws instead of
|
||||
* corrupting memory (the boundary's trust-but-verify rule). Converted
|
||||
* params are CONSUMED by the closure ABI (+1 each moves in). Sync
|
||||
* returns marshal back like the all-'any' adapters; a Promise return
|
||||
* (async callbacks) wraps as an engine promise settled when the scriptc
|
||||
* promise settles (scr_jsval_from_promise). */
|
||||
export function islandTypedAdapter(E: CEmitter, fn: IrType & { kind: "func" }): string {
|
||||
const ret = islandCallbackRet(fn.ret, (id) => E.recordsById.get(id), (id) => E.unionsById.get(id));
|
||||
if (!ret) throw new Error("emitter bug: typed island adapter with unsupported return");
|
||||
const key = `${fn.params.map((p) => typeKey(p)).join(",")}=>${ret.async ? "P:" : ""}${ret.tag}`;
|
||||
const existing = E.islandTypedAdapters.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `sc_ita_${E.islandTypedAdapters.size}`;
|
||||
E.islandTypedAdapters.set(key, name);
|
||||
const decls: string[] = [];
|
||||
const conv: string[] = [];
|
||||
const cleanup: string[] = [];
|
||||
let canFail = false;
|
||||
fn.params.forEach((p, i) => {
|
||||
const a = `sc_a${i}`;
|
||||
switch (p.kind) {
|
||||
case "jsval":
|
||||
decls.push(` ScrJsval *${a} = NULL;`);
|
||||
conv.push(` ${a} = scr_jsval_retain(argv[${i}]);`);
|
||||
cleanup.push(` scr_jsval_release(${a});`);
|
||||
break;
|
||||
case "f64":
|
||||
canFail = true;
|
||||
decls.push(` double ${a} = 0;`);
|
||||
conv.push(` if (!scr_jsval_exit_f64(argv[${i}], &${a})) goto sc_convfail;`);
|
||||
break;
|
||||
case "bool":
|
||||
canFail = true;
|
||||
decls.push(` bool ${a} = false;`);
|
||||
conv.push(` if (!scr_jsval_exit_bool(argv[${i}], &${a})) goto sc_convfail;`);
|
||||
break;
|
||||
case "string":
|
||||
canFail = true;
|
||||
decls.push(` ScrStr *${a} = NULL;`);
|
||||
conv.push(` ${a} = scr_jsval_exit_str(argv[${i}]);`, ` if (!${a}) goto sc_convfail;`);
|
||||
cleanup.push(` scr_str_release(${a});`);
|
||||
break;
|
||||
default: {
|
||||
// Composite (record/array/union): the jsExit pipeline — engine
|
||||
// JSON.stringify, json.parse, the interned dynCheck builder.
|
||||
canFail = true;
|
||||
const utag = E.undefinedArmTag(p);
|
||||
decls.push(` ${cDecl(p, a)} = NULL;`);
|
||||
const roundTrip = [
|
||||
` ScrStr *sc_j = scr_jsval_to_json(argv[${i}]);`,
|
||||
` if (!sc_j) goto sc_convfail;`,
|
||||
` ScrDyn *sc_d = scr_json_parse(sc_j);`,
|
||||
` scr_str_release(sc_j);`,
|
||||
` if (!sc_d) goto sc_convfail;`,
|
||||
` ${a} = ${E.dynCheckHelper(p)}(sc_d, NULL);`,
|
||||
` scr_dyn_release(sc_d);`,
|
||||
` if (scr_exc_pending()) goto sc_convfail;`,
|
||||
];
|
||||
if (p.kind === "union" && utag >= 0) {
|
||||
conv.push(
|
||||
` if (scr_jsval_is_undefined(argv[${i}])) {`,
|
||||
` ${a} = ${E.unitInstanceRef(p.unionId, utag)}; /* absent/undefined argument -> the undefined arm */`,
|
||||
` } else {`,
|
||||
...roundTrip,
|
||||
` }`,
|
||||
);
|
||||
} else {
|
||||
conv.push(` {`, ...roundTrip, ` }`);
|
||||
}
|
||||
cleanup.push(` ${releaseCallC(p, a)};`);
|
||||
}
|
||||
}
|
||||
});
|
||||
const call = `(${cFnPtrCast(fn)}c->fn)(${["c", ...fn.params.map((_, i) => `sc_a${i}`)].join(", ")})`;
|
||||
const body: string[] = [];
|
||||
if (ret.async) {
|
||||
// The closure returns a +1 ScrPromise (async bodies spawn eagerly to
|
||||
// the first suspension and return it; a sync body building a promise
|
||||
// may throw instead — NULL dummy with the exception pending, which
|
||||
// the wrapper bridges). from_promise takes ownership.
|
||||
const tagC = {
|
||||
void: "SCR_ISLP_VOID", f64: "SCR_ISLP_F64", bool: "SCR_ISLP_BOOL",
|
||||
string: "SCR_ISLP_STR", jsval: "SCR_ISLP_JSVAL",
|
||||
json: "", // unreachable: islandCallbackRet never tags async json
|
||||
}[ret.tag];
|
||||
body.push(
|
||||
` ScrPromise *sc_r = ${call};`,
|
||||
` if (!sc_r) return NULL;`,
|
||||
` return scr_jsval_from_promise(sc_r, ${tagC});`,
|
||||
);
|
||||
} else {
|
||||
switch (ret.tag) {
|
||||
case "void":
|
||||
body.push(` ${call};`, ` return NULL;`);
|
||||
break;
|
||||
case "jsval":
|
||||
body.push(` return ${call};`);
|
||||
break;
|
||||
case "f64":
|
||||
body.push(` return scr_jsval_from_f64(${call});`);
|
||||
break;
|
||||
case "bool":
|
||||
body.push(` return scr_jsval_from_bool(${call});`);
|
||||
break;
|
||||
case "string":
|
||||
body.push(
|
||||
// The closure's +1 string marshals in, then releases. NULL is
|
||||
// the throw-path dummy — the wrapper bridges the pending
|
||||
// exception.
|
||||
` ScrStr *sc_s = ${call};`,
|
||||
` if (!sc_s) return NULL;`,
|
||||
` ScrJsval *sc_r = scr_jsval_from_str(sc_s);`,
|
||||
` scr_str_release(sc_s);`,
|
||||
` return sc_r;`,
|
||||
);
|
||||
break;
|
||||
case "json": {
|
||||
// A JSON-safe composite return: the jsMarshal path — the
|
||||
// type-directed serializer, then the engine's JSON parser (deep
|
||||
// copy; the documented aliasing divergence). NULL result is the
|
||||
// throw-path dummy.
|
||||
const helper = E.jsonWriteHelper(fn.ret);
|
||||
body.push(
|
||||
` ${cDecl(fn.ret, "sc_rv")} = ${call};`,
|
||||
` if (scr_exc_pending()) { ${releaseCallC(fn.ret, "sc_rv")}; return NULL; }`,
|
||||
` ScrJsonBuf sc_jb; scr_jb_init(&sc_jb);`,
|
||||
` ${helper}(&sc_jb, sc_rv);`,
|
||||
` ${releaseCallC(fn.ret, "sc_rv")};`,
|
||||
` ScrStr *sc_rj = scr_jb_finish(&sc_jb);`,
|
||||
` ScrJsval *sc_r = scr_jsval_from_json(sc_rj);`,
|
||||
` scr_str_release(sc_rj);`,
|
||||
` return sc_r;`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const fail = canFail
|
||||
? [
|
||||
`sc_convfail:`,
|
||||
// Params already converted release here (NULL-tolerant; unit-arm
|
||||
// instances are immortal — their release is a no-op); the ones
|
||||
// never reached are still NULL/scalar. The pending TypeError
|
||||
// reverse-bridges in the wrapper.
|
||||
...cleanup,
|
||||
` return NULL;`,
|
||||
]
|
||||
: [];
|
||||
const sig = `static ScrJsval *${name}(ScrClosure *c, ScrJsval **argv)`;
|
||||
E.walkerProtos.push(`${sig}; /* typed island host-call adapter: ${key} */`);
|
||||
E.walkerDefs.push(
|
||||
`${sig} { /* ${key} */`,
|
||||
...(fn.params.length === 0 ? [` (void)argv;`] : []),
|
||||
...decls,
|
||||
...conv,
|
||||
...body,
|
||||
...fail,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
/* Per-shape C emission: class/record struct definitions and their RC/trace
|
||||
* helper families, the hierarchy vtable machinery (slot structs, per-class
|
||||
* instances, exact-signature adapter thunks), and the capture-box
|
||||
* constructors. Everything here is driven by the class graph (ClassMeta,
|
||||
* VtSlot) the emitter builds up front; emission ORDER is part of the C. */
|
||||
import type { CEmitter } from "./emitter.js";
|
||||
import type { IrFunction } from "../../ir/nodes.js";
|
||||
import { IrClassDef, IrType, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, isRefCounted, mapOf, STRING } from "../../ir/nodes.js";
|
||||
import { mangleClassGcFree, mangleClassNew, mangleClassRelease, mangleClassReleaseDirect, mangleClassRetain, mangleClassStruct, mangleClassTrace, mangleCtorThunk, mangleField, mangleFunction, mangleRecordGcFree, mangleRecordNew, mangleRecordRelease, mangleRecordRetain, mangleRecordStruct, mangleRecordTrace, mangleVtAdapter, mangleVtInstance, mangleVtStruct } from "../mangle.js";
|
||||
import { boxKindC, cDecl, cType, elemKindC, mapValKindC, releaseCallC, vAdapters } from "./emit-types.js";
|
||||
|
||||
/** The overflow map's C member name on index-signature record structs.
|
||||
* User fields mangle to `sc_fld_*`, so no field can collide. */
|
||||
export const OVERFLOW_MEMBER = "sc_ovf";
|
||||
|
||||
/** True when the class descends from a runtime stream class: its struct
|
||||
* embeds the FULL ScrStream prefix (registry, display name, state
|
||||
* pointer) and its RC/trace helpers delegate the state block to
|
||||
* scr_stream_st_*. */
|
||||
function streamRooted(meta: ClassMeta): boolean {
|
||||
for (let m = meta.base; m; m = m.base) {
|
||||
if (RUNTIME_STREAM_CLASSES.has(m.def.name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** One virtual method slot of a hierarchy: the ROOT-MOST declaring class
|
||||
* owns the slot; its declaration's IrFunction fixes the slot's C signature
|
||||
* (`this` typed as the declarer — implementations sit behind reinterpreting
|
||||
* adapters). Only methods overridden somewhere get slots. */
|
||||
export interface VtSlot {
|
||||
method: string;
|
||||
declarer: ClassMeta;
|
||||
fn: IrFunction;
|
||||
/** The slot's C member name, unique WITHIN its root's vtable struct:
|
||||
* sibling branches can each own a slot for the same method name
|
||||
* (mangleVtSlot's occurrence ordinal disambiguates). */
|
||||
member: string;
|
||||
}
|
||||
|
||||
/** Per-class node of the class graph (see CEmitter.classMeta). `pre`/`post`
|
||||
* are the preorder interval over the whole-program class forest — a class's
|
||||
* descendants are exactly the classes whose `pre` lies inside it, which is
|
||||
* both the instanceof check and the slot-lookup subtree test. */
|
||||
export interface ClassMeta {
|
||||
def: IrClassDef;
|
||||
base: ClassMeta | null;
|
||||
children: ClassMeta[];
|
||||
root: ClassMeta;
|
||||
pre: number;
|
||||
post: number;
|
||||
hierarchy: boolean;
|
||||
/** Root classes: the hierarchy's slots in DFS-declaration order. */
|
||||
slots: VtSlot[];
|
||||
}
|
||||
|
||||
/** Per-shape C structs + RC helpers for classes AND record shapes (the
|
||||
* layouts are identical: `size_t rc` header + fields). All structs are
|
||||
* forward-declared first so fields can reference any shape regardless of
|
||||
* order (classes nesting records, records nesting classes, mutual
|
||||
* references), and the RC helpers are prototyped before any body so a
|
||||
* release can call another shape's release regardless of emission order.
|
||||
* The `_v` adapters give boxes untyped RC entry points without UB casts.
|
||||
*
|
||||
* Cycle-capable shapes (the constructor fixpoint) additionally get a
|
||||
* cycle header (scr_cyc_alloc) plus two collector entry points: a trace
|
||||
* visiting exactly the cycle-capable fields, and a teardown releasing
|
||||
* exactly the other refcounted fields (the trace/teardown complement
|
||||
* contract in scr_runtime.h) — and their retain/release feed the
|
||||
* candidate-root buffer. Acyclic shapes keep the lean 1-word header. */
|
||||
export function emitStructDefs(E: CEmitter, out: string[]): void {
|
||||
interface StructShape {
|
||||
struct: string;
|
||||
newFn: string;
|
||||
retain: string;
|
||||
release: string;
|
||||
trace: string;
|
||||
gcFree: string;
|
||||
traced: boolean;
|
||||
fields: { name: string; type: IrType }[];
|
||||
/** Records with a string index signature: the overflow map's VALUE
|
||||
* type. The struct carries a trailing `ScrMap *` member the shape's
|
||||
* new/release/trace treat as one more (map-typed) field. */
|
||||
indexValue?: IrType;
|
||||
comment: string;
|
||||
/** Class shapes only; hierarchy members get the vtable machinery. */
|
||||
meta: ClassMeta | null;
|
||||
}
|
||||
// Runtime-provided classes (the builtin Error hierarchy) emit NOTHING
|
||||
// here — struct, RC helpers, and vtables live in the runtime. They keep
|
||||
// their ClassMeta (preorder numbering, instanceof constants, vtable
|
||||
// struct type for user subclasses); main() stamps their intervals.
|
||||
const shapes: StructShape[] = [
|
||||
...(E.mod.classes ?? []).filter((cls) => !cls.runtime).map((cls) => ({
|
||||
struct: mangleClassStruct(cls.name),
|
||||
newFn: mangleClassNew(cls.name),
|
||||
retain: mangleClassRetain(cls.name),
|
||||
release: mangleClassRelease(cls.name),
|
||||
trace: mangleClassTrace(cls.name),
|
||||
gcFree: mangleClassGcFree(cls.name),
|
||||
traced: E.tracedShapes.has(`object:${cls.name}`),
|
||||
fields: cls.fields,
|
||||
comment: `class ${cls.name}`,
|
||||
meta: E.classMeta.get(cls.name) ?? null,
|
||||
})),
|
||||
...(E.mod.records ?? []).map((rec) => ({
|
||||
struct: mangleRecordStruct(rec.id),
|
||||
newFn: mangleRecordNew(rec.id),
|
||||
retain: mangleRecordRetain(rec.id),
|
||||
release: mangleRecordRelease(rec.id),
|
||||
trace: mangleRecordTrace(rec.id),
|
||||
gcFree: mangleRecordGcFree(rec.id),
|
||||
traced: E.tracedShapes.has(`record:${rec.id}`),
|
||||
fields: rec.fields,
|
||||
...(rec.indexValue ? { indexValue: rec.indexValue } : {}),
|
||||
comment: `record ${rec.id} { ${rec.fields.map((f) => f.name).join("; ")}${rec.indexValue ? "; [key: string]" : ""} }`,
|
||||
meta: null,
|
||||
})),
|
||||
];
|
||||
if (shapes.length === 0) return;
|
||||
const inHierarchy = (s: StructShape): boolean => s.meta !== null && s.meta.hierarchy;
|
||||
// The RC-relevant members of a shape: every field, plus the overflow
|
||||
// map on index-signature shapes — release/trace/teardown treat it as
|
||||
// one more map-typed member (traceAdapterC's map rule answers whether
|
||||
// the record's trace must visit it).
|
||||
const rcMembers = (s: StructShape): { member: string; type: IrType; name: string }[] => [
|
||||
...s.fields.map((f) => ({ member: mangleField(f.name), type: f.type, name: f.name })),
|
||||
...(s.indexValue
|
||||
? [{ member: OVERFLOW_MEMBER, type: mapOf(STRING, s.indexValue), name: "[key: string] overflow" }]
|
||||
: []),
|
||||
];
|
||||
// CLASS newFns start every undefined-armed union field at JS's
|
||||
// `undefined` — the interned immortal unit instance — instead of the
|
||||
// calloc NULL: tsc's strictPropertyInitialization accepts such fields
|
||||
// with no initializer and no constructor assignment (undefined is in
|
||||
// the type), so a fresh instance is readable before any assignment
|
||||
// runs — a method assigns later, a constructor branch skips it, a base
|
||||
// constructor's virtual call reads a derived field before super()
|
||||
// returns. Node reads `undefined` there; a NULL ScrUnion would be a
|
||||
// segfault. Class shapes only: record shapes are fully written at
|
||||
// every construction site (literal lowering fills omitted optional
|
||||
// fields, the dynCheck/JSON builders fill missing keys), and the
|
||||
// immortal instance costs nothing if overwritten (releases skip it).
|
||||
const undefFieldInitC = (s: StructShape): string[] =>
|
||||
s.meta === null ? [] : s.fields.flatMap((f) => E.undefFieldInitLineC(f.name, f.type));
|
||||
// The overflow map's construction call (in the shape's newFn): value
|
||||
// handling is type-directed exactly like a user Map's.
|
||||
const overflowNewC = (s: StructShape): string => {
|
||||
const v = s.indexValue!;
|
||||
const valKind = mapValKindC(v);
|
||||
if (valKind !== "SCR_MAP_VAL_REF") {
|
||||
return `scr_map_new(SCR_MAP_KEY_STR, ${valKind}, NULL, NULL, NULL)`;
|
||||
}
|
||||
const rc = vAdapters(v);
|
||||
return `scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_REF, &${rc.retain}, &${rc.release}, ${E.traceArgC(v)})`;
|
||||
};
|
||||
|
||||
for (const s of shapes) {
|
||||
out.push(`typedef struct ${s.struct} ${s.struct}; /* ${s.comment} */`);
|
||||
}
|
||||
out.push("");
|
||||
for (const s of shapes) {
|
||||
out.push(`struct ${s.struct} { /* ${s.comment} */`, ` size_t rc;`);
|
||||
if (inHierarchy(s)) {
|
||||
// The hierarchy prefix: base fields follow at identical offsets in
|
||||
// every subclass, so vt must sit between rc and the field list.
|
||||
out.push(` const ScrVt *vt;`);
|
||||
if (s.meta!.root.def.name === RUNTIME_EMITTER_CLASS) {
|
||||
// Emitter subclasses embed ScrEmitter's remaining prefix (the
|
||||
// registry and display-name slots) so an upcast to ScrEmitter*
|
||||
// is the usual pointer reinterpret. Carried by the BACKEND —
|
||||
// the IR field lists stay empty for it.
|
||||
out.push(
|
||||
` ScrEeReg *sc_eereg; /* EventEmitter registry (ScrEmitter prefix) */`,
|
||||
` const char *sc_eecls; /* EventEmitter display name (ScrEmitter prefix) */`,
|
||||
);
|
||||
if (streamRooted(s.meta!)) {
|
||||
// Stream subclasses embed ScrStream's remaining slot: the
|
||||
// state pointer, NULL until the constructor's super(options)
|
||||
// reaches scr_stream_init_* — so an upcast to ScrStream* (and
|
||||
// on through ScrEmitter*) is the usual pointer reinterpret.
|
||||
out.push(` ScrStreamState *sc_st; /* stream state (ScrStream prefix) */`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const f of s.fields) {
|
||||
out.push(` ${cDecl(f.type, mangleField(f.name))}; /* ${f.name} */`);
|
||||
}
|
||||
if (s.indexValue) {
|
||||
out.push(` ScrMap *${OVERFLOW_MEMBER}; /* [key: string] overflow (string-keyed) */`);
|
||||
}
|
||||
out.push(`};`);
|
||||
}
|
||||
out.push("");
|
||||
// Vtable struct typedefs are named after hierarchy ROOTS, which may be
|
||||
// a runtime error class (a user `extends Error` subclass's vtable
|
||||
// instance is typed by the %Error root's struct) — so typedefs come
|
||||
// from the EMITTED hierarchy classes' roots, while adapter prototypes,
|
||||
// instances, and helpers stay shapes-only (the runtime owns the
|
||||
// builtin classes; a subclass-free Error tree emits nothing at all).
|
||||
E.emitVtableDecls(out, shapes.filter(inHierarchy).map((s) => s.meta!));
|
||||
for (const s of shapes) {
|
||||
out.push(
|
||||
`static ${s.struct} *${s.retain}(${s.struct} *o);`,
|
||||
`static void ${s.release}(${s.struct} *o);`,
|
||||
`static ${s.struct} *${s.newFn}(void);`,
|
||||
);
|
||||
if (inHierarchy(s)) {
|
||||
out.push(`static void ${mangleClassReleaseDirect(s.meta!.def.name)}(void *o0);`);
|
||||
}
|
||||
if (s.traced) {
|
||||
out.push(
|
||||
`static void ${s.trace}(void *o, ScrTraceVisit visit, void *ctx);`,
|
||||
`static void ${s.gcFree}(void *o);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
out.push("");
|
||||
E.emitVtableInstances(out, shapes.filter(inHierarchy).map((s) => s.meta!));
|
||||
for (const s of shapes) {
|
||||
if (inHierarchy(s)) {
|
||||
E.emitHierarchyClassHelpers(out, s.meta!, s);
|
||||
continue;
|
||||
}
|
||||
// NULL-tolerant: zeroed fields (calloc) and user `null as unknown as C`
|
||||
// casts can put NULL where an object is expected.
|
||||
if (!s.traced) {
|
||||
out.push(
|
||||
`static ${s.struct} *${s.retain}(${s.struct} *o) {`,
|
||||
` if (o && o->rc != SIZE_MAX) o->rc++;`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
`static void ${s.release}(${s.struct} *o) {`,
|
||||
` if (!o || o->rc == SIZE_MAX) return;`,
|
||||
` if (--o->rc == 0) {`,
|
||||
);
|
||||
for (const m of rcMembers(s)) {
|
||||
if (!isRefCounted(m.type)) continue;
|
||||
const field = `o->${m.member}`;
|
||||
out.push(` if (${field}) ${releaseCallC(m.type, field)};`);
|
||||
}
|
||||
out.push(
|
||||
` scr_obj_free_note();`,
|
||||
` free(o);`,
|
||||
` }`,
|
||||
`}`,
|
||||
`static ${s.struct} *${s.newFn}(void) {`,
|
||||
` ${s.struct} *o = calloc(1, sizeof *o);`,
|
||||
` if (!o) { fputs("scriptc: out of memory\\n", stderr); abort(); }`,
|
||||
` o->rc = 1;`,
|
||||
...undefFieldInitC(s),
|
||||
...(s.indexValue ? [` o->${OVERFLOW_MEMBER} = ${overflowNewC(s)};`] : []),
|
||||
` scr_obj_alloc_note();`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
`static void *${s.retain}_v(void *o) { return ${s.retain}((${s.struct} *)o); }`,
|
||||
`static void ${s.release}_v(void *o) { ${s.release}((${s.struct} *)o); }`,
|
||||
``,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Cycle-capable shape: cycle-headered allocation, root-buffer hooks
|
||||
// on release, and the trace/teardown complement pair.
|
||||
const tracedFields = rcMembers(s).filter((m) => E.traceAdapterC(m.type) !== null);
|
||||
const untracedRefFields = rcMembers(s).filter(
|
||||
(m) => isRefCounted(m.type) && E.traceAdapterC(m.type) === null,
|
||||
);
|
||||
out.push(
|
||||
`static ${s.struct} *${s.retain}(${s.struct} *o) {`,
|
||||
` if (o && o->rc != SIZE_MAX) {`,
|
||||
` o->rc++;`,
|
||||
` scr_cyc_mark_live(o);`,
|
||||
` }`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
`static void ${s.release}(${s.struct} *o) {`,
|
||||
` if (!o || o->rc == SIZE_MAX) return;`,
|
||||
` if (--o->rc == 0) {`,
|
||||
` scr_cyc_on_dead(o);`,
|
||||
);
|
||||
for (const m of rcMembers(s)) {
|
||||
if (!isRefCounted(m.type)) continue;
|
||||
const field = `o->${m.member}`;
|
||||
out.push(` if (${field}) ${releaseCallC(m.type, field)};`);
|
||||
}
|
||||
out.push(
|
||||
` scr_obj_free_note();`,
|
||||
` scr_cyc_free(o);`,
|
||||
` } else {`,
|
||||
` scr_cyc_on_release(o); /* possible cycle root; may collect */`,
|
||||
` }`,
|
||||
`}`,
|
||||
`static ${s.struct} *${s.newFn}(void) {`,
|
||||
` ${s.struct} *o = scr_cyc_alloc(sizeof *o, &${s.trace}, &${s.gcFree});`,
|
||||
` o->rc = 1;`,
|
||||
...undefFieldInitC(s),
|
||||
...(s.indexValue ? [` o->${OVERFLOW_MEMBER} = ${overflowNewC(s)};`] : []),
|
||||
` scr_obj_alloc_note();`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
`static void ${s.trace}(void *o0, ScrTraceVisit visit, void *ctx) {`,
|
||||
` ${s.struct} *o = (${s.struct} *)o0;`,
|
||||
...tracedFields.map(
|
||||
(m) => ` visit(o->${m.member}, ctx); /* ${m.name} */`,
|
||||
),
|
||||
`}`,
|
||||
`static void ${s.gcFree}(void *o0) {`,
|
||||
...(untracedRefFields.length > 0
|
||||
? [
|
||||
` ${s.struct} *o = (${s.struct} *)o0;`,
|
||||
...untracedRefFields.map((m) => {
|
||||
const field = `o->${m.member}`;
|
||||
return ` if (${field}) ${releaseCallC(m.type, field)}; /* ${m.name} (acyclic) */`;
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
` scr_obj_free_note();`,
|
||||
` scr_cyc_free(o0);`,
|
||||
`}`,
|
||||
`static void *${s.retain}_v(void *o) { return ${s.retain}((${s.struct} *)o); }`,
|
||||
`static void ${s.release}_v(void *o) { ${s.release}((${s.struct} *)o); }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** The root's slot list as seen by one class: the implementation the
|
||||
* class dispatches to, or null outside the slot's declaring subtree (a
|
||||
* call through this class's vtable can never reach that slot). An
|
||||
* ABSTRACT class whose chain holds only abstract declarations of the
|
||||
* slot also answers null — the class never instantiates (tsc), so its
|
||||
* own vtable entry can never dispatch. */
|
||||
export function vtEntriesFor(E: CEmitter, meta: ClassMeta): { slot: VtSlot; impl: ClassMeta | null }[] {
|
||||
return meta.root.slots.map((slot) => {
|
||||
if (!(slot.declarer.pre <= meta.pre && meta.pre <= slot.declarer.post)) {
|
||||
return { slot, impl: null };
|
||||
}
|
||||
for (let c: ClassMeta | null = meta; c; c = c.base) {
|
||||
if (c.def.methods?.includes(slot.method) && !c.def.abstractMethods?.includes(slot.method)) {
|
||||
return { slot, impl: c };
|
||||
}
|
||||
}
|
||||
if (meta.def.abstract === true) return { slot, impl: null };
|
||||
throw new Error(`emitter bug: no implementation of ${slot.method} for ${meta.def.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
/** The C parameter list of a slot (declaring-class `this` first). */
|
||||
export function vtSlotParams(E: CEmitter, slot: VtSlot, named: boolean): string[] {
|
||||
const thisParam = named
|
||||
? `${mangleClassStruct(slot.declarer.def.name)} *o`
|
||||
: `${mangleClassStruct(slot.declarer.def.name)} *`;
|
||||
const rest = slot.fn.params
|
||||
.slice(1)
|
||||
.map((p, i) => (named ? cDecl(p.type, `sc_a${i}`) : cType(p.type).trim()));
|
||||
return [thisParam, ...rest];
|
||||
}
|
||||
|
||||
/** Vtable struct typedefs + adapter prototypes (the definitions call
|
||||
* function bodies, so they flush after the signature block — see emit). */
|
||||
export function emitVtableDecls(E: CEmitter, out: string[], hierarchyClasses: ClassMeta[]): void {
|
||||
// Roots of the EMITTED classes' hierarchies — a runtime root (%Error)
|
||||
// counts exactly when some emitted subclass needs its vtable type.
|
||||
const roots = [...new Set(hierarchyClasses.map((m) => m.root))];
|
||||
for (const root of roots) {
|
||||
const vtt = mangleVtStruct(root.def.name);
|
||||
out.push(`typedef struct ${vtt} { /* vtable: hierarchy rooted at ${root.def.name} */`);
|
||||
out.push(` ScrVt head;`);
|
||||
for (const slot of root.slots) {
|
||||
const ret = cType(slot.fn.returnType).trim();
|
||||
out.push(
|
||||
` ${ret} (*${slot.member})(${E.vtSlotParams(slot, false).join(", ")}); /* ${slot.method} */`,
|
||||
);
|
||||
}
|
||||
out.push(`} ${vtt};`);
|
||||
}
|
||||
if (roots.length > 0) out.push("");
|
||||
for (const meta of hierarchyClasses) {
|
||||
for (const { slot, impl } of E.vtEntriesFor(meta)) {
|
||||
if (impl === null) continue;
|
||||
const key = `${impl.def.name}.${slot.method}`;
|
||||
if (E.vtAdapters.has(key)) continue;
|
||||
E.vtAdapters.set(key, { impl, slot });
|
||||
const ret = cType(slot.fn.returnType).trim();
|
||||
out.push(
|
||||
`static ${ret} ${mangleVtAdapter(impl.def.name, slot.method)}(${E.vtSlotParams(slot, false).join(", ")});`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (E.vtAdapters.size > 0) out.push("");
|
||||
}
|
||||
|
||||
/** One static const vtable per hierarchy class: interval, direct
|
||||
* release, and the class's dispatch entry for every slot. */
|
||||
export function emitVtableInstances(E: CEmitter, out: string[], hierarchyClasses: ClassMeta[]): void {
|
||||
for (const meta of hierarchyClasses) {
|
||||
const vtt = mangleVtStruct(meta.root.def.name);
|
||||
const head = `{ ${meta.pre}, ${meta.post}, &${mangleClassReleaseDirect(meta.def.name)} }`;
|
||||
const entries = E.vtEntriesFor(meta).map(({ slot, impl }) =>
|
||||
impl === null
|
||||
? `0 /* ${slot.method}: outside the declaring subtree */`
|
||||
: `&${mangleVtAdapter(impl.def.name, slot.method)} /* ${slot.method} */`,
|
||||
);
|
||||
out.push(
|
||||
`static const ${vtt} ${mangleVtInstance(meta.def.name)} = { /* class ${meta.def.name} */`,
|
||||
` ${[head, ...entries].join(",\n ")}`,
|
||||
`};`,
|
||||
);
|
||||
}
|
||||
if (hierarchyClasses.length > 0) out.push("");
|
||||
}
|
||||
|
||||
/** Adapter definitions (flushed after the signature block): the impl
|
||||
* class's method behind the slot's declaring-class signature. */
|
||||
export function emitVtAdapterDefs(E: CEmitter, out: string[]): void {
|
||||
for (const { impl, slot } of E.vtAdapters.values()) {
|
||||
const ret = cType(slot.fn.returnType).trim();
|
||||
const recv =
|
||||
impl === slot.declarer ? "o" : `(${mangleClassStruct(impl.def.name)} *)o`;
|
||||
const args = [recv, ...slot.fn.params.slice(1).map((_, i) => `sc_a${i}`)].join(", ");
|
||||
const call = `${mangleFunction(`%${impl.def.name}.${slot.method}`)}(${args})`;
|
||||
out.push(
|
||||
``,
|
||||
`static ${ret} ${mangleVtAdapter(impl.def.name, slot.method)}(${E.vtSlotParams(slot, true).join(", ")}) {`,
|
||||
slot.fn.returnType.kind === "void" ? ` ${call};` : ` return ${call};`,
|
||||
`}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** RC helpers of one hierarchy class. Retain is layout-generic (rc sits
|
||||
* at offset 0 in every subclass); the PUBLIC release dispatches through
|
||||
* the object's vtable so a base-typed release tears down the derived
|
||||
* object; the DIRECT release (the vtable entry) does the class's own
|
||||
* teardown. Cycle capability is hierarchy-uniform (constructor fixpoint),
|
||||
* and the cycle header's trace/teardown are stamped with the concrete
|
||||
* class's functions at allocation — the collector needs no vtable. */
|
||||
export function emitHierarchyClassHelpers(E: CEmitter, out: string[],
|
||||
meta: ClassMeta,
|
||||
s: {
|
||||
struct: string;
|
||||
newFn: string;
|
||||
retain: string;
|
||||
release: string;
|
||||
trace: string;
|
||||
gcFree: string;
|
||||
traced: boolean;
|
||||
fields: { name: string; type: IrType }[];
|
||||
},): void {
|
||||
const reld = mangleClassReleaseDirect(meta.def.name);
|
||||
const emitterRooted = meta.root.def.name === RUNTIME_EMITTER_CLASS;
|
||||
const isStreamRooted = streamRooted(meta);
|
||||
// The display name Node's leak warning prints ([My]) — the source
|
||||
// class name, without the module qualifier.
|
||||
const displayName = meta.def.name.includes(".")
|
||||
? meta.def.name.slice(meta.def.name.lastIndexOf(".") + 1)
|
||||
: meta.def.name;
|
||||
out.push(
|
||||
`static ${s.struct} *${s.retain}(${s.struct} *o) {`,
|
||||
...(s.traced
|
||||
? [` if (o && o->rc != SIZE_MAX) {`, ` o->rc++;`, ` scr_cyc_mark_live(o);`, ` }`]
|
||||
: [` if (o && o->rc != SIZE_MAX) o->rc++;`]),
|
||||
` return o;`,
|
||||
`}`,
|
||||
`static void ${s.release}(${s.struct} *o) {`,
|
||||
` if (!o || o->rc == SIZE_MAX) return;`,
|
||||
` o->vt->release(o); /* the DYNAMIC class's teardown */`,
|
||||
`}`,
|
||||
`static void ${reld}(void *o0) {`,
|
||||
` ${s.struct} *o = (${s.struct} *)o0;`,
|
||||
` if (--o->rc == 0) {`,
|
||||
...(s.traced ? [` scr_cyc_on_dead(o);`] : []),
|
||||
);
|
||||
for (const f of s.fields) {
|
||||
if (!isRefCounted(f.type)) continue;
|
||||
const field = `o->${mangleField(f.name)}`;
|
||||
out.push(` if (${field}) ${releaseCallC(f.type, field)};`);
|
||||
}
|
||||
if (emitterRooted) {
|
||||
out.push(` scr_emitter_reg_drop(o->sc_eereg); /* EventEmitter prefix */`);
|
||||
if (isStreamRooted) {
|
||||
out.push(` scr_stream_st_release(o->sc_st); /* stream state (ScrStream prefix) */`);
|
||||
}
|
||||
}
|
||||
out.push(
|
||||
` scr_obj_free_note();`,
|
||||
s.traced ? ` scr_cyc_free(o);` : ` free(o);`,
|
||||
...(s.traced
|
||||
? [` } else {`, ` scr_cyc_on_release(o); /* possible cycle root; may collect */`, ` }`]
|
||||
: [` }`]),
|
||||
`}`,
|
||||
`static ${s.struct} *${s.newFn}(void) {`,
|
||||
...(s.traced
|
||||
? [` ${s.struct} *o = scr_cyc_alloc(sizeof *o, &${s.trace}, &${s.gcFree});`]
|
||||
: [
|
||||
` ${s.struct} *o = calloc(1, sizeof *o);`,
|
||||
` if (!o) { fputs("scriptc: out of memory\\n", stderr); abort(); }`,
|
||||
]),
|
||||
` o->rc = 1;`,
|
||||
` o->vt = &${mangleVtInstance(meta.def.name)}.head;`,
|
||||
...(emitterRooted
|
||||
? [` o->sc_eecls = ${JSON.stringify(displayName)}; /* EventEmitter prefix (reg stays NULL) */`]
|
||||
: []),
|
||||
// Undefined-admitting fields start as JS's undefined, not NULL — see
|
||||
// undefFieldInitLineC. s.fields is the FLATTENED layout (base prefix
|
||||
// + own), so a derived allocation covers inherited fields too.
|
||||
...s.fields.flatMap((f) => E.undefFieldInitLineC(f.name, f.type)),
|
||||
` scr_obj_alloc_note();`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
);
|
||||
if (s.traced) {
|
||||
const tracedFields = s.fields.filter((f) => E.traceAdapterC(f.type) !== null);
|
||||
const untracedRefFields = s.fields.filter(
|
||||
(f) => isRefCounted(f.type) && E.traceAdapterC(f.type) === null,
|
||||
);
|
||||
out.push(
|
||||
`static void ${s.trace}(void *o0, ScrTraceVisit visit, void *ctx) {`,
|
||||
` ${s.struct} *o = (${s.struct} *)o0;`,
|
||||
...(emitterRooted
|
||||
? [` scr_emitter_reg_trace(o->sc_eereg, visit, ctx); /* listener closures */`]
|
||||
: []),
|
||||
...(isStreamRooted
|
||||
? [` scr_stream_st_trace(o->sc_st, visit, ctx); /* stream state closures/pipes */`]
|
||||
: []),
|
||||
...tracedFields.map((f) => ` visit(o->${mangleField(f.name)}, ctx); /* ${f.name} */`),
|
||||
`}`,
|
||||
`static void ${s.gcFree}(void *o0) {`,
|
||||
...(untracedRefFields.length > 0 || emitterRooted
|
||||
? [` ${s.struct} *o = (${s.struct} *)o0;`]
|
||||
: []),
|
||||
...(emitterRooted
|
||||
? [` scr_emitter_reg_gcfree(o->sc_eereg); /* EventEmitter prefix */`]
|
||||
: []),
|
||||
...(isStreamRooted
|
||||
? [` scr_stream_st_gcfree(o->sc_st); /* stream state (ScrStream prefix) */`]
|
||||
: []),
|
||||
...untracedRefFields.map((f) => {
|
||||
const field = `o->${mangleField(f.name)}`;
|
||||
return ` if (${field}) ${releaseCallC(f.type, field)}; /* ${f.name} (acyclic) */`;
|
||||
}),
|
||||
` scr_obj_free_note();`,
|
||||
` scr_cyc_free(o0);`,
|
||||
`}`,
|
||||
);
|
||||
}
|
||||
out.push(
|
||||
`static void *${s.retain}_v(void *o) { return ${s.retain}((${s.struct} *)o); }`,
|
||||
`static void ${s.release}_v(void *o) { ${s.release}((${s.struct} *)o); }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
/** Class objects (classes as first-class values): one immortal
|
||||
* ScrClassObj static per class some classRef in the module names, plus
|
||||
* the construct-thunk PROTOTYPES their `ctor` slots take the address of
|
||||
* (definitions land later with the other synthesized bodies —
|
||||
* emitCtorThunkDefs). The interval constants are the same numbering the
|
||||
* vtables carry, so instanceOfValue agrees with compiled instanceOf. */
|
||||
export function emitClassObjs(E: CEmitter, out: string[]): void {
|
||||
if (E.classObjs.size === 0) return;
|
||||
out.push("");
|
||||
for (const [className, sym] of E.classObjs) {
|
||||
const meta = E.classMeta.get(className);
|
||||
if (!meta) throw new Error(`emitter bug: class object for unknown class ${className}`);
|
||||
// A generic-class INSTANTIATION's class object carries its FAMILY's
|
||||
// interval: at runtime JS has ONE `Box`, so instanceof through the
|
||||
// value must answer for the whole family (every instantiation and
|
||||
// their subclasses) — IrClassDef.genericOf. Construction still
|
||||
// dispatches the instantiation's own thunk.
|
||||
const intervalMeta = meta.def.genericOf !== undefined
|
||||
? E.classMeta.get(meta.def.genericOf)
|
||||
: meta;
|
||||
if (!intervalMeta) throw new Error(`emitter bug: class object for ${className} names unknown family ${meta.def.genericOf ?? ""}`);
|
||||
const nameSym = E.internLiteral(meta.def.jsName ?? "");
|
||||
out.push(
|
||||
`static void *${mangleCtorThunk(className)}(${ctorThunkParams(E, className).decls || "void"});`,
|
||||
`static ScrClassObj ${sym} = { SIZE_MAX, ${intervalMeta.pre}, ${intervalMeta.post}, ` +
|
||||
`(void *)&${mangleCtorThunk(className)}, (const ScrStr *)&${nameSym} }; /* class ${className} */`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** The construct thunk's parameter list: the constructor's completed ABI
|
||||
* minus the `this` the thunk allocates itself. */
|
||||
function ctorThunkParams(E: CEmitter, className: string): { decls: string; names: string[] } {
|
||||
const ctor = E.fnByName.get(`%${className}.constructor`);
|
||||
if (!ctor) throw new Error(`emitter bug: class object for ${className} without a constructor`);
|
||||
const params = ctor.params.slice(1);
|
||||
return {
|
||||
decls: params.map((p, i) => cDecl(p.type, `sc_a${i}`)).join(", "),
|
||||
names: params.map((_, i) => `sc_a${i}`),
|
||||
};
|
||||
}
|
||||
|
||||
/** Construct-thunk definitions (`void *sc_ct_C(args…)`): allocate, run
|
||||
* the constructor over a +1 `this` like the inline `new` emission, and
|
||||
* hand the remaining +1 out as `void *` — the one well-defined
|
||||
* function-pointer shape every class in a classval slot shares (the
|
||||
* frontend's ABI flow rule makes the parameter lists agree). A throwing
|
||||
* constructor leaves the pending flag set: the thunk releases the
|
||||
* half-built object and returns NULL (a dummy the checked call site
|
||||
* never reads). */
|
||||
export function emitCtorThunkDefs(E: CEmitter, out: string[]): void {
|
||||
for (const className of E.classObjs.keys()) {
|
||||
const { decls, names } = ctorThunkParams(E, className);
|
||||
const struct = mangleClassStruct(className);
|
||||
const lines = [
|
||||
``,
|
||||
`static void *${mangleCtorThunk(className)}(${decls || "void"}) {`,
|
||||
` ${struct} *o = ${mangleClassNew(className)}();`,
|
||||
` ${mangleFunction(`%${className}.constructor`)}(${[`${mangleClassRetain(className)}(o)`, ...names].join(", ")});`,
|
||||
];
|
||||
if (E.mayThrow.has(`%${className}.constructor`)) {
|
||||
lines.push(
|
||||
` if (scr_exc_pending()) {`,
|
||||
` ${mangleClassRelease(className)}(o);`,
|
||||
` return NULL;`,
|
||||
` }`,
|
||||
);
|
||||
}
|
||||
lines.push(` return (void *)o;`, `}`);
|
||||
out.push(...lines);
|
||||
}
|
||||
}
|
||||
|
||||
/** main()'s stamping of the runtime error vtables: preorder intervals
|
||||
* from THIS module's numbering, plus the traced-mode switch when the
|
||||
* cycle fixpoint marked the Error hierarchy (a user subclass holds
|
||||
* cycle-capable fields — capability is hierarchy-uniform, so the
|
||||
* runtime's own allocations need collector headers too). Empty for
|
||||
* hand-written IR without the builtin defs. */
|
||||
export function errorVtStampLines(E: CEmitter): string[] {
|
||||
const lines: string[] = [];
|
||||
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
|
||||
const meta = E.classMeta.get(name);
|
||||
if (!meta) return [];
|
||||
lines.push(
|
||||
` scr_error_vts[${rec.kind}].pre = ${meta.pre}; scr_error_vts[${rec.kind}].post = ${meta.post}; /* ${rec.lib} */`,
|
||||
);
|
||||
}
|
||||
if (E.tracedShapes.has("object:%Error")) {
|
||||
lines.push(` scr_error_set_traced();`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** main()'s stamping of the runtime emitter vtable: its preorder
|
||||
* interval from THIS module's numbering (the errorVtStampLines story).
|
||||
* Empty when the program never touches the emitter surface — the class
|
||||
* def only rides modules that reference it. */
|
||||
export function emitterVtStampLines(E: CEmitter): string[] {
|
||||
const meta = E.classMeta.get(RUNTIME_EMITTER_CLASS);
|
||||
if (!meta) return [];
|
||||
return [
|
||||
` scr_emitter_vt.pre = ${meta.pre}; scr_emitter_vt.post = ${meta.post}; /* EventEmitter */`,
|
||||
];
|
||||
}
|
||||
|
||||
/** main()'s stamping of the runtime stream vtables (Readable/Writable/
|
||||
* Duplex/Transform/PassThrough) — the emitterVtStampLines story: each
|
||||
* def rides the module only when the program touches the stream
|
||||
* surface, and instanceof needs their preorder intervals under the
|
||||
* emitter root. */
|
||||
export function streamVtStampLines(E: CEmitter): string[] {
|
||||
const lines: string[] = [];
|
||||
for (const [name, rec] of RUNTIME_STREAM_CLASSES) {
|
||||
const meta = E.classMeta.get(name);
|
||||
if (!meta) continue;
|
||||
const vt = `scr_${rec.lib.toLowerCase()}_vt`;
|
||||
lines.push(` ${vt}.pre = ${meta.pre}; ${vt}.post = ${meta.post}; /* ${rec.lib} */`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** The trace entry point symbol for a payload/field type, or null when
|
||||
* the type cannot participate in a cycle (see the constructor fixpoint). */
|
||||
export function traceAdapterC(E: CEmitter, t: IrType): string | null {
|
||||
switch (t.kind) {
|
||||
case "func":
|
||||
return "scr_closure_trace_v";
|
||||
case "union":
|
||||
return E.tracedUnions.has(t.unionId) ? "scr_union_trace_v" : null;
|
||||
case "promise":
|
||||
return "scr_promise_trace_v";
|
||||
case "object":
|
||||
if (!E.tracedShapes.has(`object:${t.className}`)) return null;
|
||||
if (RUNTIME_ERROR_CLASSES.has(t.className)) return "scr_error_trace";
|
||||
if (t.className === RUNTIME_EMITTER_CLASS) return "scr_emitter_trace";
|
||||
if (RUNTIME_STREAM_CLASSES.has(t.className)) return "scr_stream_trace";
|
||||
return mangleClassTrace(t.className);
|
||||
case "record":
|
||||
return E.tracedShapes.has(`record:${t.shapeId}`) ? mangleRecordTrace(t.shapeId) : null;
|
||||
// Cycle-capable exactly when the VALUE type is (mirrors the
|
||||
// constructor fixpoint's map rule): such maps allocate with the
|
||||
// collector header and their runtime trace visits every live value.
|
||||
case "map":
|
||||
return E.traceAdapterC(t.value) !== null ? "scr_map_trace_v" : null;
|
||||
// Arrays mirror maps: cycle-capable exactly when the ELEMENT type is
|
||||
// (a record/object/union element — or a cycle-capable inner array —
|
||||
// can point back at the array holding it). Such arrays allocate with
|
||||
// the collector header (scr_arr_new_ref with a trace); scalar/
|
||||
// string/bytes-element arrays stay lean.
|
||||
case "array":
|
||||
return E.traceAdapterC(t.elem) !== null ? "scr_arr_trace_v" : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** `&<trace>` or `NULL` — the trace argument at a container call site. */
|
||||
export function traceArgC(E: CEmitter, t: IrType): string {
|
||||
const sym = E.traceAdapterC(t);
|
||||
return sym ? `&${sym}` : "NULL";
|
||||
}
|
||||
|
||||
/** Array construction expression for one element type. Ref elements
|
||||
* (records, class instances, unions — and cycle-capable inner arrays,
|
||||
* whose SCR_ELEM_ARR spelling would hide them from the outer array's
|
||||
* trace) construct through scr_arr_new_ref, which stores the element
|
||||
* type's RC entry points once per array (the map-value technique) and
|
||||
* allocates with the collector header exactly when the element type
|
||||
* carries one (trace non-NULL). Every other element kind keeps the
|
||||
* historic scr_arr_new call. */
|
||||
export function arrNewC(E: CEmitter, elem: IrType, capExpr: string | number): string {
|
||||
const useRef =
|
||||
elem.kind === "record" || elem.kind === "object" || elem.kind === "union" ||
|
||||
// Promise elements (Promise.all's food): refcounted, cycle-headered
|
||||
// — the `_v` adapters and scr_promise_trace_v ride the same REF
|
||||
// machinery as record/object/union elements.
|
||||
elem.kind === "promise" ||
|
||||
elem.kind === "jsval" || // island handles: scr_jsval_* adapters, no trace
|
||||
elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace
|
||||
elem.kind === "netServer" || // server handles: scr_net_server_* adapters, no trace
|
||||
elem.kind === "symbol" || // symbol identities: scr_sym_* adapters, no trace
|
||||
elem.kind === "classval" || // class objects: no-op adapters, no trace (immortal statics)
|
||||
// Closures: scr_closure_* adapters + scr_closure_trace_v (always
|
||||
// cycle-headered — captures can reach back through boxes).
|
||||
elem.kind === "func" ||
|
||||
(elem.kind === "array" && E.traceAdapterC(elem) !== null);
|
||||
if (!useRef) return `scr_arr_new(${elemKindC(elem)}, ${capExpr})`;
|
||||
const v = vAdapters(elem);
|
||||
return `scr_arr_new_ref(&${v.retain}, &${v.release}, ${E.traceArgC(elem)}, ${capExpr})`;
|
||||
}
|
||||
|
||||
/** Box construction expression — object/record/union/promise boxes carry
|
||||
* their RC entry points (and the payload's trace) as function pointers
|
||||
* (the SCR_BOX_OBJ mechanism: the runtime can't know per-shape layouts). */
|
||||
export function boxNewC(E: CEmitter, t: IrType): string {
|
||||
// A captured local can be typed by a class the module never collected
|
||||
// (a runtime-fenced JS class — e.g. one declared inside a block —
|
||||
// whose declaration and every use compile to runtime traps): no
|
||||
// instance can ever exist, so the box is an inert placeholder — its
|
||||
// RC adapters were never emitted and must not be referenced.
|
||||
if (t.kind === "object" && !E.classMeta.has(t.className)) {
|
||||
return `scr_box_new(SCR_BOX_F64) /* ${t.className}: uncollected class, all uses trap */`;
|
||||
}
|
||||
if (
|
||||
t.kind === "object" || t.kind === "record" || t.kind === "union" ||
|
||||
t.kind === "classval" ||
|
||||
t.kind === "map" || t.kind === "set" || t.kind === "promise" ||
|
||||
t.kind === "generator" ||
|
||||
t.kind === "regex" || t.kind === "url" || t.kind === "searchParams" ||
|
||||
t.kind === "symbol" || t.kind === "stats" ||
|
||||
t.kind === "spawnRes" || t.kind === "child" || t.kind === "bytes" ||
|
||||
t.kind === "netServer" || t.kind === "netSocket" ||
|
||||
t.kind === "http2Session" || t.kind === "http2Stream" ||
|
||||
t.kind === "dgramSocket" || t.kind === "testCtx" ||
|
||||
t.kind === "httpReq" || t.kind === "httpRes" ||
|
||||
t.kind === "httpClientReq" || t.kind === "secureCtx" ||
|
||||
t.kind === "fsWatcher" || t.kind === "childStream" ||
|
||||
// Island handles: the box carries scr_jsval_retain_v/release_v and
|
||||
// no trace — the same stance as jsval array elements.
|
||||
t.kind === "jsval" ||
|
||||
// Checked-dynamic captures (the mustCall wrapper closing over its
|
||||
// implicit-any `fn` param): the box carries scr_dyn_retain_v/
|
||||
// release_v and NO trace — a DOM tree is pure data except the
|
||||
// function kind, whose closure edge stays invisible to the
|
||||
// collector (trial deletion treats it as an external root: cycles
|
||||
// through dyn never collect, nothing dangles — SEMANTICS.md).
|
||||
t.kind === "dyn" ||
|
||||
// A CYCLE-CAPABLE array must ride the obj-box so the box's trace
|
||||
// reaches it (SCR_BOX_ARR payloads are never traced); acyclic arrays
|
||||
// keep the historic plain-kind box below.
|
||||
(t.kind === "array" && E.traceAdapterC(t) !== null)
|
||||
) {
|
||||
const v = vAdapters(t);
|
||||
return `scr_box_new_obj(&${v.retain}, &${v.release}, ${E.traceArgC(t)})`;
|
||||
}
|
||||
return `scr_box_new(${boxKindC(t)})`;
|
||||
}
|
||||
@@ -0,0 +1,951 @@
|
||||
/* Statement-level C emission: function bodies, blocks/scopes, the statement
|
||||
* dispatch (emitStmt), try/catch lowering, and switch — plus the small
|
||||
* branch/condition helpers they share with expression emission. All frame,
|
||||
* scope, and temp state lives on CEmitter; these functions drive it. */
|
||||
import type { CEmitter, ScopeEntry } from "./emitter.js";
|
||||
import type { IrFunction } from "../../ir/nodes.js";
|
||||
import { mangleField, mangleGlobal, mangleLocal, mangleRawParam } from "../mangle.js";
|
||||
import { BOOL, CAUGHT, IrExpr, IrStmt, RUNTIME_ERROR_CLASSES, isRefCounted } from "../../ir/nodes.js";
|
||||
import { boxAccess, cDecl, cStringLiteral, elemAccess, vAdapters } from "./emit-types.js";
|
||||
import { OVERFLOW_MEMBER } from "./emit-shapes.js";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export function emitFunction(E: CEmitter, fn: IrFunction): void {
|
||||
E.tempCounter = 0;
|
||||
E.frames = [];
|
||||
E.scopes = [];
|
||||
E.jumpTargets = [];
|
||||
E.tryStack = [];
|
||||
E.finallyStack = [];
|
||||
E.currentReturnType = fn.returnType;
|
||||
E.currentGenerator = fn.generator ?? null;
|
||||
E.labelCounter = 0;
|
||||
E.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
E.captureIds = new Set((fn.captures ?? []).map((c) => c.localId));
|
||||
|
||||
E.line(`${E.signature(fn)} {${E.srcComment(fn.loc)}`);
|
||||
E.indent++;
|
||||
|
||||
// The pending-return slot: a `return` crossing a finally computes its
|
||||
// value FIRST (before the finally runs — snapshotting it here is what
|
||||
// makes finally mutations of returned locals invisible, Node-exact),
|
||||
// then jumps through every crossed finally; the last dispatch returns
|
||||
// the slot. Declared only when some return actually crosses a finally.
|
||||
if (fn.returnType.kind !== "void" && returnCrossesFinally(fn.body)) {
|
||||
const init = isRefCounted(fn.returnType) ? "NULL" : "0";
|
||||
E.line(`${cDecl(fn.returnType, "sc_pret")} = ${init}; /* pending return (through finally) */`);
|
||||
}
|
||||
|
||||
// Captured bindings come in through the environment — borrowed for the
|
||||
// whole call (the closure owns them): bound here, never released here.
|
||||
(fn.captures ?? []).forEach((c, i) => {
|
||||
E.line(`ScrBox *${mangleLocal(c.localId)} = sc_env->caps[${i}]; /* captured ${c.name} */`);
|
||||
});
|
||||
|
||||
const paramIds = new Set(fn.params.map((p) => p.localId));
|
||||
for (const local of fn.locals) {
|
||||
if (paramIds.has(local.id) || E.captureIds.has(local.id)) continue;
|
||||
if (local.boxed) {
|
||||
E.line(`ScrBox *${mangleLocal(local.id)} = NULL; /* ${local.name} (boxed) */`);
|
||||
} else {
|
||||
const init = isRefCounted(local.type) ? " = NULL" : "";
|
||||
E.line(`${cDecl(local.type, mangleLocal(local.id))}${init}; /* ${local.name} */`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function scope owns refcounted params (callees own their params).
|
||||
// Boxed params: allocate the shared binding and move the raw value in.
|
||||
const fnScope: ScopeEntry[] = [];
|
||||
for (const p of fn.params) {
|
||||
const local = E.currentLocals.get(p.localId)!;
|
||||
if (local.boxed) {
|
||||
const box = mangleLocal(p.localId);
|
||||
E.line(`ScrBox *${box} = ${E.boxNewC(p.type)}; /* ${p.name} (boxed param) */`);
|
||||
E.line(`scr_box_set_${boxAccess(p.type)}(${box}, ${mangleRawParam(p.localId)});`);
|
||||
fnScope.push({ name: box, type: p.type, boxed: true });
|
||||
} else if (isRefCounted(p.type)) {
|
||||
fnScope.push({ name: mangleLocal(p.localId), type: p.type });
|
||||
}
|
||||
}
|
||||
E.scopes.push(fnScope);
|
||||
E.emitStmts(fn.body);
|
||||
// Implicit exit of a void function: release function-scope refcounted
|
||||
// locals (unless the body already ended in an explicit return or a
|
||||
// throw, whose unwind released everything down to depth 0).
|
||||
const last = fn.body[fn.body.length - 1]?.kind;
|
||||
const endedWithReturn = last === "return" || last === "throw" || last === "rethrow" || last === "runtimeFence";
|
||||
if (fn.returnType.kind === "void" && !endedWithReturn) {
|
||||
E.releaseFrame(E.scopes[0]!);
|
||||
}
|
||||
E.scopes.pop();
|
||||
|
||||
E.indent--;
|
||||
E.line(`}`);
|
||||
E.line(``);
|
||||
}
|
||||
|
||||
/** The statement bodies nested directly under a statement — the shared
|
||||
* walk for returnCrossesFinally. */
|
||||
function childBodies(s: IrStmt): IrStmt[][] {
|
||||
switch (s.kind) {
|
||||
case "if":
|
||||
return s.else_ ? [s.then, s.else_] : [s.then];
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "forOf":
|
||||
case "block":
|
||||
return [s.body];
|
||||
case "for":
|
||||
return [s.body];
|
||||
case "switch":
|
||||
return s.cases.map((c) => c.body);
|
||||
case "tryCatch":
|
||||
return [
|
||||
s.tryBody,
|
||||
...(s.catchBody ? [s.catchBody] : []),
|
||||
...(s.finallyBody ? [s.finallyBody] : []),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** True when some `return` sits inside the tryBody/catchBody of a
|
||||
* try-with-finally, at any nesting depth — exactly the returns the
|
||||
* pending-return path routes, and so exactly when emitFunction must
|
||||
* declare the sc_pret slot. */
|
||||
function returnCrossesFinally(stmts: IrStmt[]): boolean {
|
||||
const hasReturn = (body: IrStmt[]): boolean =>
|
||||
body.some((s) => s.kind === "return" || childBodies(s).some(hasReturn));
|
||||
const walk = (body: IrStmt[]): boolean =>
|
||||
body.some((s) => {
|
||||
if (
|
||||
s.kind === "tryCatch" &&
|
||||
s.finallyBody !== null &&
|
||||
(hasReturn(s.tryBody) || (s.catchBody !== null && hasReturn(s.catchBody)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return childBodies(s).some(walk);
|
||||
});
|
||||
return walk(stmts);
|
||||
}
|
||||
|
||||
/** Emits a block in its own lexical scope (refcounted locals released at
|
||||
* end). `setup` runs after the scope opens, before the statements — the
|
||||
* catch-binding hook: it may emit prelude lines and register entries the
|
||||
* scope owns (released on every exit, jumps and unwinds included). */
|
||||
export function emitBlock(E: CEmitter, stmts: IrStmt[], setup?: (scope: ScopeEntry[]) => void): void {
|
||||
E.line(`{`);
|
||||
E.indent++;
|
||||
const scope: ScopeEntry[] = [];
|
||||
E.scopes.push(scope);
|
||||
setup?.(scope);
|
||||
E.emitStmts(stmts);
|
||||
const endedWithJump = E.endsWithJump(stmts);
|
||||
E.scopes.pop();
|
||||
if (!endedWithJump) E.releaseFrame(scope);
|
||||
E.indent--;
|
||||
E.line(`}`);
|
||||
}
|
||||
|
||||
export function emitStmts(E: CEmitter, stmts: IrStmt[]): void {
|
||||
for (const s of stmts) E.emitStmt(s);
|
||||
}
|
||||
|
||||
export function emitStmt(E: CEmitter, s: IrStmt): void {
|
||||
E.frames.push([]);
|
||||
switch (s.kind) {
|
||||
case "varDecl": {
|
||||
const local = E.currentLocals.get(s.localId)!;
|
||||
const target = mangleLocal(s.localId);
|
||||
if (s.init === null) {
|
||||
// Declared, uninitialized (`let x: number;`). tsc's TS2454 rejects
|
||||
// any read before assignment, so no runtime check is needed — but
|
||||
// the declaration must still RESET the C local: inside a loop the
|
||||
// previous iteration's scope exit released the old value and left
|
||||
// a stale pointer, and the scope-exit release below runs whether
|
||||
// or not an `assign` ever did (runtime releases are NULL-tolerant).
|
||||
if (local.boxed) {
|
||||
// The box must exist immediately: closures created before the
|
||||
// first assignment capture it. (tsc's TS2454 can't see through
|
||||
// closures — a call before the first assignment reads 0/false
|
||||
// for scalars and traps for refcounted kinds, where JS has
|
||||
// `undefined`.)
|
||||
// A SCALAR TDZ box rides an ARR-kind box: the value lives in a
|
||||
// one-element array cell, so the empty (NULL) slot stays the
|
||||
// not-yet-initialized sentinel — a raw scalar slot has no spare
|
||||
// bit pattern to spend on it.
|
||||
const boxNew =
|
||||
local.tdz && boxAccess(local.type) !== "ref"
|
||||
? "scr_box_new(SCR_BOX_ARR)"
|
||||
: E.boxNewC(local.type);
|
||||
E.line(`${target} = ${boxNew};${E.srcComment(s.loc)} /* let ${local.name}; */`);
|
||||
E.scopes[E.scopes.length - 1]!.push({ name: target, type: local.type, boxed: true });
|
||||
} else if (isRefCounted(local.type)) {
|
||||
E.line(`${target} = NULL;${E.srcComment(s.loc)} /* let ${local.name}; */`);
|
||||
E.scopes[E.scopes.length - 1]!.push({ name: target, type: local.type });
|
||||
}
|
||||
// Scalars need nothing: the C local exists from the prologue and
|
||||
// no read happens before an assign writes it.
|
||||
break;
|
||||
}
|
||||
if (local.boxed) {
|
||||
// Box FIRST, then evaluate the initializer: a named function
|
||||
// expression's closure captures this box during init evaluation.
|
||||
E.line(`${target} = ${E.boxNewC(local.type)};${E.srcComment(s.loc)}`);
|
||||
E.scopes[E.scopes.length - 1]!.push({ name: target, type: local.type, boxed: true });
|
||||
const v = E.emitExpr(s.init);
|
||||
if (isRefCounted(v.type)) E.moveTemp(v); // the box takes ownership
|
||||
E.line(`scr_box_set_${boxAccess(local.type)}(${target}, ${v.name});`);
|
||||
break;
|
||||
}
|
||||
const v = E.emitExpr(s.init);
|
||||
E.moveTemp(v);
|
||||
E.line(`${target} = ${v.name};${E.srcComment(s.loc)}`);
|
||||
if (isRefCounted(v.type)) {
|
||||
E.scopes[E.scopes.length - 1]!.push({ name: target, type: v.type });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "assign": {
|
||||
const local = E.currentLocals.get(s.localId);
|
||||
if (!local) {
|
||||
// Module global: plain static storage, never boxed. Old-value
|
||||
// release is NULL-tolerant (statics start NULL).
|
||||
const g = E.globalsById.get(s.localId);
|
||||
if (!g) throw new Error(`emitter bug: assign to unknown binding ${s.localId}`);
|
||||
const target = mangleGlobal(g.id);
|
||||
const v = E.emitExpr(s.value);
|
||||
E.moveTemp(v);
|
||||
if (isRefCounted(v.type)) E.releaseValue(target, v.type);
|
||||
E.line(`${target} = ${v.name};${E.srcComment(s.loc)}`);
|
||||
break;
|
||||
}
|
||||
const target = mangleLocal(s.localId);
|
||||
const v = E.emitExpr(s.value);
|
||||
if (local!.boxed) {
|
||||
// A scalar TDZ box (forward-captured const): the initializing
|
||||
// write mints the one-element array cell — set_ref moves it in
|
||||
// (and the empty-slot sentinel ends here).
|
||||
if (local!.tdz && boxAccess(local!.type) !== "ref") {
|
||||
const acc = boxAccess(local!.type);
|
||||
const cell = `sc_t${E.tempCounter++}`;
|
||||
E.line(`ScrArr *${cell} = ${E.arrNewC(local!.type, 1)};${E.srcComment(s.loc)}`);
|
||||
E.line(`scr_arr_push_${acc}(${cell}, ${v.name});`);
|
||||
E.line(`scr_box_set_ref(${target}, ${cell});`);
|
||||
break;
|
||||
}
|
||||
if (isRefCounted(v.type)) E.moveTemp(v); // set_ref releases the old value
|
||||
E.line(`scr_box_set_${boxAccess(local!.type)}(${target}, ${v.name});${E.srcComment(s.loc)}`);
|
||||
break;
|
||||
}
|
||||
E.moveTemp(v);
|
||||
if (isRefCounted(v.type)) E.releaseValue(target, v.type);
|
||||
E.line(`${target} = ${v.name};${E.srcComment(s.loc)}`);
|
||||
break;
|
||||
}
|
||||
case "exprStmt":
|
||||
E.emitExpr(s.expr);
|
||||
break;
|
||||
case "if": {
|
||||
const cond = E.emitCondition(s.cond);
|
||||
E.line(`if (${cond}) `);
|
||||
E.mergeBrace(() => E.emitBlock(s.then));
|
||||
if (s.else_) {
|
||||
E.line(`else `);
|
||||
E.mergeBrace(() => E.emitBlock(s.else_!));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "while": {
|
||||
E.line(`for (;;) {${E.srcComment(s.loc)}`);
|
||||
E.indent++;
|
||||
const cond = E.emitCondition(s.cond);
|
||||
E.line(`if (!(${cond})) break;`);
|
||||
// C continue re-evaluates the condition at the top: exactly right
|
||||
// for the unlabeled loop. A LABELED while allocates a continue
|
||||
// label placed at the END of the body (falling off it re-enters
|
||||
// the condition — the same point) so a labeled continue from a
|
||||
// NESTED loop can goto it, and a lazy end label for labeled break.
|
||||
const loop = E.loopTarget(null, s.labels);
|
||||
E.jumpTargets.push(loop);
|
||||
E.emitBlock(s.body);
|
||||
E.jumpTargets.pop();
|
||||
if (loop.usedContinue && loop.continueLabel) E.line(`${loop.continueLabel}:;`);
|
||||
E.indent--;
|
||||
E.line(`}`);
|
||||
if (loop.usedEnd) E.line(`${loop.endLabel}:;`);
|
||||
break;
|
||||
}
|
||||
case "doWhile": {
|
||||
// `for (;;) { body; cont: cond; if (!cond) break; }` — the body runs
|
||||
// before the first condition check (at least once), and `continue`
|
||||
// routes through the label so the condition still evaluates.
|
||||
E.line(`for (;;) {${E.srcComment(s.loc)}`);
|
||||
E.indent++;
|
||||
const loop = E.loopTarget(`sc_cont_${E.labelCounter++}`, s.labels);
|
||||
E.jumpTargets.push(loop);
|
||||
E.emitBlock(s.body);
|
||||
E.jumpTargets.pop();
|
||||
if (loop.usedContinue) E.line(`${loop.continueLabel}:;`);
|
||||
const cond = E.emitCondition(s.cond);
|
||||
E.line(`if (!(${cond})) break;`);
|
||||
E.indent--;
|
||||
E.line(`}`);
|
||||
if (loop.usedEnd) E.line(`${loop.endLabel}:;`);
|
||||
break;
|
||||
}
|
||||
case "switch":
|
||||
E.emitSwitch(s);
|
||||
break;
|
||||
case "for": {
|
||||
// Desugared in place; the init's scope wraps the whole loop, so
|
||||
// break/continue must NOT release it (scopeDepth captured after).
|
||||
E.line(`{${E.srcComment(s.loc)}`);
|
||||
E.indent++;
|
||||
E.scopes.push([]);
|
||||
if (s.init) E.emitStmt(s.init);
|
||||
E.line(`for (;;) {`);
|
||||
E.indent++;
|
||||
if (s.cond) {
|
||||
const cond = E.emitCondition(s.cond);
|
||||
E.line(`if (!(${cond})) break;`);
|
||||
}
|
||||
// C continue would skip the update; route it through a label instead.
|
||||
const loop = E.loopTarget(`sc_cont_${E.labelCounter++}`, s.labels);
|
||||
E.jumpTargets.push(loop);
|
||||
E.emitBlock(s.body);
|
||||
E.jumpTargets.pop();
|
||||
if (loop.usedContinue) E.line(`${loop.continueLabel}:;`);
|
||||
// JS `for (let i ...)`: each iteration gets a FRESH binding holding a
|
||||
// copy of the previous one, and the update mutates the fresh binding
|
||||
// — that's why closures made in iteration k keep seeing iteration
|
||||
// k's value. Only observable (and only emitted) when captured.
|
||||
if (s.init?.kind === "varDecl") {
|
||||
const initLocal = E.currentLocals.get(s.init.localId);
|
||||
if (initLocal?.boxed) {
|
||||
const box = mangleLocal(initLocal.id);
|
||||
const fresh = `sc_t${E.tempCounter++}`;
|
||||
const acc = boxAccess(initLocal.type);
|
||||
E.line(`ScrBox *${fresh} = ${E.boxNewC(initLocal.type)}; /* per-iteration ${initLocal.name} */`);
|
||||
E.line(`scr_box_set_${acc}(${fresh}, scr_box_get_${acc}(${box}));`);
|
||||
E.line(`scr_box_release(${box});`);
|
||||
E.line(`${box} = ${fresh};`);
|
||||
// The wrapper scope's entry releases whatever `box` points to at
|
||||
// loop exit — which is now the freshest binding. Nothing to fix.
|
||||
}
|
||||
}
|
||||
if (s.update) E.emitStmt(s.update);
|
||||
E.indent--;
|
||||
E.line(`}`);
|
||||
// A labeled break lands exactly where C break does: BEFORE the
|
||||
// init scope's release (the goto path must run the same releases
|
||||
// as the fall-through path).
|
||||
if (loop.usedEnd) E.line(`${loop.endLabel}:;`);
|
||||
E.releaseFrame(E.scopes.pop()!);
|
||||
E.indent--;
|
||||
E.line(`}`);
|
||||
break;
|
||||
}
|
||||
case "arraySet": {
|
||||
// Evaluation order matches JS: array, index, then value. Ownership
|
||||
// of a refcounted value moves into the array (the runtime releases
|
||||
// the replaced element itself).
|
||||
const arr = E.emitExpr(s.arr);
|
||||
const idx = E.emitExpr(s.index);
|
||||
const v = E.emitExpr(s.value);
|
||||
if (s.arr.type.kind !== "array") throw new Error("emitter bug: arraySet on non-array");
|
||||
const acc = elemAccess(s.arr.type.elem);
|
||||
if (acc === "ref") E.moveTemp(v);
|
||||
E.line(`scr_arr_set_${acc}(${arr.name}, ${idx.name}, ${v.name});${E.srcComment(s.loc)}`);
|
||||
break;
|
||||
}
|
||||
case "bytesSet": {
|
||||
// Typed-array element write: same evaluation order as arraySet;
|
||||
// the value is a scalar (the runtime coerces JS-exactly), so no
|
||||
// ownership moves. Any invalid index traps — no append.
|
||||
const arr = E.emitExpr(s.arr);
|
||||
const idx = E.emitExpr(s.index);
|
||||
const v = E.emitExpr(s.value);
|
||||
E.line(`scr_bytes_set(${arr.name}, ${idx.name}, ${v.name});${E.srcComment(s.loc)}`);
|
||||
break;
|
||||
}
|
||||
case "fieldSet":
|
||||
case "recordSet": {
|
||||
// Evaluation order: obj, then value. New value moved in; the old
|
||||
// value is released AFTER the field is overwritten (releases are
|
||||
// NULL-tolerant — fields start NULL from the zeroed allocation).
|
||||
// Unlink-then-release is load-bearing: a release can trigger a
|
||||
// cycle collection, which must never see a heap edge whose count
|
||||
// was already given up (scr_cycle.c). Classes and records share
|
||||
// the struct layout, so one emission.
|
||||
const obj = E.emitExpr(s.obj);
|
||||
const v = E.emitExpr(s.value);
|
||||
// Runtime error classes use ScrError's own member names.
|
||||
const member =
|
||||
s.kind === "fieldSet" && RUNTIME_ERROR_CLASSES.has(s.className)
|
||||
? s.field
|
||||
: mangleField(s.field);
|
||||
const field = `${obj.name}->${member}`;
|
||||
if (isRefCounted(v.type)) {
|
||||
E.moveTemp(v);
|
||||
const old = `sc_t${E.tempCounter++}`;
|
||||
E.line(`${cDecl(v.type, old)} = ${field};`);
|
||||
E.line(`${field} = ${v.name};${E.srcComment(s.loc)}`);
|
||||
E.releaseValue(old, v.type);
|
||||
} else {
|
||||
E.line(`${field} = ${v.name};${E.srcComment(s.loc)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "recordKeyDelete": {
|
||||
// `delete obj[k]` on a pure index-signature shape: a Map delete on
|
||||
// the overflow (key and value released; absent keys no-op).
|
||||
const obj = E.emitExpr(s.obj);
|
||||
const key = E.emitExpr(s.key);
|
||||
E.line(`scr_map_delete_str(${obj.name}->${OVERFLOW_MEMBER}, ${key.name});${E.srcComment(s.loc)}`);
|
||||
break;
|
||||
}
|
||||
case "recordKeySet": {
|
||||
// Dynamic-keyed record write through the per-shape helper (declared
|
||||
// keys write through with validation, undeclared keys land in the
|
||||
// overflow map). Evaluation order: obj, key, value. The helper OWNS
|
||||
// the value (+1 moves in). MAY THROW when a dyn value must validate
|
||||
// against a declared field's type — the pending check runs then.
|
||||
const obj = E.emitExpr(s.obj);
|
||||
const key = E.emitExpr(s.key);
|
||||
const v = E.emitExpr(s.value);
|
||||
if (isRefCounted(v.type)) E.moveTemp(v);
|
||||
// A LITERAL key naming no declared field is a plain overflow map
|
||||
// insert — no helper, no validation, no throw.
|
||||
if (s.overflowOnly) {
|
||||
const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref";
|
||||
E.line(
|
||||
`scr_map_set_str_${acc}(${obj.name}->${OVERFLOW_MEMBER}, ${key.name}, ${v.name});${E.srcComment(s.loc)}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
const helper = E.recordKeySetHelper(s.shapeId);
|
||||
E.line(`${helper}(${obj.name}, ${key.name}, ${v.name});${E.srcComment(s.loc)}`);
|
||||
const shape = E.recordsById.get(s.shapeId);
|
||||
if (shape && shape.indexValue?.kind === "dyn" && shape.fields.length > 0) {
|
||||
E.emitPendingCheck();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "forOf": {
|
||||
// Ascending index loop; the length is re-read every iteration
|
||||
// (JS-exact — pushes inside the body extend the iteration). The
|
||||
// iterable temp lives in this statement's frame, so it is released
|
||||
// when the whole loop ends (and by `return`'s frame sweep). A real
|
||||
// C for-loop makes plain `continue` correct: the update still runs.
|
||||
if (s.iterable.type.kind !== "array") throw new Error("emitter bug: forOf over non-array");
|
||||
const elem = s.iterable.type.elem;
|
||||
const arr = E.emitExpr(s.iterable);
|
||||
const idx = `sc_t${E.tempCounter++}`;
|
||||
E.line(
|
||||
`for (double ${idx} = 0; ${idx} < scr_arr_len(${arr.name}); ${idx} += 1) {${E.srcComment(s.loc)}`,
|
||||
);
|
||||
E.indent++;
|
||||
// A real C for-loop makes plain `continue` correct (the update
|
||||
// still runs); a LABELED forOf allocates a continue label placed at
|
||||
// the END of the iteration (after the per-iteration scope release —
|
||||
// the goto path released it itself) so nested loops can goto it.
|
||||
const loop = E.loopTarget(null, s.labels);
|
||||
E.jumpTargets.push(loop);
|
||||
// The loop variable is a fresh const per iteration: its scope opens
|
||||
// here, holds the (for ref elements: owned +1) current element, and
|
||||
// releases it at the end of each iteration.
|
||||
E.scopes.push([]);
|
||||
const local = mangleLocal(s.localId);
|
||||
const localInfo = E.currentLocals.get(s.localId);
|
||||
if (localInfo?.boxed) {
|
||||
// Captured loop variable: a fresh box per iteration, matching the
|
||||
// fresh const binding (closures made in iteration k keep seeing
|
||||
// iteration k's element). The box takes ownership of a ref
|
||||
// element's +1 and is released with the iteration's scope.
|
||||
E.line(`${local} = ${E.boxNewC(elem)}; /* per-iteration ${localInfo.name} */`);
|
||||
E.line(`scr_box_set_${boxAccess(elem)}(${local}, scr_arr_get_${elemAccess(elem)}(${arr.name}, ${idx}));`);
|
||||
E.scopes[E.scopes.length - 1]!.push({ name: local, type: elem, boxed: true });
|
||||
} else {
|
||||
E.line(`${local} = scr_arr_get_${elemAccess(elem)}(${arr.name}, ${idx});`);
|
||||
if (isRefCounted(elem)) E.scopes[E.scopes.length - 1]!.push({ name: local, type: elem });
|
||||
}
|
||||
E.emitStmts(s.body);
|
||||
const endedWithJump = E.endsWithJump(s.body);
|
||||
const scope = E.scopes.pop()!;
|
||||
if (!endedWithJump) E.releaseFrame(scope);
|
||||
E.jumpTargets.pop();
|
||||
if (loop.usedContinue && loop.continueLabel) E.line(`${loop.continueLabel}:;`);
|
||||
E.indent--;
|
||||
E.line(`}`);
|
||||
if (loop.usedEnd) E.line(`${loop.endLabel}:;`);
|
||||
break;
|
||||
}
|
||||
case "block": {
|
||||
if (s.labels === undefined) {
|
||||
E.emitBlock(s.body);
|
||||
break;
|
||||
}
|
||||
// A labeled block: `break lbl` inside jumps to the end label
|
||||
// (releasing the block's scope itself); nothing else can target it.
|
||||
const target = {
|
||||
kind: "block" as const,
|
||||
endLabel: `sc_end_${E.labelCounter++}`,
|
||||
usedEnd: false,
|
||||
labels: s.labels,
|
||||
scopeDepth: E.scopes.length,
|
||||
frameDepth: E.frames.length,
|
||||
};
|
||||
E.jumpTargets.push(target);
|
||||
E.emitBlock(s.body);
|
||||
E.jumpTargets.pop();
|
||||
if (target.usedEnd) E.line(`${target.endLabel}:;`);
|
||||
break;
|
||||
}
|
||||
case "break": {
|
||||
// Unlabeled: binds to the innermost loop OR switch (labeled block
|
||||
// targets are skipped). Labeled: binds to the entry carrying the
|
||||
// label. Release every scope entered since that target started
|
||||
// (the body scope and anything nested); their natural end-of-block
|
||||
// releases are on the fall-through path this jump bypasses. Same
|
||||
// for pending frame temps of statements the jump exits (a nested
|
||||
// switch's discriminant) — but NOT the target's own frame: a
|
||||
// loop's releases after the loop and a switch's after its end
|
||||
// label are still on this jump's path.
|
||||
let target: (typeof E.jumpTargets)[number] | undefined;
|
||||
for (let i = E.jumpTargets.length - 1; i >= 0; i--) {
|
||||
const t = E.jumpTargets[i]!;
|
||||
if (s.label !== undefined ? t.labels?.includes(s.label) : t.kind !== "block") {
|
||||
target = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!target) throw new Error("emitter bug: break target not found");
|
||||
E.releaseForJump(target.frameDepth, target.scopeDepth);
|
||||
if (target.kind !== "loop" || s.label !== undefined) {
|
||||
// Switches are emitted as goto chains and blocks aren't C loops
|
||||
// at all, so a C `break` cannot target either; and a LABELED
|
||||
// break may target an outer loop a C break would never reach.
|
||||
// All three jump to the target's end label (labeled loops always
|
||||
// allocate one — loopTarget).
|
||||
target.usedEnd = true;
|
||||
E.line(`goto ${target.endLabel!};${E.srcComment(s.loc)}`);
|
||||
} else {
|
||||
E.line(`break;${E.srcComment(s.loc)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "continue": {
|
||||
// Unlabeled: binds to the innermost LOOP, skipping any switches and
|
||||
// labeled blocks in between (their scopes are still released — the
|
||||
// jump exits them). Labeled: binds to the loop carrying the label.
|
||||
let loop: ((typeof E.jumpTargets)[number] & { kind: "loop" }) | undefined;
|
||||
for (let i = E.jumpTargets.length - 1; i >= 0; i--) {
|
||||
const t = E.jumpTargets[i]!;
|
||||
if (t.kind === "loop" && (s.label === undefined || t.labels?.includes(s.label))) {
|
||||
loop = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!loop) throw new Error("emitter bug: continue target not found");
|
||||
E.releaseForJump(loop.frameDepth, loop.scopeDepth);
|
||||
if (loop.continueLabel) {
|
||||
// Labeled loops always allocate one (a labeled continue may
|
||||
// target an outer loop a C continue could never reach); for/
|
||||
// do-while allocate one for their update/condition point.
|
||||
loop.usedContinue = true;
|
||||
E.line(`goto ${loop.continueLabel};${E.srcComment(s.loc)}`);
|
||||
} else {
|
||||
E.line(`continue;${E.srcComment(s.loc)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "return": {
|
||||
const fin = E.finallyStack[E.finallyStack.length - 1];
|
||||
if (fin) {
|
||||
// Crossing ≥1 finally: the value is computed and snapshotted
|
||||
// FIRST (a finally mutating the returned local cannot change
|
||||
// it — Node's semantics), then everything down to the innermost
|
||||
// region releases and control runs that region's pending-return
|
||||
// finally copy; its tail dispatches further out or returns.
|
||||
if (s.value) {
|
||||
const v = E.emitExpr(s.value);
|
||||
E.moveTemp(v); // ownership parks in the slot until the dispatch returns it
|
||||
E.line(`sc_pret = ${v.name};${E.srcComment(s.loc)}`);
|
||||
}
|
||||
fin.used = true;
|
||||
E.releaseForJump(fin.frameDepth, fin.scopeDepth);
|
||||
E.line(`goto ${fin.label};${s.value ? "" : E.srcComment(s.loc)}`);
|
||||
break;
|
||||
}
|
||||
if (s.value) {
|
||||
const v = E.emitExpr(s.value);
|
||||
E.moveTemp(v);
|
||||
// Everything down to function depth releases; the moved result is
|
||||
// exempt (already struck from its frame).
|
||||
E.releaseForJump(0, 0);
|
||||
E.line(`return ${v.name};${E.srcComment(s.loc)}`);
|
||||
} else {
|
||||
E.releaseForJump(0, 0);
|
||||
E.line(`return;${E.srcComment(s.loc)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "throw": {
|
||||
// Evaluate, move ownership into the runtime's exception cell, then
|
||||
// unwind unconditionally (the innermost try handler, or out of the
|
||||
// function) — the same release path as return/break/continue.
|
||||
const v = E.emitExpr(s.value);
|
||||
const t = s.value.type;
|
||||
if (isRefCounted(t)) E.moveTemp(v); // the cell takes ownership
|
||||
if (t.kind === "f64") {
|
||||
E.line(`scr_throw_f64(${v.name});${E.srcComment(s.loc)}`);
|
||||
} else if (t.kind === "bool") {
|
||||
E.line(`scr_throw_bool(${v.name});${E.srcComment(s.loc)}`);
|
||||
} else if (t.kind === "string") {
|
||||
E.line(`scr_throw_str(${v.name});${E.srcComment(s.loc)}`);
|
||||
} else if (t.kind === "object" && E.classMeta.get(t.className)?.hierarchy) {
|
||||
// Hierarchy instances carry a vtable word: the OBJ kind keeps the
|
||||
// dynamic class inspectable (catch-binding instanceof, and the
|
||||
// uncaught printer's "name: message" for Error instances).
|
||||
const rc = vAdapters(t);
|
||||
E.line(`scr_throw_obj(${v.name}, &${rc.retain}, &${rc.release}, ${E.traceArgC(t)});${E.srcComment(s.loc)}`);
|
||||
} else {
|
||||
const rc = vAdapters(t);
|
||||
E.line(`scr_throw_ref(${v.name}, &${rc.retain}, &${rc.release}, ${E.traceArgC(t)});${E.srcComment(s.loc)}`);
|
||||
}
|
||||
E.emitUnwind();
|
||||
break;
|
||||
}
|
||||
case "runtimeFence": {
|
||||
// The deferred JS compile fence: throw a catchable Error naming
|
||||
// the construct (message) with the SC code stamped on `code`,
|
||||
// then unwind exactly like `throw`.
|
||||
const bytes = Buffer.from(s.message, "utf8");
|
||||
E.line(
|
||||
`scr_throw_error_msg_code(SCR_ERR_ERROR, ${cStringLiteral(bytes)}, ${bytes.length}, "${s.code}");${E.srcComment(s.loc)}`,
|
||||
);
|
||||
E.emitUnwind();
|
||||
break;
|
||||
}
|
||||
case "rethrow":
|
||||
// Re-raise the saved snapshot (payload retained — the binding local
|
||||
// releases with its scope) and unwind like `throw`.
|
||||
E.line(`scr_rethrow(${mangleLocal(s.localId)});${E.srcComment(s.loc)}`);
|
||||
E.emitUnwind();
|
||||
break;
|
||||
case "tryCatch":
|
||||
E.emitTryCatch(s);
|
||||
break;
|
||||
default: {
|
||||
const _exhaustive: never = s;
|
||||
void _exhaustive;
|
||||
}
|
||||
}
|
||||
const frame = E.frames.pop()!;
|
||||
// return/throw already released their frames on the jump path; emitting
|
||||
// the fall-through releases after them would be dead double-release code.
|
||||
if (s.kind !== "return" && s.kind !== "throw" && s.kind !== "rethrow" && s.kind !== "runtimeFence") E.releaseFrame(frame);
|
||||
}
|
||||
|
||||
/** try/catch/finally via pending-flag unwinding. Entering a try emits NO
|
||||
* code: the try context is compile-time state (tryStack) that redirects
|
||||
* unwinds inside the region to a label here instead of out of the
|
||||
* function. Shape:
|
||||
*
|
||||
* { try body } unwinds inside release frames/scopes down to
|
||||
* this statement's depths, then goto the handler
|
||||
* goto after; (normal completion skips the handler)
|
||||
* sc_catch_N:; (emitted only when some unwind targets it)
|
||||
* scr_exc_clear(); catch TAKES the exception (payload discarded —
|
||||
* { catch body } the supported catch form is bindingless)
|
||||
* after/sc_fin_N:;
|
||||
* { finally body } normal path
|
||||
* goto sc_tryend_N;
|
||||
* sc_finexc_N:; exception path: pending flag still set
|
||||
* { finally body } (emitted twice — fresh temps/labels each time;
|
||||
* <unwind> duplication is safe, and the pending flag is
|
||||
* sc_tryend_N:; the dispatch)
|
||||
*
|
||||
* Without a catch, unwinds in the try body target sc_finexc_N directly.
|
||||
* A catch body's own exceptions target sc_finexc_N (when a finally
|
||||
* exists) or the ENCLOSING context — never its own handler. After the
|
||||
* exception-path finally, propagation continues (emitUnwind: enclosing
|
||||
* handler or dummy return). A `return` inside the try/catch body rides a
|
||||
* THIRD finally copy (sc_finret_N — the pending-return path: value
|
||||
* snapshotted into sc_pret at the return site, dispatch outward after
|
||||
* the copy runs); break/continue never cross a finally and no jump
|
||||
* leaves a finally body (frontend fence + validator backstop), so
|
||||
* normal, exception, and pending-return are the only paths a finally
|
||||
* must model; jumps out of PLAIN try/catch need nothing here —
|
||||
* release-on-jump already walks the try scopes. */
|
||||
export function emitTryCatch(E: CEmitter, s: IrStmt & { kind: "tryCatch" }): void {
|
||||
const id = E.labelCounter++;
|
||||
const hasCatch = s.catchBody !== null;
|
||||
const hasFinally = s.finallyBody !== null;
|
||||
const catchLabel = `sc_catch_${id}`;
|
||||
const finExcLabel = `sc_finexc_${id}`;
|
||||
const endLabel = `sc_tryend_${id}`;
|
||||
// Where the try body's normal completion continues.
|
||||
const afterTryLabel = hasFinally ? `sc_fin_${id}` : endLabel;
|
||||
let afterTryLabelUsed = false;
|
||||
|
||||
const handler = {
|
||||
label: hasCatch ? catchLabel : finExcLabel,
|
||||
used: false,
|
||||
frameDepth: E.frames.length,
|
||||
scopeDepth: E.scopes.length,
|
||||
};
|
||||
// The pending-return region: returns inside tryBody/catchBody snapshot
|
||||
// their value and jump here-ish (the pending-return finally copy below)
|
||||
// instead of returning directly. Same depths as the unwind handler.
|
||||
const retEntry = hasFinally
|
||||
? {
|
||||
label: `sc_finret_${id}`,
|
||||
used: false,
|
||||
frameDepth: E.frames.length,
|
||||
scopeDepth: E.scopes.length,
|
||||
}
|
||||
: null;
|
||||
E.line(`/* try */${E.srcComment(s.loc)}`);
|
||||
if (retEntry) E.finallyStack.push(retEntry);
|
||||
E.tryStack.push(handler);
|
||||
E.emitBlock(s.tryBody);
|
||||
E.tryStack.pop();
|
||||
|
||||
// Exceptions raised in the CATCH body unwind to the exception-path
|
||||
// finally (pending stays set through it) when one exists.
|
||||
const excHandler = {
|
||||
label: finExcLabel,
|
||||
used: !hasCatch && handler.used,
|
||||
frameDepth: E.frames.length,
|
||||
scopeDepth: E.scopes.length,
|
||||
};
|
||||
|
||||
if (hasCatch && handler.used) {
|
||||
if (!E.endsWithJump(s.tryBody)) {
|
||||
E.line(`goto ${afterTryLabel};`);
|
||||
afterTryLabelUsed = true;
|
||||
}
|
||||
if (hasFinally) E.tryStack.push(excHandler);
|
||||
// Generator bodies: a pending GENRET sentinel (.return(v) injected at
|
||||
// a yield) is a RETURN completion, not a throw — catch must not take
|
||||
// it. Re-unwind past this handler (finally still runs — the unwind
|
||||
// targets the exception-path finally or the enclosing context; the
|
||||
// runtime depths here equal the handler's, so no double release).
|
||||
const genretPrologue = (): void => {
|
||||
if (E.currentGenerator === null) return;
|
||||
E.line(`if (scr_exc_genret_pending()) { /* .return(): not catchable */`);
|
||||
E.indent++;
|
||||
E.emitUnwind();
|
||||
E.indent--;
|
||||
E.line(`}`);
|
||||
};
|
||||
if (s.catchLocalId !== null) {
|
||||
// catch (e): the exception MOVES into the binding's snapshot box,
|
||||
// owned by the catch body's scope (released on every exit — normal
|
||||
// fall-through, jumps out, and unwinds from the body).
|
||||
const binding = mangleLocal(s.catchLocalId);
|
||||
E.line(`${catchLabel}:; /* catch (${E.currentLocals.get(s.catchLocalId)?.name ?? "e"}) — takes the exception */`);
|
||||
genretPrologue();
|
||||
E.emitBlock(s.catchBody!, (scope) => {
|
||||
E.line(`${binding} = scr_exc_take();`);
|
||||
scope.push({ name: binding, type: CAUGHT });
|
||||
});
|
||||
} else {
|
||||
E.line(`${catchLabel}:; /* catch — takes the exception */`);
|
||||
genretPrologue();
|
||||
E.line(`scr_exc_clear();`);
|
||||
E.emitBlock(s.catchBody!);
|
||||
}
|
||||
if (hasFinally) E.tryStack.pop();
|
||||
// Normal completion of the catch falls through to afterTryLabel.
|
||||
}
|
||||
if (retEntry) E.finallyStack.pop();
|
||||
|
||||
if (hasFinally) {
|
||||
if (afterTryLabelUsed) E.line(`${afterTryLabel}:;`);
|
||||
E.line(`/* finally (normal path) */`);
|
||||
E.emitBlock(s.finallyBody!);
|
||||
const needEnd = excHandler.used || retEntry!.used;
|
||||
if (needEnd) E.line(`goto ${endLabel};`);
|
||||
if (excHandler.used) {
|
||||
// The pending exception is STASHED across the finally body (a
|
||||
// ScrCaught snapshot, re-raised after) so the body runs with a
|
||||
// CLEAN cell: its own may-throw calls' pending checks answer for
|
||||
// themselves — not for the in-flight exception — and a generator
|
||||
// suspending here (a yield inside a finally on the .return()/
|
||||
// .throw() unwind) resumes into the REST of the finally, exactly
|
||||
// Node. A throw inside the body REPLACES the stash (it unwinds
|
||||
// past the release through the synthetic scope entry below —
|
||||
// JS's replace semantics); normal completion re-raises the stash
|
||||
// and keeps propagating.
|
||||
const stash = `sc_fexc_${id}`;
|
||||
E.line(`${finExcLabel}:; /* finally (exception path — stashed) */`);
|
||||
E.line(`ScrCaught *${stash} = scr_exc_take();`);
|
||||
E.scopes.push([{ name: stash, type: CAUGHT }]);
|
||||
E.emitBlock(s.finallyBody!);
|
||||
E.scopes.pop(); // normal completion keeps the stash for the re-raise
|
||||
E.line(`scr_rethrow(${stash});`);
|
||||
E.line(`scr_caught_release(${stash});`);
|
||||
E.emitUnwind();
|
||||
}
|
||||
if (retEntry!.used) {
|
||||
// Pending-return path: a return in the try/catch body parked its
|
||||
// value in sc_pret and jumped here after releasing down to this
|
||||
// region. The finally body runs (third copy — fresh temps/labels,
|
||||
// like the exception copy), then the dispatch continues outward:
|
||||
// the next enclosing finally region of this function, or the
|
||||
// actual return. A THROW inside this copy replaces the pending
|
||||
// return (JS): the slot's owned value rides a synthetic scope
|
||||
// entry so the unwind releases it.
|
||||
E.line(`${retEntry!.label}:; /* finally (pending-return path) */`);
|
||||
const retT = E.currentReturnType;
|
||||
const own = isRefCounted(retT);
|
||||
if (own) E.scopes.push([{ name: "sc_pret", type: retT }]);
|
||||
E.emitBlock(s.finallyBody!);
|
||||
if (own) E.scopes.pop();
|
||||
const outer = E.finallyStack[E.finallyStack.length - 1];
|
||||
if (outer) {
|
||||
outer.used = true;
|
||||
E.releaseForJump(outer.frameDepth, outer.scopeDepth);
|
||||
E.line(`goto ${outer.label};`);
|
||||
} else {
|
||||
E.releaseForJump(0, 0);
|
||||
E.line(retT.kind === "void" ? `return;` : `return sc_pret;`);
|
||||
}
|
||||
}
|
||||
if (needEnd) E.line(`${endLabel}:;`);
|
||||
} else if (afterTryLabelUsed) {
|
||||
E.line(`${endLabel}:;`);
|
||||
}
|
||||
}
|
||||
|
||||
/** JS-exact switch as a goto chain — C `switch` cannot express lazily
|
||||
* evaluated, arbitrary-expression case tests. Shape:
|
||||
*
|
||||
* disc temp
|
||||
* NULL-reset of refcounted case-body locals (see below)
|
||||
* per test, in source order: eval test; if (== disc) goto case_i;
|
||||
* goto default (or end when there is none)
|
||||
* case_0:; body_0 ─┐ bodies in source order fall through
|
||||
* case_1:; body_1 ─┘ naturally, JS-exact
|
||||
* scope releases (natural fall-off path)
|
||||
* end:; (break jumps here, after releasing scopes itself)
|
||||
*
|
||||
* All case bodies share ONE scope (JS: one lexical scope per switch body).
|
||||
* Because dispatch can jump PAST a varDecl into a later case, a refcounted
|
||||
* local of a skipped body is never written — and inside an enclosing loop
|
||||
* it still holds the pointer a previous iteration's scope exit already
|
||||
* released. The declarations are therefore NULL-reset up front; the
|
||||
* scope-exit releases rely on the runtime's NULL-tolerant release calls. */
|
||||
export function emitSwitch(E: CEmitter, s: IrStmt & { kind: "switch" }): void {
|
||||
if (s.disc.type.kind !== "f64" && s.disc.type.kind !== "string" && s.disc.type.kind !== "bool") {
|
||||
throw new Error(`emitter bug: switch on ${s.disc.type.kind}`);
|
||||
}
|
||||
const id = E.labelCounter++;
|
||||
const endLabel = `sc_swend_${id}`;
|
||||
const caseLabel = (i: number) => `sc_swcase_${id}_${i}`;
|
||||
|
||||
// The disc temp lives in the whole statement's frame: for a string
|
||||
// discriminant it stays alive across every test and body, released when
|
||||
// the switch statement ends (break lands before that release; return
|
||||
// sweeps frames itself).
|
||||
const disc = E.emitExpr(s.disc);
|
||||
|
||||
// NULL-reset refcounted/boxed locals declared at the top level of case
|
||||
// bodies (nested blocks manage their own scopes on normal control flow).
|
||||
for (const c of s.cases) {
|
||||
for (const stmt of c.body) {
|
||||
if (stmt.kind !== "varDecl") continue;
|
||||
const local = E.currentLocals.get(stmt.localId)!;
|
||||
if (local.boxed || isRefCounted(local.type)) {
|
||||
E.line(`${mangleLocal(local.id)} = NULL; /* case-scoped ${local.name} */`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch: lazy source-order test evaluation (a test after the match
|
||||
// never runs). Each test's temps release right after its comparison.
|
||||
let defaultIdx = -1;
|
||||
s.cases.forEach((c, i) => {
|
||||
if (c.test === null) {
|
||||
defaultIdx = i;
|
||||
return;
|
||||
}
|
||||
E.frames.push([]);
|
||||
const t = E.emitExpr(c.test);
|
||||
const cmp =
|
||||
c.test.type.kind === "string"
|
||||
? `scr_str_eq(${disc.name}, ${t.name})`
|
||||
: `${disc.name} == ${t.name}`;
|
||||
const hit = E.newTemp(BOOL, cmp);
|
||||
E.releaseFrame(E.frames.pop()!);
|
||||
E.line(`if (${hit.name}) goto ${caseLabel(i)};`);
|
||||
});
|
||||
E.line(`goto ${defaultIdx >= 0 ? caseLabel(defaultIdx) : endLabel};`);
|
||||
|
||||
// Bodies in source order: entering one falls through the rest (JS-exact)
|
||||
// until a break jumps to the end label.
|
||||
const target = {
|
||||
kind: "switch" as const,
|
||||
endLabel,
|
||||
usedEnd: defaultIdx < 0,
|
||||
...(s.labels !== undefined && { labels: s.labels }),
|
||||
scopeDepth: E.scopes.length,
|
||||
frameDepth: E.frames.length,
|
||||
};
|
||||
E.jumpTargets.push(target);
|
||||
E.scopes.push([]);
|
||||
s.cases.forEach((c, i) => {
|
||||
E.line(`${caseLabel(i)}:;`);
|
||||
E.indent++;
|
||||
E.emitStmts(c.body);
|
||||
E.indent--;
|
||||
});
|
||||
E.jumpTargets.pop();
|
||||
const scope = E.scopes.pop()!;
|
||||
// Natural fall-off of the last body releases the shared scope here; a
|
||||
// jump (break/return/continue/throw) already released it before jumping.
|
||||
const lastBody = s.cases[s.cases.length - 1]?.body;
|
||||
if (!lastBody || !E.endsWithJump(lastBody)) E.releaseFrame(scope);
|
||||
if (target.usedEnd) E.line(`${endLabel}:;`);
|
||||
}
|
||||
|
||||
/** Emits `if (cond) ` followed by a block on the same line for readability. */
|
||||
export function mergeBrace(E: CEmitter, emitBlockFn: () => void): void {
|
||||
const head = E.lines.pop()!;
|
||||
const before = E.lines.length;
|
||||
emitBlockFn();
|
||||
E.lines[before] = head + E.lines[before]!.trimStart();
|
||||
}
|
||||
|
||||
/** Evaluates `expr` in its own statement frame inside an already-open
|
||||
* branch and moves the result into the C lvalue `target`: the chosen
|
||||
* value's ownership transfers, every other temp the evaluation allocated
|
||||
* releases inside the branch. The shared core of the lazily-branched
|
||||
* expressions (`logical`, `ternary`); the caller owns the surrounding
|
||||
* braces/indentation and registers `target` in ITS frame. */
|
||||
export function emitBranchInto(E: CEmitter, target: string, expr: IrExpr): void {
|
||||
E.frames.push([]);
|
||||
const v = E.emitExpr(expr);
|
||||
E.moveTemp(v);
|
||||
E.line(`${target} = ${v.name};`);
|
||||
E.releaseFrame(E.frames.pop()!);
|
||||
}
|
||||
|
||||
/** Evaluates a condition; its string temps are released before the branch,
|
||||
* which is safe because the result is a scalar bool. */
|
||||
export function emitCondition(E: CEmitter, cond: IrExpr): string {
|
||||
const t = E.emitExpr(cond);
|
||||
const frame = E.currentFrame();
|
||||
E.releaseFrame(frame);
|
||||
frame.length = 0;
|
||||
return t.name;
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
/* Type-directed dispatch tables of the C emitter: the C spelling of every IR
|
||||
* type and the per-type runtime entry points (retain/release, box kinds,
|
||||
* array element kinds, map key/value kinds), plus C literal spelling. Pure
|
||||
* functions of IrType/values — every emission module leans on these, so they
|
||||
* live in ONE place with no emitter state. */
|
||||
import type { IrType } from "../../ir/nodes.js";
|
||||
import { RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES } from "../../ir/nodes.js";
|
||||
import {
|
||||
mangleClassRelease,
|
||||
mangleClassRetain,
|
||||
mangleClassStruct,
|
||||
mangleRecordRelease,
|
||||
mangleRecordRetain,
|
||||
mangleRecordStruct,
|
||||
} from "../mangle.js";
|
||||
|
||||
export function cType(t: IrType): string {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
return "double";
|
||||
case "bool":
|
||||
return "bool";
|
||||
case "string":
|
||||
return "ScrStr *";
|
||||
case "array":
|
||||
return "ScrArr *";
|
||||
case "map":
|
||||
case "set":
|
||||
// Sets ARE the map runtime with the value slot unused (SCR_MAP_VAL_F64
|
||||
// storing a constant 0) — one struct, one RC family, one allocator.
|
||||
return "ScrMap *";
|
||||
case "regex":
|
||||
return "ScrRegex *";
|
||||
case "bytes":
|
||||
// One struct for every element kind (the runtime dispatches on the
|
||||
// stored elem tag) — exactly the ScrArr pattern.
|
||||
return "ScrBytes *";
|
||||
case "url":
|
||||
return "ScrUrl *";
|
||||
case "searchParams":
|
||||
return "ScrSearchParams *";
|
||||
case "symbol":
|
||||
return "ScrSym *";
|
||||
case "stats":
|
||||
return "ScrStats *";
|
||||
case "spawnRes":
|
||||
return "ScrSpawnRes *";
|
||||
case "child":
|
||||
return "ScrChild *";
|
||||
case "netServer":
|
||||
return "ScrNetServer *";
|
||||
case "netSocket":
|
||||
return "ScrNetSocket *";
|
||||
case "http2Session":
|
||||
return "ScrH2Session *";
|
||||
case "http2Stream":
|
||||
return "ScrH2Stream *";
|
||||
case "dgramSocket":
|
||||
return "ScrDgramSocket *";
|
||||
case "testCtx":
|
||||
return "ScrTestCtx *";
|
||||
case "httpReq":
|
||||
return "ScrHttpReq *";
|
||||
case "httpRes":
|
||||
return "ScrHttpRes *";
|
||||
case "httpClientReq":
|
||||
return "ScrHttpClientReq *";
|
||||
case "secureCtx":
|
||||
return "ScrSecureCtx *";
|
||||
case "fsWatcher":
|
||||
return "ScrWatcher *";
|
||||
case "childStream":
|
||||
return "ScrChildStream *";
|
||||
case "procStream":
|
||||
// A SCALAR kind: the stream value IS its fd (1 = stdout, 2 =
|
||||
// stderr) — no heap, no refcount.
|
||||
return "double";
|
||||
case "func":
|
||||
return "ScrClosure *";
|
||||
case "classval":
|
||||
// ONE struct type for every class (the fields are class-independent).
|
||||
return "ScrClassObj *";
|
||||
case "object":
|
||||
// Runtime-provided error classes share the runtime's ScrError struct
|
||||
// (all four builtins have the same layout; user subclasses embed it
|
||||
// as their emitted struct's prefix).
|
||||
if (RUNTIME_ERROR_CLASSES.has(t.className)) return "ScrError *";
|
||||
// The runtime emitter class shares the runtime's ScrEmitter struct
|
||||
// (user subclasses embed its prefix in their emitted structs).
|
||||
if (t.className === RUNTIME_EMITTER_CLASS) return "ScrEmitter *";
|
||||
// The five runtime stream classes share ONE runtime struct (the
|
||||
// emitter prefix plus the stream-state pointer) — upcasts among
|
||||
// them and to ScrEmitter are pointer reinterprets.
|
||||
if (RUNTIME_STREAM_CLASSES.has(t.className)) return "ScrStream *";
|
||||
return `${mangleClassStruct(t.className)} *`;
|
||||
case "record":
|
||||
return `${mangleRecordStruct(t.shapeId)} *`;
|
||||
case "union":
|
||||
return "ScrUnion *";
|
||||
case "dyn":
|
||||
return "ScrDyn *";
|
||||
case "jsval":
|
||||
return "ScrJsval *";
|
||||
case "caught":
|
||||
return "ScrCaught *";
|
||||
case "promise":
|
||||
return "ScrPromise *";
|
||||
case "generator":
|
||||
return "ScrGen *";
|
||||
case "void":
|
||||
return "void";
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
// Unit kinds have no C value form: they exist only as union arms
|
||||
// (the box carries the tag and nothing else) — a unit type asked to
|
||||
// declare a C value is an emitter bug.
|
||||
throw new Error(`emitter bug: ${t.kind} has no C value form`);
|
||||
default: {
|
||||
const _exhaustive: never = t;
|
||||
void _exhaustive;
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The retain call (+1, returns the value) for one refcounted type. */
|
||||
export function retainCallC(type: IrType, expr: string): string {
|
||||
switch (type.kind) {
|
||||
case "string":
|
||||
return `scr_str_retain(${expr})`;
|
||||
case "array":
|
||||
return `scr_arr_retain(${expr})`;
|
||||
case "map":
|
||||
case "set":
|
||||
return `scr_map_retain(${expr})`;
|
||||
case "regex":
|
||||
return `scr_regex_retain(${expr})`;
|
||||
case "bytes":
|
||||
return `scr_bytes_retain(${expr})`;
|
||||
case "url":
|
||||
return `scr_url_retain(${expr})`;
|
||||
case "searchParams":
|
||||
return `scr_sp_retain(${expr})`;
|
||||
case "symbol":
|
||||
return `scr_sym_retain(${expr})`;
|
||||
case "stats":
|
||||
return `scr_stats_retain(${expr})`;
|
||||
case "spawnRes":
|
||||
return `scr_spawn_res_retain(${expr})`;
|
||||
case "child":
|
||||
return `scr_child_retain(${expr})`;
|
||||
case "netServer":
|
||||
return `scr_net_server_retain(${expr})`;
|
||||
case "netSocket":
|
||||
return `scr_net_sock_retain(${expr})`;
|
||||
case "http2Session":
|
||||
return `scr_http2_session_retain(${expr})`;
|
||||
case "http2Stream":
|
||||
return `scr_http2_stream_retain(${expr})`;
|
||||
case "dgramSocket":
|
||||
return `scr_dgram_retain(${expr})`;
|
||||
case "testCtx":
|
||||
return `scr_testctx_retain(${expr})`;
|
||||
case "httpReq":
|
||||
return `scr_http_req_retain(${expr})`;
|
||||
case "httpRes":
|
||||
return `scr_http_res_retain(${expr})`;
|
||||
case "httpClientReq":
|
||||
return `scr_http_client_retain(${expr})`;
|
||||
case "secureCtx":
|
||||
return `scr_secure_ctx_retain(${expr})`;
|
||||
case "fsWatcher":
|
||||
return `scr_watcher_retain(${expr})`;
|
||||
case "childStream":
|
||||
return `scr_child_stream_retain(${expr})`;
|
||||
case "func":
|
||||
return `scr_closure_retain(${expr})`;
|
||||
case "classval":
|
||||
// A no-op on the immortal static — kept for ownership uniformity.
|
||||
return `scr_classobj_retain(${expr})`;
|
||||
case "object":
|
||||
if (RUNTIME_ERROR_CLASSES.has(type.className)) return `scr_error_retain(${expr})`;
|
||||
if (type.className === RUNTIME_EMITTER_CLASS) return `scr_emitter_retain(${expr})`;
|
||||
if (RUNTIME_STREAM_CLASSES.has(type.className)) return `scr_stream_retain(${expr})`;
|
||||
return `${mangleClassRetain(type.className)}(${expr})`;
|
||||
case "record":
|
||||
return `${mangleRecordRetain(type.shapeId)}(${expr})`;
|
||||
case "promise":
|
||||
return `scr_promise_retain(${expr})`;
|
||||
case "generator":
|
||||
return `scr_gen_retain(${expr})`;
|
||||
case "union":
|
||||
return `scr_union_retain(${expr})`;
|
||||
case "dyn":
|
||||
return `scr_dyn_retain(${expr})`;
|
||||
case "jsval":
|
||||
return `scr_jsval_retain(${expr})`;
|
||||
case "caught":
|
||||
return `scr_caught_retain(${expr})`;
|
||||
default:
|
||||
throw new Error(`emitter bug: retain of non-refcounted type ${type.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The release call for one owned refcounted value (all NULL-tolerant). */
|
||||
export function releaseCallC(type: IrType, expr: string): string {
|
||||
switch (type.kind) {
|
||||
case "string":
|
||||
return `scr_str_release(${expr})`;
|
||||
case "array":
|
||||
return `scr_arr_release(${expr})`;
|
||||
case "map":
|
||||
case "set":
|
||||
return `scr_map_release(${expr})`;
|
||||
case "regex":
|
||||
return `scr_regex_release(${expr})`;
|
||||
case "bytes":
|
||||
return `scr_bytes_release(${expr})`;
|
||||
case "url":
|
||||
return `scr_url_release(${expr})`;
|
||||
case "searchParams":
|
||||
return `scr_sp_release(${expr})`;
|
||||
case "symbol":
|
||||
return `scr_sym_release(${expr})`;
|
||||
case "stats":
|
||||
return `scr_stats_release(${expr})`;
|
||||
case "spawnRes":
|
||||
return `scr_spawn_res_release(${expr})`;
|
||||
case "child":
|
||||
return `scr_child_release(${expr})`;
|
||||
case "netServer":
|
||||
return `scr_net_server_release(${expr})`;
|
||||
case "netSocket":
|
||||
return `scr_net_sock_release(${expr})`;
|
||||
case "http2Session":
|
||||
return `scr_http2_session_release(${expr})`;
|
||||
case "http2Stream":
|
||||
return `scr_http2_stream_release(${expr})`;
|
||||
case "dgramSocket":
|
||||
return `scr_dgram_release(${expr})`;
|
||||
case "testCtx":
|
||||
return `scr_testctx_release(${expr})`;
|
||||
case "httpReq":
|
||||
return `scr_http_req_release(${expr})`;
|
||||
case "httpRes":
|
||||
return `scr_http_res_release(${expr})`;
|
||||
case "httpClientReq":
|
||||
return `scr_http_client_release(${expr})`;
|
||||
case "secureCtx":
|
||||
return `scr_secure_ctx_release(${expr})`;
|
||||
case "fsWatcher":
|
||||
return `scr_watcher_release(${expr})`;
|
||||
case "childStream":
|
||||
return `scr_child_stream_release(${expr})`;
|
||||
case "func":
|
||||
return `scr_closure_release(${expr})`;
|
||||
case "classval":
|
||||
return `scr_classobj_release(${expr})`;
|
||||
case "object":
|
||||
if (RUNTIME_ERROR_CLASSES.has(type.className)) return `scr_error_release(${expr})`;
|
||||
if (type.className === RUNTIME_EMITTER_CLASS) return `scr_emitter_release(${expr})`;
|
||||
if (RUNTIME_STREAM_CLASSES.has(type.className)) return `scr_stream_release(${expr})`;
|
||||
return `${mangleClassRelease(type.className)}(${expr})`;
|
||||
case "record":
|
||||
return `${mangleRecordRelease(type.shapeId)}(${expr})`;
|
||||
case "promise":
|
||||
return `scr_promise_release(${expr})`;
|
||||
case "generator":
|
||||
return `scr_gen_release(${expr})`;
|
||||
case "union":
|
||||
return `scr_union_release(${expr})`;
|
||||
case "dyn":
|
||||
return `scr_dyn_release(${expr})`;
|
||||
case "jsval":
|
||||
return `scr_jsval_release(${expr})`;
|
||||
case "caught":
|
||||
return `scr_caught_release(${expr})`;
|
||||
default:
|
||||
throw new Error(`emitter bug: release of non-refcounted type ${type.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The runtime's box-kind tag for a boxed (captured) variable's type. */
|
||||
export function boxKindC(t: IrType): string {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
return "SCR_BOX_F64";
|
||||
case "bool":
|
||||
return "SCR_BOX_BOOL";
|
||||
case "string":
|
||||
return "SCR_BOX_STR";
|
||||
case "array":
|
||||
return "SCR_BOX_ARR";
|
||||
case "func":
|
||||
return "SCR_BOX_FUNC";
|
||||
case "object":
|
||||
case "classval":
|
||||
case "record":
|
||||
case "union":
|
||||
case "map":
|
||||
case "set":
|
||||
case "regex":
|
||||
case "url":
|
||||
case "searchParams":
|
||||
case "symbol":
|
||||
case "stats":
|
||||
case "spawnRes":
|
||||
case "child":
|
||||
case "netServer":
|
||||
case "netSocket":
|
||||
case "http2Session":
|
||||
case "http2Stream":
|
||||
case "dgramSocket":
|
||||
case "testCtx":
|
||||
case "httpReq":
|
||||
case "httpRes":
|
||||
case "httpClientReq":
|
||||
case "secureCtx":
|
||||
case "fsWatcher":
|
||||
case "childStream":
|
||||
case "bytes":
|
||||
throw new Error(`emitter bug: ${t.kind} boxes go through boxNewC, not boxKindC`);
|
||||
case "procStream":
|
||||
// Scalar (the fd double) — the f64 box carries it.
|
||||
return "SCR_BOX_F64";
|
||||
case "dyn":
|
||||
// dyn never rides capture boxes (frontend rejects dyn captures).
|
||||
throw new Error("emitter bug: box of dyn");
|
||||
case "jsval":
|
||||
throw new Error("emitter bug: jsval boxes go through boxNewC, not boxKindC");
|
||||
case "promise":
|
||||
// promises ride obj-boxes (boxNewC), never plain kind boxes
|
||||
throw new Error("emitter bug: promise boxes go through boxNewC");
|
||||
case "generator":
|
||||
// generators ride obj-boxes too (boxNewC — vAdapters carries the
|
||||
// ScrGen RC entry points; no trace, like child).
|
||||
throw new Error("emitter bug: generator boxes go through boxNewC");
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
case "caught":
|
||||
// unit kinds never stand alone (and catch bindings never box), so
|
||||
// nothing to box
|
||||
throw new Error(`emitter bug: box of ${t.kind}`);
|
||||
case "void":
|
||||
throw new Error("emitter bug: box of void");
|
||||
default: {
|
||||
const _exhaustive: never = t;
|
||||
void _exhaustive;
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The runtime `_v` (void*-signature) RC entry points for one refcounted
|
||||
* type — the currency of every generic container that must retain/release
|
||||
* payloads whose concrete struct it cannot know: union values, capture
|
||||
* boxes, promises, and the exception cell. Classes and records use their
|
||||
* emitted per-shape adapters; everything else has runtime-provided ones.
|
||||
* Bare symbol names — call sites prefix `&` where a fn ptr is passed. */
|
||||
export function vAdapters(t: IrType): { retain: string; release: string } {
|
||||
switch (t.kind) {
|
||||
case "string":
|
||||
return { retain: "scr_str_retain_v", release: "scr_str_release_v" };
|
||||
case "array":
|
||||
return { retain: "scr_arr_retain_v", release: "scr_arr_release_v" };
|
||||
case "map":
|
||||
case "set":
|
||||
return { retain: "scr_map_retain_v", release: "scr_map_release_v" };
|
||||
case "regex":
|
||||
return { retain: "scr_regex_retain_v", release: "scr_regex_release_v" };
|
||||
case "bytes":
|
||||
return { retain: "scr_bytes_retain_v", release: "scr_bytes_release_v" };
|
||||
case "url":
|
||||
return { retain: "scr_url_retain_v", release: "scr_url_release_v" };
|
||||
case "searchParams":
|
||||
return { retain: "scr_sp_retain_v", release: "scr_sp_release_v" };
|
||||
case "symbol":
|
||||
return { retain: "scr_sym_retain_v", release: "scr_sym_release_v" };
|
||||
case "stats":
|
||||
return { retain: "scr_stats_retain_v", release: "scr_stats_release_v" };
|
||||
case "spawnRes":
|
||||
return { retain: "scr_spawn_res_retain_v", release: "scr_spawn_res_release_v" };
|
||||
case "child":
|
||||
return { retain: "scr_child_retain_v", release: "scr_child_release_v" };
|
||||
case "netServer":
|
||||
return { retain: "scr_net_server_retain_v", release: "scr_net_server_release_v" };
|
||||
case "netSocket":
|
||||
return { retain: "scr_net_sock_retain_v", release: "scr_net_sock_release_v" };
|
||||
case "http2Session":
|
||||
return { retain: "scr_http2_session_retain_v", release: "scr_http2_session_release_v" };
|
||||
case "http2Stream":
|
||||
return { retain: "scr_http2_stream_retain_v", release: "scr_http2_stream_release_v" };
|
||||
case "dgramSocket":
|
||||
return { retain: "scr_dgram_retain_v", release: "scr_dgram_release_v" };
|
||||
case "testCtx":
|
||||
return { retain: "scr_testctx_retain_v", release: "scr_testctx_release_v" };
|
||||
case "httpReq":
|
||||
return { retain: "scr_http_req_retain_v", release: "scr_http_req_release_v" };
|
||||
case "httpRes":
|
||||
return { retain: "scr_http_res_retain_v", release: "scr_http_res_release_v" };
|
||||
case "httpClientReq":
|
||||
return { retain: "scr_http_client_retain_v", release: "scr_http_client_release_v" };
|
||||
case "secureCtx":
|
||||
return { retain: "scr_secure_ctx_retain_v", release: "scr_secure_ctx_release_v" };
|
||||
case "fsWatcher":
|
||||
return { retain: "scr_watcher_retain_v", release: "scr_watcher_release_v" };
|
||||
case "childStream":
|
||||
return { retain: "scr_child_stream_retain_v", release: "scr_child_stream_release_v" };
|
||||
case "func":
|
||||
return { retain: "scr_closure_retain_v", release: "scr_closure_release_v" };
|
||||
case "classval":
|
||||
// No-ops on the immortal class object; the container machinery
|
||||
// stays uniform.
|
||||
return { retain: "scr_classobj_retain_v", release: "scr_classobj_release_v" };
|
||||
case "union":
|
||||
return { retain: "scr_union_retain_v", release: "scr_union_release_v" };
|
||||
case "object":
|
||||
if (RUNTIME_ERROR_CLASSES.has(t.className)) {
|
||||
return { retain: "scr_error_retain_v", release: "scr_error_release_v" };
|
||||
}
|
||||
if (t.className === RUNTIME_EMITTER_CLASS) {
|
||||
return { retain: "scr_emitter_retain_v", release: "scr_emitter_release_v" };
|
||||
}
|
||||
if (RUNTIME_STREAM_CLASSES.has(t.className)) {
|
||||
return { retain: "scr_stream_retain_v", release: "scr_stream_release_v" };
|
||||
}
|
||||
return { retain: `${mangleClassRetain(t.className)}_v`, release: `${mangleClassRelease(t.className)}_v` };
|
||||
case "record":
|
||||
return { retain: `${mangleRecordRetain(t.shapeId)}_v`, release: `${mangleRecordRelease(t.shapeId)}_v` };
|
||||
case "promise":
|
||||
return { retain: "scr_promise_retain_v", release: "scr_promise_release_v" };
|
||||
case "generator":
|
||||
return { retain: "scr_gen_retain_v", release: "scr_gen_release_v" };
|
||||
case "dyn":
|
||||
return { retain: "scr_dyn_retain_v", release: "scr_dyn_release_v" };
|
||||
case "jsval":
|
||||
return { retain: "scr_jsval_retain_v", release: "scr_jsval_release_v" };
|
||||
default:
|
||||
throw new Error(`emitter bug: no RC adapters for ${t.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Box accessor suffix: scalars stored unboxed, ref kinds as pointers. */
|
||||
export function boxAccess(t: IrType): "f64" | "bool" | "ref" {
|
||||
// procStream is a scalar (the stream's fd double — boxKindC agrees with
|
||||
// SCR_BOX_F64), so its captures ride the f64 slot like any number.
|
||||
return t.kind === "f64" || t.kind === "procStream" ? "f64" : t.kind === "bool" ? "bool" : "ref";
|
||||
}
|
||||
|
||||
/** C function-pointer cast for calling through a closure: the callee
|
||||
* receives its own ScrClosure first, then the declared params. */
|
||||
export function cFnPtrCast(ft: IrType & { kind: "func" }): string {
|
||||
const params = ["ScrClosure *", ...ft.params.map((p) => cType(p).trim())].join(", ");
|
||||
return `(${cType(ft.ret).trim()} (*)(${params}))`;
|
||||
}
|
||||
|
||||
/** The runtime's element-kind tag for an array's element type. Record/
|
||||
* object/union elements are SCR_ELEM_REF (void* slots + per-array RC entry
|
||||
* points); array elements answer SCR_ELEM_ARR here, but a CYCLE-CAPABLE
|
||||
* inner array rides SCR_ELEM_REF instead so the outer array's trace can
|
||||
* reach it — that answer needs emitter state, so construction goes through
|
||||
* CEmitter.arrNewC, which overrides this for traced-array elements. */
|
||||
export function elemKindC(elem: IrType): string {
|
||||
switch (elem.kind) {
|
||||
case "f64":
|
||||
return "SCR_ELEM_F64";
|
||||
case "bool":
|
||||
return "SCR_ELEM_BOOL";
|
||||
case "string":
|
||||
return "SCR_ELEM_STR";
|
||||
case "array":
|
||||
return "SCR_ELEM_ARR";
|
||||
case "bytes":
|
||||
return "SCR_ELEM_BYTES";
|
||||
case "record":
|
||||
case "object":
|
||||
case "union":
|
||||
// Island handles are ordinary refcounted pointers (scr_jsval_retain/
|
||||
// release adapters) — `any[]` under --dynamic is a native array of
|
||||
// handles, one element per island value.
|
||||
case "jsval":
|
||||
// Spawned child handles (ChildProcess[] — the running-apps list):
|
||||
// ordinary refcounted pointers, no trace (they drop their closures at
|
||||
// reap, so never part of a cycle).
|
||||
case "child":
|
||||
// Server handles (ProxyServer[] — the [...set] drain of the auxiliary
|
||||
// registries): same refcounted-pointer story; listeners drop at close,
|
||||
// so a handle-in-array cycle is temporary like child's.
|
||||
case "netServer":
|
||||
// Symbols (symbol[] — heterogeneous sentinel lists): refcounted
|
||||
// identity pointers holding only strings — no trace, no cycles ever.
|
||||
case "symbol":
|
||||
// Class objects ((typeof Shape)[] — the registry idiom): immortal
|
||||
// statics behind no-op RC adapters — no trace, no cycles ever;
|
||||
// indexOf/includes/=== are the REF kind's pointer identity, exactly
|
||||
// JS class identity.
|
||||
case "classval":
|
||||
return "SCR_ELEM_REF";
|
||||
// Closures: refcounted, cycle-headered (captures can reach back
|
||||
// through boxes), scr_closure_* `_v` adapters + scr_closure_trace_v.
|
||||
// Identity semantics (indexOf/includes/===) are the REF kind's
|
||||
// pointer identity — exactly JS function identity.
|
||||
case "func":
|
||||
return "SCR_ELEM_REF";
|
||||
case "map":
|
||||
case "set":
|
||||
case "regex":
|
||||
case "url":
|
||||
case "searchParams":
|
||||
case "stats":
|
||||
case "spawnRes":
|
||||
case "netSocket":
|
||||
case "http2Session":
|
||||
case "http2Stream":
|
||||
case "dgramSocket":
|
||||
case "testCtx":
|
||||
case "httpReq":
|
||||
case "httpRes":
|
||||
case "httpClientReq":
|
||||
case "secureCtx":
|
||||
case "fsWatcher":
|
||||
case "childStream":
|
||||
case "procStream":
|
||||
case "dyn":
|
||||
case "caught":
|
||||
case "promise":
|
||||
case "generator":
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
throw new Error(`emitter bug: array of ${elem.kind} (frontend rejects these)`);
|
||||
case "void":
|
||||
throw new Error("emitter bug: array of void");
|
||||
default: {
|
||||
const _exhaustive: never = elem;
|
||||
void _exhaustive;
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The runtime's element-kind tag for a bytes (typed array) type. */
|
||||
export function bytesElemKindC(elem: "u8" | "u32" | "i32" | "f32"): string {
|
||||
return elem === "u8" ? "SCR_BYTES_U8" : elem === "u32" ? "SCR_BYTES_U32" : elem === "i32" ? "SCR_BYTES_I32" : "SCR_BYTES_F32";
|
||||
}
|
||||
|
||||
/** The runtime's ScrBytesNumKind tag + littleEndian flag per readNum/
|
||||
* writeNum kind token (the strLit args[0] the frontend mints). The
|
||||
* variable-width family's tokens map to sign + endian flags instead. */
|
||||
export const BYTES_NUM_KIND_C: Record<string, { kind: string; le: boolean } | undefined> = {
|
||||
u8: { kind: "SCR_BN_U8", le: false },
|
||||
i8: { kind: "SCR_BN_I8", le: false },
|
||||
u16be: { kind: "SCR_BN_U16", le: false },
|
||||
u16le: { kind: "SCR_BN_U16", le: true },
|
||||
i16be: { kind: "SCR_BN_I16", le: false },
|
||||
i16le: { kind: "SCR_BN_I16", le: true },
|
||||
u32be: { kind: "SCR_BN_U32", le: false },
|
||||
u32le: { kind: "SCR_BN_U32", le: true },
|
||||
i32be: { kind: "SCR_BN_I32", le: false },
|
||||
i32le: { kind: "SCR_BN_I32", le: true },
|
||||
f32be: { kind: "SCR_BN_F32", le: false },
|
||||
f32le: { kind: "SCR_BN_F32", le: true },
|
||||
f64be: { kind: "SCR_BN_F64", le: false },
|
||||
f64le: { kind: "SCR_BN_F64", le: true },
|
||||
};
|
||||
|
||||
/** The variable-width (read/writeUIntLE-style) kind tokens: sign + endian. */
|
||||
export const BYTES_NUM_VAR_C: Record<string, { sign: boolean; le: boolean } | undefined> = {
|
||||
ube: { sign: false, le: false },
|
||||
ule: { sign: false, le: true },
|
||||
ibe: { sign: true, le: false },
|
||||
ile: { sign: true, le: true },
|
||||
};
|
||||
|
||||
/** The runtime's ScrDataViewGet tag per dvGet* bytesIntrinsic method. */
|
||||
export const DV_GET_KIND_C: Record<string, string> = {
|
||||
dvGetUint8: "SCR_DV_U8",
|
||||
dvGetInt8: "SCR_DV_I8",
|
||||
dvGetUint16: "SCR_DV_U16",
|
||||
dvGetInt16: "SCR_DV_I16",
|
||||
dvGetUint32: "SCR_DV_U32",
|
||||
dvGetInt32: "SCR_DV_I32",
|
||||
dvGetFloat32: "SCR_DV_F32",
|
||||
dvGetFloat64: "SCR_DV_F64",
|
||||
dvGetBigUint64Number: "SCR_DV_BIGU64",
|
||||
dvGetBigInt64Number: "SCR_DV_BIGI64",
|
||||
};
|
||||
|
||||
/** Runtime accessor suffix for an element type: arrays store f64 and bool
|
||||
* unboxed and everything refcounted as a pointer (`_ref`). */
|
||||
export function elemAccess(elem: IrType): "f64" | "bool" | "ref" {
|
||||
return elem.kind === "f64" ? "f64" : elem.kind === "bool" ? "bool" : "ref";
|
||||
}
|
||||
|
||||
/** Runtime suffix for a map's KEY kind (f64 with SameValueZero, or string
|
||||
* content) — the first suffix of the scr_map_* two-suffix family. */
|
||||
export function mapKeyAccess(key: IrType): "f64" | "str" | "ref" {
|
||||
if (key.kind === "f64") return "f64";
|
||||
if (key.kind === "string") return "str";
|
||||
// Handle-kind SET elements (identity hashing — isSupportedSetElem);
|
||||
// Map keys proper stay f64/string.
|
||||
if (key.kind === "netServer" || key.kind === "symbol") return "ref";
|
||||
throw new Error(`emitter bug: map key of ${key.kind} (frontend rejects these)`);
|
||||
}
|
||||
|
||||
/** The runtime's key-kind/value-kind tags for scr_map_new. */
|
||||
export function mapKeyKindC(key: IrType): string {
|
||||
const acc = mapKeyAccess(key);
|
||||
return acc === "str" ? "SCR_MAP_KEY_STR" : acc === "ref" ? "SCR_MAP_KEY_REF" : "SCR_MAP_KEY_F64";
|
||||
}
|
||||
|
||||
export function mapValKindC(value: IrType): string {
|
||||
return value.kind === "f64"
|
||||
? "SCR_MAP_VAL_F64"
|
||||
: value.kind === "bool"
|
||||
? "SCR_MAP_VAL_BOOL"
|
||||
: "SCR_MAP_VAL_REF";
|
||||
}
|
||||
|
||||
/** UTF-8 bytes as an unambiguous C string literal (octal escapes are always
|
||||
* three digits, so a following digit can never extend them — unlike \xHH). */
|
||||
export function cStringLiteral(bytes: Buffer): string {
|
||||
let out = '"';
|
||||
for (const b of bytes) {
|
||||
if (b === 0x22) out += '\\"';
|
||||
else if (b === 0x5c) out += "\\\\";
|
||||
// '?' escapes to defuse TRIGRAPHS: under -std=c11 the preprocessor
|
||||
// rewrites `??=` (and the other eight `??x` sequences) INSIDE string
|
||||
// literals — an embedded JS `wasmBinaryFile ??= f()` would reach the
|
||||
// engine as `wasmBinaryFile #` and die as a SyntaxError. `\?` is
|
||||
// standard C, exactly for this.
|
||||
else if (b === 0x3f) out += "\\?";
|
||||
else if (b >= 0x20 && b < 0x7f) out += String.fromCharCode(b);
|
||||
else out += "\\" + b.toString(8).padStart(3, "0");
|
||||
}
|
||||
return out + '"';
|
||||
}
|
||||
|
||||
/** JS shortest-roundtrip decimal re-parses to the identical double in C
|
||||
* (strtod is correctly rounded), so String(value) is a faithful C literal. */
|
||||
export function cNumberLiteral(value: number): string {
|
||||
// ±Infinity and NaN numLits are real (the globals `Infinity`/`NaN`); C
|
||||
// spells them with math.h's INFINITY/NAN macros (the emitted unit always
|
||||
// includes math.h).
|
||||
if (value === Infinity) return "INFINITY";
|
||||
if (value === -Infinity) return "-INFINITY";
|
||||
if (Number.isNaN(value)) return "NAN";
|
||||
if (Object.is(value, -0)) return "-0.0"; // String(-0) is "0", which would lose the sign
|
||||
const text = String(value);
|
||||
// Integral shortest-roundtrip text ("118059162071741140000") would be a
|
||||
// C INTEGER literal — invalid beyond unsigned long long's range, and in
|
||||
// [2^63, 2^64) clang's unsigned-interpretation extension would let a
|
||||
// NEGATED literal wrap modulo 2^64, a silent wrong value; the ".0" keeps
|
||||
// every numLit a double literal (decimal parsing is correctly rounded
|
||||
// either way).
|
||||
return /[.eE]/.test(text) ? text : text + ".0";
|
||||
}
|
||||
|
||||
/** `<type> <name>` with pointer types spaced C-style (`ScrStr *x`). */
|
||||
export function cDecl(type: IrType, name: string): string {
|
||||
const t = cType(type);
|
||||
return t.endsWith("*") ? t + name : `${t} ${name}`;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,246 @@
|
||||
/* Cheap whole-module may-throw analysis (see computeMayThrow). Pure function
|
||||
* of the IR module; the emitter consults the result to place unwind checks. */
|
||||
import type { IrBytesIntrinsicMethod, IrLibFn, IrModule } from "../../ir/nodes.js";
|
||||
import { MAY_THROW_BYTES_METHODS, MAY_THROW_LIB_FNS } from "../../ir/nodes.js";
|
||||
|
||||
/** Cheap may-throw analysis (cost discipline: functions that transitively
|
||||
* CANNOT throw pay for no pending-exception checks). A function may throw
|
||||
* iff it contains a `throw`, calls a may-throw function (direct `call` or
|
||||
* `new`-invoked constructor), or performs a `callValue` while ANY function
|
||||
* a closure is ever made over may throw (the callee of an indirect call is
|
||||
* unknown, but it must be a closure target of this whole-program module).
|
||||
* Fixpoint over the call graph; runtime traps (array OOB etc.) are aborts,
|
||||
* not exceptions, so intrinsics never contribute — EXCEPT throwing library
|
||||
* calls: a `libCall` in MAY_THROW_LIB_FNS (the fs.* surface) raises a
|
||||
* catchable error on failure and seeds the fixpoint like a `throw`. */
|
||||
export function computeMayThrow(mod: IrModule): { fns: Set<string>; indirect: boolean } {
|
||||
interface Facts {
|
||||
throws: boolean;
|
||||
callees: string[];
|
||||
callsValue: boolean;
|
||||
}
|
||||
const facts = new Map<string, Facts>();
|
||||
const closureTargets = new Set<string>();
|
||||
// FUNC-targeted dynChecks synthesize adapter closures outside the IR's
|
||||
// closure table; any callValue may then reach a throwing body.
|
||||
let sawDynFuncAdapter = false;
|
||||
const asyncFns = new Set(mod.functions.filter((fn) => fn.async).map((fn) => fn.name));
|
||||
// Generator functions follow the async exclusion: CALLING one only
|
||||
// allocates the suspended fiber (nothing runs, nothing can throw) — a
|
||||
// body throw surfaces at genResume, which seeds unconditionally below.
|
||||
const genFns = new Set(mod.functions.filter((fn) => fn.generator).map((fn) => fn.name));
|
||||
// Method name → every class's implementation of it (virtualCall callees).
|
||||
const methodImpls = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
for (const m of cls.methods ?? []) {
|
||||
let list = methodImpls.get(m);
|
||||
if (!list) methodImpls.set(m, (list = []));
|
||||
list.push(`%${cls.name}.${m}`);
|
||||
}
|
||||
}
|
||||
for (const fn of mod.functions) {
|
||||
const f: Facts = { throws: false, callees: [], callsValue: false };
|
||||
// TDZ locals (forward-captured consts): every read tests the box and
|
||||
// throws the catchable ReferenceError while it is empty.
|
||||
const tdzIds = new Set(fn.locals.filter((l) => l.tdz).map((l) => l.id));
|
||||
// The IR is plain JSON: a generic walk keyed on `kind` stays correct as
|
||||
// nodes grow fields (types' own `kind`s never collide with these).
|
||||
const visit = (node: unknown): void => {
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) visit(item);
|
||||
return;
|
||||
}
|
||||
if (node === null || typeof node !== "object") return;
|
||||
const rec = node as Record<string, unknown>;
|
||||
switch (rec["kind"]) {
|
||||
case "throw":
|
||||
case "rethrow":
|
||||
// The deferred JS compile fence throws catchably when executed.
|
||||
case "runtimeFence":
|
||||
f.throws = true;
|
||||
break;
|
||||
case "varRef":
|
||||
if (tdzIds.size > 0 && tdzIds.has(rec["localId"] as string)) f.throws = true;
|
||||
break;
|
||||
case "dynCheck":
|
||||
case "caughtCheck":
|
||||
// A checked cast throws (catchably) on validation failure —
|
||||
// seeds the fixpoint exactly like a `throw` statement. A
|
||||
// FUNC-targeted dynCheck can also mint an ADAPTER closure whose
|
||||
// body throws (argument/result validation inside a later
|
||||
// callValue) — those adapters are emitter-synthesized, invisible
|
||||
// to closureTargets, so they force the indirect answer below.
|
||||
f.throws = true;
|
||||
if ((rec["type"] as { kind?: string } | undefined)?.kind === "func") {
|
||||
sawDynFuncAdapter = true;
|
||||
}
|
||||
break;
|
||||
case "fieldIncDec":
|
||||
// A checked-dynamic field's ++/-- validates the number out of
|
||||
// the box — that dynCheck throws catchably on non-numbers.
|
||||
if (rec["fieldDyn"] === true) f.throws = true;
|
||||
break;
|
||||
case "dynCall":
|
||||
// Prototype dispatch throws the same family (not-a-function,
|
||||
// cannot-read, the loud unimplemented fence, callback throws).
|
||||
case "dynInvoke":
|
||||
// Calling a dyn value throws catchably: the not-a-function
|
||||
// TypeError, the boxed thunk's per-argument checks, and
|
||||
// whatever the boxed closure itself throws all surface here.
|
||||
f.throws = true;
|
||||
break;
|
||||
case "dynKeyGet":
|
||||
// The keyed read throws JS's TypeError on an undefined/null
|
||||
// receiver (the `?.` form answers undefined instead), and
|
||||
// HANDLE receivers can throw the loud unmodeled-property
|
||||
// ladder on either form — seed both.
|
||||
f.throws = true;
|
||||
break;
|
||||
case "dynDestrCheck":
|
||||
case "dynIterN":
|
||||
// Destructuring guards throw V8's TypeErrors on nullish /
|
||||
// non-iterable sources.
|
||||
f.throws = true;
|
||||
break;
|
||||
case "awaitExpr":
|
||||
case "awaitUnionExpr":
|
||||
// Awaiting a rejected promise re-throws into the awaiter.
|
||||
f.throws = true;
|
||||
break;
|
||||
case "yieldExpr":
|
||||
// A consumer .throw() surfaces at the yield (and .return()'s
|
||||
// GENRET sentinel unwinds from here too).
|
||||
f.throws = true;
|
||||
break;
|
||||
case "genResume":
|
||||
// A body exception (or the injected .throw payload on a
|
||||
// non-suspended generator) propagates into the resumer; the
|
||||
// reentrancy TypeError throws here too.
|
||||
f.throws = true;
|
||||
break;
|
||||
case "recordKeySet": {
|
||||
// A dynamic-keyed write can collide with a DECLARED field, where
|
||||
// a dyn value validates against the field's type (dynCheck) —
|
||||
// that path throws the catchable TypeError. Declared-field-free
|
||||
// shapes and typed value slots never do.
|
||||
const shape = (mod.records ?? []).find((r) => r.id === rec["shapeId"]);
|
||||
if (
|
||||
rec["overflowOnly"] !== true &&
|
||||
shape && shape.indexValue?.kind === "dyn" && shape.fields.length > 0
|
||||
) {
|
||||
f.throws = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "jsOp":
|
||||
case "jsExit":
|
||||
case "jsMarshal":
|
||||
case "jsBridgePromise":
|
||||
// Island operations bridge engine exceptions into the cell
|
||||
// (jsMarshal only on engine-level surprise, but the emitted
|
||||
// pending check exists — seed conservatively).
|
||||
f.throws = true;
|
||||
break;
|
||||
case "libCall":
|
||||
// The may-throw seed hook: throwing library calls (fs.*,
|
||||
// json.parse) count exactly like a `throw` statement.
|
||||
if (MAY_THROW_LIB_FNS.has(rec["fn"] as IrLibFn)) f.throws = true;
|
||||
break;
|
||||
case "bytesNew": {
|
||||
// The size form (`new Uint8Array(n)`) throws Node's "Invalid
|
||||
// typed array length" RangeError on a bad length; copy/array
|
||||
// sources never throw.
|
||||
const source = rec["source"] as { type?: { kind?: string } } | null;
|
||||
if (source && source.type?.kind === "f64") f.throws = true;
|
||||
break;
|
||||
}
|
||||
case "bytesIntrinsic":
|
||||
// setFrom and the numeric read/write families throw catchable
|
||||
// RangeErrors (Node's bounds discipline); the rest trap or
|
||||
// cannot fail.
|
||||
if (MAY_THROW_BYTES_METHODS.has(rec["method"] as IrBytesIntrinsicMethod)) {
|
||||
f.throws = true;
|
||||
}
|
||||
break;
|
||||
case "regexIntrinsic":
|
||||
// replaceAll and matchAll without /g throw Node's TypeError;
|
||||
// split throws on a pattern with capture groups — all catchable.
|
||||
if (rec["method"] === "replaceAll" || rec["method"] === "split" || rec["method"] === "matchAll" || rec["method"] === "matchAllInto") {
|
||||
f.throws = true;
|
||||
}
|
||||
break;
|
||||
case "call": {
|
||||
// Calling an ASYNC function never unwinds the caller: a body
|
||||
// throw becomes a promise rejection (visible only at await).
|
||||
const callee = rec["callee"] as string;
|
||||
if (!asyncFns.has(callee) && !genFns.has(callee)) f.callees.push(callee);
|
||||
break;
|
||||
}
|
||||
case "new":
|
||||
f.callees.push(`%${rec["className"] as string}.constructor`);
|
||||
break;
|
||||
case "newValue": {
|
||||
// Construction through a class VALUE reaches the static class's
|
||||
// constructor or any strict descendant's — a sound (slightly
|
||||
// wide) cover is every constructor of the named class's
|
||||
// hierarchy; classval flows never leave it.
|
||||
const cls = ((rec["callee"] as { type?: { className?: string } }).type)?.className;
|
||||
if (cls !== undefined) {
|
||||
const descends = (name: string): boolean => {
|
||||
for (let c = (mod.classes ?? []).find((k) => k.name === name); c; c = (mod.classes ?? []).find((k) => k.name === c!.base)) {
|
||||
if (c.name === cls) return true;
|
||||
if (c.base === undefined) break;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const k of mod.classes ?? []) {
|
||||
if (descends(k.name)) f.callees.push(`%${k.name}.constructor`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "virtualCall": {
|
||||
// The concrete callee is any implementation of this method name
|
||||
// on a class — a sound (slightly wide, cross-hierarchy) cover of
|
||||
// the override set the dispatch can actually reach.
|
||||
const impls = methodImpls.get(rec["method"] as string);
|
||||
if (impls) f.callees.push(...impls);
|
||||
break;
|
||||
}
|
||||
case "callValue":
|
||||
f.callsValue = true;
|
||||
break;
|
||||
case "closure":
|
||||
closureTargets.add(rec["fnName"] as string);
|
||||
break;
|
||||
}
|
||||
for (const key of Object.keys(rec)) {
|
||||
if (key !== "loc" && key !== "type") visit(rec[key]);
|
||||
}
|
||||
};
|
||||
visit(fn.body);
|
||||
facts.set(fn.name, f);
|
||||
}
|
||||
|
||||
const may = new Set<string>();
|
||||
for (const [name, f] of facts) if (f.throws) may.add(name);
|
||||
let indirect =
|
||||
sawDynFuncAdapter ||
|
||||
[...closureTargets].some((t) => may.has(t) && !asyncFns.has(t) && !genFns.has(t));
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const [name, f] of facts) {
|
||||
if (may.has(name)) continue;
|
||||
if ((f.callsValue && indirect) || f.callees.some((c) => may.has(c))) {
|
||||
may.add(name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!indirect && [...closureTargets].some((t) => may.has(t) && !asyncFns.has(t) && !genFns.has(t))) {
|
||||
indirect = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return { fns: may, indirect };
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/* The one structure the C emitter never needed: a basic-block builder
|
||||
* lowering the structured statement tree to labeled blocks with explicit
|
||||
* terminators. clang -O0 style — every local is an alloca, every read a
|
||||
* load — so no SSA construction is ever needed; LLVM's mem2reg promotes
|
||||
* the slots at -O2. Ported from the survey prototype (ll-emit.mjs).
|
||||
*
|
||||
* Terminator discipline: text after a terminator is unreachable — the C
|
||||
* emitter relies on the same property when a body ends in return (dead C
|
||||
* code after it); here the lines are DROPPED instead, and emitStmts skips
|
||||
* whole statements once the current block is terminated, so no dropped
|
||||
* definition can leak into a later block. */
|
||||
|
||||
export class BlockBuilder {
|
||||
private readonly blocks: { label: string; lines: string[]; term: string | null }[] = [];
|
||||
private cur: { label: string; lines: string[]; term: string | null };
|
||||
/** Lines spliced into the head of the entry block at render time —
|
||||
* allocas (locals, result slots, the log-arg array) all live there so
|
||||
* every block is dominated by them. */
|
||||
readonly entryAllocas: string[] = [];
|
||||
private tempCounter = 0;
|
||||
private labelCounter = 0;
|
||||
|
||||
constructor() {
|
||||
this.cur = { label: "entry", lines: [], term: null };
|
||||
this.blocks.push(this.cur);
|
||||
}
|
||||
|
||||
newLabel(hint: string): string {
|
||||
return `${hint}${this.labelCounter++}`;
|
||||
}
|
||||
|
||||
startBlock(label: string): void {
|
||||
this.cur = { label, lines: [], term: null };
|
||||
this.blocks.push(this.cur);
|
||||
}
|
||||
|
||||
line(s: string): void {
|
||||
if (this.cur.term === null) this.cur.lines.push(` ${s}`);
|
||||
}
|
||||
|
||||
tmp(): string {
|
||||
return `%t${this.tempCounter++}`;
|
||||
}
|
||||
|
||||
slot(): string {
|
||||
return `%s${this.tempCounter++}`;
|
||||
}
|
||||
|
||||
isTerminated(): boolean {
|
||||
return this.cur.term !== null;
|
||||
}
|
||||
|
||||
terminate(s: string): void {
|
||||
if (this.cur.term === null) this.cur.term = ` ${s}`;
|
||||
}
|
||||
|
||||
br(label: string): void {
|
||||
this.terminate(`br label %${label}`);
|
||||
}
|
||||
|
||||
condBr(v: string, t: string, f: string): void {
|
||||
this.terminate(`br i1 ${v}, label %${t}, label %${f}`);
|
||||
}
|
||||
|
||||
render(): string {
|
||||
return this.blocks
|
||||
.map((b, i) => {
|
||||
const lines = i === 0 ? [...this.entryAllocas.map((l) => ` ${l}`), ...b.lines] : b.lines;
|
||||
// A block left unterminated is a structurally unreachable join
|
||||
// (every predecessor jumped elsewhere); the verifier still wants a
|
||||
// terminator.
|
||||
const term = b.term ?? " unreachable";
|
||||
return `${b.label}:\n${lines.join("\n")}${lines.length ? "\n" : ""}${term}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
/* The LLVM backend's class machinery — the .ll mirror of the C emitter's
|
||||
* class slice (emit-shapes.ts): the class graph (base/children links, the
|
||||
* whole-program preorder numbering behind O(1) instanceof, hierarchy
|
||||
* membership, per-hierarchy virtual slot lists), per-class struct types and
|
||||
* RC/trace helper families, the hierarchy vtable instances, and the class
|
||||
* objects (classes as first-class values) with their construct thunks.
|
||||
*
|
||||
* Two deliberate simplifications over the C emission, both possible because
|
||||
* every pointer is `ptr` in LLVM:
|
||||
* - NO vtable slot adapters: the C backend needs sc_vm_* thunks because a
|
||||
* slot's function-pointer TYPE spells the declaring class's `this`; here
|
||||
* every implementation already has the slot's exact LLVM signature
|
||||
* (override exactness fixes the ABI), so the vtable stores the method
|
||||
* function directly.
|
||||
* - ONE struct spelling per class (%sc_o_*); the runtime error classes GEP
|
||||
* through the emitter-declared %ScrError instead (their structs live in
|
||||
* the runtime).
|
||||
*
|
||||
* Emitter- and stream-rooted classes stay out of the tier (their prefixes
|
||||
* embed runtime registry/state slots and their surfaces are async-shaped);
|
||||
* the emitter refuses them by name before anything here runs. */
|
||||
import type { IrClassDef, IrFunction, IrModule, IrType } from "../../ir/nodes.js";
|
||||
import { isRefCounted, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES } from "../../ir/nodes.js";
|
||||
import {
|
||||
mangleClassGcFree,
|
||||
mangleClassNew,
|
||||
mangleClassObj,
|
||||
mangleClassRelease,
|
||||
mangleClassReleaseDirect,
|
||||
mangleClassRetain,
|
||||
mangleClassStruct,
|
||||
mangleClassTrace,
|
||||
mangleCtorThunk,
|
||||
mangleFunction,
|
||||
mangleVtInstance,
|
||||
mangleVtStruct,
|
||||
} from "../mangle.js";
|
||||
import { FN_ATTRS, llFieldType, releaseSym, traceAdapter, type ShapeHost } from "./shapes.js";
|
||||
|
||||
/** One virtual method slot of a hierarchy: the ROOT-MOST declaring class
|
||||
* owns the slot; its declaration's IrFunction fixes the slot's ABI (the
|
||||
* frontend's override exactness makes every implementation identical). */
|
||||
export interface LlVtSlot {
|
||||
method: string;
|
||||
declarer: LlClassMeta;
|
||||
fn: IrFunction;
|
||||
}
|
||||
|
||||
/** Per-class node of the class graph — the CEmitter ClassMeta shape. */
|
||||
export interface LlClassMeta {
|
||||
def: IrClassDef;
|
||||
base: LlClassMeta | null;
|
||||
children: LlClassMeta[];
|
||||
root: LlClassMeta;
|
||||
pre: number;
|
||||
post: number;
|
||||
hierarchy: boolean;
|
||||
/** Root classes: the hierarchy's slots in DFS-declaration order. */
|
||||
slots: LlVtSlot[];
|
||||
}
|
||||
|
||||
/** The class graph: link base/children, number the forest in preorder
|
||||
* (roots and children in module class order — the SAME numbering the C
|
||||
* backend computes, so instanceof agrees between backends and with the
|
||||
* runtime's stamped error vtables), and compute each hierarchy's virtual
|
||||
* slots — a class's method gets a slot iff no ancestor declares it AND some
|
||||
* strict descendant redeclares it (whole-program devirtualization).
|
||||
* Ported from the CEmitter constructor. */
|
||||
export function buildClassGraph(mod: IrModule, fnByName: Map<string, IrFunction>): Map<string, LlClassMeta> {
|
||||
const metaMap = new Map<string, LlClassMeta>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
metaMap.set(cls.name, {
|
||||
def: cls,
|
||||
base: null,
|
||||
children: [],
|
||||
root: undefined as unknown as LlClassMeta,
|
||||
pre: 0,
|
||||
post: 0,
|
||||
hierarchy: false,
|
||||
slots: [],
|
||||
});
|
||||
}
|
||||
for (const meta of metaMap.values()) {
|
||||
if (meta.def.base === undefined) continue;
|
||||
const base = metaMap.get(meta.def.base);
|
||||
if (!base) throw new Error(`llvm emitter bug: undeclared base class ${meta.def.base}`);
|
||||
meta.base = base;
|
||||
base.children.push(meta);
|
||||
}
|
||||
let preCounter = 0;
|
||||
const number = (meta: LlClassMeta, root: LlClassMeta): void => {
|
||||
meta.root = root;
|
||||
meta.pre = preCounter++;
|
||||
for (const c of meta.children) number(c, root);
|
||||
meta.post = preCounter - 1;
|
||||
};
|
||||
for (const meta of metaMap.values()) {
|
||||
if (meta.base === null) number(meta, meta);
|
||||
meta.hierarchy = meta.base !== null || meta.children.length > 0;
|
||||
}
|
||||
const declares = (m: LlClassMeta, method: string): boolean => m.def.methods?.includes(method) ?? false;
|
||||
const declaredBelow = (m: LlClassMeta, method: string): boolean =>
|
||||
m.children.some((c) => declares(c, method) || declaredBelow(c, method));
|
||||
const collectSlots = (m: LlClassMeta, root: LlClassMeta): void => {
|
||||
for (const method of m.def.methods ?? []) {
|
||||
let inherited = false;
|
||||
for (let a = m.base; a; a = a.base) inherited ||= declares(a, method);
|
||||
if (!inherited && declaredBelow(m, method)) {
|
||||
let fn = fnByName.get(`%${m.def.name}.${method}`);
|
||||
if (!fn && m.def.abstractMethods?.includes(method)) {
|
||||
// Abstract declarer: the slot ABI comes from any concrete
|
||||
// descendant implementation; none anywhere means the slot can
|
||||
// never dispatch (only abstract classes declare it) — skip.
|
||||
const findImpl = (c: LlClassMeta): IrFunction | undefined => {
|
||||
for (const child of c.children) {
|
||||
const f = declares(child, method) && !child.def.abstractMethods?.includes(method)
|
||||
? fnByName.get(`%${child.def.name}.${method}`)
|
||||
: undefined;
|
||||
const found = f ?? findImpl(child);
|
||||
if (found) return found;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
fn = findImpl(m);
|
||||
if (!fn) continue;
|
||||
}
|
||||
if (!fn) throw new Error(`llvm emitter bug: missing method function %${m.def.name}.${method}`);
|
||||
root.slots.push({ method, declarer: m, fn });
|
||||
}
|
||||
}
|
||||
for (const c of m.children) collectSlots(c, root);
|
||||
};
|
||||
for (const meta of metaMap.values()) {
|
||||
if (meta.base === null && meta.hierarchy) collectSlots(meta, meta);
|
||||
}
|
||||
return metaMap;
|
||||
}
|
||||
|
||||
/** The root's slot list as seen by one class: the implementation the class
|
||||
* dispatches to, or null outside the slot's declaring subtree / on a
|
||||
* fully-abstract chain (vtEntriesFor, ported). */
|
||||
export function vtEntriesFor(meta: LlClassMeta): { slot: LlVtSlot; impl: LlClassMeta | null }[] {
|
||||
return meta.root.slots.map((slot) => {
|
||||
if (!(slot.declarer.pre <= meta.pre && meta.pre <= slot.declarer.post)) {
|
||||
return { slot, impl: null };
|
||||
}
|
||||
for (let c: LlClassMeta | null = meta; c; c = c.base) {
|
||||
if (c.def.methods?.includes(slot.method) && !c.def.abstractMethods?.includes(slot.method)) {
|
||||
return { slot, impl: c };
|
||||
}
|
||||
}
|
||||
if (meta.def.abstract === true) return { slot, impl: null };
|
||||
throw new Error(`llvm emitter bug: no implementation of ${slot.method} for ${meta.def.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
/** The LLVM struct type name a class's instances GEP through: the runtime
|
||||
* error classes share the runtime's ScrError layout (emitted subclasses
|
||||
* embed it as their prefix); every emitted class has its own %sc_o_*. */
|
||||
export function classStructSym(className: string): string {
|
||||
if (RUNTIME_ERROR_CLASSES.has(className)) return "ScrError";
|
||||
if (className === RUNTIME_EMITTER_CLASS) return "ScrEmitter";
|
||||
if (RUNTIME_STREAM_CLASSES.has(className)) return "ScrStream";
|
||||
return mangleClassStruct(className);
|
||||
}
|
||||
|
||||
/** True when the class descends from the runtime emitter class: its
|
||||
* struct embeds ScrEmitter's remaining prefix (the registry pointer and
|
||||
* the display-name slot) after the vtable word, so an upcast to
|
||||
* ScrEmitter* is the usual pointer reinterpret (emit-shapes.ts's rule). */
|
||||
export function emitterRooted(meta: LlClassMeta): boolean {
|
||||
return meta.root.def.name === RUNTIME_EMITTER_CLASS;
|
||||
}
|
||||
|
||||
/** True when the class descends from a runtime STREAM class: its struct
|
||||
* embeds the FULL ScrStream prefix (registry, display name, state
|
||||
* pointer) and its RC/trace helpers delegate the state block to
|
||||
* scr_stream_st_* (emit-shapes.ts's streamRooted). */
|
||||
export function streamRooted(meta: LlClassMeta): boolean {
|
||||
for (let m = meta.base; m; m = m.base) {
|
||||
if (RUNTIME_STREAM_CLASSES.has(m.def.name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The GEP index where a class's own fields start: rc at 0, the vtable
|
||||
* word at 1 on hierarchy members, then the emitter prefix (registry +
|
||||
* display name) on emitter-rooted classes, then the stream-state slot on
|
||||
* stream-rooted ones. */
|
||||
function fieldBase(meta: LlClassMeta): number {
|
||||
if (!meta.hierarchy) return 1;
|
||||
if (streamRooted(meta)) return 5;
|
||||
return emitterRooted(meta) ? 4 : 2;
|
||||
}
|
||||
|
||||
/** A field's GEP index inside its class struct: rc at 0, the vtable word
|
||||
* at 1 on hierarchy members, then the flattened field list. */
|
||||
export function classFieldIndex(meta: LlClassMeta, field: string): { index: number; type: IrType } {
|
||||
const idx = meta.def.fields.findIndex((f) => f.name === field);
|
||||
if (idx < 0) throw new Error(`llvm emitter bug: unknown field ${field} on class ${meta.def.name}`);
|
||||
return { index: fieldBase(meta) + idx, type: meta.def.fields[idx]!.type };
|
||||
}
|
||||
|
||||
/** What class emission needs beyond ShapeHost: interned unit instances for
|
||||
* undefined-admitting field initialization (fields typed by an
|
||||
* undefined-armed union start as JS's `undefined`, never NULL), and the
|
||||
* interned NUL-terminated constants (emitter subclass display names). */
|
||||
export interface ClassHost extends ShapeHost {
|
||||
unitInstanceRef(unionId: string, tag: number): string;
|
||||
undefinedArmTag(t: IrType): number;
|
||||
cstr(text: string): string;
|
||||
}
|
||||
|
||||
/** The newFn initialization stores for fields whose type ADMITS undefined
|
||||
* (undefFieldInitLineC's LLVM twin): undefined-armed union fields start
|
||||
* at the interned unit instance; jsval fields (an `any` class field under
|
||||
* --dynamic) start at the engine's undefined cell. */
|
||||
function undefFieldInits(host: ClassHost, meta: LlClassMeta): string[] {
|
||||
const out: string[] = [];
|
||||
meta.def.fields.forEach((f, i) => {
|
||||
const { index } = classFieldIndex(meta, f.name);
|
||||
if (f.type.kind === "jsval") {
|
||||
host.declare(`declare ptr @scr_jsval_undefined()`);
|
||||
out.push(
|
||||
` %ufv${i} = call ptr @scr_jsval_undefined()`,
|
||||
` %uf${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr %o, i64 0, i32 ${index}`,
|
||||
` store ptr %ufv${i}, ptr %uf${i} ; ${f.name} starts undefined`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (f.type.kind !== "union") return;
|
||||
const tag = host.undefinedArmTag(f.type);
|
||||
if (tag < 0) return;
|
||||
out.push(
|
||||
` %uf${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr %o, i64 0, i32 ${index}`,
|
||||
` store ptr ${host.unitInstanceRef(f.type.unionId, tag)}, ptr %uf${i} ; ${f.name} starts undefined`,
|
||||
);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Per-class LLVM emission for every non-runtime class: the struct types
|
||||
* (`typeDefs`), the vtable struct types + per-class constant instances,
|
||||
* and the new/retain/release (+releaseDirect for hierarchy members,
|
||||
* +trace/gcFree for cycle-capable shapes) definitions (`defs`). Mirrors
|
||||
* emitStructDefs + emitHierarchyClassHelpers. */
|
||||
export function emitClassShapes(
|
||||
host: ClassHost,
|
||||
mod: IrModule,
|
||||
metaMap: Map<string, LlClassMeta>,
|
||||
): { typeDefs: string[]; defs: string[] } {
|
||||
const typeDefs: string[] = [];
|
||||
const defs: string[] = [];
|
||||
const emitted = (mod.classes ?? []).filter((c) => !c.runtime);
|
||||
if (emitted.length === 0) return { typeDefs, defs };
|
||||
host.declare(`declare void @scr_obj_alloc_note()`);
|
||||
host.declare(`declare void @scr_obj_free_note()`);
|
||||
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
const fieldTys = cls.fields.map((f) => llFieldType(f.type));
|
||||
// Emitter subclasses embed ScrEmitter's remaining prefix (the registry
|
||||
// and display-name slots) between the vtable word and the field list;
|
||||
// stream subclasses add the state pointer — upcasts to ScrEmitter* /
|
||||
// ScrStream* are the usual pointer reinterprets.
|
||||
const prefix = meta.hierarchy
|
||||
? streamRooted(meta) ? ["ptr", "ptr", "ptr", "ptr"]
|
||||
: emitterRooted(meta) ? ["ptr", "ptr", "ptr"]
|
||||
: ["ptr"]
|
||||
: [];
|
||||
const members = [...prefix, ...fieldTys];
|
||||
typeDefs.push(
|
||||
`%${mangleClassStruct(cls.name)} = type { i64${members.length ? ", " + members.join(", ") : ""} } ` +
|
||||
`; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : emitterRooted(meta) ? " (ScrEmitter prefix at 2)" : ""} { ${cls.fields.map((f) => f.name).join("; ")} }`,
|
||||
);
|
||||
}
|
||||
|
||||
// Vtable struct types, one per ROOT of the emitted hierarchy classes (a
|
||||
// runtime root — %Error — counts exactly when an emitted subclass needs
|
||||
// its type): a ScrVt head plus one ptr per slot. All slots are `ptr`, so
|
||||
// the type is layout-only.
|
||||
const roots = [...new Set(emitted.map((c) => metaMap.get(c.name)!).filter((m) => m.hierarchy).map((m) => m.root))];
|
||||
for (const root of roots) {
|
||||
const slotPtrs = root.slots.map(() => "ptr").join(", ");
|
||||
typeDefs.push(
|
||||
`%${mangleVtStruct(root.def.name)} = type { %ScrVt${root.slots.length ? ", " + slotPtrs : ""} } ` +
|
||||
`; vtable: hierarchy rooted at ${root.def.name}${root.slots.length ? ` [${root.slots.map((s) => s.method).join(", ")}]` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
// One constant vtable instance per emitted hierarchy class: interval,
|
||||
// direct release, and the class's dispatch entry for every slot — the
|
||||
// method functions THEMSELVES (no adapters; see the header comment).
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
if (!meta.hierarchy) continue;
|
||||
const entries = vtEntriesFor(meta).map(({ slot, impl }) =>
|
||||
impl === null
|
||||
? `ptr null` // outside the declaring subtree / fully-abstract chain
|
||||
: `ptr @${mangleFunction(`%${impl.def.name}.${slot.method}`)}`,
|
||||
);
|
||||
const head = `%ScrVt { i64 ${meta.pre}, i64 ${meta.post}, ptr @${mangleClassReleaseDirect(cls.name)} }`;
|
||||
defs.push(
|
||||
`@${mangleVtInstance(cls.name)} = internal constant %${mangleVtStruct(meta.root.def.name)} ` +
|
||||
`{ ${[head, ...entries].join(", ")} } ; class ${cls.name}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
const struct = mangleClassStruct(cls.name);
|
||||
const traced = host.tracedShapes.has(`object:${cls.name}`);
|
||||
const isEmitterRooted = emitterRooted(meta);
|
||||
const isStreamRooted = streamRooted(meta);
|
||||
const fieldIndex = (i: number): number => fieldBase(meta) + i;
|
||||
const refFields = cls.fields
|
||||
.map((f, i) => ({ ...f, index: fieldIndex(i) }))
|
||||
.filter((f) => isRefCounted(f.type));
|
||||
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to i64)`;
|
||||
// An embedded prefix slot (the emitter registry at 2, the stream
|
||||
// state at 4) handed to one of the runtime's prefix helpers —
|
||||
// teardown/trace/collector.
|
||||
const prefixCall = (tag: string, slot: number, entry: string, tail: string, what: string): string[] => {
|
||||
host.declare(`declare void @${entry}(ptr${tail ? ", ptr, ptr" : ""})`);
|
||||
return [
|
||||
` %${tag}p = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${slot}`,
|
||||
` %${tag}v = load ptr, ptr %${tag}p`,
|
||||
` call void @${entry}(ptr %${tag}v${tail}) ; ${what}`,
|
||||
];
|
||||
};
|
||||
const regCall = (tag: string, entry: string, tail: string): string[] =>
|
||||
prefixCall(tag, 2, entry, tail, "EventEmitter prefix");
|
||||
const stCall = (tag: string, entry: string, tail: string): string[] =>
|
||||
prefixCall(tag, 4, entry, tail, "stream state (ScrStream prefix)");
|
||||
|
||||
// retain: NULL-tolerant, immortal-skip, mark-live on traced shapes —
|
||||
// layout-generic (rc at 0), so hierarchy members need no dispatch.
|
||||
defs.push(
|
||||
`define internal ptr @${mangleClassRetain(cls.name)}(ptr %o) ${FN_ATTRS} { ; retain ${cls.name}`,
|
||||
`entry:`,
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %done, label %check`,
|
||||
`check:`,
|
||||
` %rc = load i64, ptr %o`,
|
||||
` %imm = icmp eq i64 %rc, -1`,
|
||||
` br i1 %imm, label %done, label %inc`,
|
||||
`inc:`,
|
||||
` %n = add i64 %rc, 1`,
|
||||
` store i64 %n, ptr %o`,
|
||||
...(traced
|
||||
? [` %colorp = getelementptr i8, ptr %o, i64 -16`, ` store i32 0, ptr %colorp ; mark live`]
|
||||
: []),
|
||||
` br label %done`,
|
||||
`done:`,
|
||||
` ret ptr %o`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
|
||||
// The field-releasing teardown body shared by both release shapes
|
||||
// (the public one on standalone classes, the DIRECT one on hierarchy
|
||||
// members). Runs at rc == 0.
|
||||
const teardown = (lines: string[]): void => {
|
||||
if (traced) {
|
||||
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
|
||||
lines.push(` call void @scr_cyc_on_dead(ptr %o)`);
|
||||
}
|
||||
refFields.forEach((f, t) => {
|
||||
lines.push(
|
||||
` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
|
||||
` %v${t} = load ptr, ptr %f${t}`,
|
||||
` call void ${releaseSym(host, f.type)}(ptr %v${t}) ; ${f.name}`,
|
||||
);
|
||||
});
|
||||
if (isEmitterRooted) lines.push(...regCall("td", "scr_emitter_reg_drop", ""));
|
||||
if (isStreamRooted) lines.push(...stCall("tds", "scr_stream_st_release", ""));
|
||||
lines.push(` call void @scr_obj_free_note()`);
|
||||
if (traced) {
|
||||
host.declare(`declare void @scr_cyc_free(ptr)`);
|
||||
lines.push(` call void @scr_cyc_free(ptr %o)`);
|
||||
} else {
|
||||
host.declare(`declare void @free(ptr)`);
|
||||
lines.push(` call void @free(ptr %o)`);
|
||||
}
|
||||
};
|
||||
|
||||
if (meta.hierarchy) {
|
||||
// Public release: NULL/immortal checks, then dispatch through the
|
||||
// object's vtable so a base-typed release tears down the DERIVED
|
||||
// object (scr_error_release's contract exactly).
|
||||
defs.push(
|
||||
`define internal void @${mangleClassRelease(cls.name)}(ptr %o) ${FN_ATTRS} { ; release ${cls.name} (dispatches)`,
|
||||
`entry:`,
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %done, label %check`,
|
||||
`check:`,
|
||||
` %rc = load i64, ptr %o`,
|
||||
` %imm = icmp eq i64 %rc, -1`,
|
||||
` br i1 %imm, label %done, label %disp`,
|
||||
`disp:`,
|
||||
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
` %relp = getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2`,
|
||||
` %rel = load ptr, ptr %relp`,
|
||||
` call void %rel(ptr %o) ; the DYNAMIC class's teardown`,
|
||||
` br label %done`,
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
// Direct release (the vtable entry): the class's own teardown. The
|
||||
// caller already handled NULL/immortal.
|
||||
const reld: string[] = [
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`,
|
||||
`entry:`,
|
||||
` %rc = load i64, ptr %o`,
|
||||
` %n = sub i64 %rc, 1`,
|
||||
` store i64 %n, ptr %o`,
|
||||
` %dead = icmp eq i64 %n, 0`,
|
||||
` br i1 %dead, label %free, label %${traced ? "root" : "done"}`,
|
||||
`free:`,
|
||||
];
|
||||
teardown(reld);
|
||||
reld.push(` br label %done`);
|
||||
if (traced) {
|
||||
host.declare(`declare void @scr_cyc_on_release(ptr)`);
|
||||
reld.push(
|
||||
`root:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; possible cycle root; may collect`,
|
||||
` br label %done`,
|
||||
);
|
||||
}
|
||||
reld.push(`done:`, ` ret void`, `}`, ``);
|
||||
defs.push(...reld);
|
||||
} else {
|
||||
const rel: string[] = [
|
||||
`define internal void @${mangleClassRelease(cls.name)}(ptr %o) ${FN_ATTRS} { ; release ${cls.name}`,
|
||||
`entry:`,
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %done, label %check`,
|
||||
`check:`,
|
||||
` %rc = load i64, ptr %o`,
|
||||
` %imm = icmp eq i64 %rc, -1`,
|
||||
` br i1 %imm, label %done, label %dec`,
|
||||
`dec:`,
|
||||
` %n = sub i64 %rc, 1`,
|
||||
` store i64 %n, ptr %o`,
|
||||
` %dead = icmp eq i64 %n, 0`,
|
||||
` br i1 %dead, label %free, label %${traced ? "root" : "done"}`,
|
||||
`free:`,
|
||||
];
|
||||
teardown(rel);
|
||||
rel.push(` br label %done`);
|
||||
if (traced) {
|
||||
host.declare(`declare void @scr_cyc_on_release(ptr)`);
|
||||
rel.push(
|
||||
`root:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; possible cycle root; may collect`,
|
||||
` br label %done`,
|
||||
);
|
||||
}
|
||||
rel.push(`done:`, ` ret void`, `}`, ``);
|
||||
defs.push(...rel);
|
||||
}
|
||||
|
||||
// new: zeroed allocation, rc = 1, the vtable word on hierarchy
|
||||
// members, undefined-admitting union fields at the interned unit
|
||||
// instance, alloc note. Traced shapes allocate with the collector
|
||||
// header (scr_cyc_alloc zeroes and aborts on OOM itself).
|
||||
const nw: string[] = [
|
||||
`define internal ptr @${mangleClassNew(cls.name)}() ${FN_ATTRS} { ; new ${cls.name}`,
|
||||
`entry:`,
|
||||
];
|
||||
if (traced) {
|
||||
host.declare(`declare ptr @scr_cyc_alloc(i64, ptr, ptr)`);
|
||||
nw.push(
|
||||
` %o = call ptr @scr_cyc_alloc(i64 ${sizeOf}, ptr @${mangleClassTrace(cls.name)}, ptr @${mangleClassGcFree(cls.name)})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare ptr @calloc(i64, i64)`);
|
||||
host.needOom();
|
||||
nw.push(
|
||||
` %o = call ptr @calloc(i64 1, i64 ${sizeOf})`,
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %oom, label %ok`,
|
||||
`oom:`,
|
||||
` call void @sc_oom()`,
|
||||
` unreachable`,
|
||||
`ok:`,
|
||||
);
|
||||
}
|
||||
nw.push(` store i64 1, ptr %o`);
|
||||
if (meta.hierarchy) {
|
||||
nw.push(
|
||||
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
|
||||
` store ptr @${mangleVtInstance(cls.name)}, ptr %vtp`,
|
||||
);
|
||||
}
|
||||
if (isEmitterRooted) {
|
||||
// The ScrEmitter prefix: registry stays NULL (zeroed allocation);
|
||||
// the display name Node's leak warning prints ([My]) is the source
|
||||
// class name, without the module qualifier (emit-shapes.ts's rule).
|
||||
const displayName = cls.name.includes(".")
|
||||
? cls.name.slice(cls.name.lastIndexOf(".") + 1)
|
||||
: cls.name;
|
||||
nw.push(
|
||||
` %clsp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 3`,
|
||||
` store ptr ${host.cstr(displayName)}, ptr %clsp ; EventEmitter prefix display name`,
|
||||
);
|
||||
}
|
||||
nw.push(...undefFieldInits(host, meta));
|
||||
nw.push(` call void @scr_obj_alloc_note()`, ` ret ptr %o`, `}`, ``);
|
||||
defs.push(...nw);
|
||||
|
||||
if (traced) {
|
||||
// trace: visit exactly the cycle-capable fields; gcFree: release
|
||||
// exactly the complement, then free (the trace/teardown complement
|
||||
// contract in scr_runtime.h).
|
||||
const indexed = cls.fields.map((f, i) => ({ ...f, index: fieldIndex(i) }));
|
||||
const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null);
|
||||
const untracedRefFields = indexed.filter(
|
||||
(f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null,
|
||||
);
|
||||
const tr: string[] = [
|
||||
`define internal void @${mangleClassTrace(cls.name)}(ptr %o, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; trace ${cls.name}`,
|
||||
`entry:`,
|
||||
];
|
||||
if (isEmitterRooted) tr.push(...regCall("tr", "scr_emitter_reg_trace", ", ptr %visit, ptr %ctx"));
|
||||
if (isStreamRooted) tr.push(...stCall("trs", "scr_stream_st_trace", ", ptr %visit, ptr %ctx"));
|
||||
tracedFields.forEach((f, i) => {
|
||||
tr.push(
|
||||
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
|
||||
` %v${i} = load ptr, ptr %f${i}`,
|
||||
` call void %visit(ptr %v${i}, ptr %ctx) ; ${f.name}`,
|
||||
);
|
||||
});
|
||||
tr.push(` ret void`, `}`, ``);
|
||||
defs.push(...tr);
|
||||
|
||||
const gf: string[] = [
|
||||
`define internal void @${mangleClassGcFree(cls.name)}(ptr %o) ${FN_ATTRS} { ; gcFree ${cls.name}`,
|
||||
`entry:`,
|
||||
];
|
||||
if (isEmitterRooted) gf.push(...regCall("gf", "scr_emitter_reg_gcfree", ""));
|
||||
if (isStreamRooted) gf.push(...stCall("gfs", "scr_stream_st_gcfree", ""));
|
||||
untracedRefFields.forEach((f, i) => {
|
||||
gf.push(
|
||||
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
|
||||
` %v${i} = load ptr, ptr %f${i}`,
|
||||
` call void ${releaseSym(host, f.type)}(ptr %v${i}) ; ${f.name} (acyclic)`,
|
||||
);
|
||||
});
|
||||
host.declare(`declare void @scr_cyc_free(ptr)`);
|
||||
gf.push(` call void @scr_obj_free_note()`, ` call void @scr_cyc_free(ptr %o)`, ` ret void`, `}`, ``);
|
||||
defs.push(...gf);
|
||||
}
|
||||
}
|
||||
return { typeDefs, defs };
|
||||
}
|
||||
|
||||
/** Class objects (classes as first-class values): the immortal ScrClassObj
|
||||
* statics plus their construct thunks — allocate, run the constructor over
|
||||
* a +1 `this`, hand the remaining +1 out (the C emitCtorThunkDefs, minus
|
||||
* the pending check: in-tier constructors are throw-free by construction).
|
||||
* `classObjs` maps className → { nameSym } registered during body emission
|
||||
* (the literal interned while the table was open). */
|
||||
export function emitClassObjDefs(
|
||||
host: ClassHost,
|
||||
metaMap: Map<string, LlClassMeta>,
|
||||
classObjs: Map<string, { nameSym: string }>,
|
||||
fnByName: Map<string, IrFunction>,
|
||||
llType: (t: IrType) => string,
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
for (const [className, { nameSym }] of classObjs) {
|
||||
const meta = metaMap.get(className);
|
||||
if (!meta) throw new Error(`llvm emitter bug: class object for unknown class ${className}`);
|
||||
// A generic instantiation's class object carries its FAMILY's interval
|
||||
// (JS has ONE `Box` at runtime); construction still dispatches the
|
||||
// instantiation's own thunk.
|
||||
const intervalMeta = meta.def.genericOf !== undefined ? metaMap.get(meta.def.genericOf) : meta;
|
||||
if (!intervalMeta) {
|
||||
throw new Error(`llvm emitter bug: class object for ${className} names unknown family ${meta.def.genericOf ?? ""}`);
|
||||
}
|
||||
const ctor = fnByName.get(`%${className}.constructor`);
|
||||
if (!ctor) throw new Error(`llvm emitter bug: class object for ${className} without a constructor`);
|
||||
const params = ctor.params.slice(1);
|
||||
const paramDecls = params.map((p, i) => `${llType(p.type)} %a${i}`).join(", ");
|
||||
const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`);
|
||||
out.push(
|
||||
`define internal ptr @${mangleCtorThunk(className)}(${paramDecls}) ${FN_ATTRS} { ; construct thunk ${className}`,
|
||||
`entry:`,
|
||||
` %o = call ptr @${mangleClassNew(className)}()`,
|
||||
` %r = call ptr @${mangleClassRetain(className)}(ptr %o)`,
|
||||
` call void @${mangleFunction(`%${className}.constructor`)}(${[`ptr %r`, ...ctorArgs].join(", ")})`,
|
||||
` ret ptr %o`,
|
||||
`}`,
|
||||
`@${mangleClassObj(className)} = internal global %ScrClassObj ` +
|
||||
`{ i64 -1, i64 ${intervalMeta.pre}, i64 ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym} } ; class ${className}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,841 @@
|
||||
/* Type-directed RC/trace/box dispatch tables of the LLVM backend, the
|
||||
* cycle-capability fixpoint, and per-record-shape emission — the .ll
|
||||
* mirror of the C emitter's emit-types.ts + emit-shapes.ts slice that the
|
||||
* phase-2 tier needs. Everything here follows the SAME contracts the C
|
||||
* backend compiled into the runtime: `_v` adapters where a container
|
||||
* stores RC entry points as data, per-shape retain/release/new (and
|
||||
* trace/teardown for cycle-capable shapes) with `size_t rc` at offset 0,
|
||||
* scr_obj_alloc_note/scr_obj_free_note bracketing every shape allocation
|
||||
* so the sanitized lane's RC audit stays exact.
|
||||
*
|
||||
* Anything outside the tier refuses loudly (LlvmUnsupportedError naming
|
||||
* the type kind) — the tables never guess. */
|
||||
import type { IrModule, IrRecordShape, IrType } from "../../ir/nodes.js";
|
||||
import { funcOf, isRefCounted, mapOf, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING, VOID } from "../../ir/nodes.js";
|
||||
import {
|
||||
mangleClassRelease,
|
||||
mangleClassRetain,
|
||||
mangleClassTrace,
|
||||
mangleRecordGcFree,
|
||||
mangleRecordNew,
|
||||
mangleRecordRelease,
|
||||
mangleRecordRetain,
|
||||
mangleRecordStruct,
|
||||
mangleRecordTrace,
|
||||
} from "../mangle.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
/** What the tables need from the emitter: the extern-declaration ledger
|
||||
* and the module-wide cycle/shape indexes. */
|
||||
export interface ShapeHost {
|
||||
declare(decl: string): void;
|
||||
/** Request the shared OOM abort helper (@sc_oom) — emitted once. */
|
||||
needOom(): void;
|
||||
readonly tracedShapes: Set<string>;
|
||||
readonly tracedUnions: Set<string>;
|
||||
readonly recordsById: Map<string, IrRecordShape>;
|
||||
}
|
||||
|
||||
/** Every emitted function/helper carries #0 = { sanitize_address } — see
|
||||
* the emitter header. */
|
||||
export const FN_ATTRS = "#0";
|
||||
|
||||
/* ── cycle capability (the CEmitter constructor's fixpoint, ported) ────
|
||||
* Greatest fixpoint over shapes and unions: start optimistic (everything
|
||||
* cycle-capable), repeatedly drop shapes with no cycle-capable field and
|
||||
* unions with no cycle-capable arm until stable. Closures and promises
|
||||
* are always cycle-capable; strings never are; arrays/maps inherit their
|
||||
* element/value type's capability. A HIERARCHY is one unit of capability
|
||||
* (a base-typed slot can hold any subclass and retain touches the cycle
|
||||
* header, so header presence must be uniform across an extends tree): a
|
||||
* unit is cycle-capable iff ANY member is — CEmitter's unit grouping,
|
||||
* ported. */
|
||||
export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set<string> } {
|
||||
const tracedShapes = new Set<string>();
|
||||
const tracedUnions = new Set<string>();
|
||||
const classes = mod.classes ?? [];
|
||||
const shapeDefs = [
|
||||
...classes.map((c) => ({
|
||||
key: `object:${c.name}`,
|
||||
fields: c.name === RUNTIME_EMITTER_CLASS
|
||||
? [...c.fields, { name: "<listeners>", type: funcOf([], VOID) }]
|
||||
: c.fields,
|
||||
})),
|
||||
...(mod.records ?? []).map((r) => ({
|
||||
key: `record:${r.id}`,
|
||||
fields: r.indexValue
|
||||
? [...r.fields, { name: "<overflow>", type: mapOf(STRING, r.indexValue) }]
|
||||
: r.fields,
|
||||
})),
|
||||
];
|
||||
for (const s of shapeDefs) tracedShapes.add(s.key);
|
||||
for (const u of mod.unions ?? []) tracedUnions.add(u.id);
|
||||
// Hierarchy units: root lookup over the base links (classes with a base
|
||||
// or a subclass — and the runtime emitter class — form units under their
|
||||
// root; standalone classes and records stay singleton units).
|
||||
const baseOf = new Map(classes.map((c) => [c.name, c.base ?? null] as const));
|
||||
const hasChildren = new Set(classes.map((c) => c.base).filter((b): b is string => b !== undefined));
|
||||
const rootOf = (name: string): string => {
|
||||
let cur = name;
|
||||
for (let b = baseOf.get(cur); b !== null && b !== undefined; b = baseOf.get(cur)) cur = b;
|
||||
return cur;
|
||||
};
|
||||
const unitKeyOf = (key: string): string => {
|
||||
if (!key.startsWith("object:")) return key;
|
||||
const name = key.slice("object:".length);
|
||||
const inHierarchy =
|
||||
typeof baseOf.get(name) === "string" || hasChildren.has(name) || name === RUNTIME_EMITTER_CLASS;
|
||||
return inHierarchy ? `object:${rootOf(name)}` : key;
|
||||
};
|
||||
const units = new Map<string, typeof shapeDefs>();
|
||||
for (const s of shapeDefs) {
|
||||
const unit = unitKeyOf(s.key);
|
||||
let members = units.get(unit);
|
||||
if (!members) units.set(unit, (members = []));
|
||||
members.push(s);
|
||||
}
|
||||
const cycleCapable = (t: IrType): boolean => {
|
||||
switch (t.kind) {
|
||||
case "func":
|
||||
case "promise":
|
||||
return true;
|
||||
case "object":
|
||||
return tracedShapes.has(`object:${t.className}`);
|
||||
case "record":
|
||||
return tracedShapes.has(`record:${t.shapeId}`);
|
||||
case "union":
|
||||
return tracedUnions.has(t.unionId);
|
||||
case "map":
|
||||
return cycleCapable(t.value);
|
||||
case "array":
|
||||
return cycleCapable(t.elem);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let shrunk = true;
|
||||
while (shrunk) {
|
||||
shrunk = false;
|
||||
for (const members of units.values()) {
|
||||
if (
|
||||
tracedShapes.has(members[0]!.key) &&
|
||||
!members.some((s) => s.fields.some((f) => cycleCapable(f.type)))
|
||||
) {
|
||||
for (const s of members) tracedShapes.delete(s.key);
|
||||
shrunk = true;
|
||||
}
|
||||
}
|
||||
for (const u of mod.unions ?? []) {
|
||||
if (tracedUnions.has(u.id) && !u.arms.some(cycleCapable)) {
|
||||
tracedUnions.delete(u.id);
|
||||
shrunk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { shapes: tracedShapes, unions: tracedUnions };
|
||||
}
|
||||
|
||||
|
||||
/* ── RC dispatch ──────────────────────────────────────────────────────── */
|
||||
|
||||
/** The runtime's `_v` (ptr → ptr / ptr → void) RC entry points for one
|
||||
* refcounted type — used both as the CALL targets of the LLVM tier's
|
||||
* retain/release (everything is `ptr` here, so the `_v` shape IS the
|
||||
* direct shape) and as the function-pointer arguments of every container
|
||||
* construction (unions, ref arrays, obj boxes, overflow maps). Records
|
||||
* use their emitted per-shape helpers, whose signatures are already
|
||||
* `_v`-shaped. */
|
||||
export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } {
|
||||
switch (t.kind) {
|
||||
case "caught":
|
||||
// Catch-binding snapshot boxes (ScrCaught): the runtime pair is
|
||||
// already `_v`-shaped. Caught values never enter containers — these
|
||||
// arms serve retainSym/releaseSym only.
|
||||
host.declare(`declare ptr @scr_caught_retain(ptr)`);
|
||||
host.declare(`declare void @scr_caught_release(ptr)`);
|
||||
return { retain: "@scr_caught_retain", release: "@scr_caught_release" };
|
||||
case "string":
|
||||
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_str_release_v(ptr)`);
|
||||
return { retain: "@scr_str_retain_v", release: "@scr_str_release_v" };
|
||||
case "array":
|
||||
host.declare(`declare ptr @scr_arr_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_arr_release_v(ptr)`);
|
||||
return { retain: "@scr_arr_retain_v", release: "@scr_arr_release_v" };
|
||||
case "map":
|
||||
case "set":
|
||||
host.declare(`declare ptr @scr_map_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_map_release_v(ptr)`);
|
||||
return { retain: "@scr_map_retain_v", release: "@scr_map_release_v" };
|
||||
case "union":
|
||||
host.declare(`declare ptr @scr_union_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_union_release_v(ptr)`);
|
||||
return { retain: "@scr_union_retain_v", release: "@scr_union_release_v" };
|
||||
case "promise":
|
||||
host.declare(`declare ptr @scr_promise_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_promise_release_v(ptr)`);
|
||||
return { retain: "@scr_promise_retain_v", release: "@scr_promise_release_v" };
|
||||
case "bytes":
|
||||
host.declare(`declare ptr @scr_bytes_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_bytes_release_v(ptr)`);
|
||||
return { retain: "@scr_bytes_retain_v", release: "@scr_bytes_release_v" };
|
||||
case "url":
|
||||
host.declare(`declare ptr @scr_url_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_url_release_v(ptr)`);
|
||||
return { retain: "@scr_url_retain_v", release: "@scr_url_release_v" };
|
||||
case "searchParams":
|
||||
host.declare(`declare ptr @scr_sp_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_sp_release_v(ptr)`);
|
||||
return { retain: "@scr_sp_retain_v", release: "@scr_sp_release_v" };
|
||||
case "stats":
|
||||
host.declare(`declare ptr @scr_stats_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_stats_release_v(ptr)`);
|
||||
return { retain: "@scr_stats_retain_v", release: "@scr_stats_release_v" };
|
||||
case "spawnRes":
|
||||
host.declare(`declare ptr @scr_spawn_res_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_spawn_res_release_v(ptr)`);
|
||||
return { retain: "@scr_spawn_res_retain_v", release: "@scr_spawn_res_release_v" };
|
||||
case "child":
|
||||
host.declare(`declare ptr @scr_child_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_child_release_v(ptr)`);
|
||||
return { retain: "@scr_child_retain_v", release: "@scr_child_release_v" };
|
||||
case "childStream":
|
||||
host.declare(`declare ptr @scr_child_stream_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_child_stream_release_v(ptr)`);
|
||||
return { retain: "@scr_child_stream_retain_v", release: "@scr_child_stream_release_v" };
|
||||
case "generator":
|
||||
host.declare(`declare ptr @scr_gen_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_gen_release_v(ptr)`);
|
||||
return { retain: "@scr_gen_retain_v", release: "@scr_gen_release_v" };
|
||||
case "func":
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_closure_release_v(ptr)`);
|
||||
return { retain: "@scr_closure_retain_v", release: "@scr_closure_release_v" };
|
||||
case "symbol":
|
||||
host.declare(`declare ptr @scr_sym_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_sym_release_v(ptr)`);
|
||||
return { retain: "@scr_sym_retain_v", release: "@scr_sym_release_v" };
|
||||
case "regex":
|
||||
host.declare(`declare ptr @scr_regex_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_regex_release_v(ptr)`);
|
||||
return { retain: "@scr_regex_retain_v", release: "@scr_regex_release_v" };
|
||||
case "record":
|
||||
return { retain: `@${mangleRecordRetain(t.shapeId)}`, release: `@${mangleRecordRelease(t.shapeId)}` };
|
||||
case "object":
|
||||
if (RUNTIME_ERROR_CLASSES.has(t.className)) {
|
||||
host.declare(`declare ptr @scr_error_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_error_release_v(ptr)`);
|
||||
return { retain: "@scr_error_retain_v", release: "@scr_error_release_v" };
|
||||
}
|
||||
if (t.className === RUNTIME_EMITTER_CLASS) {
|
||||
// Bare EventEmitter instances: the runtime's `_v` pair (release
|
||||
// dispatches through the stamped vtable, so a base-typed release
|
||||
// tears down a user subclass too).
|
||||
host.declare(`declare ptr @scr_emitter_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_emitter_release_v(ptr)`);
|
||||
return { retain: "@scr_emitter_retain_v", release: "@scr_emitter_release_v" };
|
||||
}
|
||||
if (RUNTIME_STREAM_CLASSES.has(t.className)) {
|
||||
// The five runtime stream classes share ONE runtime layout; the
|
||||
// `_v` pair dispatches teardown through the stamped vtable.
|
||||
host.declare(`declare ptr @scr_stream_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
return { retain: "@scr_stream_retain_v", release: "@scr_stream_release_v" };
|
||||
}
|
||||
// Emitted per-class helpers are already `_v`-shaped (ptr → ptr /
|
||||
// ptr → void), so the same symbols serve as container entry points.
|
||||
return { retain: `@${mangleClassRetain(t.className)}`, release: `@${mangleClassRelease(t.className)}` };
|
||||
case "classval":
|
||||
// No-ops on the immortal class object; container machinery uniform.
|
||||
host.declare(`declare ptr @scr_classobj_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_classobj_release_v(ptr)`);
|
||||
return { retain: "@scr_classobj_retain_v", release: "@scr_classobj_release_v" };
|
||||
case "fsWatcher":
|
||||
// fs.watch handles (ScrWatcher): the runtime's `_v` pair; no trace
|
||||
// (listeners drop at close — never part of a lasting cycle).
|
||||
host.declare(`declare ptr @scr_watcher_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_watcher_release_v(ptr)`);
|
||||
return { retain: "@scr_watcher_retain_v", release: "@scr_watcher_release_v" };
|
||||
case "netServer":
|
||||
host.declare(`declare ptr @scr_net_server_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_net_server_release_v(ptr)`);
|
||||
return { retain: "@scr_net_server_retain_v", release: "@scr_net_server_release_v" };
|
||||
case "netSocket":
|
||||
host.declare(`declare ptr @scr_net_sock_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_net_sock_release_v(ptr)`);
|
||||
return { retain: "@scr_net_sock_retain_v", release: "@scr_net_sock_release_v" };
|
||||
case "dgramSocket":
|
||||
host.declare(`declare ptr @scr_dgram_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_dgram_release_v(ptr)`);
|
||||
return { retain: "@scr_dgram_retain_v", release: "@scr_dgram_release_v" };
|
||||
case "httpReq":
|
||||
host.declare(`declare ptr @scr_http_req_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_http_req_release_v(ptr)`);
|
||||
return { retain: "@scr_http_req_retain_v", release: "@scr_http_req_release_v" };
|
||||
case "httpRes":
|
||||
host.declare(`declare ptr @scr_http_res_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_http_res_release_v(ptr)`);
|
||||
return { retain: "@scr_http_res_retain_v", release: "@scr_http_res_release_v" };
|
||||
case "httpClientReq":
|
||||
host.declare(`declare ptr @scr_http_client_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_http_client_release_v(ptr)`);
|
||||
return { retain: "@scr_http_client_retain_v", release: "@scr_http_client_release_v" };
|
||||
case "secureCtx":
|
||||
host.declare(`declare ptr @scr_secure_ctx_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_secure_ctx_release_v(ptr)`);
|
||||
return { retain: "@scr_secure_ctx_retain_v", release: "@scr_secure_ctx_release_v" };
|
||||
case "testCtx":
|
||||
host.declare(`declare ptr @scr_testctx_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_testctx_release_v(ptr)`);
|
||||
return { retain: "@scr_testctx_retain_v", release: "@scr_testctx_release_v" };
|
||||
case "jsval":
|
||||
// Island handles (the --dynamic engine boundary): the runtime's
|
||||
// `_v` pair; UNTRACED (engine values never join static cycles).
|
||||
host.declare(`declare ptr @scr_jsval_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_jsval_release_v(ptr)`);
|
||||
return { retain: "@scr_jsval_retain_v", release: "@scr_jsval_release_v" };
|
||||
case "dyn":
|
||||
// DOM values (the `unknown` boundary): the runtime's `_v` pair —
|
||||
// scr_dyn_retain is a header inline, so the `_v` symbols serve both
|
||||
// roles. UNTRACED (the dyn→closure stance: cycles through dyn are
|
||||
// never collected, SEMANTICS.md) — traceAdapter answers null.
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
return { retain: "@scr_dyn_retain_v", release: "@scr_dyn_release_v" };
|
||||
default:
|
||||
throw new LlvmUnsupportedError(`rc:${t.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The retain call target (ptr → ptr, +1 unless immortal) — the `_v`
|
||||
* table above; the split exists so call sites read type-directedly. */
|
||||
export function retainSym(host: ShapeHost, t: IrType): string {
|
||||
return vAdapters(host, t).retain;
|
||||
}
|
||||
|
||||
/** The release call target (ptr → void, NULL-tolerant). The runtime's
|
||||
* typed releases are external symbols, so the direct (non-`_v`) entry
|
||||
* points serve where one exists; records use their emitted helper. */
|
||||
export function releaseSym(host: ShapeHost, t: IrType): string {
|
||||
switch (t.kind) {
|
||||
case "caught":
|
||||
host.declare(`declare void @scr_caught_release(ptr)`);
|
||||
return "@scr_caught_release";
|
||||
case "string":
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
return "@scr_str_release";
|
||||
case "array":
|
||||
host.declare(`declare void @scr_arr_release(ptr)`);
|
||||
return "@scr_arr_release";
|
||||
case "map":
|
||||
case "set":
|
||||
host.declare(`declare void @scr_map_release(ptr)`);
|
||||
return "@scr_map_release";
|
||||
case "union":
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
return "@scr_union_release";
|
||||
case "promise":
|
||||
host.declare(`declare void @scr_promise_release(ptr)`);
|
||||
return "@scr_promise_release";
|
||||
case "bytes":
|
||||
host.declare(`declare void @scr_bytes_release(ptr)`);
|
||||
return "@scr_bytes_release";
|
||||
case "url":
|
||||
host.declare(`declare void @scr_url_release_v(ptr)`);
|
||||
return "@scr_url_release_v";
|
||||
case "searchParams":
|
||||
host.declare(`declare void @scr_sp_release_v(ptr)`);
|
||||
return "@scr_sp_release_v";
|
||||
case "stats":
|
||||
host.declare(`declare void @scr_stats_release_v(ptr)`);
|
||||
return "@scr_stats_release_v";
|
||||
case "spawnRes":
|
||||
host.declare(`declare void @scr_spawn_res_release_v(ptr)`);
|
||||
return "@scr_spawn_res_release_v";
|
||||
case "child":
|
||||
host.declare(`declare void @scr_child_release_v(ptr)`);
|
||||
return "@scr_child_release_v";
|
||||
case "childStream":
|
||||
host.declare(`declare void @scr_child_stream_release_v(ptr)`);
|
||||
return "@scr_child_stream_release_v";
|
||||
case "generator":
|
||||
host.declare(`declare void @scr_gen_release(ptr)`);
|
||||
return "@scr_gen_release";
|
||||
case "func":
|
||||
host.declare(`declare void @scr_closure_release(ptr)`);
|
||||
return "@scr_closure_release";
|
||||
case "symbol":
|
||||
host.declare(`declare void @scr_sym_release(ptr)`);
|
||||
return "@scr_sym_release";
|
||||
case "regex":
|
||||
host.declare(`declare void @scr_regex_release(ptr)`);
|
||||
return "@scr_regex_release";
|
||||
case "record":
|
||||
return `@${mangleRecordRelease(t.shapeId)}`;
|
||||
case "object":
|
||||
if (RUNTIME_ERROR_CLASSES.has(t.className)) {
|
||||
host.declare(`declare void @scr_error_release_v(ptr)`);
|
||||
return "@scr_error_release_v";
|
||||
}
|
||||
if (t.className === RUNTIME_EMITTER_CLASS) {
|
||||
host.declare(`declare void @scr_emitter_release_v(ptr)`);
|
||||
return "@scr_emitter_release_v";
|
||||
}
|
||||
if (RUNTIME_STREAM_CLASSES.has(t.className)) {
|
||||
host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
return "@scr_stream_release_v";
|
||||
}
|
||||
return `@${mangleClassRelease(t.className)}`;
|
||||
case "classval":
|
||||
host.declare(`declare void @scr_classobj_release_v(ptr)`);
|
||||
return "@scr_classobj_release_v";
|
||||
case "fsWatcher":
|
||||
host.declare(`declare void @scr_watcher_release_v(ptr)`);
|
||||
return "@scr_watcher_release_v";
|
||||
case "netServer":
|
||||
case "netSocket":
|
||||
case "dgramSocket":
|
||||
case "httpReq":
|
||||
case "httpRes":
|
||||
case "httpClientReq":
|
||||
case "secureCtx":
|
||||
case "testCtx":
|
||||
// The handle kinds share their `_v` pair for both roles.
|
||||
return vAdapters(host, t).release;
|
||||
case "jsval":
|
||||
host.declare(`declare void @scr_jsval_release_v(ptr)`);
|
||||
return "@scr_jsval_release_v";
|
||||
case "dyn":
|
||||
// scr_dyn_release releases the tree recursively; NULL-tolerant.
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
return "@scr_dyn_release";
|
||||
default:
|
||||
throw new LlvmUnsupportedError(`rc:${t.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The trace entry point for a payload/field type, or null when the type
|
||||
* cannot participate in a cycle — traceAdapterC's table over the LLVM
|
||||
* tier's kinds (promise/object rows are out of tier and unreachable:
|
||||
* their RC rows refuse first). */
|
||||
export function traceAdapter(host: ShapeHost, t: IrType): string | null {
|
||||
switch (t.kind) {
|
||||
case "func":
|
||||
host.declare(`declare void @scr_closure_trace_v(ptr, ptr, ptr)`);
|
||||
return "@scr_closure_trace_v";
|
||||
case "promise":
|
||||
// Promises are unconditionally cycle-capable (a rejection payload
|
||||
// is an arbitrary thrown value) — emit-shapes.ts's row.
|
||||
host.declare(`declare void @scr_promise_trace_v(ptr, ptr, ptr)`);
|
||||
return "@scr_promise_trace_v";
|
||||
case "union":
|
||||
if (!host.tracedUnions.has(t.unionId)) return null;
|
||||
host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`);
|
||||
return "@scr_union_trace_v";
|
||||
case "record":
|
||||
return host.tracedShapes.has(`record:${t.shapeId}`) ? `@${mangleRecordTrace(t.shapeId)}` : null;
|
||||
case "object":
|
||||
if (!host.tracedShapes.has(`object:${t.className}`)) return null;
|
||||
if (RUNTIME_ERROR_CLASSES.has(t.className)) {
|
||||
host.declare(`declare void @scr_error_trace(ptr, ptr, ptr)`);
|
||||
return "@scr_error_trace";
|
||||
}
|
||||
if (t.className === RUNTIME_EMITTER_CLASS) {
|
||||
// Unconditionally cycle-capable (the registry owns listener
|
||||
// closures) — the fixpoint's <listeners> field keeps the whole
|
||||
// emitter hierarchy in the traced set.
|
||||
host.declare(`declare void @scr_emitter_trace(ptr, ptr, ptr)`);
|
||||
return "@scr_emitter_trace";
|
||||
}
|
||||
if (RUNTIME_STREAM_CLASSES.has(t.className)) {
|
||||
host.declare(`declare void @scr_stream_trace(ptr, ptr, ptr)`);
|
||||
return "@scr_stream_trace";
|
||||
}
|
||||
return `@${mangleClassTrace(t.className)}`;
|
||||
case "map":
|
||||
if (traceAdapter(host, t.value) === null) return null;
|
||||
host.declare(`declare void @scr_map_trace_v(ptr, ptr, ptr)`);
|
||||
return "@scr_map_trace_v";
|
||||
case "array":
|
||||
if (traceAdapter(host, t.elem) === null) return null;
|
||||
host.declare(`declare void @scr_arr_trace_v(ptr, ptr, ptr)`);
|
||||
return "@scr_arr_trace_v";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** `@trace` or `null` — the trace argument at a container call site. */
|
||||
export function traceArg(host: ShapeHost, t: IrType): string {
|
||||
return traceAdapter(host, t) ?? "null";
|
||||
}
|
||||
|
||||
/* ── arrays ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/** Runtime accessor suffix for an element type (matches emit-types.ts:
|
||||
* f64 and bool unboxed, everything refcounted through the `_ref` family). */
|
||||
export function elemAccess(elem: IrType): "f64" | "bool" | "ref" {
|
||||
return elem.kind === "f64" ? "f64" : elem.kind === "bool" ? "bool" : "ref";
|
||||
}
|
||||
|
||||
/** The ScrElemKind constant for the plain (non-REF) construction path. */
|
||||
function elemKindNum(elem: IrType): number {
|
||||
switch (elem.kind) {
|
||||
case "f64":
|
||||
return 0; // SCR_ELEM_F64
|
||||
case "bool":
|
||||
return 1; // SCR_ELEM_BOOL
|
||||
case "string":
|
||||
return 2; // SCR_ELEM_STR
|
||||
case "array":
|
||||
return 3; // SCR_ELEM_ARR
|
||||
case "bytes":
|
||||
return 4; // SCR_ELEM_BYTES
|
||||
default:
|
||||
throw new LlvmUnsupportedError(`arrayElem:${elem.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Array construction call text (arrNewC's dispatch): ref elements
|
||||
* (records, unions, closures — and cycle-capable inner arrays, whose
|
||||
* SCR_ELEM_ARR spelling would hide them from the outer array's trace)
|
||||
* construct through scr_arr_new_ref with the element type's `_v` RC entry
|
||||
* points; every other element kind keeps the plain scr_arr_new call. */
|
||||
export function arrNewCall(host: ShapeHost, elem: IrType, capText: string): string {
|
||||
const useRef =
|
||||
elem.kind === "record" || elem.kind === "object" || elem.kind === "union" || elem.kind === "func" ||
|
||||
elem.kind === "symbol" || // symbol identities: scr_sym_* adapters, no trace
|
||||
elem.kind === "classval" || // class objects: no-op adapters, no trace (immortal statics)
|
||||
elem.kind === "promise" || // promise entries (Promise.all inputs): full REF story
|
||||
elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace
|
||||
elem.kind === "netServer" || // server handles ([...set] drains): REF, no trace
|
||||
elem.kind === "jsval" || // island handles (`any[]` under --dynamic): REF, no trace
|
||||
(elem.kind === "array" && traceAdapter(host, elem) !== null);
|
||||
if (!useRef) {
|
||||
host.declare(`declare ptr @scr_arr_new(i32, i64)`);
|
||||
return `call ptr @scr_arr_new(i32 ${elemKindNum(elem)}, i64 ${capText})`;
|
||||
}
|
||||
const v = vAdapters(host, elem);
|
||||
host.declare(`declare ptr @scr_arr_new_ref(ptr, ptr, ptr, i64)`);
|
||||
return `call ptr @scr_arr_new_ref(ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host, elem)}, i64 ${capText})`;
|
||||
}
|
||||
|
||||
/* ── capture boxes ────────────────────────────────────────────────────── */
|
||||
|
||||
/** Box construction call text (boxNewC's dispatch): plain-kind boxes for
|
||||
* the runtime-known payloads, obj-kind boxes (RC entry points + trace as
|
||||
* data) for per-shape payloads and cycle-capable arrays. SCR_BOX_* tags
|
||||
* from scr_runtime.h. */
|
||||
export function boxNewCall(host: ShapeHost, t: IrType): string {
|
||||
const plain: Partial<Record<IrType["kind"], number>> = { f64: 0, bool: 1, string: 2, func: 4 };
|
||||
const kind = plain[t.kind];
|
||||
if (kind !== undefined) {
|
||||
host.declare(`declare ptr @scr_box_new(i32)`);
|
||||
return `call ptr @scr_box_new(i32 ${kind})`;
|
||||
}
|
||||
if (t.kind === "array" && traceAdapter(host, t) === null) {
|
||||
host.declare(`declare ptr @scr_box_new(i32)`);
|
||||
return `call ptr @scr_box_new(i32 3)`; // SCR_BOX_ARR
|
||||
}
|
||||
if (
|
||||
t.kind === "record" || t.kind === "object" || t.kind === "classval" || t.kind === "union" ||
|
||||
t.kind === "array" || t.kind === "map" || t.kind === "set" || t.kind === "symbol" || t.kind === "regex" ||
|
||||
t.kind === "promise" || t.kind === "bytes" || t.kind === "url" || t.kind === "searchParams" ||
|
||||
t.kind === "stats" || t.kind === "spawnRes" || t.kind === "child" || t.kind === "childStream" ||
|
||||
t.kind === "generator" ||
|
||||
t.kind === "netServer" || t.kind === "netSocket" || t.kind === "dgramSocket" ||
|
||||
t.kind === "httpReq" || t.kind === "httpRes" || t.kind === "httpClientReq" ||
|
||||
t.kind === "secureCtx" || t.kind === "testCtx" ||
|
||||
// Island handles: the box carries scr_jsval_retain_v/release_v and
|
||||
// no trace — the same stance as jsval array elements.
|
||||
t.kind === "jsval" ||
|
||||
// Checked-dynamic captures (the mustCall wrapper closing over its
|
||||
// implicit-any `fn` param): the box carries scr_dyn_retain_v/release_v
|
||||
// and NO trace — cycles through dyn never collect (SEMANTICS.md).
|
||||
t.kind === "dyn" ||
|
||||
t.kind === "fsWatcher"
|
||||
) {
|
||||
const v = vAdapters(host, t);
|
||||
host.declare(`declare ptr @scr_box_new_obj(ptr, ptr, ptr)`);
|
||||
return `call ptr @scr_box_new_obj(ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host, t)})`;
|
||||
}
|
||||
throw new LlvmUnsupportedError(`box:${t.kind}`);
|
||||
}
|
||||
|
||||
/** Box accessor suffix (boxAccess): scalars unboxed, ref kinds pointers. */
|
||||
export function boxAccess(t: IrType): "f64" | "bool" | "ref" {
|
||||
return t.kind === "f64" ? "f64" : t.kind === "bool" ? "bool" : "ref";
|
||||
}
|
||||
|
||||
/* ── record shapes ────────────────────────────────────────────────────── */
|
||||
|
||||
/** A record field's in-struct LLVM type. bool fields store as i8 (the C
|
||||
* _Bool layout); loads/stores convert at the access site. */
|
||||
export function llFieldType(t: IrType): "double" | "i8" | "ptr" {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
return "double";
|
||||
case "bool":
|
||||
return "i8";
|
||||
case "string":
|
||||
case "array":
|
||||
case "record":
|
||||
case "object":
|
||||
case "classval":
|
||||
case "union":
|
||||
case "func":
|
||||
case "map":
|
||||
case "set":
|
||||
case "symbol":
|
||||
case "regex":
|
||||
case "promise":
|
||||
case "bytes":
|
||||
case "url":
|
||||
case "searchParams":
|
||||
case "stats":
|
||||
case "spawnRes":
|
||||
case "child":
|
||||
case "childStream":
|
||||
case "generator":
|
||||
case "dyn":
|
||||
case "jsval":
|
||||
case "fsWatcher":
|
||||
case "netServer":
|
||||
case "netSocket":
|
||||
case "dgramSocket":
|
||||
case "httpReq":
|
||||
case "httpRes":
|
||||
case "httpClientReq":
|
||||
case "secureCtx":
|
||||
case "testCtx":
|
||||
return "ptr";
|
||||
default:
|
||||
throw new LlvmUnsupportedError(`type:${t.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── maps and sets ────────────────────────────────────────────────────── */
|
||||
|
||||
/** Runtime suffix for a map's KEY kind (mapKeyAccess's table): f64 with
|
||||
* SameValueZero, string content, or handle-identity REF (symbols). */
|
||||
export function mapKeyAccess(key: IrType): "f64" | "str" | "ref" {
|
||||
if (key.kind === "f64") return "f64";
|
||||
if (key.kind === "string") return "str";
|
||||
if (key.kind === "symbol") return "ref";
|
||||
if (key.kind === "netServer") return "ref"; // handle identity (Set<Server>)
|
||||
throw new LlvmUnsupportedError(`mapKey:${key.kind}`);
|
||||
}
|
||||
|
||||
/** The ScrMapKeyKind / ScrMapValKind constants for scr_map_new. */
|
||||
export function mapKeyKindNum(key: IrType): number {
|
||||
const acc = mapKeyAccess(key);
|
||||
return acc === "f64" ? 0 : acc === "str" ? 1 : 2;
|
||||
}
|
||||
|
||||
export function mapValKindNum(value: IrType): number {
|
||||
return value.kind === "f64" ? 0 : value.kind === "bool" ? 1 : 2;
|
||||
}
|
||||
|
||||
/** The RC-relevant members of a shape: every field, plus the overflow map
|
||||
* on index-signature shapes (one more map-typed member). `index` is the
|
||||
* member's field position in the emitted struct type (rc header at 0). */
|
||||
function rcMembers(shape: IrRecordShape): { index: number; type: IrType; name: string }[] {
|
||||
return [
|
||||
...shape.fields.map((f, i) => ({ index: i + 1, type: f.type, name: f.name })),
|
||||
...(shape.indexValue
|
||||
? [{ index: shape.fields.length + 1, type: mapOf(STRING, shape.indexValue), name: "[key: string] overflow" }]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
/** The immortal-skip + mark-live retain body shared by every shape. The
|
||||
* cycle header sits 32 bytes before the object; `color` is at header+16,
|
||||
* so mark-live is one i32 store at obj-16 (scr_cyc_mark_live inlined —
|
||||
* the runtime's is a static inline with no external symbol). */
|
||||
function retainBody(fnName: string, traced: boolean): string[] {
|
||||
return [
|
||||
`define internal ptr @${fnName}(ptr %o) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %done, label %check`,
|
||||
`check:`,
|
||||
` %rc = load i64, ptr %o`,
|
||||
` %imm = icmp eq i64 %rc, -1`,
|
||||
` br i1 %imm, label %done, label %inc`,
|
||||
`inc:`,
|
||||
` %n = add i64 %rc, 1`,
|
||||
` store i64 %n, ptr %o`,
|
||||
...(traced
|
||||
? [` %colorp = getelementptr i8, ptr %o, i64 -16`, ` store i32 0, ptr %colorp ; mark live`]
|
||||
: []),
|
||||
` br label %done`,
|
||||
`done:`,
|
||||
` ret ptr %o`,
|
||||
`}`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Per-record-shape LLVM emission: the named struct types (returned as
|
||||
* `typeDefs`) and the new/retain/release (+trace/gcFree for cycle-capable
|
||||
* shapes) function definitions (`defs`). Layout mirrors the C emitter:
|
||||
* `{ i64 rc, fields..., [ptr overflow] }`; the retain/release signatures
|
||||
* are already `_v`-shaped, so the same symbols serve as container RC
|
||||
* entry points. */
|
||||
export function emitRecordShapes(host: ShapeHost, mod: IrModule): { typeDefs: string[]; defs: string[] } {
|
||||
const typeDefs: string[] = [];
|
||||
const defs: string[] = [];
|
||||
const records = mod.records ?? [];
|
||||
if (records.length === 0) return { typeDefs, defs };
|
||||
host.declare(`declare void @scr_obj_alloc_note()`);
|
||||
host.declare(`declare void @scr_obj_free_note()`);
|
||||
|
||||
for (const shape of records) {
|
||||
const struct = mangleRecordStruct(shape.id);
|
||||
const fieldTys = shape.fields.map((f) => llFieldType(f.type));
|
||||
if (shape.indexValue) fieldTys.push("ptr"); // the overflow ScrMap *
|
||||
typeDefs.push(
|
||||
`%${struct} = type { i64${fieldTys.length ? ", " + fieldTys.join(", ") : ""} } ` +
|
||||
`; record ${shape.id} { ${shape.fields.map((f) => f.name).join("; ")}${shape.indexValue ? "; [key: string]" : ""} }`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const shape of records) {
|
||||
const struct = mangleRecordStruct(shape.id);
|
||||
const traced = host.tracedShapes.has(`record:${shape.id}`);
|
||||
const members = rcMembers(shape);
|
||||
const refMembers = members.filter((m) => isRefCounted(m.type));
|
||||
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to i64)`;
|
||||
|
||||
defs.push(...retainBody(mangleRecordRetain(shape.id), traced), ``);
|
||||
|
||||
// release: NULL-tolerant, immortal-skip; at rc == 0 release every
|
||||
// refcounted member (runtime releases are NULL-tolerant) and free —
|
||||
// traced shapes route through the collector (on_dead/on_release,
|
||||
// scr_cyc_free) exactly like emit-shapes.ts.
|
||||
const rel: string[] = [
|
||||
`define internal void @${mangleRecordRelease(shape.id)}(ptr %o) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %done, label %check`,
|
||||
`check:`,
|
||||
` %rc = load i64, ptr %o`,
|
||||
` %imm = icmp eq i64 %rc, -1`,
|
||||
` br i1 %imm, label %done, label %dec`,
|
||||
`dec:`,
|
||||
` %n = sub i64 %rc, 1`,
|
||||
` store i64 %n, ptr %o`,
|
||||
` %dead = icmp eq i64 %n, 0`,
|
||||
` br i1 %dead, label %free, label %${traced ? "root" : "done"}`,
|
||||
`free:`,
|
||||
];
|
||||
if (traced) {
|
||||
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
|
||||
rel.push(` call void @scr_cyc_on_dead(ptr %o)`);
|
||||
}
|
||||
let t = 0;
|
||||
for (const m of refMembers) {
|
||||
rel.push(
|
||||
` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`,
|
||||
` %v${t} = load ptr, ptr %f${t}`,
|
||||
` call void ${releaseSym(host, m.type)}(ptr %v${t}) ; ${m.name}`,
|
||||
);
|
||||
t++;
|
||||
}
|
||||
rel.push(` call void @scr_obj_free_note()`);
|
||||
if (traced) {
|
||||
host.declare(`declare void @scr_cyc_free(ptr)`);
|
||||
host.declare(`declare void @scr_cyc_on_release(ptr)`);
|
||||
rel.push(
|
||||
` call void @scr_cyc_free(ptr %o)`,
|
||||
` br label %done`,
|
||||
`root:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; possible cycle root; may collect`,
|
||||
` br label %done`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare void @free(ptr)`);
|
||||
rel.push(` call void @free(ptr %o)`, ` br label %done`);
|
||||
}
|
||||
rel.push(`done:`, ` ret void`, `}`, ``);
|
||||
defs.push(...rel);
|
||||
|
||||
// new: zeroed allocation (+ the overflow map on index-signature
|
||||
// shapes), rc = 1, alloc note. Traced shapes allocate with the
|
||||
// collector header (scr_cyc_alloc zeroes and aborts on OOM itself).
|
||||
const nw: string[] = [
|
||||
`define internal ptr @${mangleRecordNew(shape.id)}() ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
];
|
||||
if (traced) {
|
||||
host.declare(`declare ptr @scr_cyc_alloc(i64, ptr, ptr)`);
|
||||
nw.push(
|
||||
` %o = call ptr @scr_cyc_alloc(i64 ${sizeOf}, ptr @${mangleRecordTrace(shape.id)}, ptr @${mangleRecordGcFree(shape.id)})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare ptr @calloc(i64, i64)`);
|
||||
host.needOom();
|
||||
nw.push(
|
||||
` %o = call ptr @calloc(i64 1, i64 ${sizeOf})`,
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %oom, label %ok`,
|
||||
`oom:`,
|
||||
` call void @sc_oom()`,
|
||||
` unreachable`,
|
||||
`ok:`,
|
||||
);
|
||||
}
|
||||
nw.push(` store i64 1, ptr %o`);
|
||||
if (shape.indexValue) {
|
||||
// The overflow map (string-keyed): value handling is type-directed
|
||||
// exactly like emit-shapes.ts's overflowNewC.
|
||||
host.declare(`declare ptr @scr_map_new(i32, i32, ptr, ptr, ptr)`);
|
||||
const v = shape.indexValue;
|
||||
const valKind = v.kind === "f64" ? 0 : v.kind === "bool" ? 1 : 2;
|
||||
const rc = valKind === 2 ? vAdapters(host, v) : { retain: "null", release: "null" };
|
||||
const trace = valKind === 2 ? traceArg(host, v) : "null";
|
||||
nw.push(
|
||||
` %ovf = call ptr @scr_map_new(i32 1, i32 ${valKind}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${trace})`,
|
||||
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
` store ptr %ovf, ptr %ovfp`,
|
||||
);
|
||||
}
|
||||
nw.push(` call void @scr_obj_alloc_note()`, ` ret ptr %o`, `}`, ``);
|
||||
defs.push(...nw);
|
||||
|
||||
if (traced) {
|
||||
// trace: visit exactly the cycle-capable members; gcFree: release
|
||||
// exactly the complement, then free (the trace/teardown complement
|
||||
// contract in scr_runtime.h).
|
||||
const tracedMembers = members.filter((m) => traceAdapter(host, m.type) !== null);
|
||||
const untracedRefMembers = refMembers.filter((m) => traceAdapter(host, m.type) === null);
|
||||
const tr: string[] = [
|
||||
`define internal void @${mangleRecordTrace(shape.id)}(ptr %o, ptr %visit, ptr %ctx) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
];
|
||||
tracedMembers.forEach((m, i) => {
|
||||
tr.push(
|
||||
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`,
|
||||
` %v${i} = load ptr, ptr %f${i}`,
|
||||
` call void %visit(ptr %v${i}, ptr %ctx) ; ${m.name}`,
|
||||
);
|
||||
});
|
||||
tr.push(` ret void`, `}`, ``);
|
||||
defs.push(...tr);
|
||||
|
||||
const gf: string[] = [
|
||||
`define internal void @${mangleRecordGcFree(shape.id)}(ptr %o) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
];
|
||||
untracedRefMembers.forEach((m, i) => {
|
||||
gf.push(
|
||||
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`,
|
||||
` %v${i} = load ptr, ptr %f${i}`,
|
||||
` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${m.name} (acyclic)`,
|
||||
);
|
||||
});
|
||||
gf.push(` call void @scr_obj_free_note()`, ` call void @scr_cyc_free(ptr %o)`, ` ret void`, `}`, ``);
|
||||
defs.push(...gf);
|
||||
}
|
||||
}
|
||||
return { typeDefs, defs };
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* The LLVM backend's refusal: an IR construct outside the current tier.
|
||||
* `kind` is a stable machine-readable tag ("stmt:switch", "expr:closure",
|
||||
* "type:union", ...) — the differential harness histograms the next
|
||||
* phase's queue from it. Never catch-and-continue: any refusal aborts the
|
||||
* whole module. compile()'s DEFAULT lane catches exactly this error and
|
||||
* re-emits through the C backend (the transparent fallback); an explicit
|
||||
* backend "llvm" surfaces it as diagnostic SC3001 instead — the
|
||||
* fail-loudly pin. Its own module so the emitter and the shape/RC tables
|
||||
* can both throw it without an import cycle. */
|
||||
import type { SrcLoc } from "../../ir/nodes.js";
|
||||
|
||||
export class LlvmUnsupportedError extends Error {
|
||||
constructor(
|
||||
readonly kind: string,
|
||||
readonly loc?: SrcLoc,
|
||||
) {
|
||||
super(
|
||||
`the LLVM backend does not support this construct yet (${kind}); ` +
|
||||
`build with the reference C backend (--backend c, or the default's transparent fallback)`,
|
||||
);
|
||||
this.name = "LlvmUnsupportedError";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,885 @@
|
||||
/* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror
|
||||
* of emit-walkers.ts's phase-3 slice: type-directed JSON serializers over
|
||||
* the external scr_jb_* string builder (one emitted function per typeKey,
|
||||
* interned), the pretty-print re-indenter (Node's gap algorithm), and the
|
||||
* per-union ToString/join pair Array#join needs over union elements.
|
||||
* Interning ORDER is part of the emitted .ll, so the registries live on one
|
||||
* LlWalkers instance the emitter owns.
|
||||
*
|
||||
* Everything here is throw-free (the jsonStringify node never throws; join
|
||||
* walks only f64/string/bool/unit arms — the frontend's fence), so nothing
|
||||
* touches the pending-exception protocol the tier excludes. */
|
||||
import type { IrRecordShape, IrType, IrUnionDef } from "../../ir/nodes.js";
|
||||
import { isRefCounted, typeKey } from "../../ir/nodes.js";
|
||||
import { mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { llFieldType, releaseSym, type ShapeHost } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
/** What the walkers need from the emitter beyond the shape tables: union
|
||||
* defs, the undefined-arm probe, interned ScrStr literals (unit-arm
|
||||
* ToString), and NUL-terminated C-string constants (scr_jb_puts /
|
||||
* scr_error-style label texts). */
|
||||
export interface WalkerHost extends ShapeHost {
|
||||
readonly unionsById: Map<string, IrUnionDef>;
|
||||
undefinedArmTag(t: IrType): number;
|
||||
/** `@`-ref of an interned immortal ScrStr literal. */
|
||||
internLiteral(text: string): string;
|
||||
/** `@`-ref of an interned NUL-terminated byte-array constant. */
|
||||
cstr(text: string): string;
|
||||
/** Request the shared invalid-union-tag abort helper (@sc_bad_tag). */
|
||||
needBadTag(): void;
|
||||
}
|
||||
|
||||
/** Exact double literal (the emitter's f64Lit — duplicated to avoid a
|
||||
* cyclic import; both spell every bit pattern identically). */
|
||||
function f64Lit(n: number): string {
|
||||
const buf = new ArrayBuffer(8);
|
||||
new DataView(buf).setFloat64(0, n);
|
||||
return `0x${[...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase()}`;
|
||||
}
|
||||
|
||||
const FN_ATTRS = "#0";
|
||||
|
||||
export class LlWalkers {
|
||||
private readonly jsonWriters = new Map<string, string>();
|
||||
private readonly unionToStrFns = new Map<string, string>();
|
||||
private readonly unionJoinFns = new Map<string, string>();
|
||||
private indentFn: string | null = null;
|
||||
/** Emitted function definitions, in interning order. */
|
||||
readonly defs: string[] = [];
|
||||
|
||||
constructor(private readonly host: WalkerHost) {}
|
||||
|
||||
/** The value-parameter LLVM type of a writer for `t`. */
|
||||
private valTy(t: IrType): string {
|
||||
return t.kind === "f64" ? "double" : t.kind === "bool" ? "i1" : "ptr";
|
||||
}
|
||||
|
||||
/* ── the byte plumbing shared by every walker ─────────────────────────── */
|
||||
|
||||
private putc(B: BlockBuilder, buf: string, byte: string): void {
|
||||
this.host.declare(`declare void @scr_jb_putc(ptr, i8)`);
|
||||
B.line(`call void @scr_jb_putc(ptr ${buf}, i8 ${byte})`);
|
||||
}
|
||||
|
||||
private puts(B: BlockBuilder, buf: string, text: string): void {
|
||||
this.host.declare(`declare void @scr_jb_puts(ptr, ptr)`);
|
||||
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr ${this.host.cstr(text)}) ; ${JSON.stringify(text)}`);
|
||||
}
|
||||
|
||||
/** Appends an ScrStr's bytes: for (j < s->len) putc(s->data[j]). */
|
||||
private putScrStr(B: BlockBuilder, buf: string, s: string): void {
|
||||
const lenp = B.tmp();
|
||||
const len = B.tmp();
|
||||
const data = B.tmp();
|
||||
B.line(`${lenp} = getelementptr inbounds %ScrStr, ptr ${s}, i64 0, i32 1`);
|
||||
B.line(`${len} = load i64, ptr ${lenp}`);
|
||||
B.line(`${data} = getelementptr inbounds i8, ptr ${s}, i64 24 ; ->data`);
|
||||
const jSlot = B.slot();
|
||||
B.entryAllocas.push(`${jSlot} = alloca i64`);
|
||||
B.line(`store i64 0, ptr ${jSlot}`);
|
||||
const lc = B.newLabel("ps.c");
|
||||
const lb = B.newLabel("ps.b");
|
||||
const le = B.newLabel("ps.e");
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
const j = B.tmp();
|
||||
const cont = B.tmp();
|
||||
B.line(`${j} = load i64, ptr ${jSlot}`);
|
||||
B.line(`${cont} = icmp ult i64 ${j}, ${len}`);
|
||||
B.condBr(cont, lb, le);
|
||||
B.startBlock(lb);
|
||||
const cp = B.tmp();
|
||||
const c = B.tmp();
|
||||
B.line(`${cp} = getelementptr inbounds i8, ptr ${data}, i64 ${j}`);
|
||||
B.line(`${c} = load i8, ptr ${cp}`);
|
||||
this.putc(B, buf, c);
|
||||
const j2 = B.tmp();
|
||||
B.line(`${j2} = add i64 ${j}, 1`);
|
||||
B.line(`store i64 ${j2}, ptr ${jSlot}`);
|
||||
B.br(lc);
|
||||
B.startBlock(le);
|
||||
}
|
||||
|
||||
/** Loads a union box's tag. */
|
||||
private unionTag(B: BlockBuilder, uName: string): string {
|
||||
const p = B.tmp();
|
||||
const t = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 1`);
|
||||
B.line(`${t} = load i32, ptr ${p}`);
|
||||
return t;
|
||||
}
|
||||
|
||||
private unionPeek(B: BlockBuilder, uName: string): string {
|
||||
const p = B.tmp();
|
||||
const t = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 5`);
|
||||
B.line(`${t} = load ptr, ptr ${p}`);
|
||||
return t;
|
||||
}
|
||||
|
||||
/* ── type-directed JSON serializers (jsonWriteHelper, ported) ─────────── */
|
||||
|
||||
jsonWriteHelper(t: IrType): string {
|
||||
const key = typeKey(t);
|
||||
const existing = this.jsonWriters.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `sc_jw_${this.jsonWriters.size}`;
|
||||
this.jsonWriters.set(key, name);
|
||||
const B = new BlockBuilder();
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
this.host.declare(`declare void @scr_jb_put_f64(ptr, double)`);
|
||||
B.line(`call void @scr_jb_put_f64(ptr %b, double %v) ; NaN/Infinity -> null, -0 -> 0, like JS`);
|
||||
break;
|
||||
case "bool": {
|
||||
const s = B.tmp();
|
||||
this.host.declare(`declare void @scr_jb_puts(ptr, ptr)`);
|
||||
B.line(`${s} = select i1 %v, ptr ${this.host.cstr("true")}, ptr ${this.host.cstr("false")}`);
|
||||
B.line(`call void @scr_jb_puts(ptr %b, ptr ${s})`);
|
||||
break;
|
||||
}
|
||||
case "string":
|
||||
this.host.declare(`declare void @scr_jb_put_json_str(ptr, ptr)`);
|
||||
B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`);
|
||||
break;
|
||||
case "dyn":
|
||||
// Overflow values under an `unknown` index signature: the DOM
|
||||
// serializes itself (runtime walker). Bare stringify of dyn stays
|
||||
// frontend-fenced; this writer is reachable only through overflow
|
||||
// entries.
|
||||
this.host.declare(`declare void @scr_jb_put_dyn(ptr, ptr)`);
|
||||
B.line(`call void @scr_jb_put_dyn(ptr %b, ptr %v)`);
|
||||
break;
|
||||
case "record":
|
||||
this.emitRecordWriter(B, t.shapeId);
|
||||
break;
|
||||
case "array":
|
||||
this.emitArrayWriter(B, t.elem);
|
||||
break;
|
||||
case "union":
|
||||
this.emitUnionWriter(B, t.unionId);
|
||||
break;
|
||||
default:
|
||||
throw new LlvmUnsupportedError(`jsonStringify:${t.kind}`);
|
||||
}
|
||||
B.terminate("ret void");
|
||||
this.defs.push(
|
||||
`define internal void @${name}(ptr %b, ${this.valTy(t)} %v) ${FN_ATTRS} { ; stringify ${key}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Loads a record field slot (i8-stored bools trunc to i1). */
|
||||
private loadField(B: BlockBuilder, recName: string, shapeId: string, index: number, t: IrType): string {
|
||||
const p = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${recName}, i64 0, i32 ${index}`);
|
||||
const fieldTy = llFieldType(t);
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = load ${fieldTy}, ptr ${p}`);
|
||||
if (fieldTy !== "i8") return raw;
|
||||
const b = B.tmp();
|
||||
B.line(`${b} = trunc i8 ${raw} to i1`);
|
||||
return b;
|
||||
}
|
||||
|
||||
private emitRecordWriter(B: BlockBuilder, shapeId: string): void {
|
||||
const shape = this.host.recordsById.get(shapeId);
|
||||
if (!shape) throw new Error(`llvm emitter bug: jsonStringify of unknown shape ${shapeId}`);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
// A tuple serializes as a JSON ARRAY in index order — JS-exact. Every
|
||||
// position is required, so commas are static.
|
||||
if (shape.tuple) {
|
||||
const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
|
||||
this.putc(B, "%b", "91"); // '['
|
||||
byIndex.forEach((f, i) => {
|
||||
if (i > 0) this.putc(B, "%b", "44"); // ','
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
B.line(`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; [${f.name}]`);
|
||||
});
|
||||
this.putc(B, "%b", "93"); // ']'
|
||||
return;
|
||||
}
|
||||
// Fields serialize in DECLARED order (JS insertion order); internal
|
||||
// '%'-fields stay hidden — jsonWriteHelper's contract, ported comment
|
||||
// and all.
|
||||
const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
|
||||
const inOrder = new Set(order);
|
||||
if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) {
|
||||
throw new Error(`llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`);
|
||||
}
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
const emitFields = order.map((n) => byName.get(n)).filter((f) => f !== undefined);
|
||||
const droppable =
|
||||
emitFields.some((f) => this.host.undefinedArmTag(f.type) >= 0) || !!shape.indexValue;
|
||||
this.putc(B, "%b", "123"); // '{'
|
||||
if (!droppable) {
|
||||
emitFields.forEach((f, i) => {
|
||||
this.puts(B, "%b", `${i > 0 ? "," : ""}"${f.name}":`);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
B.line(`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${f.name}`);
|
||||
});
|
||||
} else {
|
||||
const first = B.slot();
|
||||
B.entryAllocas.push(`${first} = alloca i1`);
|
||||
B.line(`store i1 true, ptr ${first}`);
|
||||
const comma = (): void => {
|
||||
const isf = B.tmp();
|
||||
const lc = B.newLabel("jwc.c");
|
||||
const lj = B.newLabel("jwc.j");
|
||||
B.line(`${isf} = load i1, ptr ${first}`);
|
||||
B.condBr(isf, lj, lc);
|
||||
B.startBlock(lc);
|
||||
this.putc(B, "%b", "44"); // ','
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
B.line(`store i1 false, ptr ${first}`);
|
||||
};
|
||||
for (const f of emitFields) {
|
||||
const utag = this.host.undefinedArmTag(f.type);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
let skip: string | null = null;
|
||||
if (utag >= 0) {
|
||||
// Undefined-valued field: dropped, like Node.
|
||||
const tag = this.unionTag(B, v);
|
||||
const isu = B.tmp();
|
||||
B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`);
|
||||
const lw = B.newLabel("jwf.w");
|
||||
skip = B.newLabel("jwf.s");
|
||||
B.condBr(isu, skip, lw);
|
||||
B.startBlock(lw);
|
||||
}
|
||||
comma();
|
||||
this.puts(B, "%b", `"${f.name}":`);
|
||||
B.line(`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${f.name}`);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
}
|
||||
}
|
||||
if (shape.indexValue) this.emitOverflowEntries(B, shape, first);
|
||||
}
|
||||
this.putc(B, "%b", "125"); // '}'
|
||||
}
|
||||
|
||||
/** Overflow entries follow the declared fields, in JS OWN-KEY order
|
||||
* (scr_map_keys_js_order); keys escape like any JSON string;
|
||||
* undefined-valued entries drop (the optional-field rule). */
|
||||
private emitOverflowEntries(B: BlockBuilder, shape: IrRecordShape, first: string): void {
|
||||
const iv = shape.indexValue!;
|
||||
const host = this.host;
|
||||
const ovfp = B.tmp();
|
||||
const ovf = B.tmp();
|
||||
B.line(`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`);
|
||||
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
||||
host.declare(`declare ptr @scr_map_keys_js_order(ptr)`);
|
||||
host.declare(`declare double @scr_arr_len(ptr)`);
|
||||
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
const ks = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(`${ks} = call ptr @scr_map_keys_js_order(ptr ${ovf})`);
|
||||
B.line(`${len} = call double @scr_arr_len(ptr ${ks})`);
|
||||
const iSlot = B.slot();
|
||||
B.entryAllocas.push(`${iSlot} = alloca double`);
|
||||
B.line(`store double ${f64Lit(0)}, ptr ${iSlot}`);
|
||||
const lc = B.newLabel("ovf.c");
|
||||
const lb = B.newLabel("ovf.b");
|
||||
const ln = B.newLabel("ovf.n");
|
||||
const le = B.newLabel("ovf.e");
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
const i = B.tmp();
|
||||
const cont = B.tmp();
|
||||
B.line(`${i} = load double, ptr ${iSlot}`);
|
||||
B.line(`${cont} = fcmp olt double ${i}, ${len}`);
|
||||
B.condBr(cont, lb, le);
|
||||
B.startBlock(lb);
|
||||
const k = B.tmp();
|
||||
B.line(`${k} = call ptr @scr_arr_get_ref(ptr ${ks}, double ${i}) ; key (+1)`);
|
||||
// The entry value, type-directed off the overflow VALUE type.
|
||||
let val: string;
|
||||
if (iv.kind === "f64" || iv.kind === "bool") {
|
||||
const outTy = iv.kind === "f64" ? "double" : "i8";
|
||||
const outSlot = B.slot();
|
||||
B.entryAllocas.push(`${outSlot} = alloca ${outTy}`);
|
||||
B.line(`store ${outTy} ${iv.kind === "f64" ? f64Lit(0) : "0"}, ptr ${outSlot}`);
|
||||
host.declare(`declare zeroext i1 @scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}(ptr, ptr, ptr)`);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = call zeroext i1 @scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}(ptr ${ovf}, ptr ${k}, ptr ${outSlot})`);
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = load ${outTy}, ptr ${outSlot}`);
|
||||
if (iv.kind === "bool") {
|
||||
val = B.tmp();
|
||||
B.line(`${val} = trunc i8 ${raw} to i1`);
|
||||
} else {
|
||||
val = raw;
|
||||
}
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_map_get_str_ref(ptr, ptr)`);
|
||||
val = B.tmp();
|
||||
B.line(`${val} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${k}) ; value (+1)`);
|
||||
}
|
||||
// Undefined-valued entries drop, exactly the optional-field rule: a
|
||||
// dyn value whose DOM kind is SCR_DYN_UNDEF (i32 at offset 8, enum
|
||||
// member 6 — scr_runtime.h's ScrDynKind), or a union holding its
|
||||
// undefined arm.
|
||||
let skipUndef: string | null = null;
|
||||
if (iv.kind === "dyn") {
|
||||
const kp = B.tmp();
|
||||
const kd = B.tmp();
|
||||
const isu = B.tmp();
|
||||
B.line(`${kp} = getelementptr inbounds i8, ptr ${val}, i64 8 ; ->kind`);
|
||||
B.line(`${kd} = load i32, ptr ${kp}`);
|
||||
B.line(`${isu} = icmp eq i32 ${kd}, 6 ; SCR_DYN_UNDEF (NULL is 0 — null members DO serialize)`);
|
||||
skipUndef = isu;
|
||||
} else if (this.host.undefinedArmTag(iv) >= 0) {
|
||||
const tag = this.unionTag(B, val);
|
||||
const isu = B.tmp();
|
||||
B.line(`${isu} = icmp eq i32 ${tag}, ${this.host.undefinedArmTag(iv)}`);
|
||||
skipUndef = isu;
|
||||
}
|
||||
if (skipUndef !== null) {
|
||||
const ld = B.newLabel("ovf.d");
|
||||
const write = B.newLabel("ovf.w");
|
||||
B.condBr(skipUndef, ld, write);
|
||||
B.startBlock(ld);
|
||||
if (isRefCounted(iv)) B.line(`call void ${releaseSym(host, iv)}(ptr ${val})`);
|
||||
B.line(`call void @scr_str_release(ptr ${k})`);
|
||||
B.br(ln);
|
||||
B.startBlock(write);
|
||||
}
|
||||
// The comma dance over the shared `first` flag.
|
||||
{
|
||||
const isf = B.tmp();
|
||||
const lcm = B.newLabel("ovf.cm");
|
||||
const lj = B.newLabel("ovf.cj");
|
||||
B.line(`${isf} = load i1, ptr ${first}`);
|
||||
B.condBr(isf, lj, lcm);
|
||||
B.startBlock(lcm);
|
||||
this.putc(B, "%b", "44"); // ','
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
B.line(`store i1 false, ptr ${first}`);
|
||||
}
|
||||
host.declare(`declare void @scr_jb_put_json_str(ptr, ptr)`);
|
||||
B.line(`call void @scr_jb_put_json_str(ptr %b, ptr ${k})`);
|
||||
this.putc(B, "%b", "58"); // ':'
|
||||
B.line(`call void @${this.jsonWriteHelper(iv)}(ptr %b, ${this.valTy(iv)} ${val})`);
|
||||
B.line(`call void @scr_str_release(ptr ${k})`);
|
||||
if (isRefCounted(iv)) B.line(`call void ${releaseSym(host, iv)}(ptr ${val})`);
|
||||
B.br(ln);
|
||||
B.startBlock(ln);
|
||||
const i2 = B.tmp();
|
||||
B.line(`${i2} = fadd double ${i}, ${f64Lit(1)}`);
|
||||
B.line(`store double ${i2}, ptr ${iSlot}`);
|
||||
B.br(lc);
|
||||
B.startBlock(le);
|
||||
host.declare(`declare void @scr_arr_release(ptr)`);
|
||||
B.line(`call void @scr_arr_release(ptr ${ks})`);
|
||||
}
|
||||
|
||||
private emitArrayWriter(B: BlockBuilder, elem: IrType): void {
|
||||
const host = this.host;
|
||||
const w = this.jsonWriteHelper(elem);
|
||||
host.declare(`declare double @scr_arr_len(ptr)`);
|
||||
this.putc(B, "%b", "91"); // '['
|
||||
const len = B.tmp();
|
||||
B.line(`${len} = call double @scr_arr_len(ptr %v)`);
|
||||
const iSlot = B.slot();
|
||||
B.entryAllocas.push(`${iSlot} = alloca double`);
|
||||
B.line(`store double ${f64Lit(0)}, ptr ${iSlot}`);
|
||||
const lc = B.newLabel("jwa.c");
|
||||
const lb = B.newLabel("jwa.b");
|
||||
const le = B.newLabel("jwa.e");
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
const i = B.tmp();
|
||||
const cont = B.tmp();
|
||||
B.line(`${i} = load double, ptr ${iSlot}`);
|
||||
B.line(`${cont} = fcmp olt double ${i}, ${len}`);
|
||||
B.condBr(cont, lb, le);
|
||||
B.startBlock(lb);
|
||||
const nz = B.tmp();
|
||||
const lcm = B.newLabel("jwa.cm");
|
||||
const lj = B.newLabel("jwa.cj");
|
||||
B.line(`${nz} = fcmp ogt double ${i}, ${f64Lit(0)}`);
|
||||
B.condBr(nz, lcm, lj);
|
||||
B.startBlock(lcm);
|
||||
this.putc(B, "%b", "44"); // ','
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
if (elem.kind === "f64" || elem.kind === "bool") {
|
||||
const acc = elem.kind;
|
||||
const accTy = elem.kind === "f64" ? "double" : "i1";
|
||||
host.declare(`declare ${elem.kind === "bool" ? "zeroext i1" : accTy} @scr_arr_get_${acc}(ptr, double)`);
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = call ${accTy} @scr_arr_get_${acc}(ptr %v, double ${i})`);
|
||||
B.line(`call void @${w}(ptr %b, ${accTy} ${v})`);
|
||||
} else {
|
||||
// _get_ref returns +1; release after writing.
|
||||
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = call ptr @scr_arr_get_ref(ptr %v, double ${i})`);
|
||||
B.line(`call void @${w}(ptr %b, ptr ${v})`);
|
||||
B.line(`call void ${releaseSym(host, elem)}(ptr ${v})`);
|
||||
}
|
||||
const i2 = B.tmp();
|
||||
B.line(`${i2} = fadd double ${i}, ${f64Lit(1)}`);
|
||||
B.line(`store double ${i2}, ptr ${iSlot}`);
|
||||
B.br(lc);
|
||||
B.startBlock(le);
|
||||
this.putc(B, "%b", "93"); // ']'
|
||||
}
|
||||
|
||||
private emitUnionWriter(B: BlockBuilder, unionId: string): void {
|
||||
const def = this.host.unionsById.get(unionId);
|
||||
if (!def) throw new Error(`llvm emitter bug: jsonStringify of unknown union ${unionId}`);
|
||||
const tag = this.unionTag(B, "%v");
|
||||
const bad = B.newLabel("jwu.bad");
|
||||
const done = B.newLabel("jwu.d");
|
||||
const labels = def.arms.map(() => B.newLabel("jwu.a"));
|
||||
B.terminate(
|
||||
`switch i32 ${tag}, label %${bad} [ ${def.arms.map((_, i) => `i32 ${i}, label %${labels[i]}`).join(" ")} ]`,
|
||||
);
|
||||
def.arms.forEach((arm, i) => {
|
||||
B.startBlock(labels[i]!);
|
||||
if (arm.kind === "nullT") {
|
||||
// Payload-less arm: JSON.stringify(null) is the text `null`.
|
||||
this.puts(B, "%b", "null");
|
||||
B.br(done);
|
||||
return;
|
||||
}
|
||||
if (arm.kind === "undefinedT") {
|
||||
// Reachable only as a record FIELD's serializer, and the record
|
||||
// writer drops the field while it holds this tag before calling —
|
||||
// so the tag can never arrive here.
|
||||
B.br(bad);
|
||||
return;
|
||||
}
|
||||
const w = this.jsonWriteHelper(arm);
|
||||
if (arm.kind === "f64") {
|
||||
this.host.declare(`declare double @scr_union_get_f64(ptr)`);
|
||||
const x = B.tmp();
|
||||
B.line(`${x} = call double @scr_union_get_f64(ptr %v)`);
|
||||
B.line(`call void @${w}(ptr %b, double ${x})`);
|
||||
} else if (arm.kind === "bool") {
|
||||
this.host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`);
|
||||
const x = B.tmp();
|
||||
B.line(`${x} = call zeroext i1 @scr_union_get_bool(ptr %v)`);
|
||||
B.line(`call void @${w}(ptr %b, i1 ${x})`);
|
||||
} else {
|
||||
// Payload is BORROWED out of the box for the write.
|
||||
const p = this.unionPeek(B, "%v");
|
||||
B.line(`call void @${w}(ptr %b, ptr ${p}) ; ${arm.kind}`);
|
||||
}
|
||||
B.br(done);
|
||||
});
|
||||
B.startBlock(bad);
|
||||
this.host.needBadTag();
|
||||
B.line(`call void @sc_bad_tag()`);
|
||||
B.terminate(`unreachable`);
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
/* ── the pretty-print re-indenter (jsonIndentHelper, ported) ──────────── */
|
||||
|
||||
/** `JSON.stringify(v, null, space)` as a REWRITE of the compact text —
|
||||
* Node's gap algorithm exactly (see emit-walkers.ts's sc_ji). The indent
|
||||
* rides as a NUL-terminated constant + byte length. */
|
||||
jsonIndentHelper(): string {
|
||||
if (this.indentFn) return this.indentFn;
|
||||
const name = "sc_ji";
|
||||
this.indentFn = name;
|
||||
const host = this.host;
|
||||
host.declare(`declare void @scr_jb_init(ptr)`);
|
||||
host.declare(`declare ptr @scr_jb_finish(ptr)`);
|
||||
host.declare(`declare void @scr_jb_putc(ptr, i8)`);
|
||||
|
||||
// The newline + depth * indent writer, shared by the three break sites.
|
||||
this.defs.push(
|
||||
`define internal void @sc_ji_ind(ptr %b, i64 %depth, ptr %ind, i64 %ilen) ${FN_ATTRS} { ; stringify gap indent`,
|
||||
`entry:`,
|
||||
` call void @scr_jb_putc(ptr %b, i8 10)`,
|
||||
` br label %oc`,
|
||||
`oc:`,
|
||||
` %d = phi i64 [ 0, %entry ], [ %d2, %ie ]`,
|
||||
` %ocont = icmp ult i64 %d, %depth`,
|
||||
` br i1 %ocont, label %ic0, label %done`,
|
||||
`ic0:`,
|
||||
` br label %ic`,
|
||||
`ic:`,
|
||||
` %k = phi i64 [ 0, %ic0 ], [ %k2, %ib ]`,
|
||||
` %icont = icmp ult i64 %k, %ilen`,
|
||||
` br i1 %icont, label %ib, label %ie`,
|
||||
`ib:`,
|
||||
` %cp = getelementptr inbounds i8, ptr %ind, i64 %k`,
|
||||
` %c = load i8, ptr %cp`,
|
||||
` call void @scr_jb_putc(ptr %b, i8 %c)`,
|
||||
` %k2 = add i64 %k, 1`,
|
||||
` br label %ic`,
|
||||
`ie:`,
|
||||
` %d2 = add i64 %d, 1`,
|
||||
` br label %oc`,
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
|
||||
const B = new BlockBuilder();
|
||||
const buf = "%jb";
|
||||
B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`);
|
||||
const depth = "%depth";
|
||||
const instr = "%instr";
|
||||
const iSlot = "%i";
|
||||
B.entryAllocas.push(`${depth} = alloca i64`, `${instr} = alloca i1`, `${iSlot} = alloca i64`);
|
||||
B.line(`call void @scr_jb_init(ptr ${buf})`);
|
||||
B.line(`store i64 0, ptr ${depth}`);
|
||||
B.line(`store i1 false, ptr ${instr}`);
|
||||
B.line(`store i64 0, ptr ${iSlot}`);
|
||||
const np = B.tmp();
|
||||
const n = B.tmp();
|
||||
const data = B.tmp();
|
||||
B.line(`${np} = getelementptr inbounds %ScrStr, ptr %compact, i64 0, i32 1`);
|
||||
B.line(`${n} = load i64, ptr ${np}`);
|
||||
B.line(`${data} = getelementptr inbounds i8, ptr %compact, i64 24 ; ->data`);
|
||||
|
||||
const loop = B.newLabel("ji.c");
|
||||
const body = B.newLabel("ji.b");
|
||||
const inc = B.newLabel("ji.n");
|
||||
const end = B.newLabel("ji.e");
|
||||
B.br(loop);
|
||||
B.startBlock(loop);
|
||||
const i = B.tmp();
|
||||
const cont = B.tmp();
|
||||
B.line(`${i} = load i64, ptr ${iSlot}`);
|
||||
B.line(`${cont} = icmp ult i64 ${i}, ${n}`);
|
||||
B.condBr(cont, body, end);
|
||||
B.startBlock(body);
|
||||
const cp = B.tmp();
|
||||
const c = B.tmp();
|
||||
B.line(`${cp} = getelementptr inbounds i8, ptr ${data}, i64 ${i}`);
|
||||
B.line(`${c} = load i8, ptr ${cp}`);
|
||||
const ins = B.tmp();
|
||||
B.line(`${ins} = load i1, ptr ${instr}`);
|
||||
const inStr = B.newLabel("ji.s");
|
||||
const outStr = B.newLabel("ji.o");
|
||||
B.condBr(ins, inStr, outStr);
|
||||
|
||||
// In-string state: emit verbatim, skip the escaped char, leave on '"'.
|
||||
B.startBlock(inStr);
|
||||
this.putc(B, buf, c);
|
||||
const isbs = B.tmp();
|
||||
const i1p = B.tmp();
|
||||
const hasNext = B.tmp();
|
||||
const escTake = B.tmp();
|
||||
B.line(`${isbs} = icmp eq i8 ${c}, 92 ; backslash`);
|
||||
B.line(`${i1p} = add i64 ${i}, 1`);
|
||||
B.line(`${hasNext} = icmp ult i64 ${i1p}, ${n}`);
|
||||
B.line(`${escTake} = and i1 ${isbs}, ${hasNext}`);
|
||||
const esc = B.newLabel("ji.se");
|
||||
const noesc = B.newLabel("ji.sq");
|
||||
B.condBr(escTake, esc, noesc);
|
||||
B.startBlock(esc);
|
||||
const ecp = B.tmp();
|
||||
const ec = B.tmp();
|
||||
B.line(`${ecp} = getelementptr inbounds i8, ptr ${data}, i64 ${i1p}`);
|
||||
B.line(`${ec} = load i8, ptr ${ecp}`);
|
||||
this.putc(B, buf, ec);
|
||||
B.line(`store i64 ${i1p}, ptr ${iSlot} ; consumed the escaped char`);
|
||||
B.br(inc);
|
||||
B.startBlock(noesc);
|
||||
const isq = B.tmp();
|
||||
const stay = B.tmp();
|
||||
B.line(`${isq} = icmp eq i8 ${c}, 34 ; '"'`);
|
||||
B.line(`${stay} = xor i1 ${isq}, true`);
|
||||
B.line(`store i1 ${stay}, ptr ${instr}`);
|
||||
B.br(inc);
|
||||
|
||||
// Out of string: the character switch.
|
||||
B.startBlock(outStr);
|
||||
const lQuote = B.newLabel("ji.q");
|
||||
const lOpenB = B.newLabel("ji.ob");
|
||||
const lOpenK = B.newLabel("ji.ok");
|
||||
const lClose = B.newLabel("ji.cl");
|
||||
const lComma = B.newLabel("ji.cm");
|
||||
const lColon = B.newLabel("ji.co");
|
||||
const lOther = B.newLabel("ji.ot");
|
||||
B.terminate(
|
||||
`switch i8 ${c}, label %${lOther} [ i8 34, label %${lQuote} i8 123, label %${lOpenB} i8 91, label %${lOpenK} ` +
|
||||
`i8 125, label %${lClose} i8 93, label %${lClose} i8 44, label %${lComma} i8 58, label %${lColon} ]`,
|
||||
);
|
||||
B.startBlock(lQuote);
|
||||
B.line(`store i1 true, ptr ${instr}`);
|
||||
this.putc(B, buf, c);
|
||||
B.br(inc);
|
||||
|
||||
// '{' / '[': emit, then `{}`/`[]` stay inline (like Node) or a break
|
||||
// one level deeper.
|
||||
const openBody = (label: string, closer: number): void => {
|
||||
B.startBlock(label);
|
||||
this.putc(B, buf, c);
|
||||
const i1b = B.tmp();
|
||||
const hasNextB = B.tmp();
|
||||
B.line(`${i1b} = add i64 ${i}, 1`);
|
||||
B.line(`${hasNextB} = icmp ult i64 ${i1b}, ${n}`);
|
||||
const chk = B.newLabel("ji.opc");
|
||||
const deep = B.newLabel("ji.opd");
|
||||
const inline = B.newLabel("ji.opi");
|
||||
B.condBr(hasNextB, chk, deep);
|
||||
B.startBlock(chk);
|
||||
const ncp = B.tmp();
|
||||
const nc = B.tmp();
|
||||
const isClose = B.tmp();
|
||||
B.line(`${ncp} = getelementptr inbounds i8, ptr ${data}, i64 ${i1b}`);
|
||||
B.line(`${nc} = load i8, ptr ${ncp}`);
|
||||
B.line(`${isClose} = icmp eq i8 ${nc}, ${closer}`);
|
||||
B.condBr(isClose, inline, deep);
|
||||
B.startBlock(inline);
|
||||
const nc2p = B.tmp();
|
||||
const nc2 = B.tmp();
|
||||
B.line(`${nc2p} = getelementptr inbounds i8, ptr ${data}, i64 ${i1b}`);
|
||||
B.line(`${nc2} = load i8, ptr ${nc2p}`);
|
||||
this.putc(B, buf, nc2);
|
||||
B.line(`store i64 ${i1b}, ptr ${iSlot} ; empty {} / [] stay inline, like Node`);
|
||||
B.br(inc);
|
||||
B.startBlock(deep);
|
||||
const d0 = B.tmp();
|
||||
const d1 = B.tmp();
|
||||
B.line(`${d0} = load i64, ptr ${depth}`);
|
||||
B.line(`${d1} = add i64 ${d0}, 1`);
|
||||
B.line(`store i64 ${d1}, ptr ${depth}`);
|
||||
B.line(`call void @sc_ji_ind(ptr ${buf}, i64 ${d1}, ptr %ind, i64 %ilen)`);
|
||||
B.br(inc);
|
||||
};
|
||||
openBody(lOpenB, 125); // '{' closes with '}'
|
||||
openBody(lOpenK, 93); // '[' closes with ']'
|
||||
|
||||
B.startBlock(lClose);
|
||||
const dc0 = B.tmp();
|
||||
const dc1 = B.tmp();
|
||||
B.line(`${dc0} = load i64, ptr ${depth}`);
|
||||
B.line(`${dc1} = sub i64 ${dc0}, 1`);
|
||||
B.line(`store i64 ${dc1}, ptr ${depth}`);
|
||||
B.line(`call void @sc_ji_ind(ptr ${buf}, i64 ${dc1}, ptr %ind, i64 %ilen)`);
|
||||
this.putc(B, buf, c);
|
||||
B.br(inc);
|
||||
|
||||
B.startBlock(lComma);
|
||||
this.putc(B, buf, "44");
|
||||
const dm = B.tmp();
|
||||
B.line(`${dm} = load i64, ptr ${depth}`);
|
||||
B.line(`call void @sc_ji_ind(ptr ${buf}, i64 ${dm}, ptr %ind, i64 %ilen)`);
|
||||
B.br(inc);
|
||||
|
||||
B.startBlock(lColon);
|
||||
this.putc(B, buf, "58");
|
||||
this.putc(B, buf, "32"); // ' '
|
||||
B.br(inc);
|
||||
|
||||
B.startBlock(lOther);
|
||||
this.putc(B, buf, c);
|
||||
B.br(inc);
|
||||
|
||||
B.startBlock(inc);
|
||||
const iN = B.tmp();
|
||||
const iN2 = B.tmp();
|
||||
B.line(`${iN} = load i64, ptr ${iSlot}`);
|
||||
B.line(`${iN2} = add i64 ${iN}, 1`);
|
||||
B.line(`store i64 ${iN2}, ptr ${iSlot}`);
|
||||
B.br(loop);
|
||||
B.startBlock(end);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @scr_jb_finish(ptr ${buf})`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %compact, ptr %ind, i64 %ilen) ${FN_ATTRS} { ; stringify space re-indent (Node's gap algorithm)`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/* ── the per-union ToString / Array#join pair (ported) ────────────────── */
|
||||
|
||||
/** The per-union ToString helper: unit arms return the interned
|
||||
* "undefined"/"null" texts, string arms retain the payload, f64/bool
|
||||
* arms format (String(x) semantics). Ref arms never arrive (frontend
|
||||
* fence). Borrows the operand; the result is owned (+1). */
|
||||
unionToStrHelper(unionId: string): string {
|
||||
const existing = this.unionToStrFns.get(unionId);
|
||||
if (existing) return existing;
|
||||
const def = this.host.unionsById.get(unionId);
|
||||
if (!def) throw new Error(`llvm emitter bug: ToString of unknown union ${unionId}`);
|
||||
const name = `sc_us_${this.unionToStrFns.size}`;
|
||||
this.unionToStrFns.set(unionId, name);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
const tag = this.unionTag(B, "%v");
|
||||
const bad = B.newLabel("us.bad");
|
||||
const labels = def.arms.map(() => B.newLabel("us.a"));
|
||||
B.terminate(
|
||||
`switch i32 ${tag}, label %${bad} [ ${def.arms.map((_, i) => `i32 ${i}, label %${labels[i]}`).join(" ")} ]`,
|
||||
);
|
||||
def.arms.forEach((arm, i) => {
|
||||
B.startBlock(labels[i]!);
|
||||
switch (arm.kind) {
|
||||
case "undefinedT":
|
||||
case "nullT": {
|
||||
const lit = host.internLiteral(arm.kind === "undefinedT" ? "undefined" : "null");
|
||||
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @scr_str_retain_v(ptr ${lit})`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
case "string": {
|
||||
const p = this.unionPeek(B, "%v");
|
||||
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @scr_str_retain_v(ptr ${p})`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
case "f64": {
|
||||
host.declare(`declare double @scr_union_get_f64(ptr)`);
|
||||
host.declare(`declare ptr @scr_f64_to_scrstr(double)`);
|
||||
const x = B.tmp();
|
||||
const r = B.tmp();
|
||||
B.line(`${x} = call double @scr_union_get_f64(ptr %v)`);
|
||||
B.line(`${r} = call ptr @scr_f64_to_scrstr(double ${x})`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
case "bool": {
|
||||
host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`);
|
||||
host.declare(`declare ptr @scr_bool_to_scrstr(i1 zeroext)`);
|
||||
const x = B.tmp();
|
||||
const r = B.tmp();
|
||||
B.line(`${x} = call zeroext i1 @scr_union_get_bool(ptr %v)`);
|
||||
B.line(`${r} = call ptr @scr_bool_to_scrstr(i1 zeroext ${x})`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new LlvmUnsupportedError(`unionToStr:${arm.kind}`);
|
||||
}
|
||||
});
|
||||
B.startBlock(bad);
|
||||
host.needBadTag();
|
||||
B.line(`call void @sc_bad_tag()`);
|
||||
B.terminate(`unreachable`);
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %v) ${FN_ATTRS} { ; ToString ${unionId}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Array.prototype.join over union elements: undefined/null arms print
|
||||
* EMPTY (exactly JS's join), every other arm goes through the per-union
|
||||
* ToString walker. Borrows the array and separator; result +1. */
|
||||
unionJoinHelper(unionId: string): string {
|
||||
const existing = this.unionJoinFns.get(unionId);
|
||||
if (existing) return existing;
|
||||
const def = this.host.unionsById.get(unionId);
|
||||
if (!def) throw new Error(`llvm emitter bug: join of unknown union ${unionId}`);
|
||||
const name = `sc_uj_${this.unionJoinFns.size}`;
|
||||
this.unionJoinFns.set(unionId, name);
|
||||
const toStr = this.unionToStrHelper(unionId);
|
||||
const unitTags = def.arms.flatMap((a, i) => (a.kind === "undefinedT" || a.kind === "nullT" ? [i] : []));
|
||||
const host = this.host;
|
||||
host.declare(`declare void @scr_jb_init(ptr)`);
|
||||
host.declare(`declare ptr @scr_jb_finish(ptr)`);
|
||||
host.declare(`declare double @scr_arr_len(ptr)`);
|
||||
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
const B = new BlockBuilder();
|
||||
const buf = "%jb";
|
||||
const iSlot = "%i";
|
||||
B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`, `${iSlot} = alloca double`);
|
||||
B.line(`call void @scr_jb_init(ptr ${buf})`);
|
||||
B.line(`store double ${f64Lit(0)}, ptr ${iSlot}`);
|
||||
const len = B.tmp();
|
||||
B.line(`${len} = call double @scr_arr_len(ptr %a)`);
|
||||
const lc = B.newLabel("uj.c");
|
||||
const lb = B.newLabel("uj.b");
|
||||
const ln = B.newLabel("uj.n");
|
||||
const le = B.newLabel("uj.e");
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
const i = B.tmp();
|
||||
const cont = B.tmp();
|
||||
B.line(`${i} = load double, ptr ${iSlot}`);
|
||||
B.line(`${cont} = fcmp olt double ${i}, ${len}`);
|
||||
B.condBr(cont, lb, le);
|
||||
B.startBlock(lb);
|
||||
// `if (i) put sep bytes` — separators between elements only.
|
||||
const nz = B.tmp();
|
||||
const lsep = B.newLabel("uj.s");
|
||||
const lel = B.newLabel("uj.v");
|
||||
B.line(`${nz} = fcmp ogt double ${i}, ${f64Lit(0)}`);
|
||||
B.condBr(nz, lsep, lel);
|
||||
B.startBlock(lsep);
|
||||
this.putScrStr(B, buf, "%sep");
|
||||
B.br(lel);
|
||||
B.startBlock(lel);
|
||||
const u = B.tmp();
|
||||
B.line(`${u} = call ptr @scr_arr_get_ref(ptr %a, double ${i}) ; element (+1)`);
|
||||
if (unitTags.length > 0) {
|
||||
// Nullish arms print empty, exactly JS's join.
|
||||
const tag = this.unionTag(B, u);
|
||||
let acc = "";
|
||||
for (const t of unitTags) {
|
||||
const cnd = B.tmp();
|
||||
B.line(`${cnd} = icmp eq i32 ${tag}, ${t}`);
|
||||
if (acc === "") {
|
||||
acc = cnd;
|
||||
} else {
|
||||
const o = B.tmp();
|
||||
B.line(`${o} = or i1 ${acc}, ${cnd}`);
|
||||
acc = o;
|
||||
}
|
||||
}
|
||||
const lskip = B.newLabel("uj.k");
|
||||
const lwrite = B.newLabel("uj.w");
|
||||
B.condBr(acc, lskip, lwrite);
|
||||
B.startBlock(lskip);
|
||||
B.line(`call void @scr_union_release(ptr ${u})`);
|
||||
B.br(ln);
|
||||
B.startBlock(lwrite);
|
||||
}
|
||||
const s = B.tmp();
|
||||
B.line(`${s} = call ptr @${toStr}(ptr ${u})`);
|
||||
this.putScrStr(B, buf, s);
|
||||
B.line(`call void @scr_str_release(ptr ${s})`);
|
||||
B.line(`call void @scr_union_release(ptr ${u})`);
|
||||
B.br(ln);
|
||||
B.startBlock(ln);
|
||||
const i2 = B.tmp();
|
||||
B.line(`${i2} = fadd double ${i}, ${f64Lit(1)}`);
|
||||
B.line(`store double ${i2}, ptr ${iSlot}`);
|
||||
B.br(lc);
|
||||
B.startBlock(le);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @scr_jb_finish(ptr ${buf})`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %a, ptr %sep) ${FN_ATTRS} { ; Array#join over ${unionId}: nullish arms print empty`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/* Name mangling. Distinct prefixes partition the C namespace:
|
||||
* sc_f_ user functions sc_l_ locals
|
||||
* sc_t emitter temporaries sc_lit_ interned string literals
|
||||
* scr_/Scr* runtime
|
||||
* so no denylist of C keywords or library names is needed.
|
||||
*
|
||||
* TS identifiers may contain characters C identifiers cannot ($, unicode);
|
||||
* those are encoded as _xHH..._ (hex code point) so distinct TS names can
|
||||
* never collide after mangling. '.' (from scope-flat local ids like "x.1")
|
||||
* maps to '_' — ids differing only by '.'/'_' cannot both come from the
|
||||
* frontend, which separates name and counter with '.' exclusively.
|
||||
*/
|
||||
|
||||
function sanitize(name: string): string {
|
||||
return name.replace(/[^A-Za-z0-9_]/g, (ch) =>
|
||||
ch === "." ? "_" : `_x${ch.codePointAt(0)!.toString(16)}_`,
|
||||
);
|
||||
}
|
||||
|
||||
export function mangleFunction(name: string): string {
|
||||
return `sc_f_${sanitize(name)}`;
|
||||
}
|
||||
|
||||
export function mangleLocal(localId: string): string {
|
||||
return `sc_l_${sanitize(localId)}`;
|
||||
}
|
||||
|
||||
/** Raw incoming value of a boxed parameter (the box itself takes the
|
||||
* sc_l_ name so every access goes through it). */
|
||||
export function mangleRawParam(localId: string): string {
|
||||
return `sc_p_${sanitize(localId)}`;
|
||||
}
|
||||
|
||||
/** Env-signature wrapper letting a plain declared function be a closure. */
|
||||
export function mangleWrapper(fnName: string): string {
|
||||
return `sc_w_${sanitize(fnName)}`;
|
||||
}
|
||||
|
||||
/** The interned (immortal) closure value of a declared function. */
|
||||
export function mangleFnClosure(fnName: string): string {
|
||||
return `sc_fc_${sanitize(fnName)}`;
|
||||
}
|
||||
|
||||
/** Per-class emitted C struct and RC helpers. */
|
||||
export function mangleClassStruct(className: string): string {
|
||||
return `sc_o_${sanitize(className)}`;
|
||||
}
|
||||
/** The immortal class OBJECT static (classes as first-class values). */
|
||||
export function mangleClassObj(className: string): string {
|
||||
return `sc_co_${sanitize(className)}`;
|
||||
}
|
||||
/** The construct thunk a class object's `ctor` slot points at. */
|
||||
export function mangleCtorThunk(className: string): string {
|
||||
return `sc_ct_${sanitize(className)}`;
|
||||
}
|
||||
export function mangleClassNew(className: string): string {
|
||||
return `sc_new_${sanitize(className)}`;
|
||||
}
|
||||
export function mangleClassRetain(className: string): string {
|
||||
return `sc_retain_${sanitize(className)}`;
|
||||
}
|
||||
export function mangleClassRelease(className: string): string {
|
||||
return `sc_release_${sanitize(className)}`;
|
||||
}
|
||||
export function mangleField(fieldName: string): string {
|
||||
return `sc_fld_${sanitize(fieldName)}`;
|
||||
}
|
||||
/** Hierarchy classes only (an `extends` anywhere): the per-class DIRECT
|
||||
* release — the whole-object teardown stored in the class's vtable, which
|
||||
* the public release reaches through the DYNAMIC object's vt pointer so a
|
||||
* base-typed release tears down the derived object's fields. */
|
||||
export function mangleClassReleaseDirect(className: string): string {
|
||||
return `sc_reld_${sanitize(className)}`;
|
||||
}
|
||||
/** The per-HIERARCHY vtable struct type (named after the root class): a
|
||||
* ScrVt head plus one member per virtual method slot. */
|
||||
export function mangleVtStruct(rootClassName: string): string {
|
||||
return `sc_vtt_${sanitize(rootClassName)}`;
|
||||
}
|
||||
/** The per-class static vtable instance. `sc_vtable_` and `sc_vtt_`
|
||||
* differ before any user-controlled text, so the two can never collide. */
|
||||
export function mangleVtInstance(className: string): string {
|
||||
return `sc_vtable_${sanitize(className)}`;
|
||||
}
|
||||
/** A virtual slot's member name inside the vtable struct. Sibling
|
||||
* branches of one hierarchy can each declare a slot for the SAME method
|
||||
* name (root-most in their own subtree, overridden below — mixin layers
|
||||
* make this routine): occurrence 0 keeps the historical spelling, later
|
||||
* occurrences take an ordinal after `sc_vs` — a method name cannot begin
|
||||
* with a digit, so the two spellings never collide. */
|
||||
export function mangleVtSlot(method: string, occurrence = 0): string {
|
||||
return occurrence === 0 ? `sc_vs_${sanitize(method)}` : `sc_vs${occurrence}_${sanitize(method)}`;
|
||||
}
|
||||
/** The adapter stored in a vtable slot: the implementing class's method
|
||||
* behind the slot's declaring-class `this` signature (the reinterpret that
|
||||
* prefix layout makes sound). */
|
||||
export function mangleVtAdapter(className: string, method: string): string {
|
||||
return `sc_vm_${sanitize(`${className}.${method}`)}`;
|
||||
}
|
||||
/** Cycle-collector entry points of a cycle-capable class: the trace
|
||||
* (enumerates cycle-capable fields) and the teardown (frees without
|
||||
* releasing traced fields) stored in the object's cycle header. */
|
||||
export function mangleClassTrace(className: string): string {
|
||||
return `sc_trace_${sanitize(className)}`;
|
||||
}
|
||||
export function mangleClassGcFree(className: string): string {
|
||||
return `sc_gcfree_${sanitize(className)}`;
|
||||
}
|
||||
|
||||
/** Per-record-shape emitted C struct and RC helpers. Shape ids are
|
||||
* compiler-generated (`r0`, `r1`, ...), but the prefixes are still disjoint
|
||||
* from every class prefix above (sc_rs_X vs sc_o_Y, sc_rnew_X vs
|
||||
* sc_new_Y, ...) so no user class name can collide with a record symbol. */
|
||||
export function mangleRecordStruct(shapeId: string): string {
|
||||
return `sc_rs_${sanitize(shapeId)}`;
|
||||
}
|
||||
export function mangleRecordNew(shapeId: string): string {
|
||||
return `sc_rnew_${sanitize(shapeId)}`;
|
||||
}
|
||||
export function mangleRecordRetain(shapeId: string): string {
|
||||
return `sc_rretain_${sanitize(shapeId)}`;
|
||||
}
|
||||
export function mangleRecordRelease(shapeId: string): string {
|
||||
return `sc_rrelease_${sanitize(shapeId)}`;
|
||||
}
|
||||
/** Cycle-collector entry points of a cycle-capable record shape (mirrors
|
||||
* the class pair above; prefixes stay disjoint from class symbols). */
|
||||
export function mangleRecordTrace(shapeId: string): string {
|
||||
return `sc_rtrace_${sanitize(shapeId)}`;
|
||||
}
|
||||
export function mangleRecordGcFree(shapeId: string): string {
|
||||
return `sc_rgcfree_${sanitize(shapeId)}`;
|
||||
}
|
||||
|
||||
/** Module-level variable ("%g.e.count" → "sc_g_e_count"). The id's tag
|
||||
* segment ("e."/"m<i>.") plus dot-free user identifiers make this injective. */
|
||||
export function mangleGlobal(globalId: string): string {
|
||||
return `sc_g_${sanitize(globalId.replace(/^%g\./, ""))}`;
|
||||
}
|
||||
|
||||
/** Async function machinery: spawn wrapper, trampoline, argument pack. */
|
||||
export function mangleAsyncSpawn(fnName: string): string {
|
||||
return `sc_as_${sanitize(fnName)}`;
|
||||
}
|
||||
export function mangleTrampoline(fnName: string): string {
|
||||
return `sc_tr_${sanitize(fnName)}`;
|
||||
}
|
||||
export function mangleArgPack(fnName: string): string {
|
||||
return `sc_ap_${sanitize(fnName)}`;
|
||||
}
|
||||
/** Generator function machinery: spawn wrapper (allocates the suspended
|
||||
* fiber — nothing runs) and the never-started argpack drop. The pack and
|
||||
* trampoline manglers are shared with async (a function is one or the
|
||||
* other, never both). */
|
||||
export function mangleGenSpawn(fnName: string): string {
|
||||
return `sc_gs_${sanitize(fnName)}`;
|
||||
}
|
||||
export function mangleGenDrop(fnName: string): string {
|
||||
return `sc_gd_${sanitize(fnName)}`;
|
||||
}
|
||||
/** Emitted ref-kind resolve thunk, interned per inner-type key. */
|
||||
export function mangleResolveThunk(n: number): string {
|
||||
return `sc_rslv_${n}`;
|
||||
}
|
||||
/** Emitted generator-resume result builder (the IteratorResult record),
|
||||
* interned per generator-type key. */
|
||||
export function mangleGenResThunk(n: number): string {
|
||||
return `sc_gres_${n}`;
|
||||
}
|
||||
/** Emitted Promise.race fulfillment adapter (entry inner type → result
|
||||
* inner type), interned per type pair. */
|
||||
export function mangleRaceThunk(n: number): string {
|
||||
return `sc_race_${n}`;
|
||||
}
|
||||
/** Emitted child-process exit adapter (the (code: number | null) callback
|
||||
* shape), interned per union id. */
|
||||
export function mangleChildExitThunk(n: number): string {
|
||||
return `sc_chexit_${n}`;
|
||||
}
|
||||
/** Emitted dgram message adapter (the (msg: Buffer, rinfo) listener shape
|
||||
* — the rinfo RECORD is program data), interned per shape id. */
|
||||
export function mangleDgramMsgThunk(n: number): string {
|
||||
return `sc_dgmsg_${n}`;
|
||||
}
|
||||
/** Emitted child-stream data adapter (the (chunk: Buffer | string)-union
|
||||
* listener shape — the union's tags are program data), interned per
|
||||
* union id. */
|
||||
export function mangleChildDataThunk(n: number): string {
|
||||
return `sc_chdata_${n}`;
|
||||
}
|
||||
/** Emitted bound-close adapter (`server.close.bind(server)` — the
|
||||
* callback union's tags are program data), interned per union id. */
|
||||
export function mangleCloseBindThunk(n: number): string {
|
||||
return `sc_clsbnd_${n}`;
|
||||
}
|
||||
/** Emitted close-override zero-arg wrapper (`server.close = fn`),
|
||||
* interned per (union id, return kind). */
|
||||
export function mangleCloseOverrideWrap(n: number): string {
|
||||
return `sc_ovwrap_${n}`;
|
||||
}
|
||||
/** Emitted dns.lookup callback adapter (the (err: Error | null, address,
|
||||
* family?) shape — the union's tags are program data), interned per
|
||||
* (union id, param count). */
|
||||
export function mangleDnsLookupThunk(n: number): string {
|
||||
return `sc_dnslk_${n}`;
|
||||
}
|
||||
/** Emitted SNI answer-closure thunk (the `(err, ctx?) => void` callback a
|
||||
* TLS server's SNICallback receives — its unions' tags are program data),
|
||||
* interned per cb func-type key. */
|
||||
export function mangleSniAnswerThunk(n: number): string {
|
||||
return `sc_snians_${n}`;
|
||||
}
|
||||
/** Emitted net.connect lookup answer thunk (the `(err, addresses) => void`
|
||||
* callback the caller's lookup option receives — its err union's tags and
|
||||
* the address record's layout are program data), interned per cb
|
||||
* func-type key. */
|
||||
export function mangleNetLookupAnswerThunk(n: number): string {
|
||||
return `sc_netlk_${n}`;
|
||||
}
|
||||
/** Emitted CONNECT-listener adapter for a union socket slot (the h2
|
||||
* compat listener — its union's tag is program data), interned per cb
|
||||
* func-type key. */
|
||||
export function mangleConnectSockThunk(n: number): string {
|
||||
return `sc_h2conn_${n}`;
|
||||
}
|
||||
|
||||
/** Emitted EventEmitter listener invoke adapter (va_list → the listener's
|
||||
* typed parameter prefix, each retained per the callee-owns convention),
|
||||
* interned per listener func-type key. */
|
||||
export function mangleEmitterInvokeThunk(n: number): string {
|
||||
return `sc_eeinv_${n}`;
|
||||
}
|
||||
|
||||
/** Emitted stream option-callback invoke adapter (the runtime calls the
|
||||
* user's read/write/... closure through it, stream-as-`this` first),
|
||||
* interned per (kind, callback func-type) key. */
|
||||
export function mangleStreamCbThunk(n: number): string {
|
||||
return `sc_scbinv_${n}`;
|
||||
}
|
||||
|
||||
/** Emitted stream completion-callback closure fn (the `callback` handed
|
||||
* to a user's write/final/destroy/transform/flush), interned per
|
||||
* (kind, done func-type) key. */
|
||||
export function mangleStreamDoneFn(n: number): string {
|
||||
return `sc_sdone_${n}`;
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
/* The static coverage report — the product's thesis rendered as text.
|
||||
*
|
||||
* Reframes "does my program compile?" (binary, frustrating) into "how static
|
||||
* is my program?" (a gradient you can improve): how many statements lowered
|
||||
* to native code and what exactly blocked the rest, blockers grouped by
|
||||
* code and ordered by impact. Built entirely from what the lowerer already
|
||||
* collects — the diagnostics registry is the coverage taxonomy.
|
||||
*/
|
||||
import type { Milestone, ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { NpmBuiltinUse, NpmLazyTrap } from "../frontend/npm.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
|
||||
export interface CoverageInput {
|
||||
file: string;
|
||||
/** Analyzed as a --dynamic build: island sites LOWER (their statements
|
||||
* land in stats.statementsIsland) instead of SC2013-fencing, and the
|
||||
* report renders them as "compile dynamically". */
|
||||
dynamic?: boolean;
|
||||
stats: LowerStats;
|
||||
diagnostics: ScrDiagnostic[];
|
||||
/** JS statements whose compile fences deferred to RUNTIME (runtimeFence
|
||||
* statements): the program builds, but executing one throws — the
|
||||
* report lists them so nothing hides. */
|
||||
runtimeFences?: ScrDiagnostic[];
|
||||
/** The unreached remainder: bodies nothing on the entry path reaches,
|
||||
* lowered in a throwaway analysis pass. Its blockers cannot fail a
|
||||
* build — the report shows them as a secondary, dimmed group. */
|
||||
unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[] };
|
||||
/** --dynamic only: every Node builtin the embedded npm graph imports —
|
||||
* what the island must provide for this program (unshimmed ones also
|
||||
* appear as SC2030 blockers, EXCEPT lazily-reached ones, which embed
|
||||
* the island's call-time throw instead of failing the build). */
|
||||
npmBuiltins?: NpmBuiltinUse[];
|
||||
/** --dynamic only: unresolvable specifiers reached ONLY by require()/
|
||||
* import() edges — embedded as runtime traps with Node's call-time
|
||||
* error shape; listed in the builtins table so the build output stays
|
||||
* honest about what the binary cannot reach. */
|
||||
npmLazyTraps?: NpmLazyTrap[];
|
||||
/** --npm-static: each requested (or auto-detected) package's outcome —
|
||||
* compiled statically as program modules, or fallen back to the island
|
||||
* with the first refusal reason. */
|
||||
npmStatic?: NpmStaticStatus[];
|
||||
/** --provenance-sources: the resolved source-mapped packages and the
|
||||
* fallback notes — the report renders per-package attribution from
|
||||
* statsByFile (statements inside each package's source dir). */
|
||||
provenance?: ProvenanceSources;
|
||||
/** --provenance-sources: per-file statement attribution from the emit
|
||||
* pass (LowerResult.statsByFile). */
|
||||
statsByFile?: Map<string, { total: number; failed: number; island: number }>;
|
||||
/** --provenance-sources: diagnostics of elided pure-annotated dead
|
||||
* consts (off the build; the provenance section names them). */
|
||||
provenanceElided?: ScrDiagnostic[];
|
||||
/** True when tsc preflight failed — no coverage can be computed. */
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
|
||||
/** One --npm-static package's outcome for the report. */
|
||||
export interface NpmStaticStatus {
|
||||
package: string;
|
||||
status: "static" | "fallback";
|
||||
/** The fallback's first refusal reason (fallback rows only). */
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
const GREEN = "\x1b[32m";
|
||||
const YELLOW = "\x1b[33m";
|
||||
const RED = "\x1b[31m";
|
||||
const DIM = "\x1b[2m";
|
||||
const BOLD = "\x1b[1m";
|
||||
const RESET = "\x1b[0m";
|
||||
|
||||
const MILESTONE_ORDER: Milestone[] = ["M1", "M2", "M3", "M4", "M5", "later"];
|
||||
|
||||
/** Codes whose construct RUNS under --dynamic (the embedded engine) — the
|
||||
* report separates "one flag away" from "cannot compile at all". */
|
||||
const DYNAMIC_CAPABLE = new Set(["SC2010", "SC2011", "SC2012", "SC2013"]);
|
||||
|
||||
interface Blocker {
|
||||
code: string;
|
||||
/** Message with the " (planned: ...)" suffix stripped — the milestone gets
|
||||
* its own column. */
|
||||
what: string;
|
||||
milestone: Milestone | undefined;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function renderCoverage(input: CoverageInput, opts: { color?: boolean } = {}): string {
|
||||
const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
|
||||
const out: string[] = [];
|
||||
out.push(`${c(BOLD, "scriptc coverage")} ${DIMPath(input.file, opts.color ?? false)}`);
|
||||
out.push("");
|
||||
|
||||
if (input.preflightFailed) {
|
||||
// Three preflight failure shapes, in order of "whose problem is it":
|
||||
// an incompatible project config (SC0002 — ours to explain), real
|
||||
// TypeScript errors (theirs to fix), and OUR import-form fences
|
||||
// (SC1xxx from checkPreflight — lowering-style blockers that shape
|
||||
// the module graph, reported as blockers instead of masquerading as
|
||||
// "TypeScript errors").
|
||||
const config = input.diagnostics.filter((d) => d.code === "SC0002");
|
||||
if (config.length > 0) {
|
||||
out.push(` ${c(RED, "not analyzable")}: ${config[0]!.message}`);
|
||||
return out.join("\n");
|
||||
}
|
||||
const tsc = input.diagnostics.filter((d) => d.code === "SC0001");
|
||||
if (tsc.length > 0) {
|
||||
out.push(
|
||||
` ${c(RED, "not analyzable")}: ${tsc.length} TypeScript error${tsc.length === 1 ? "" : "s"} — ` +
|
||||
`fix type errors first (scriptc only analyzes programs that typecheck)`,
|
||||
);
|
||||
return out.join("\n");
|
||||
}
|
||||
const n = input.diagnostics.length;
|
||||
out.push(
|
||||
` ${c(RED, "analysis stopped at preflight")}: ${n} unsupported import site${n === 1 ? "" : "s"} — ` +
|
||||
`the module graph must load before statements can be counted`,
|
||||
);
|
||||
const blockers = groupBlockers(input.diagnostics);
|
||||
const widest = Math.max(...blockers.map((b) => b.what.length));
|
||||
for (const b of blockers) {
|
||||
out.push(
|
||||
` ${c(RED, `×${b.count}`.padStart(4))} ${b.what.padEnd(widest)} ${c(DIM, b.code)}`,
|
||||
);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// Top-line numbers stay whole-program: reached statements plus the
|
||||
// unreached remainder the analysis lowered anyway. Under --dynamic the
|
||||
// island-lowered statements split out as "compile dynamically" — they
|
||||
// BUILD, but their work runs in the embedded engine, and counting them
|
||||
// as static would overstate how native the program is.
|
||||
const un = input.unreached;
|
||||
const total = input.stats.statementsTotal + (un?.stats.statementsTotal ?? 0);
|
||||
const failed = input.stats.statementsFailed + (un?.stats.statementsFailed ?? 0);
|
||||
const island = input.stats.statementsIsland + (un?.stats.statementsIsland ?? 0);
|
||||
const skipped = input.stats.functionsSkipped + (un?.stats.functionsSkipped ?? 0);
|
||||
const ok = total - failed - island;
|
||||
const pct = total === 0 ? 100 : Math.floor((ok / total) * 100);
|
||||
const pctColor = pct === 100 ? GREEN : pct >= 75 ? YELLOW : RED;
|
||||
|
||||
out.push(` statements analyzed ${total}`);
|
||||
out.push(` compile statically ${ok} ${c(pctColor, c(BOLD, `(${pct}%)`))}`);
|
||||
if (input.dynamic) {
|
||||
const ipct = total === 0 ? 0 : Math.floor((island / total) * 100);
|
||||
out.push(
|
||||
` compile dynamically ${island} ${c(YELLOW, c(BOLD, `(${ipct}%)`))} ${c(DIM, "(island sites — the embedded engine runs them)")}`,
|
||||
);
|
||||
}
|
||||
if (skipped > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, `(+${skipped} function${skipped === 1 ? "" : "s"} not analyzed — signature blocked)`)}`,
|
||||
);
|
||||
}
|
||||
out.push("");
|
||||
|
||||
// --npm-static outcomes: which opted-in packages compiled statically as
|
||||
// program modules and which fell back to the island (with the first
|
||||
// refusal reason) — the flag's honesty section.
|
||||
const npmStatic = input.npmStatic ?? [];
|
||||
if (npmStatic.length > 0) {
|
||||
out.push(` ${c(DIM, "npm packages compiled statically (--npm-static):")}`);
|
||||
const widestP = Math.max(...npmStatic.map((s) => s.package.length));
|
||||
for (const s of npmStatic) {
|
||||
const status =
|
||||
s.status === "static"
|
||||
? c(GREEN, "static")
|
||||
: c(YELLOW, "island fallback") + (s.detail !== undefined ? ` ${c(DIM, `(${s.detail})`)}` : "");
|
||||
out.push(` ${s.package.padEnd(widestP)} ${status}`);
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// What the embedded npm code asks of the island: every Node builtin the
|
||||
// graph imports, with its shim status, plus every unresolvable specifier
|
||||
// only require()/import() edges reach. Eagerly-blocked builtins also
|
||||
// surface as SC2030 blockers below; "lazy trap" rows DON'T fail the
|
||||
// build — the binary embeds Node's call-time error and the call throws
|
||||
// at runtime, exactly where Node would have failed.
|
||||
const builtins = (input.dynamic && input.npmBuiltins) || [];
|
||||
const traps = (input.dynamic && input.npmLazyTraps) || [];
|
||||
if (builtins.length > 0 || traps.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, traps.length > 0 ? "embedded npm code imports Node builtins and unresolved specifiers:" : "embedded npm code imports Node builtins:")}`,
|
||||
);
|
||||
const widestB = Math.max(
|
||||
...builtins.map((b) => b.builtin.length),
|
||||
...traps.map((t) => t.specifier.length),
|
||||
);
|
||||
// Pad the plain words before coloring — escape codes have no width.
|
||||
const widestS = Math.max(
|
||||
...builtins.map((b) => (b.shimmed ? 7 : b.lazy ? 23 : 11)),
|
||||
...traps.map(() => 24),
|
||||
);
|
||||
for (const b of builtins) {
|
||||
const status = b.shimmed
|
||||
? c(GREEN, "shimmed".padEnd(widestS))
|
||||
: b.lazy
|
||||
? c(YELLOW, "not shimmed — lazy trap".padEnd(widestS))
|
||||
: c(RED, "not shimmed".padEnd(widestS));
|
||||
out.push(
|
||||
` ${b.builtin.padEnd(widestB)} ${status} ${c(DIM, `(${b.packages.join(", ")})`)}`,
|
||||
);
|
||||
}
|
||||
for (const t of traps) {
|
||||
// A native .node addon RESOLVED — the island cannot dlopen it, so
|
||||
// the build embedded a throwing ERR_DLOPEN_FAILED stub instead of
|
||||
// the addon's machine code; unresolvable rows keep Node's not-found
|
||||
// error shapes.
|
||||
const what = t.native ? "native addon — lazy trap" : "unresolvable — lazy trap";
|
||||
out.push(
|
||||
` ${t.specifier.padEnd(widestB)} ${c(YELLOW, what.padEnd(widestS))} ` +
|
||||
c(DIM, `(${t.via.join("/")} in ${t.packages.join(", ")})`),
|
||||
);
|
||||
}
|
||||
if (builtins.some((b) => b.lazy) || traps.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, "(lazy trap: only reachable through require()/import() boundaries — the build embeds Node's call-time error; the call throws at runtime)")}`,
|
||||
);
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// --provenance-sources: per-package attribution — the answer to "did
|
||||
// the DEPENDENCY's statements compile static?", computed by summing
|
||||
// statsByFile entries under each package's source dir. Elided
|
||||
// pure-annotated dead consts and fallback notes render here too, so
|
||||
// the provenance story is complete in one place.
|
||||
if (input.provenance !== undefined) {
|
||||
const prov = input.provenance;
|
||||
out.push(` ${c(BOLD, "provenance sources")} ${c(DIM, "(attested source compiled as program modules)")}`);
|
||||
for (const pkg of prov.packages) {
|
||||
let pTotal = 0;
|
||||
let pFailed = 0;
|
||||
let pIsland = 0;
|
||||
const dir = pkg.dir.endsWith("/") ? pkg.dir : `${pkg.dir}/`;
|
||||
for (const [file, s] of input.statsByFile ?? []) {
|
||||
if (!file.startsWith(dir)) continue;
|
||||
pTotal += s.total;
|
||||
pFailed += s.failed;
|
||||
pIsland += s.island;
|
||||
}
|
||||
const elided = (input.provenanceElided ?? []).filter((d) => d.loc.file.startsWith(dir)).length;
|
||||
const pOk = pTotal - pFailed - pIsland;
|
||||
const pPct = pTotal === 0 ? 100 : Math.floor((pOk / pTotal) * 100);
|
||||
const pColor = pPct === 100 ? GREEN : pPct >= 75 ? YELLOW : RED;
|
||||
const shortRepo = pkg.repo.replace(/^git\+/, "").replace(/^https:\/\//, "").replace(/@refs\/.*$/, "");
|
||||
out.push(
|
||||
` ${pkg.name}@${pkg.version} ${c(DIM, `${shortRepo} @ ${pkg.commit.slice(0, 12)}`)}`,
|
||||
);
|
||||
out.push(
|
||||
` ${pTotal} statements: ${pOk} static ${c(pColor, c(BOLD, `(${pPct}%)`))}, ${pIsland} island, ${pFailed} fenced` +
|
||||
(elided > 0 ? ` ${c(DIM, `(${elided} fence${elided === 1 ? "" : "s"} elided as @__PURE__ dead consts — uses would fence per site)`)}` : ""),
|
||||
);
|
||||
}
|
||||
for (const note of prov.notes) out.push(` ${c(YELLOW, "note")} ${note}`);
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// JS statements whose fences DEFERRED to runtime (the runtimeFence
|
||||
// statements the module carries): they compile, but executing one
|
||||
// throws its SC-coded error — the report lists them so a "compiling"
|
||||
// JS program never hides its dynamic gaps.
|
||||
const fences = input.runtimeFences ?? [];
|
||||
if (fences.length > 0) {
|
||||
const grouped = groupBlockers(fences);
|
||||
const widestF = Math.max(...grouped.map((b) => b.what.length));
|
||||
out.push(
|
||||
` ${c(YELLOW, "deferred to runtime")} ${fences.length} site${fences.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`,
|
||||
);
|
||||
for (const b of grouped) {
|
||||
out.push(
|
||||
` ${c(YELLOW, `×${b.count}`.padStart(4))} ${b.what.padEnd(widestF)} ${c(DIM, b.code)}`,
|
||||
);
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
|
||||
const unreachedDiags = un?.diagnostics ?? [];
|
||||
if (failed === 0 && input.diagnostics.length === 0 && unreachedDiags.length === 0) {
|
||||
out.push(
|
||||
island > 0
|
||||
? ` ${c(GREEN, "builds with --dynamic")} — no remaining blockers (the island sites above run in the embedded engine).`
|
||||
: ` ${c(GREEN, "fully static")} — this program has no dynamic remainder.`,
|
||||
);
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// Ordered by internal scheduling (soonest first), then by frequency —
|
||||
// the top line is the blocker most likely to disappear next. The
|
||||
// milestone labels themselves are maintainer business and never rendered.
|
||||
// Blockers split by what a flag can fix: sites that RUN under --dynamic
|
||||
// (the embedded engine) versus constructs with no lowering at all.
|
||||
const blockers = groupBlockers(input.diagnostics);
|
||||
const unreachedBlockers = groupBlockers(unreachedDiags);
|
||||
const dynamic = blockers.filter((b) => DYNAMIC_CAPABLE.has(b.code));
|
||||
const rejected = blockers.filter((b) => !DYNAMIC_CAPABLE.has(b.code));
|
||||
const widest = Math.max(...[...blockers, ...unreachedBlockers].map((b) => b.what.length));
|
||||
const renderGroup = (group: Blocker[], dim = false) => {
|
||||
for (const b of group) {
|
||||
const what = b.what.padEnd(widest);
|
||||
out.push(
|
||||
` ${c(dim ? DIM : RED, `×${b.count}`.padStart(4))} ${dim ? c(DIM, what) : what} ${c(DIM, b.code)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
if (dynamic.length > 0) {
|
||||
const sites = dynamic.reduce((n, b) => n + b.count, 0);
|
||||
out.push(
|
||||
` ${c(YELLOW, `runs with --dynamic`)} ${sites} site${sites === 1 ? "" : "s"} ${c(DIM, "(embeds a JS engine, ~620KB — static stays the default)")}`,
|
||||
);
|
||||
renderGroup(dynamic);
|
||||
out.push("");
|
||||
}
|
||||
if (rejected.length > 0) {
|
||||
out.push(` blockers:`);
|
||||
renderGroup(rejected);
|
||||
}
|
||||
// Blockers in code nothing on the entry path reaches: never lowered by
|
||||
// a build, so they cannot fail one — secondary by design.
|
||||
if (unreachedBlockers.length > 0) {
|
||||
if (rejected.length > 0) out.push("");
|
||||
out.push(` ${c(DIM, "in unreached code")} ${c(DIM, "(never lowered — cannot fail a build)")}`);
|
||||
renderGroup(unreachedBlockers, true);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function DIMPath(file: string, color: boolean): string {
|
||||
return color ? DIM + file + RESET : file;
|
||||
}
|
||||
|
||||
function groupBlockers(diags: ScrDiagnostic[]): Blocker[] {
|
||||
const byCode = new Map<string, Blocker>();
|
||||
for (const d of diags) {
|
||||
const what = d.message.replace(/ (?:is|are) not supported yet$/, "");
|
||||
const key = `${d.code}:${what}`;
|
||||
const existing = byCode.get(key);
|
||||
if (existing) existing.count++;
|
||||
else byCode.set(key, { code: d.code, what, milestone: d.milestone, count: 1 });
|
||||
}
|
||||
const rank = (m: Milestone | undefined) =>
|
||||
m === undefined ? MILESTONE_ORDER.length : MILESTONE_ORDER.indexOf(m);
|
||||
return [...byCode.values()].sort(
|
||||
(a, b) =>
|
||||
rank(a.milestone) - rank(b.milestone) ||
|
||||
b.count - a.count ||
|
||||
a.code.localeCompare(b.code),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
/* Diagnostic codes live in ONE registry. Message strings are never inlined
|
||||
* at rejection sites — every "not supported yet" the compiler can produce is
|
||||
* enumerable here, which is the seed of the future coverage report
|
||||
* ("N constructs prevented static compilation, grouped by code").
|
||||
*
|
||||
* Code bands:
|
||||
* SC0xxx preflight gates: TypeScript errors passed through (SC0001),
|
||||
* project-config incompatibilities (SC0002), malformed imported
|
||||
* JSON modules (SC0003), and upstream tsgo checker panics
|
||||
* surfaced as source-anchored diagnostics (SC0004)
|
||||
* SC1xxx supported-TypeScript-not-yet: valid TS outside the current subset
|
||||
* SC2xxx scriptc type rules (types we cannot compile yet)
|
||||
* SC3xxx backend coverage: the program is compilable (the C backend
|
||||
* builds it) but the selected alternate backend's tier does not
|
||||
* include some IR construct yet (SC3001 — the LLVM backend's
|
||||
* refusal, minted in index.ts from LlvmUnsupportedError)
|
||||
* SC9xxx internal compiler errors (still source-anchored)
|
||||
*/
|
||||
import type { SrcLoc } from "../ir/nodes.js";
|
||||
|
||||
/* Internal prioritization buckets. NEVER rendered to users — user-facing
|
||||
* output says only what is and isn't supported (plus hints); scheduling is
|
||||
* maintainer business. Used to order blockers in the coverage report. */
|
||||
export type Milestone = "M1" | "M2" | "M3" | "M4" | "M5" | "later";
|
||||
|
||||
export interface ScrDiagnostic {
|
||||
code: `SC${number}`;
|
||||
message: string;
|
||||
loc: SrcLoc;
|
||||
milestone?: Milestone;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
interface UnsupportedEntry {
|
||||
feature: string;
|
||||
milestone: Milestone;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
|
||||
SC1010: {
|
||||
feature: "package imports",
|
||||
milestone: "M4",
|
||||
hint:
|
||||
"relative imports (./file, ../dir/file), package.json-mediated project " +
|
||||
"imports (#alias via the imports field, self-name references via " +
|
||||
"exports), installed npm packages (their code runs under --dynamic), " +
|
||||
"and the built-in fs, fs/promises, path, os, url, crypto, zlib, " +
|
||||
"child_process, net, http, tls, https, http2, dgram, dns, util, " +
|
||||
"util/types, string_decoder, readline, events, stream, buffer, " +
|
||||
"assert, assert/strict, worker_threads, cluster, tty, async_hooks, " +
|
||||
"timers, timers/promises, diagnostics_channel, and node:test modules " +
|
||||
"(bare or node:-prefixed) are supported",
|
||||
},
|
||||
// SC1011 (exports) shipped via export modifiers — retired, do not reuse.
|
||||
// Default exports/imports SHIPPED (expression, function/class declaration
|
||||
// — named and anonymous — `export { x as default }`, default re-exports,
|
||||
// and the CJS default interop). SC1012's remaining uses are the residual
|
||||
// edges, each named at its site: default imports of builtin modules
|
||||
// without a default surface, default-exporting a MUTABLE binding (Node
|
||||
// snapshots; not modeled), named imports from JSON modules, and the
|
||||
// require()-of-JSON forms.
|
||||
SC1012: {
|
||||
feature: "default exports/imports",
|
||||
milestone: "later",
|
||||
hint: "use named exports: export function f() {} / import { f } from \"./m\"",
|
||||
},
|
||||
// Namespace imports of USER modules shipped (`import * as ns from
|
||||
// "./m"` and `export * as ns from "./m"`: ns.member resolves statically
|
||||
// to the exporter's own registrations). SC1013's remaining uses name
|
||||
// their edges: the ns OBJECT as a first-class value, namespace imports
|
||||
// of JSON/CommonJS modules, runtime import = require(...) forms, and
|
||||
// require() in an ES module.
|
||||
SC1013: { feature: "namespace imports (* as ns)", milestone: "later" },
|
||||
SC1014: {
|
||||
feature: "re-exports and export lists",
|
||||
milestone: "later",
|
||||
hint: "export declarations directly: export function f() {}",
|
||||
},
|
||||
SC1015: { feature: "dynamic import()", milestone: "M4" },
|
||||
SC1016: { feature: "circular imports", milestone: "later" },
|
||||
// Class DECLARATIONS shipped; SC1020's remaining use is class expressions.
|
||||
SC1020: { feature: "class expressions", milestone: "later" },
|
||||
// SC1021 (arrow functions/closures) and SC1022 (nested function
|
||||
// declarations) shipped — codes retired, do not reuse.
|
||||
// SC1023 (object literals) shipped as records — code retired, do not
|
||||
// reuse. (Spread, computed keys, getters/setters and `this` in
|
||||
// object-literal methods remain rejected via SC1090.)
|
||||
// SC1024 (arrays) shipped — code retired, do not reuse.
|
||||
// `var` declarations shipped (function-scope hoisting, redeclaration
|
||||
// merge, the shared-binding loop capture semantics). SC1030's remaining
|
||||
// uses are the var edges with no honest lowering — each names its shape
|
||||
// and remedy inline (a reference above the declaration whose early reads
|
||||
// would be an unrepresentable `undefined`; `var` loop bindings in
|
||||
// `for await`).
|
||||
SC1030: {
|
||||
feature: "var declarations",
|
||||
milestone: "later",
|
||||
},
|
||||
SC1031: { feature: "destructuring", milestone: "M2" },
|
||||
// SC1032 (multiple declarations in one statement) shipped — code retired,
|
||||
// do not reuse. (Multi-declaration for-loop initializers remain rejected
|
||||
// via SC1090.)
|
||||
// SC1033 (let without an initializer) shipped — code retired, do not
|
||||
// reuse.
|
||||
SC1040: {
|
||||
feature: "loose equality (== and !=)",
|
||||
milestone: "M4",
|
||||
hint: "use === / !== ('x == null' / 'x != null' — the null-or-undefined test — is supported; other loose comparisons need dynamic coercion semantics)",
|
||||
},
|
||||
// SC1041 (bitwise operators, ToInt32 semantics) shipped — code retired,
|
||||
// do not reuse.
|
||||
SC1042: {
|
||||
feature: "logical operators on mixed operand types",
|
||||
milestone: "later",
|
||||
hint: "give both operands the same type (number, string, or boolean)",
|
||||
},
|
||||
SC1043: { feature: "comparing non-number, non-string values", milestone: "M1" },
|
||||
SC1045: {
|
||||
feature: "increment/decrement in expression position",
|
||||
milestone: "later",
|
||||
hint: "use ++/-- as a standalone statement, or write x = x + 1",
|
||||
},
|
||||
SC1050: { feature: "labeled break/continue", milestone: "later" },
|
||||
// SC1051 (do-while loops) shipped — code retired, do not reuse.
|
||||
SC1052: { feature: "for-in loops", milestone: "M4" }, // for-of over arrays shipped in M1
|
||||
// SC1060 (switch statements) shipped — code retired, do not reuse.
|
||||
// SC1061 (exceptions: throw/try/catch/finally) shipped — code retired, do
|
||||
// not reuse. (Identifier catch bindings shipped too; what remains rejected
|
||||
// is destructuring patterns — SC1062 — un-narrowed binding uses —
|
||||
// SC1063 — and jumps crossing a finally — SC1090.)
|
||||
SC1062: {
|
||||
feature: "destructuring catch clause bindings",
|
||||
milestone: "later",
|
||||
hint: "bind an identifier (catch (e)) and narrow it — 'e instanceof Error' exposes .name/.message, typeof tests expose primitives",
|
||||
},
|
||||
SC1063: {
|
||||
feature: "this use of a catch binding",
|
||||
milestone: "later",
|
||||
hint: "a catch binding is typed by what was thrown — narrow before reading: 'if (e instanceof Error)' (then .name/.message), 'typeof e === \"string\"/\"number\"/\"boolean\"', or rethrow with 'throw e'; String(e) and `${e}` also compile ('Error: msg' for Error payloads — Node's String(), which has no stack), so 'e instanceof Error ? e.message : String(e)' works as-is",
|
||||
},
|
||||
SC1070: { feature: "async/await", milestone: "M3" },
|
||||
SC1071: { feature: "generators", milestone: "later" },
|
||||
// `this` in class methods (+ lexical capture by arrows) shipped; what
|
||||
// remains is `this` outside any method (top level, plain functions).
|
||||
SC1080: { feature: "'this' outside a class method", milestone: "later" },
|
||||
SC1090: { feature: "this syntax", milestone: "later" }, // generic fallback; message names the construct
|
||||
SC1100: {
|
||||
feature: "operations on 'unknown' values",
|
||||
milestone: "later",
|
||||
hint: "validate with 'as <type>' first — the cast checks the dynamic value at runtime and throws on mismatch",
|
||||
},
|
||||
SC1101: {
|
||||
feature: "converting typed values to 'unknown'",
|
||||
milestone: "later",
|
||||
hint: "numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy — the 'unknown' value never aliases the original), and functions over those (boxed, identity preserved) convert into 'unknown' slots; 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.
|
||||
SC1120: {
|
||||
feature: "this regex feature",
|
||||
milestone: "later",
|
||||
hint: "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .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)",
|
||||
},
|
||||
};
|
||||
|
||||
export function unsupportedDiag(
|
||||
code: keyof typeof UNSUPPORTED & `SC${number}`,
|
||||
loc: SrcLoc,
|
||||
featureOverride?: string,
|
||||
hintOverride?: string,
|
||||
): ScrDiagnostic {
|
||||
const entry = UNSUPPORTED[code]!;
|
||||
const feature = featureOverride ?? entry.feature;
|
||||
const diag: ScrDiagnostic = {
|
||||
code,
|
||||
message: `${feature} ${plural(feature) ? "are" : "is"} not supported yet`,
|
||||
loc,
|
||||
milestone: entry.milestone,
|
||||
};
|
||||
const hint = hintOverride ?? entry.hint;
|
||||
if (hint !== undefined) diag.hint = hint;
|
||||
return diag;
|
||||
}
|
||||
|
||||
function plural(feature: string): boolean {
|
||||
const head = feature.split(" (")[0]!.trim();
|
||||
return /s$/.test(head) && !/(?:ness|this|ss)$/.test(head);
|
||||
}
|
||||
|
||||
export function tscPassthroughDiag(message: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return { code: "SC0001", message, loc };
|
||||
}
|
||||
|
||||
/** The project's tsconfig disables strictNullChecks, which scriptc cannot
|
||||
* adopt: null and undefined are DISTINCT STATIC TYPES in the value model
|
||||
* (union arms with their own runtime representation) — with the knob off,
|
||||
* every type silently includes them and nothing about the model holds. The
|
||||
* config gate fails loudly instead of silently re-enabling the knob. */
|
||||
/** True for the sync channel's surfaced Go panics: typescript-go wraps a
|
||||
* server-side panic into a thrown Error whose message starts "panic:" and
|
||||
* keeps the server intact (the prefetch fence's original finding). The
|
||||
* 2026-07-19 sweep's three upstream panics — the TupleType interface
|
||||
* conversion, the import.defer Debug failure, the 1e999 JSON marshal — all
|
||||
* arrive in this shape. */
|
||||
export function isCheckerPanic(e: unknown): e is Error {
|
||||
return e instanceof Error && e.message.startsWith("panic:");
|
||||
}
|
||||
|
||||
/** A Go panic inside the typescript-go checker reached through a query
|
||||
* about this source position. Upstream bugs from scriptc's point of view —
|
||||
* but scriptc still owes a source-anchored diagnostic, never a crashed
|
||||
* CLI. */
|
||||
export function checkerPanicDiag(detail: string, loc: SrcLoc): ScrDiagnostic {
|
||||
// Go's encoding/json/v2 Hyrum-proofs its error strings: each process
|
||||
// coin-flips between "cannot" and "unable to" as the modal verb
|
||||
// (errorModalVerb over randomized map iteration), so the same panic
|
||||
// renders two ways across tsgo spawns. scriptc's output is deterministic;
|
||||
// pin one spelling before relaying.
|
||||
const stable = detail.replace(/\bunable to (marshal|unmarshal|handle)\b/g, "cannot $1");
|
||||
return {
|
||||
code: "SC0004",
|
||||
message: `the TypeScript checker crashed answering a query here (an upstream typescript-go bug): ${stable}`,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** An imported .json module whose text strict JSON.parse rejects (a leading
|
||||
* `//` comment line — the importAttributes11 shape). tsgo tolerates it, so
|
||||
* the SC0001 passthrough never fires; Node itself refuses to import such a
|
||||
* module at runtime, and the honest answer is a source-anchored gate at the
|
||||
* import, never an uncaught parse throw. */
|
||||
export function invalidJsonModuleDiag(fileName: string, detail: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC0003",
|
||||
message: `imported JSON module '${fileName}' is not valid JSON (${detail})`,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function strictNullChecksFloorDiag(configFile: string): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC0002",
|
||||
message:
|
||||
"this project's tsconfig disables strictNullChecks, which scriptc requires " +
|
||||
"(null and undefined are modeled as distinct static types; without the knob " +
|
||||
"every type silently includes them)",
|
||||
loc: { file: configFile, start: 0, end: 0 },
|
||||
hint: 'set "strictNullChecks": true (or "strict": true) in the project tsconfig',
|
||||
};
|
||||
}
|
||||
|
||||
export function unsupportedTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic {
|
||||
// `any` gets its own wording AND its own code: it is not an unsupported
|
||||
// value type but a construct with a DYNAMIC lowering — under --dynamic,
|
||||
// any-typed code runs in the embedded engine. This build didn't opt in,
|
||||
// so each site reports the choice: pay ~620KB, or stay static with
|
||||
// 'unknown' + a checked cast. The distinct code lets the coverage report
|
||||
// separate "runs with --dynamic" from "cannot compile". One voice with
|
||||
// SC2010/SC2012: same message shape, same cost figure, same
|
||||
// stay-static alternative shape.
|
||||
if (typeText === "any") {
|
||||
return {
|
||||
code: "SC2011",
|
||||
message: `values of type 'any' run in the embedded dynamic engine, which this build does not include`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
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')",
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: "SC2001",
|
||||
message:
|
||||
`values of type '${typeText}' cannot be compiled yet ` +
|
||||
`(supported: number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')`,
|
||||
loc,
|
||||
milestone: "M2",
|
||||
};
|
||||
}
|
||||
|
||||
/** A type with a DYNAMIC representation but no static one — `any[]`,
|
||||
* records/functions with `any`-typed members, or a shipped-.d.ts type whose
|
||||
* values are island handles. The caller PROVED the claim by re-running
|
||||
* mapType with `dynamic: true` before choosing this wording, so the message
|
||||
* is a measured fact, not a guess. SC2011 like the bare-`any` arm of
|
||||
* unsupportedTypeDiag (one dynamic-family code per story: this is the
|
||||
* "values of this TYPE run in the engine" story), and "runs in" voice like
|
||||
* SC2012 — the value exists under --dynamic; the build just didn't opt in. */
|
||||
export function requiresDynamicTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2011",
|
||||
message: `values of type '${typeText}' have no static representation but run in the embedded dynamic engine, which this build does not include`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
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)",
|
||||
};
|
||||
}
|
||||
|
||||
/** A value whose type keeps a GENERIC call signature (`let g: <T>(x: T) =>
|
||||
* T`, a stored generic function, a call result the checker left
|
||||
* higher-order generic): every compiled function is one concrete
|
||||
* signature, so a slot that keeps type parameters has no instance to hold
|
||||
* — the generic-value monomorphization rule, named instead of the generic
|
||||
* supported-types recitation. */
|
||||
export function genericSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2001",
|
||||
message: `values of type '${typeText}' cannot be compiled: the signature keeps its type parameters, and a compiled function is always one concrete signature (generic functions monomorphize per pinned signature — declarations, methods, and never-reassigned module-scope bindings initialized with a generic arrow/function expression)`,
|
||||
loc,
|
||||
milestone: "M2",
|
||||
hint: "annotate the destination with a concrete signature (e.g. '(x: number) => number'), instantiate explicitly ('f<number>'), or call the generic function directly",
|
||||
};
|
||||
}
|
||||
|
||||
/** An index-signature object type that STILL does not map: STRING and
|
||||
* NUMBER index signatures compile (declared fields + an overflow map for
|
||||
* undeclared keys, number keys canonicalized to their JS string spelling),
|
||||
* so reaching this means a symbol-keyed signature, dual signatures with
|
||||
* UNEQUAL value types, a value type outside the supported set (functions,
|
||||
* Maps, promises, ...), or a declared `unknown` field under the signature.
|
||||
* The message points at the working alternatives. */
|
||||
export function indexSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2001",
|
||||
message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, or 'unknown')`,
|
||||
loc,
|
||||
milestone: "M2",
|
||||
hint: "string- and number-keyed index signatures over the supported value types compile directly; richer value types want Map<string, V>, and symbol keys have no lowering",
|
||||
};
|
||||
}
|
||||
|
||||
/** A REACHED use of standard-library surface with no lowering — the scope
|
||||
* fence of the real-lib type world. The checker sees the full es2025
|
||||
* standard library, so `Symbol`, `new Date()`, `p.then(...)`, `Math.min`
|
||||
* with three arguments all TYPECHECK; each use site of surface (or a call
|
||||
* form of it) that nothing lowers reports here instead of the old minimal-
|
||||
* ambient "Cannot find name" — and never ICEs. `surface` names what was
|
||||
* used ('Symbol', 'Math.sumPrecise', 'new Date', 'string.normalize'). */
|
||||
export function noLoweringDiag(
|
||||
surface: string,
|
||||
loc: SrcLoc,
|
||||
hint?: string,
|
||||
viaNodeTypes = false,
|
||||
): ScrDiagnostic {
|
||||
// Same code, two provenances: with the project's @types/node adopted,
|
||||
// the checker also sees everything IT declares (Buffer, process.stdout,
|
||||
// fetch, the undici web globals, ...) — a reached use of node-typed
|
||||
// surface nothing lowers is the same fence, blamed at @types/node so the
|
||||
// report reads honestly ("typed by @types/node" — not "standard
|
||||
// library").
|
||||
return {
|
||||
code: "SC2020",
|
||||
message: viaNodeTypes
|
||||
? `'${surface}' is typed by @types/node but has no scriptc lowering yet`
|
||||
: `'${surface}' is part of the standard library types but has no scriptc lowering yet`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint:
|
||||
hint ??
|
||||
(viaNodeTypes
|
||||
? "the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations)"
|
||||
: "the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Records are monomorphic structs, so a value's shape must match the
|
||||
* expected shape EXACTLY — TypeScript's structural width subtyping
|
||||
* (`{a: 1, b: 2}` where `{a: number}` is expected) would need a runtime
|
||||
* shape coercion that doesn't exist yet. */
|
||||
export function recordShapeMismatchDiag(
|
||||
expectedText: string,
|
||||
actualText: string,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2002",
|
||||
message:
|
||||
`record shapes must match exactly: expected '${expectedText}', got '${actualText}' ` +
|
||||
`(structural width subtyping needs a shape coercion that is not supported yet)`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "build a new literal with exactly the expected fields instead of passing a wider value",
|
||||
};
|
||||
}
|
||||
|
||||
/** Union values carry a runtime tag assigned per-union: a value of one
|
||||
* union type flowing into a DIFFERENT union's slot re-tags at runtime when
|
||||
* every source arm has a home in the destination (`A | B` into `A | B | C`
|
||||
* — see unionRetagHelper). What this rejects is the remainder: a source arm
|
||||
* with no identical destination arm, where the value could not be
|
||||
* represented in the target union at all. */
|
||||
export function unionMismatchDiag(
|
||||
expectedText: string,
|
||||
actualText: string,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2003",
|
||||
message:
|
||||
`union types must match exactly: expected '${expectedText}', got '${actualText}' ` +
|
||||
`(a union re-tags into another union only when every arm of the source has an identical arm in the destination, or is a record/array that width-coerces into exactly one destination arm)`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "narrow the value to a SINGLE arm first (a discriminant check, or '!== undefined'/'!== null' for unit arms); a plain arm value widens into any union that contains it, and a whole union widens into any union whose arms include the source's",
|
||||
};
|
||||
}
|
||||
|
||||
/** A use of a binding whose DECLARATION did not compile: the declaration
|
||||
* site already carries the real diagnostic (a fenced type, a blocked
|
||||
* signature, an unsupported binding form), and every later reference falls
|
||||
* through the resolution steps because no local/global/function was ever
|
||||
* registered under the symbol. This cascade marker names the binding and
|
||||
* points back at the root cause instead of misreporting the reference as
|
||||
* an unsupported construct of its own. */
|
||||
export function blockedBindingUseDiag(name: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2004",
|
||||
message: `uses of '${name}' inherit the blocker on its declaration`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: `the declaration of '${name}' did not compile — fix the diagnostic reported there and these sites clear with it`,
|
||||
};
|
||||
}
|
||||
|
||||
/** An OPERATION on an `any`-typed value with no static lowering, in a
|
||||
* static build. The honest static subset of `any` compiles (bindings ride
|
||||
* the checked-dynamic DOM — declarations, assignments, reads, equality,
|
||||
* typeof, truthiness, templates, calls through boxed functions), but the
|
||||
* operations that need JS's full coercion/mutation semantics (operators,
|
||||
* expando writes, computed member names, iteration) execute only in the
|
||||
* engine. Same code as the `any` type fence (SC2011) so the coverage
|
||||
* report and the two-tier retry treat both as one dynamic-capable family:
|
||||
* the island lifts exactly these sites. */
|
||||
export function anyOpRequiresDynamicDiag(feature: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2011",
|
||||
message: `${feature} on 'any'-typed values runs in the embedded dynamic engine, which this build does not include`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
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')",
|
||||
};
|
||||
}
|
||||
|
||||
/** A construct that needs the embedded dynamic-island engine was used in a
|
||||
* static build. Static is the default and never silently embeds the engine
|
||||
* (~620KB); the fix is spelled out in the hint, not applied implicitly.
|
||||
* "Requires" (vs SC2011/SC2012's "runs in"): the construct is engine-only
|
||||
* — it has no static form to stay with, so the hint offers no alternative. */
|
||||
export function requiresDynamicDiag(feature: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2010",
|
||||
message: `${feature} requires the embedded dynamic engine, which this build does not include`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
hint: "build with --dynamic to embed the engine (adds ~620KB to the binary); static builds never include it",
|
||||
};
|
||||
}
|
||||
|
||||
/** An ambient API with an island-backed lowering (Math.*, number/string
|
||||
* methods beyond the static set, parseFloat, ...) was used in a static
|
||||
* build. Same contract as SC2010, its own code so the coverage report can
|
||||
* name the construct: the call RUNS under --dynamic (the engine executes
|
||||
* it with JS-exact semantics); without the flag each use site reports the
|
||||
* choice instead of ICEing or failing at link time. */
|
||||
export function requiresDynamicApiDiag(feature: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2012",
|
||||
message: `${feature} runs in the embedded dynamic engine, which this build does not include`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
hint: "build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it",
|
||||
};
|
||||
}
|
||||
|
||||
/** An npm package import in a static build. The package's shipped JS has
|
||||
* exactly one execution home — the embedded engine — so the import itself
|
||||
* "requires" (SC2010's voice); its own code so the coverage report can
|
||||
* attribute sites per package. */
|
||||
export function requiresDynamicImportDiag(pkg: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2013",
|
||||
message: `importing '${pkg}' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
hint: "build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it",
|
||||
};
|
||||
}
|
||||
|
||||
/** A USE SITE of a value whose type the package's .d.ts declares, in a
|
||||
* static build. Message names ONLY the package (never the type) so the
|
||||
* coverage report groups all of a package's sites into one line. */
|
||||
export function requiresDynamicPackageDiag(pkg: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2013",
|
||||
message: `values from the '${pkg}' package run in the embedded dynamic engine, which this build does not include`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
hint: "build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it",
|
||||
};
|
||||
}
|
||||
|
||||
/** Building the embedded npm runtime graph failed — the package's entry
|
||||
* or one of its internal modules can't resolve, or it needs a Node builtin
|
||||
* the island doesn't shim. Named per problem, at the import site. */
|
||||
export function npmEmbedFailedDiag(detail: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2030",
|
||||
message: `cannot embed npm package code: ${detail}`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "the package's runtime JS is embedded at build time and executed in the island; packages that reach outside the shimmed builtins (events, path, fs, os, process, util, buffer, stream, crypto, zlib, url, assert, and the rest of `scriptc coverage`'s shimmed table — http/https clients and net/tls load too; http2 and worker_threads are the notable absences) do not run yet",
|
||||
};
|
||||
}
|
||||
|
||||
/** comptime went wrong at evaluation time. The callback runs in an isolated
|
||||
* `node:vm` context inside the compiler's own Node process and its RESULT is
|
||||
* baked into the binary as a literal — everything that can fail at that
|
||||
* stage lands here with a specific detail: the callback threw, evaluation
|
||||
* exceeded the compile-time budget, or the returned value cannot be written
|
||||
* as a literal of the expected type (NaN/Infinity, undefined/null/functions,
|
||||
* shape mismatches — the detail names the offending path, dynCheck-style). */
|
||||
export function comptimeFailedDiag(detail: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC1110",
|
||||
message: `comptime evaluation failed: ${detail}`,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function iceDiag(message: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC9001",
|
||||
message: `internal compiler error: ${message} — please report this`,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* Terminal rendering: file:line:col, a hand-rolled code frame with ±1 line
|
||||
* of context and a ^~~~ underline sized to the span, optional hint line.
|
||||
*/
|
||||
import type { ScrDiagnostic } from "./diagnostic.js";
|
||||
|
||||
export interface RenderOptions {
|
||||
color?: boolean;
|
||||
}
|
||||
|
||||
interface SourceLookup {
|
||||
/** Full text of the file the diagnostic points into. */
|
||||
text: string;
|
||||
}
|
||||
|
||||
const RED = "\x1b[31m";
|
||||
const CYAN = "\x1b[36m";
|
||||
const DIM = "\x1b[2m";
|
||||
const BOLD = "\x1b[1m";
|
||||
const RESET = "\x1b[0m";
|
||||
|
||||
export function renderDiagnostic(
|
||||
diag: ScrDiagnostic,
|
||||
source: SourceLookup | undefined,
|
||||
opts: RenderOptions = {},
|
||||
): string {
|
||||
const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
|
||||
const out: string[] = [];
|
||||
|
||||
let lineNum = 1;
|
||||
let colNum = 1;
|
||||
const frame: string[] = [];
|
||||
|
||||
if (source) {
|
||||
const lineStarts = [0];
|
||||
for (let i = 0; i < source.text.length; i++) {
|
||||
if (source.text[i] === "\n") lineStarts.push(i + 1);
|
||||
}
|
||||
let lo = 0;
|
||||
while (lo + 1 < lineStarts.length && lineStarts[lo + 1]! <= diag.loc.start) lo++;
|
||||
lineNum = lo + 1;
|
||||
colNum = diag.loc.start - lineStarts[lo]! + 1;
|
||||
|
||||
const lines = source.text.split("\n");
|
||||
const gutterWidth = String(Math.min(lineNum + 1, lines.length)).length;
|
||||
const emitLine = (n: number) => {
|
||||
const text = lines[n - 1];
|
||||
if (text === undefined) return;
|
||||
frame.push(c(DIM, ` ${String(n).padStart(gutterWidth)} | `) + text);
|
||||
};
|
||||
emitLine(lineNum - 1);
|
||||
emitLine(lineNum);
|
||||
const lineText = lines[lineNum - 1] ?? "";
|
||||
const spanOnLine = Math.max(
|
||||
1,
|
||||
Math.min(diag.loc.end - diag.loc.start, lineText.length - (colNum - 1)),
|
||||
);
|
||||
frame.push(
|
||||
c(DIM, ` ${" ".repeat(gutterWidth)} | `) +
|
||||
" ".repeat(colNum - 1) +
|
||||
c(RED, "^" + "~".repeat(spanOnLine - 1)),
|
||||
);
|
||||
emitLine(lineNum + 1);
|
||||
}
|
||||
|
||||
out.push(
|
||||
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`,
|
||||
);
|
||||
if (frame.length) {
|
||||
out.push("", ...frame);
|
||||
}
|
||||
if (diag.hint) {
|
||||
out.push("", ` ${c(CYAN, "hint:")} ${diag.hint}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
export function renderAll(
|
||||
diags: ScrDiagnostic[],
|
||||
sourceTextByFile: Map<string, string>,
|
||||
opts: RenderOptions = {},
|
||||
): string {
|
||||
const sorted = [...diags].sort(
|
||||
(a, b) => a.loc.file.localeCompare(b.loc.file) || a.loc.start - b.loc.start,
|
||||
);
|
||||
return sorted
|
||||
.map((d) => {
|
||||
const text = sourceTextByFile.get(d.loc.file);
|
||||
return renderDiagnostic(d, text === undefined ? undefined : { text }, opts);
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
@@ -0,0 +1,805 @@
|
||||
/* Node's CommonJS named-export DETECTABILITY test — the compile-time
|
||||
* answer to "which names can `import { x } from './cjs.js'` actually
|
||||
* link?". Node never executes a CJS module to build its ESM facade: it
|
||||
* lexes the source with its vendored CJS lexer (Node v24.15.0 vendors
|
||||
* deps/merve, the native `cjs_lexer` binding that replaced the
|
||||
* cjs-module-lexer wasm build; the detection patterns are FROZEN by
|
||||
* upstream policy) and only the names that lexer detects become named
|
||||
* exports. The checker sees the real export table, so it happily binds
|
||||
* names the lexer cannot see — `module.exports = { a: 7 }` type-checks a
|
||||
* named import of `a` that Node refuses at link time with a SyntaxError.
|
||||
* This module mirrors merve's detection so the compiler can answer
|
||||
* exactly what Node answers — for the program's own CJS files (program.ts
|
||||
* gates the ESM instantiate graph) AND for the --dynamic island's embedded
|
||||
* npm modules (npm.ts synthesizes each CJS facade from this lexer, so an
|
||||
* embedded named import fails exactly where Node's would).
|
||||
*
|
||||
* INPUT IS SOURCE TEXT. merve is a byte lexer; this port parses the text
|
||||
* with the 5.9.3 island parser and walks the AST, reading the raw source
|
||||
* wherever merve's byte quirks demand it. Only strings and name sets cross
|
||||
* this module's boundary — no AST object from either typescript world —
|
||||
* which is what lets both the 7.0.2-world program preflight and the
|
||||
* typescript5-world npm scan share the one implementation.
|
||||
*
|
||||
* The port is deliberately quirk-faithful — these all match probed Node
|
||||
* v24.15.0 behavior, not what a clean reimplementation would choose:
|
||||
* - `module.exports = { ... }` props scan LEFT TO RIGHT and the scan
|
||||
* STOPS at the first prop it cannot shape — names added before the
|
||||
* stop are kept, everything after is invisible. `{ vis: v, lit: 7 }`
|
||||
* exports `vis` only; `{ lit: 7, vis: v }` exports nothing.
|
||||
* - A prop VALUE must start with an identifier-ish token (an identifier,
|
||||
* keyword, `require(...)` call — anything whose first character is
|
||||
* [A-Za-z_$] or non-ASCII). Literals (`7`, `"s"`, backticks, arrows'
|
||||
* `(`) stop the scan WITHOUT adding the key.
|
||||
* - After an accepted value the lexer consumes ONE identifier run and
|
||||
* requires the very next CHARACTER to be `,` or `}` — `a: b, c: d`
|
||||
* keeps both, `a: b , c: d` keeps only `a` (the space stops the scan),
|
||||
* `a: o.x` / `a: f()` keep `a` and stop.
|
||||
* - A `require('spec')` VALUE adds the key, records the spec as a
|
||||
* REEXPORT, and stops the scan unconditionally — `{ a: require('x'),
|
||||
* b: v }` exports `a` plus x's names, never `b`. Trailing tokens on the
|
||||
* require (`.foo`, `()`, `|| y`) change nothing.
|
||||
* - `get name() {...}` stops the scan cold (no add); `set name(v) {}`
|
||||
* adds the literal name "set" and stops; `async name() {}` adds
|
||||
* "async" and stops; a plain method `name() {}` adds `name` and stops.
|
||||
* - `exports.NAME = ` / `module.exports.NAME = ` / the ['string'] forms
|
||||
* match ANYWHERE in the file — nested functions, dead branches — with
|
||||
* no scope analysis, and nothing ever removes a detected name.
|
||||
* - `module.exports = require('./x')` records a REEXPORT whenever the
|
||||
* require call is the LEADING token of the right-hand side — trailers
|
||||
* (`.foo`, `()`, `|| {}`, `? a : b`) are ignored, but a leading `(`
|
||||
* breaks the match (`(require('x'))` records nothing). Node resolves
|
||||
* each reexport and unions the target's detected names when the target
|
||||
* is itself CommonJS. Every `module.exports =` assignment CLEARS the
|
||||
* pending reexport list first — including star-pattern reexports
|
||||
* recorded earlier — so only reexports at or after the last assignment
|
||||
* count, while detected export NAMES accumulate across everything.
|
||||
* - `Object.defineProperty(exports, 'n', {...})` adds `n` for the exact
|
||||
* descriptor shapes merve accepts: optional leading `enumerable: true`
|
||||
* then `value:` (anything), or a getter whose whole body is `return
|
||||
* IDENT` / `return IDENT.IDENT` / `return IDENT['str']` and which is
|
||||
* the descriptor's last property. Unlike cjs-module-lexer 2.2.0, a
|
||||
* NON-matching defineProperty of the same name elsewhere does NOT
|
||||
* poison the valid one (probed: Node keeps the export).
|
||||
* - The transpiler star-reexport patterns ARE detected (tsc/babel output
|
||||
* all over npm depends on them):
|
||||
* · `__export(require('x'))` / `__exportStar(require('x'), exports)`
|
||||
* — callee is the bare identifier or any member chain ending in
|
||||
* one of those two exact names, at TOP LEVEL (brace depth 0 — an
|
||||
* unbraced `if (c) __exportStar(...)` still counts, a braced block
|
||||
* does not), and the source bytes must read `NAME(require` with NO
|
||||
* whitespace or comment between the name, the `(`, and `require`
|
||||
* (probed: one space kills the match; spaces anywhere inside the
|
||||
* require call are fine).
|
||||
* · The Babel copy loop: a top-level `var/const/let ID =
|
||||
* [_interopRequireWildcard(]require('x')[)]` (ID the FIRST
|
||||
* declarator; `_interopRequireWildcard` bare and byte-adjacent to
|
||||
* `(require` like the star form) linked by ID to a top-level
|
||||
* `Object.keys(ID).forEach(function (KEY) {...})` whose body is
|
||||
* byte-for-byte one of merve's copy shapes: the `if (KEY ===
|
||||
* 'default' || KEY === '__esModule') return;` filter (exact order,
|
||||
* unbraced return) with optional hasOwnProperty / in-exports
|
||||
* guards, or the single `if (KEY !== 'default' [&& !hasOwn]) copy`
|
||||
* form; the copy either `EXPORTS[KEY] = ID[KEY]` or the
|
||||
* enumerable-getter defineProperty. Named callbacks, arrow
|
||||
* callbacks, braced returns, and reordered filters all miss.
|
||||
* The reexport's clearing position is the LOOP's, not the var's.
|
||||
* NOT modeled: cjs-module-lexer 2.2.0's phantom "get" export for an
|
||||
* identifier-named getter in the table (`{ get a() {} }` exports nothing
|
||||
* in Node — but `get [k]() {}` / `get "a"() {}` DO export the word "get",
|
||||
* because the name token stops the lexer after it took `get` as a
|
||||
* shorthand key). */
|
||||
|
||||
import ts from "typescript5";
|
||||
|
||||
export interface CjsLexedExports {
|
||||
/** Names Node's lexer detects in THIS file (no reexport resolution). */
|
||||
exports: Set<string>;
|
||||
/** Reexport specifiers surviving the last `module.exports =` clearing,
|
||||
* in source order: the assignment's own require forms plus every later
|
||||
* (or unclear-ed) star-pattern / Babel-loop match. */
|
||||
reexports: string[];
|
||||
}
|
||||
|
||||
/** merve's byte classification: [A-Za-z_$] plus every non-ASCII byte. */
|
||||
function isIdentStartChar(ch: string): boolean {
|
||||
return /[A-Za-z_$]/.test(ch) || ch.charCodeAt(0) > 127;
|
||||
}
|
||||
|
||||
function isIdentChar(ch: string): boolean {
|
||||
return /[A-Za-z0-9_$]/.test(ch) || ch.charCodeAt(0) > 127;
|
||||
}
|
||||
|
||||
/** End of the identifier run starting at `pos` (merve consumes maximal
|
||||
* identifier characters and nothing else — no member chains, no calls). */
|
||||
function identRunEnd(text: string, pos: number): number {
|
||||
let i = pos;
|
||||
while (i < text.length && isIdentChar(text[i]!)) i++;
|
||||
return i;
|
||||
}
|
||||
|
||||
/** True when the node's SOURCE spelling starts like a lexer identifier —
|
||||
* a `a`-escaped name parses to the same AST as its plain spelling,
|
||||
* but merve reads bytes and stops at the backslash. */
|
||||
function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
|
||||
const ch = sf.text[node.getStart(sf)];
|
||||
return ch !== undefined && isIdentStartChar(ch);
|
||||
}
|
||||
|
||||
/** True when the node's WHOLE source span is one identifier run — what
|
||||
* merve takes for IDENT. Keywords count (`return this.x` satisfies the
|
||||
* defineProperty getter's `return IDENT.IDENT` because `this` lexes as an
|
||||
* identifier run — probed), escapes and private names do not. */
|
||||
function isIdentTokenNode(node: ts.Node, sf: ts.SourceFile): boolean {
|
||||
const start = node.getStart(sf);
|
||||
const ch = sf.text[start];
|
||||
return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.getEnd();
|
||||
}
|
||||
|
||||
/** The identifier run at the node's START — the whole name when its
|
||||
* spelling is plain, the byte prefix merve consumed when a unicode escape
|
||||
* interrupts the run mid-name. */
|
||||
function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string {
|
||||
const start = node.getStart(sf);
|
||||
return sf.text.slice(start, identRunEnd(sf.text, start));
|
||||
}
|
||||
|
||||
/** The bare `require('spec')` call merve recognizes: callee is exactly the
|
||||
* identifier `require` (no optional chaining), one string-literal argument
|
||||
* (quotes only — templates never match). */
|
||||
function bareRequireSpecOf(e: ts.Node): string | null {
|
||||
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return null;
|
||||
if (!ts.isIdentifier(e.expression) || e.expression.text !== "require") return null;
|
||||
if (e.arguments.length !== 1) return null;
|
||||
const arg = e.arguments[0]!;
|
||||
return ts.isStringLiteral(arg) ? arg.text : null;
|
||||
}
|
||||
|
||||
/** The `require(...)` call at the very START of `e`'s source span, if any:
|
||||
* merve records the reexport as soon as it lexes `require('spec')` and any
|
||||
* TRAILER (`.foo`, `()`, `|| y`, `? a : b`) merely stops whatever scan
|
||||
* follows — so the recognizer descends the leftmost-child chain while it
|
||||
* still starts where `e` starts. A ParenthesizedExpression breaks the
|
||||
* chain naturally (its child starts after the `(`), matching merve. */
|
||||
function leadingRequireOf(e: ts.Expression, sf: ts.SourceFile): { spec: string; end: number } | null {
|
||||
const start = e.getStart(sf);
|
||||
let cur: ts.Node = e;
|
||||
for (;;) {
|
||||
const spec = bareRequireSpecOf(cur);
|
||||
if (spec !== null && cur.getStart(sf) === start) return { spec, end: cur.getEnd() };
|
||||
let first: ts.Node | undefined;
|
||||
ts.forEachChild(cur, (c) => {
|
||||
first ??= c;
|
||||
});
|
||||
if (first === undefined || first.getStart(sf) !== start) return null;
|
||||
cur = first;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when `e` is exactly the `exports` identifier. */
|
||||
function isExportsIdent(e: ts.Expression): boolean {
|
||||
return ts.isIdentifier(e) && e.text === "exports";
|
||||
}
|
||||
|
||||
/** True when `e` is exactly `module.exports`. */
|
||||
function isModuleExports(e: ts.Expression): boolean {
|
||||
return (
|
||||
ts.isPropertyAccessExpression(e) &&
|
||||
e.questionDotToken === undefined &&
|
||||
ts.isIdentifier(e.expression) &&
|
||||
e.expression.text === "module" &&
|
||||
ts.isIdentifier(e.name) &&
|
||||
e.name.text === "exports"
|
||||
);
|
||||
}
|
||||
|
||||
/** Skips ECMA whitespace and comments forward from `pos` (merve's
|
||||
* commentWhitespace) and answers the next meaningful character (or ""). */
|
||||
function nextMeaningfulChar(text: string, pos: number): string {
|
||||
let i = pos;
|
||||
while (i < text.length) {
|
||||
const ch = text[i]!;
|
||||
if (ch === " " || (ch.charCodeAt(0) > 8 && ch.charCodeAt(0) < 14)) {
|
||||
i++;
|
||||
} else if (ch === "/" && text[i + 1] === "/") {
|
||||
const nl = text.indexOf("\n", i + 2);
|
||||
if (nl < 0) return "";
|
||||
i = nl;
|
||||
} else if (ch === "/" && text[i + 1] === "*") {
|
||||
const close = text.indexOf("*/", i + 2);
|
||||
if (close < 0) return "";
|
||||
i = close + 2;
|
||||
} else {
|
||||
return ch;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** The `module.exports = { ... }` table scan — merve's tryParseLiteralExports
|
||||
* over the AST. Adds detected names to `out`, appends reexports (spread /
|
||||
* value requires) to `reexports`, stops at the first unshapeable prop. */
|
||||
function scanTableLiteral(obj: ts.ObjectLiteralExpression, sf: ts.SourceFile, out: Set<string>, reexports: string[]): void {
|
||||
const text = sf.text;
|
||||
for (const prop of obj.properties) {
|
||||
if (ts.isGetAccessor(prop)) {
|
||||
// `get name() {...}` — merve's explicit early termination... unless
|
||||
// the accessor's NAME is not a plain identifier, where the lexer
|
||||
// already added the word "get" as a shorthand key before the odd
|
||||
// token stops it (`get [k]() {}` exports "get").
|
||||
if (!ts.isIdentifier(prop.name) || !sourceSpellsIdentifier(prop.name, sf)) out.add("get");
|
||||
return;
|
||||
}
|
||||
if (ts.isSetAccessor(prop)) {
|
||||
// The word `set` lexes as a shorthand key, then the accessor name
|
||||
// stops the scan — Node really does export "set" (value undefined).
|
||||
out.add("set");
|
||||
return;
|
||||
}
|
||||
if (ts.isMethodDeclaration(prop)) {
|
||||
// `name() {}` adds the name, then `(` stops the scan. The modifier /
|
||||
// asterisk spellings shift which token the lexer takes as the key:
|
||||
// `async name()` exports "async", `*name()` exports nothing. A
|
||||
// unicode escape mid-name adds only the byte prefix before it (the
|
||||
// key adds BEFORE the separator check fails — see the key path).
|
||||
if (prop.asteriskToken !== undefined) return;
|
||||
const mods = ts.getModifiers(prop) ?? [];
|
||||
if (mods.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) {
|
||||
out.add("async");
|
||||
return;
|
||||
}
|
||||
if (ts.isIdentifier(prop.name) && sourceSpellsIdentifier(prop.name, sf)) out.add(identPrefixOf(prop.name, sf));
|
||||
return;
|
||||
}
|
||||
if (ts.isShorthandPropertyAssignment(prop)) {
|
||||
if (prop.objectAssignmentInitializer !== undefined) return; // `{ a = 1 }` — cover pattern, not a table
|
||||
if (!ts.isIdentifier(prop.name) || !sourceSpellsIdentifier(prop.name, sf)) return;
|
||||
if (!isIdentTokenNode(prop.name, sf)) {
|
||||
// an escape mid-name: the byte prefix adds, then the backslash
|
||||
// fails the separator check and stops the scan
|
||||
out.add(identPrefixOf(prop.name, sf));
|
||||
return;
|
||||
}
|
||||
out.add(prop.name.text);
|
||||
continue; // whitespace before `,` is skipped on the shorthand path
|
||||
}
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const req = leadingRequireOf(prop.expression, sf);
|
||||
let consumedEnd: number;
|
||||
if (req !== null) {
|
||||
reexports.push(req.spec);
|
||||
consumedEnd = req.end;
|
||||
} else if (sourceSpellsIdentifier(prop.expression, sf)) {
|
||||
consumedEnd = identRunEnd(text, prop.expression.getStart(sf));
|
||||
} else {
|
||||
return; // `...{}` and friends stop the scan
|
||||
}
|
||||
// The spread path DOES skip whitespace/comments before the `,` check
|
||||
// — `...o , a: b` continues where `a: b , c` would stop.
|
||||
const ch = nextMeaningfulChar(text, consumedEnd);
|
||||
if (ch === ",") continue;
|
||||
return; // `...o.p`, `...f()` — the leftover token stops the scan
|
||||
// (a `}` here means the spread was last; stopping is the same)
|
||||
}
|
||||
if (ts.isPropertyAssignment(prop)) {
|
||||
const name = prop.name;
|
||||
let key: string | null = null;
|
||||
if (ts.isIdentifier(name) && sourceSpellsIdentifier(name, sf)) {
|
||||
if (!isIdentTokenNode(name, sf)) {
|
||||
// merve adds the key BEFORE validating the `:` — an escape
|
||||
// mid-name adds the byte prefix, then the backslash stops the
|
||||
// scan (probed: the key's prefix exports, nothing after does)
|
||||
out.add(identPrefixOf(name, sf));
|
||||
return;
|
||||
}
|
||||
key = name.text;
|
||||
} else if (ts.isStringLiteral(name)) {
|
||||
key = name.text;
|
||||
}
|
||||
if (key === null) return; // numeric / computed / template keys stop the scan
|
||||
const value = prop.initializer;
|
||||
const req = leadingRequireOf(value, sf);
|
||||
if (req !== null) {
|
||||
// A require VALUE adds the key, records the reexport, and stops
|
||||
// the scan UNCONDITIONALLY — probed: `{ a: require('x'), b: v }`
|
||||
// exports `a` plus x's names and never `b` (the spread path above
|
||||
// genuinely continues past a `,`; the value path does not).
|
||||
out.add(key);
|
||||
reexports.push(req.spec);
|
||||
return;
|
||||
}
|
||||
const start = value.getStart(sf);
|
||||
const ch0 = text[start];
|
||||
if (ch0 === undefined || !isIdentStartChar(ch0)) return; // literal values stop WITHOUT adding the key
|
||||
const consumedEnd = identRunEnd(text, start);
|
||||
out.add(key);
|
||||
// NO whitespace skip here — merve requires `,` or `}` as the very
|
||||
// next character after the consumed value token. `a: b , c` and
|
||||
// `a: b.c, d` both keep `a` and stop.
|
||||
const ch = text[consumedEnd];
|
||||
if (ch === ",") continue;
|
||||
return; // `}` ends the props anyway; anything else stops the scan
|
||||
}
|
||||
return; // any other prop kind stops the scan
|
||||
}
|
||||
}
|
||||
|
||||
/** `Object.defineProperty(exports|module.exports, 'name', {...})` — adds
|
||||
* the name for merve's exact descriptor shapes (see the header). */
|
||||
function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set<string>): void {
|
||||
const callee = call.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(callee) ||
|
||||
callee.questionDotToken !== undefined ||
|
||||
!ts.isIdentifier(callee.expression) ||
|
||||
callee.expression.text !== "Object" ||
|
||||
!ts.isIdentifier(callee.name) ||
|
||||
callee.name.text !== "defineProperty"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (call.arguments.length !== 3) return;
|
||||
const [recv, nameArg, desc] = call.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
|
||||
if (!isExportsIdent(recv) && !isModuleExports(recv)) return;
|
||||
if (!ts.isStringLiteral(nameArg)) return;
|
||||
if (!ts.isObjectLiteralExpression(desc)) return;
|
||||
let i = 0;
|
||||
const props = desc.properties;
|
||||
const first = props[0];
|
||||
if (
|
||||
first !== undefined &&
|
||||
ts.isPropertyAssignment(first) &&
|
||||
ts.isIdentifier(first.name) &&
|
||||
first.name.text === "enumerable"
|
||||
) {
|
||||
// Only the exact `enumerable: true` prefix is consumed; anything else
|
||||
// in the slot fails the whole match.
|
||||
if (first.initializer.kind !== ts.SyntaxKind.TrueKeyword) return;
|
||||
i = 1;
|
||||
}
|
||||
const p = props[i];
|
||||
if (p === undefined) return;
|
||||
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "value") {
|
||||
out.add(nameArg.text); // merve stops right at `value:` — the value and the rest of the descriptor are unchecked
|
||||
return;
|
||||
}
|
||||
// The getter forms: `get() {...}` method, or `get: function [name]() {...}`
|
||||
// (never an arrow), body EXACTLY `return IDENT` / IDENT.IDENT / IDENT['s'],
|
||||
// and the getter must be the descriptor's LAST property.
|
||||
const body = getterBodyOf(p);
|
||||
if (body === undefined || i !== props.length - 1) return;
|
||||
if (body.statements.length !== 1) return;
|
||||
const ret = body.statements[0]!;
|
||||
if (!ts.isReturnStatement(ret) || ret.expression === undefined) return;
|
||||
const r = ret.expression;
|
||||
const returnsIdentish =
|
||||
isIdentTokenNode(r, sf) ||
|
||||
(ts.isPropertyAccessExpression(r) && r.questionDotToken === undefined && isIdentTokenNode(r.expression, sf) && isIdentTokenNode(r.name, sf)) ||
|
||||
(ts.isElementAccessExpression(r) && r.questionDotToken === undefined && isIdentTokenNode(r.expression, sf) && ts.isStringLiteral(r.argumentExpression));
|
||||
if (returnsIdentish) out.add(nameArg.text);
|
||||
}
|
||||
|
||||
/** The body of a `get() {...}` method / `get: function [name]() {...}`
|
||||
* descriptor prop (merve's two getter spellings — arrows never match). */
|
||||
function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined {
|
||||
if (
|
||||
ts.isMethodDeclaration(p) &&
|
||||
ts.isIdentifier(p.name) &&
|
||||
p.name.text === "get" &&
|
||||
p.asteriskToken === undefined &&
|
||||
(ts.getModifiers(p) ?? []).length === 0 &&
|
||||
p.parameters.length === 0
|
||||
) {
|
||||
return p.body;
|
||||
}
|
||||
if (
|
||||
ts.isPropertyAssignment(p) &&
|
||||
ts.isIdentifier(p.name) &&
|
||||
p.name.text === "get" &&
|
||||
ts.isFunctionExpression(p.initializer) &&
|
||||
p.initializer.parameters.length === 0 &&
|
||||
p.initializer.asteriskToken === undefined &&
|
||||
(ts.getModifiers(p.initializer) ?? []).length === 0
|
||||
) {
|
||||
return p.initializer.body;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/* ── the transpiler star-reexport patterns ─────────────────────────────── */
|
||||
|
||||
/** The star-export call: callee is `__export` or `__exportStar` (bare, or
|
||||
* the NAME of any member chain — `tslib_1.__exportStar`, `a.b.__exportStar`)
|
||||
* and the first argument leads with `require('spec')`. Byte quirk: the
|
||||
* source must read `NAME(require` with no trivia between the name, the
|
||||
* `(`, and `require` — one space anywhere in that seam kills the match
|
||||
* (probed; spaces INSIDE the require call are fine). */
|
||||
function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string | null {
|
||||
if (call.questionDotToken !== undefined) return null;
|
||||
const callee = call.expression;
|
||||
let nameNode: ts.Identifier | undefined;
|
||||
if (ts.isIdentifier(callee)) nameNode = callee;
|
||||
else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) nameNode = callee.name;
|
||||
if (nameNode === undefined) return null;
|
||||
if (nameNode.text !== "__export" && nameNode.text !== "__exportStar") return null;
|
||||
if (!sourceSpellsIdentifier(nameNode, sf)) return null;
|
||||
const arg0 = call.arguments[0];
|
||||
if (arg0 === undefined) return null;
|
||||
if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null;
|
||||
return leadingRequireOf(arg0, sf)?.spec ?? null;
|
||||
}
|
||||
|
||||
/** True when the bytes between a callee name ending at `nameEnd` and its
|
||||
* first argument starting at `argStart` are exactly `(` — merve's anchored
|
||||
* `NAME(require` seam. */
|
||||
function byteAdjacentParen(sf: ts.SourceFile, nameEnd: number, argStart: number): boolean {
|
||||
return argStart === nameEnd + 1 && sf.text[nameEnd] === "(";
|
||||
}
|
||||
|
||||
/** The Babel star-copy assignment: a variable statement whose FIRST
|
||||
* declarator is `ID = require('spec')` or
|
||||
* `ID = _interopRequireWildcard(require('spec'))` (the wildcard helper a
|
||||
* bare identifier, byte-adjacent to `(require` like the star form —
|
||||
* probed: a member-qualified helper or one space after its paren misses).
|
||||
* Answers [ID, spec]. */
|
||||
function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, string] | null {
|
||||
const decl = stmt.declarationList.declarations[0];
|
||||
if (decl === undefined || !ts.isIdentifier(decl.name) || !sourceSpellsIdentifier(decl.name, sf)) return null;
|
||||
const init = decl.initializer;
|
||||
if (init === undefined) return null;
|
||||
const direct = bareRequireSpecOf(init);
|
||||
if (direct !== null) return [decl.name.text, direct];
|
||||
if (
|
||||
ts.isCallExpression(init) &&
|
||||
init.questionDotToken === undefined &&
|
||||
ts.isIdentifier(init.expression) &&
|
||||
init.expression.text === "_interopRequireWildcard" &&
|
||||
init.arguments.length >= 1
|
||||
) {
|
||||
const arg0 = init.arguments[0]!;
|
||||
if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null;
|
||||
const spec = bareRequireSpecOf(arg0);
|
||||
if (spec !== null) return [decl.name.text, spec];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `exports` / `module.exports` — the copy loop accepts either spelling. */
|
||||
function isExportsTarget(e: ts.Expression): boolean {
|
||||
return isExportsIdent(e) || isModuleExports(e);
|
||||
}
|
||||
|
||||
/** `X === 'str'` / `X !== 'str'` with X the loop key identifier. */
|
||||
function keyStringCompare(e: ts.Expression, key: string, op: ts.SyntaxKind, str: string): boolean {
|
||||
return (
|
||||
ts.isBinaryExpression(e) &&
|
||||
e.operatorToken.kind === op &&
|
||||
ts.isIdentifier(e.left) &&
|
||||
e.left.text === key &&
|
||||
ts.isStringLiteral(e.right) &&
|
||||
e.right.text === str
|
||||
);
|
||||
}
|
||||
|
||||
/** `Object[.prototype].hasOwnProperty.call(ANY_ID, KEY)` — the guard merve
|
||||
* accepts in both loop filter forms. */
|
||||
function isHasOwnCall(e: ts.Expression, key: string): boolean {
|
||||
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return false;
|
||||
const callee = e.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name) || callee.name.text !== "call") return false;
|
||||
let cur = callee.expression;
|
||||
if (!ts.isPropertyAccessExpression(cur) || !ts.isIdentifier(cur.name) || cur.name.text !== "hasOwnProperty") return false;
|
||||
cur = cur.expression;
|
||||
if (ts.isPropertyAccessExpression(cur) && ts.isIdentifier(cur.name) && cur.name.text === "prototype") cur = cur.expression;
|
||||
if (!ts.isIdentifier(cur) || cur.text !== "Object") return false;
|
||||
if (e.arguments.length !== 2) return false;
|
||||
const [recv, k] = e.arguments as unknown as [ts.Expression, ts.Expression];
|
||||
return ts.isIdentifier(recv) && ts.isIdentifier(k) && k.text === key;
|
||||
}
|
||||
|
||||
/** `ID.hasOwnProperty(KEY)` — form B's alternative hasOwn spelling. */
|
||||
function isDirectHasOwnCall(e: ts.Expression, key: string): boolean {
|
||||
return (
|
||||
ts.isCallExpression(e) &&
|
||||
e.questionDotToken === undefined &&
|
||||
ts.isPropertyAccessExpression(e.expression) &&
|
||||
ts.isIdentifier(e.expression.name) &&
|
||||
e.expression.name.text === "hasOwnProperty" &&
|
||||
ts.isIdentifier(e.expression.expression) &&
|
||||
e.arguments.length === 1 &&
|
||||
ts.isIdentifier(e.arguments[0]!) &&
|
||||
(e.arguments[0] as ts.Identifier).text === key
|
||||
);
|
||||
}
|
||||
|
||||
/** The loop-body COPY statement: `EXPORTS[KEY] = ID[KEY];` or the
|
||||
* enumerable-getter defineProperty (`Object.defineProperty(EXPORTS, KEY,
|
||||
* { enumerable: true, get[: function[name]] () { return ID[KEY]; } })`). */
|
||||
function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean {
|
||||
if (!ts.isExpressionStatement(stmt)) return false;
|
||||
const e = stmt.expression;
|
||||
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||
return isKeyedAccess(e.left, isExportsTarget, key) && isKeyedAccess(e.right, (x) => ts.isIdentifier(x) && x.text === id1, key);
|
||||
}
|
||||
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return false;
|
||||
const callee = e.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(callee) ||
|
||||
!ts.isIdentifier(callee.expression) ||
|
||||
callee.expression.text !== "Object" ||
|
||||
!ts.isIdentifier(callee.name) ||
|
||||
callee.name.text !== "defineProperty"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (e.arguments.length !== 3) return false;
|
||||
const [recv, nameArg, desc] = e.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
|
||||
if (!isExportsTarget(recv)) return false;
|
||||
if (!ts.isIdentifier(nameArg) || nameArg.text !== key) return false;
|
||||
if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 2) return false;
|
||||
const [en, get] = desc.properties as unknown as [ts.ObjectLiteralElementLike, ts.ObjectLiteralElementLike];
|
||||
if (!ts.isPropertyAssignment(en) || !ts.isIdentifier(en.name) || en.name.text !== "enumerable") return false;
|
||||
if (en.initializer.kind !== ts.SyntaxKind.TrueKeyword) return false;
|
||||
const body = getterBodyOf(get);
|
||||
if (body === undefined || body.statements.length !== 1) return false;
|
||||
const ret = body.statements[0]!;
|
||||
if (!ts.isReturnStatement(ret) || ret.expression === undefined) return false;
|
||||
return isKeyedAccess(ret.expression, (x) => ts.isIdentifier(x) && x.text === id1, key);
|
||||
}
|
||||
|
||||
/** `RECV[KEY]` where `recvOk` approves the receiver. */
|
||||
function isKeyedAccess(e: ts.Expression, recvOk: (x: ts.Expression) => boolean, key: string): boolean {
|
||||
return (
|
||||
ts.isElementAccessExpression(e) &&
|
||||
e.questionDotToken === undefined &&
|
||||
recvOk(e.expression) &&
|
||||
ts.isIdentifier(e.argumentExpression) &&
|
||||
e.argumentExpression.text === key
|
||||
);
|
||||
}
|
||||
|
||||
/** An UNBRACED bare `return;` — merve reads the literal `return` token
|
||||
* right after the filter's `)`; a `{ return; }` block misses (probed). */
|
||||
function isBareReturn(stmt: ts.Statement): boolean {
|
||||
return ts.isReturnStatement(stmt) && stmt.expression === undefined;
|
||||
}
|
||||
|
||||
/** The Babel copy loop `Object.keys(ID1).forEach(function (KEY) {...})` —
|
||||
* answers ID1 when the whole shape matches merve's EXPORT_STAR_LIB
|
||||
* grammar; the caller links ID1 to a star assignment for the spec. */
|
||||
function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null {
|
||||
if (call.questionDotToken !== undefined) return null;
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined) return null;
|
||||
if (!ts.isIdentifier(callee.name) || callee.name.text !== "forEach") return null;
|
||||
const keysCall = callee.expression;
|
||||
if (!ts.isCallExpression(keysCall) || keysCall.questionDotToken !== undefined) return null;
|
||||
const keysCallee = keysCall.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(keysCallee) ||
|
||||
!ts.isIdentifier(keysCallee.expression) ||
|
||||
keysCallee.expression.text !== "Object" ||
|
||||
!ts.isIdentifier(keysCallee.name) ||
|
||||
keysCallee.name.text !== "keys"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (keysCall.arguments.length !== 1) return null;
|
||||
const id1Node = keysCall.arguments[0]!;
|
||||
if (!ts.isIdentifier(id1Node) || !sourceSpellsIdentifier(id1Node, sf)) return null;
|
||||
const id1 = id1Node.text;
|
||||
const fn = call.arguments[0];
|
||||
if (fn === undefined || !ts.isFunctionExpression(fn)) return null; // arrows never match (probed)
|
||||
if (fn.name !== undefined) return null; // a NAMED callback misses (probed)
|
||||
if (fn.asteriskToken !== undefined || (ts.getModifiers(fn) ?? []).length > 0) return null;
|
||||
if (fn.parameters.length !== 1) return null;
|
||||
const keyParam = fn.parameters[0]!;
|
||||
if (!ts.isIdentifier(keyParam.name) || keyParam.initializer !== undefined || keyParam.dotDotDotToken !== undefined) return null;
|
||||
const key = keyParam.name.text;
|
||||
const stmts = fn.body.statements;
|
||||
|
||||
// Form B: the single `if (KEY !== 'default' [&& !hasOwn]) COPY` statement.
|
||||
if (stmts.length === 1 && ts.isIfStatement(stmts[0]!) && stmts[0]!.elseStatement === undefined) {
|
||||
const ifStmt = stmts[0]!;
|
||||
let cond = ifStmt.expression;
|
||||
if (ts.isBinaryExpression(cond) && cond.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
||||
const right = cond.right;
|
||||
if (
|
||||
!ts.isPrefixUnaryExpression(right) ||
|
||||
right.operator !== ts.SyntaxKind.ExclamationToken ||
|
||||
!(isHasOwnCall(right.operand, key) || isDirectHasOwnCall(right.operand, key))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
cond = cond.left;
|
||||
}
|
||||
if (!keyStringCompare(cond, key, ts.SyntaxKind.ExclamationEqualsEqualsToken, "default")) return null;
|
||||
return isCopyStatement(ifStmt.thenStatement, id1, key) ? id1 : null;
|
||||
}
|
||||
|
||||
// Form A: `if (KEY === 'default' || KEY === '__esModule') return;` (exact
|
||||
// order), then optional hasOwn / in-exports guards, then the copy.
|
||||
if (stmts.length < 2 || stmts.length > 4) return null;
|
||||
const head = stmts[0]!;
|
||||
if (!ts.isIfStatement(head) || head.elseStatement !== undefined || !isBareReturn(head.thenStatement)) return null;
|
||||
const headCond = head.expression;
|
||||
if (
|
||||
!ts.isBinaryExpression(headCond) ||
|
||||
headCond.operatorToken.kind !== ts.SyntaxKind.BarBarToken ||
|
||||
!keyStringCompare(headCond.left, key, ts.SyntaxKind.EqualsEqualsEqualsToken, "default") ||
|
||||
!keyStringCompare(headCond.right, key, ts.SyntaxKind.EqualsEqualsEqualsToken, "__esModule")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
let i = 1;
|
||||
if (i < stmts.length - 1) {
|
||||
const s = stmts[i]!;
|
||||
if (ts.isIfStatement(s) && s.elseStatement === undefined && isBareReturn(s.thenStatement) && isHasOwnCall(s.expression, key)) i++;
|
||||
}
|
||||
if (i < stmts.length - 1) {
|
||||
// `if (KEY in EXPORTS && EXPORTS[KEY] === ID1[KEY]) return;`
|
||||
const s = stmts[i]!;
|
||||
if (ts.isIfStatement(s) && s.elseStatement === undefined && isBareReturn(s.thenStatement)) {
|
||||
const c = s.expression;
|
||||
const inOk =
|
||||
ts.isBinaryExpression(c) &&
|
||||
c.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
||||
ts.isBinaryExpression(c.left) &&
|
||||
c.left.operatorToken.kind === ts.SyntaxKind.InKeyword &&
|
||||
ts.isIdentifier(c.left.left) &&
|
||||
c.left.left.text === key &&
|
||||
isExportsTarget(c.left.right) &&
|
||||
ts.isBinaryExpression(c.right) &&
|
||||
c.right.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
|
||||
isKeyedAccess(c.right.left, isExportsTarget, key) &&
|
||||
isKeyedAccess(c.right.right, (x) => ts.isIdentifier(x) && x.text === id1, key);
|
||||
if (inOk) i++;
|
||||
}
|
||||
}
|
||||
if (i !== stmts.length - 1) return null;
|
||||
return isCopyStatement(stmts[i]!, id1, key) ? id1 : null;
|
||||
}
|
||||
|
||||
/* ── the walk ──────────────────────────────────────────────────────────── */
|
||||
|
||||
/** Node kinds whose CHILDREN sit behind a `{` (or `${`) — merve's
|
||||
* "top-level" for the star patterns is brace depth 0, so an unbraced
|
||||
* `if (c) __exportStar(...)` still counts while any block hides it. */
|
||||
function opensBraces(n: ts.Node): boolean {
|
||||
switch (n.kind) {
|
||||
case ts.SyntaxKind.Block:
|
||||
case ts.SyntaxKind.ModuleBlock:
|
||||
case ts.SyntaxKind.CaseBlock:
|
||||
case ts.SyntaxKind.ObjectLiteralExpression:
|
||||
case ts.SyntaxKind.ObjectBindingPattern:
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
case ts.SyntaxKind.ClassExpression:
|
||||
case ts.SyntaxKind.EnumDeclaration:
|
||||
case ts.SyntaxKind.TemplateSpan:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Preorder walk with an EXPLICIT stack (minified npm bundles nest deeply
|
||||
* enough to overflow recursive visits) carrying each node's brace depth. */
|
||||
function walkWithBraceDepth(sf: ts.SourceFile, cb: (node: ts.Node, braceDepth: number) => void): void {
|
||||
const stack: [ts.Node, number][] = [[sf, 0]];
|
||||
const children: ts.Node[] = [];
|
||||
while (stack.length > 0) {
|
||||
const [n, depth] = stack.pop()!;
|
||||
cb(n, depth);
|
||||
const childDepth = depth + (opensBraces(n) ? 1 : 0);
|
||||
children.length = 0;
|
||||
ts.forEachChild(n, (c) => {
|
||||
children.push(c);
|
||||
});
|
||||
for (let i = children.length - 1; i >= 0; i--) stack.push([children[i]!, childDepth]);
|
||||
}
|
||||
}
|
||||
|
||||
/** The single-file lex over SOURCE TEXT: every detection site, position-
|
||||
* blind like the real lexer for the assignment forms (nested functions and
|
||||
* dead branches count), brace-depth-0 for the star patterns. */
|
||||
export function cjsLexedExportsOf(source: string, fileName = "module.cjs"): CjsLexedExports {
|
||||
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, false, ts.ScriptKind.JS);
|
||||
const exports = new Set<string>();
|
||||
/** Position-ordered reexport EVENTS: every `module.exports =` assignment
|
||||
* clears the pending list before contributing its own require forms;
|
||||
* star calls and Babel loops append at their own positions (a loop's
|
||||
* position is the LOOP's, not its var's — probed). */
|
||||
type ReexportEvent =
|
||||
| { pos: number; kind: "assign"; rhs: ts.Expression }
|
||||
| { pos: number; kind: "spec"; spec: string }
|
||||
| { pos: number; kind: "loop"; id1: string };
|
||||
const events: ReexportEvent[] = [];
|
||||
/** Star-assignment declarators by identifier — the Babel loop's linkage. */
|
||||
const starAssigns = new Map<string, string[]>();
|
||||
walkWithBraceDepth(sf, (n, braceDepth) => {
|
||||
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||
const lhs = n.left;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(lhs) &&
|
||||
lhs.questionDotToken === undefined &&
|
||||
ts.isIdentifier(lhs.name) &&
|
||||
(isExportsIdent(lhs.expression) || isModuleExports(lhs.expression)) &&
|
||||
// the whole NAME must be one identifier run — a unicode escape
|
||||
// mid-name breaks merve's run before the `=`, so the pattern
|
||||
// never matches (probed: Node detects nothing)
|
||||
isIdentTokenNode(lhs.name, sf)
|
||||
) {
|
||||
exports.add(lhs.name.text);
|
||||
} else if (
|
||||
ts.isElementAccessExpression(lhs) &&
|
||||
lhs.questionDotToken === undefined &&
|
||||
ts.isStringLiteral(lhs.argumentExpression) &&
|
||||
(isExportsIdent(lhs.expression) || isModuleExports(lhs.expression))
|
||||
) {
|
||||
exports.add(lhs.argumentExpression.text);
|
||||
} else if (isModuleExports(lhs)) {
|
||||
events.push({ pos: n.getStart(sf), kind: "assign", rhs: n.right });
|
||||
}
|
||||
} else if (ts.isCallExpression(n)) {
|
||||
scanDefineProperty(n, sf, exports);
|
||||
if (braceDepth === 0) {
|
||||
const starSpec = starExportSpecOf(n, sf);
|
||||
if (starSpec !== null) events.push({ pos: n.getStart(sf), kind: "spec", spec: starSpec });
|
||||
const loopId = starLoopIdOf(n, sf);
|
||||
if (loopId !== null) events.push({ pos: n.getStart(sf), kind: "loop", id1: loopId });
|
||||
}
|
||||
} else if (ts.isVariableStatement(n) && braceDepth === 0) {
|
||||
const assign = starAssignOf(n, sf);
|
||||
if (assign !== null) {
|
||||
const specs = starAssigns.get(assign[0]);
|
||||
if (specs === undefined) starAssigns.set(assign[0], [assign[1]]);
|
||||
else specs.push(assign[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
events.sort((a, b) => a.pos - b.pos);
|
||||
let reexports: string[] = [];
|
||||
for (const ev of events) {
|
||||
if (ev.kind === "spec") {
|
||||
reexports.push(ev.spec);
|
||||
} else if (ev.kind === "loop") {
|
||||
for (const spec of starAssigns.get(ev.id1) ?? []) reexports.push(spec);
|
||||
} else {
|
||||
reexports = []; // every `module.exports =` clears — stars included (probed)
|
||||
const req = leadingRequireOf(ev.rhs, sf);
|
||||
if (req !== null) {
|
||||
reexports.push(req.spec);
|
||||
} else if (ts.isObjectLiteralExpression(ev.rhs)) {
|
||||
// A PARENTHESIZED literal never matches — merve needs the `{` right
|
||||
// after the `=` — so no unwrapping here (unlike the checker's view).
|
||||
scanTableLiteral(ev.rhs, sf, exports, reexports);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { exports, reexports };
|
||||
}
|
||||
|
||||
/** The full Node-visible named-export set of a CommonJS module: its own
|
||||
* lexed names plus, recursively, the names of every reexport target that
|
||||
* itself resolves to a CommonJS module (Node's cjsPreparseModuleExports —
|
||||
* ESM / JSON / builtin / unresolved targets contribute nothing). Generic
|
||||
* over the caller's module HANDLE so both typescript worlds can drive it
|
||||
* with their own file objects — only strings cross this boundary.
|
||||
* Cycle-safe. */
|
||||
export function cjsLexerVisibleNames<H>(
|
||||
mod: H,
|
||||
sourceOf: (mod: H) => string,
|
||||
resolveCjsDep: (from: H, spec: string) => H | null,
|
||||
memo: Map<H, Set<string>> = new Map(),
|
||||
): Set<string> {
|
||||
const hit = memo.get(mod);
|
||||
if (hit !== undefined) return hit;
|
||||
const { exports, reexports } = cjsLexedExportsOf(sourceOf(mod));
|
||||
memo.set(mod, exports); // set BEFORE recursing, like Node — cycles converge
|
||||
for (const spec of reexports) {
|
||||
const dep = resolveCjsDep(mod, spec);
|
||||
if (dep === null) continue;
|
||||
for (const name of cjsLexerVisibleNames(dep, sourceOf, resolveCjsDep, memo)) exports.add(name);
|
||||
}
|
||||
return exports;
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/* node:http2 constants — the full table Node v24.15.0 exposes as
|
||||
* http2.constants (240 members: NGHTTP2_* protocol codes, NGHTTP2_ERR_*
|
||||
* library codes, HTTP2_HEADER_* names, HTTP2_METHOD_* verbs, and the
|
||||
* HTTP_STATUS_* table), generated with
|
||||
* node -p "require('http2').constants"
|
||||
* and baked as literals at every access site: the object itself never
|
||||
* materializes at runtime (the fs.constants precedent, scaled up).
|
||||
* Every value is a number or a string, so each read is a numLit/strLit. */
|
||||
|
||||
export const HTTP2_CONSTANTS: Record<string, number | string> = {
|
||||
"NGHTTP2_ERR_FRAME_SIZE_ERROR": -522,
|
||||
"NGHTTP2_SESSION_SERVER": 0,
|
||||
"NGHTTP2_SESSION_CLIENT": 1,
|
||||
"NGHTTP2_STREAM_STATE_IDLE": 1,
|
||||
"NGHTTP2_STREAM_STATE_OPEN": 2,
|
||||
"NGHTTP2_STREAM_STATE_RESERVED_LOCAL": 3,
|
||||
"NGHTTP2_STREAM_STATE_RESERVED_REMOTE": 4,
|
||||
"NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL": 5,
|
||||
"NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE": 6,
|
||||
"NGHTTP2_STREAM_STATE_CLOSED": 7,
|
||||
"NGHTTP2_FLAG_NONE": 0,
|
||||
"NGHTTP2_FLAG_END_STREAM": 1,
|
||||
"NGHTTP2_FLAG_END_HEADERS": 4,
|
||||
"NGHTTP2_FLAG_ACK": 1,
|
||||
"NGHTTP2_FLAG_PADDED": 8,
|
||||
"NGHTTP2_FLAG_PRIORITY": 32,
|
||||
"DEFAULT_SETTINGS_HEADER_TABLE_SIZE": 4096,
|
||||
"DEFAULT_SETTINGS_ENABLE_PUSH": 1,
|
||||
"DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS": 4294967295,
|
||||
"DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE": 65535,
|
||||
"DEFAULT_SETTINGS_MAX_FRAME_SIZE": 16384,
|
||||
"DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE": 65535,
|
||||
"DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL": 0,
|
||||
"MAX_MAX_FRAME_SIZE": 16777215,
|
||||
"MIN_MAX_FRAME_SIZE": 16384,
|
||||
"MAX_INITIAL_WINDOW_SIZE": 2147483647,
|
||||
"NGHTTP2_SETTINGS_HEADER_TABLE_SIZE": 1,
|
||||
"NGHTTP2_SETTINGS_ENABLE_PUSH": 2,
|
||||
"NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS": 3,
|
||||
"NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE": 4,
|
||||
"NGHTTP2_SETTINGS_MAX_FRAME_SIZE": 5,
|
||||
"NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE": 6,
|
||||
"NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL": 8,
|
||||
"PADDING_STRATEGY_NONE": 0,
|
||||
"PADDING_STRATEGY_ALIGNED": 1,
|
||||
"PADDING_STRATEGY_MAX": 2,
|
||||
"PADDING_STRATEGY_CALLBACK": 1,
|
||||
"NGHTTP2_NO_ERROR": 0,
|
||||
"NGHTTP2_PROTOCOL_ERROR": 1,
|
||||
"NGHTTP2_INTERNAL_ERROR": 2,
|
||||
"NGHTTP2_FLOW_CONTROL_ERROR": 3,
|
||||
"NGHTTP2_SETTINGS_TIMEOUT": 4,
|
||||
"NGHTTP2_STREAM_CLOSED": 5,
|
||||
"NGHTTP2_FRAME_SIZE_ERROR": 6,
|
||||
"NGHTTP2_REFUSED_STREAM": 7,
|
||||
"NGHTTP2_CANCEL": 8,
|
||||
"NGHTTP2_COMPRESSION_ERROR": 9,
|
||||
"NGHTTP2_CONNECT_ERROR": 10,
|
||||
"NGHTTP2_ENHANCE_YOUR_CALM": 11,
|
||||
"NGHTTP2_INADEQUATE_SECURITY": 12,
|
||||
"NGHTTP2_HTTP_1_1_REQUIRED": 13,
|
||||
"NGHTTP2_DEFAULT_WEIGHT": 16,
|
||||
"HTTP2_HEADER_STATUS": ":status",
|
||||
"HTTP2_HEADER_METHOD": ":method",
|
||||
"HTTP2_HEADER_AUTHORITY": ":authority",
|
||||
"HTTP2_HEADER_SCHEME": ":scheme",
|
||||
"HTTP2_HEADER_PATH": ":path",
|
||||
"HTTP2_HEADER_PROTOCOL": ":protocol",
|
||||
"HTTP2_HEADER_ACCEPT_ENCODING": "accept-encoding",
|
||||
"HTTP2_HEADER_ACCEPT_LANGUAGE": "accept-language",
|
||||
"HTTP2_HEADER_ACCEPT_RANGES": "accept-ranges",
|
||||
"HTTP2_HEADER_ACCEPT": "accept",
|
||||
"HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS": "access-control-allow-credentials",
|
||||
"HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS": "access-control-allow-headers",
|
||||
"HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS": "access-control-allow-methods",
|
||||
"HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN": "access-control-allow-origin",
|
||||
"HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS": "access-control-expose-headers",
|
||||
"HTTP2_HEADER_ACCESS_CONTROL_REQUEST_HEADERS": "access-control-request-headers",
|
||||
"HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD": "access-control-request-method",
|
||||
"HTTP2_HEADER_AGE": "age",
|
||||
"HTTP2_HEADER_AUTHORIZATION": "authorization",
|
||||
"HTTP2_HEADER_CACHE_CONTROL": "cache-control",
|
||||
"HTTP2_HEADER_CONNECTION": "connection",
|
||||
"HTTP2_HEADER_CONTENT_DISPOSITION": "content-disposition",
|
||||
"HTTP2_HEADER_CONTENT_ENCODING": "content-encoding",
|
||||
"HTTP2_HEADER_CONTENT_LENGTH": "content-length",
|
||||
"HTTP2_HEADER_CONTENT_TYPE": "content-type",
|
||||
"HTTP2_HEADER_COOKIE": "cookie",
|
||||
"HTTP2_HEADER_DATE": "date",
|
||||
"HTTP2_HEADER_ETAG": "etag",
|
||||
"HTTP2_HEADER_FORWARDED": "forwarded",
|
||||
"HTTP2_HEADER_HOST": "host",
|
||||
"HTTP2_HEADER_IF_MODIFIED_SINCE": "if-modified-since",
|
||||
"HTTP2_HEADER_IF_NONE_MATCH": "if-none-match",
|
||||
"HTTP2_HEADER_IF_RANGE": "if-range",
|
||||
"HTTP2_HEADER_LAST_MODIFIED": "last-modified",
|
||||
"HTTP2_HEADER_LINK": "link",
|
||||
"HTTP2_HEADER_LOCATION": "location",
|
||||
"HTTP2_HEADER_RANGE": "range",
|
||||
"HTTP2_HEADER_REFERER": "referer",
|
||||
"HTTP2_HEADER_SERVER": "server",
|
||||
"HTTP2_HEADER_SET_COOKIE": "set-cookie",
|
||||
"HTTP2_HEADER_STRICT_TRANSPORT_SECURITY": "strict-transport-security",
|
||||
"HTTP2_HEADER_TRANSFER_ENCODING": "transfer-encoding",
|
||||
"HTTP2_HEADER_TE": "te",
|
||||
"HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS": "upgrade-insecure-requests",
|
||||
"HTTP2_HEADER_UPGRADE": "upgrade",
|
||||
"HTTP2_HEADER_USER_AGENT": "user-agent",
|
||||
"HTTP2_HEADER_VARY": "vary",
|
||||
"HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS": "x-content-type-options",
|
||||
"HTTP2_HEADER_X_FRAME_OPTIONS": "x-frame-options",
|
||||
"HTTP2_HEADER_KEEP_ALIVE": "keep-alive",
|
||||
"HTTP2_HEADER_PROXY_CONNECTION": "proxy-connection",
|
||||
"HTTP2_HEADER_X_XSS_PROTECTION": "x-xss-protection",
|
||||
"HTTP2_HEADER_ALT_SVC": "alt-svc",
|
||||
"HTTP2_HEADER_CONTENT_SECURITY_POLICY": "content-security-policy",
|
||||
"HTTP2_HEADER_EARLY_DATA": "early-data",
|
||||
"HTTP2_HEADER_EXPECT_CT": "expect-ct",
|
||||
"HTTP2_HEADER_ORIGIN": "origin",
|
||||
"HTTP2_HEADER_PURPOSE": "purpose",
|
||||
"HTTP2_HEADER_TIMING_ALLOW_ORIGIN": "timing-allow-origin",
|
||||
"HTTP2_HEADER_X_FORWARDED_FOR": "x-forwarded-for",
|
||||
"HTTP2_HEADER_PRIORITY": "priority",
|
||||
"HTTP2_HEADER_ACCEPT_CHARSET": "accept-charset",
|
||||
"HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE": "access-control-max-age",
|
||||
"HTTP2_HEADER_ALLOW": "allow",
|
||||
"HTTP2_HEADER_CONTENT_LANGUAGE": "content-language",
|
||||
"HTTP2_HEADER_CONTENT_LOCATION": "content-location",
|
||||
"HTTP2_HEADER_CONTENT_MD5": "content-md5",
|
||||
"HTTP2_HEADER_CONTENT_RANGE": "content-range",
|
||||
"HTTP2_HEADER_DNT": "dnt",
|
||||
"HTTP2_HEADER_EXPECT": "expect",
|
||||
"HTTP2_HEADER_EXPIRES": "expires",
|
||||
"HTTP2_HEADER_FROM": "from",
|
||||
"HTTP2_HEADER_IF_MATCH": "if-match",
|
||||
"HTTP2_HEADER_IF_UNMODIFIED_SINCE": "if-unmodified-since",
|
||||
"HTTP2_HEADER_MAX_FORWARDS": "max-forwards",
|
||||
"HTTP2_HEADER_PREFER": "prefer",
|
||||
"HTTP2_HEADER_PROXY_AUTHENTICATE": "proxy-authenticate",
|
||||
"HTTP2_HEADER_PROXY_AUTHORIZATION": "proxy-authorization",
|
||||
"HTTP2_HEADER_REFRESH": "refresh",
|
||||
"HTTP2_HEADER_RETRY_AFTER": "retry-after",
|
||||
"HTTP2_HEADER_TRAILER": "trailer",
|
||||
"HTTP2_HEADER_TK": "tk",
|
||||
"HTTP2_HEADER_VIA": "via",
|
||||
"HTTP2_HEADER_WARNING": "warning",
|
||||
"HTTP2_HEADER_WWW_AUTHENTICATE": "www-authenticate",
|
||||
"HTTP2_HEADER_HTTP2_SETTINGS": "http2-settings",
|
||||
"HTTP2_METHOD_ACL": "ACL",
|
||||
"HTTP2_METHOD_BASELINE_CONTROL": "BASELINE-CONTROL",
|
||||
"HTTP2_METHOD_BIND": "BIND",
|
||||
"HTTP2_METHOD_CHECKIN": "CHECKIN",
|
||||
"HTTP2_METHOD_CHECKOUT": "CHECKOUT",
|
||||
"HTTP2_METHOD_CONNECT": "CONNECT",
|
||||
"HTTP2_METHOD_COPY": "COPY",
|
||||
"HTTP2_METHOD_DELETE": "DELETE",
|
||||
"HTTP2_METHOD_GET": "GET",
|
||||
"HTTP2_METHOD_HEAD": "HEAD",
|
||||
"HTTP2_METHOD_LABEL": "LABEL",
|
||||
"HTTP2_METHOD_LINK": "LINK",
|
||||
"HTTP2_METHOD_LOCK": "LOCK",
|
||||
"HTTP2_METHOD_MERGE": "MERGE",
|
||||
"HTTP2_METHOD_MKACTIVITY": "MKACTIVITY",
|
||||
"HTTP2_METHOD_MKCALENDAR": "MKCALENDAR",
|
||||
"HTTP2_METHOD_MKCOL": "MKCOL",
|
||||
"HTTP2_METHOD_MKREDIRECTREF": "MKREDIRECTREF",
|
||||
"HTTP2_METHOD_MKWORKSPACE": "MKWORKSPACE",
|
||||
"HTTP2_METHOD_MOVE": "MOVE",
|
||||
"HTTP2_METHOD_OPTIONS": "OPTIONS",
|
||||
"HTTP2_METHOD_ORDERPATCH": "ORDERPATCH",
|
||||
"HTTP2_METHOD_PATCH": "PATCH",
|
||||
"HTTP2_METHOD_POST": "POST",
|
||||
"HTTP2_METHOD_PRI": "PRI",
|
||||
"HTTP2_METHOD_PROPFIND": "PROPFIND",
|
||||
"HTTP2_METHOD_PROPPATCH": "PROPPATCH",
|
||||
"HTTP2_METHOD_PUT": "PUT",
|
||||
"HTTP2_METHOD_REBIND": "REBIND",
|
||||
"HTTP2_METHOD_REPORT": "REPORT",
|
||||
"HTTP2_METHOD_SEARCH": "SEARCH",
|
||||
"HTTP2_METHOD_TRACE": "TRACE",
|
||||
"HTTP2_METHOD_UNBIND": "UNBIND",
|
||||
"HTTP2_METHOD_UNCHECKOUT": "UNCHECKOUT",
|
||||
"HTTP2_METHOD_UNLINK": "UNLINK",
|
||||
"HTTP2_METHOD_UNLOCK": "UNLOCK",
|
||||
"HTTP2_METHOD_UPDATE": "UPDATE",
|
||||
"HTTP2_METHOD_UPDATEREDIRECTREF": "UPDATEREDIRECTREF",
|
||||
"HTTP2_METHOD_VERSION_CONTROL": "VERSION-CONTROL",
|
||||
"HTTP_STATUS_CONTINUE": 100,
|
||||
"HTTP_STATUS_SWITCHING_PROTOCOLS": 101,
|
||||
"HTTP_STATUS_PROCESSING": 102,
|
||||
"HTTP_STATUS_EARLY_HINTS": 103,
|
||||
"HTTP_STATUS_OK": 200,
|
||||
"HTTP_STATUS_CREATED": 201,
|
||||
"HTTP_STATUS_ACCEPTED": 202,
|
||||
"HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION": 203,
|
||||
"HTTP_STATUS_NO_CONTENT": 204,
|
||||
"HTTP_STATUS_RESET_CONTENT": 205,
|
||||
"HTTP_STATUS_PARTIAL_CONTENT": 206,
|
||||
"HTTP_STATUS_MULTI_STATUS": 207,
|
||||
"HTTP_STATUS_ALREADY_REPORTED": 208,
|
||||
"HTTP_STATUS_IM_USED": 226,
|
||||
"HTTP_STATUS_MULTIPLE_CHOICES": 300,
|
||||
"HTTP_STATUS_MOVED_PERMANENTLY": 301,
|
||||
"HTTP_STATUS_FOUND": 302,
|
||||
"HTTP_STATUS_SEE_OTHER": 303,
|
||||
"HTTP_STATUS_NOT_MODIFIED": 304,
|
||||
"HTTP_STATUS_USE_PROXY": 305,
|
||||
"HTTP_STATUS_TEMPORARY_REDIRECT": 307,
|
||||
"HTTP_STATUS_PERMANENT_REDIRECT": 308,
|
||||
"HTTP_STATUS_BAD_REQUEST": 400,
|
||||
"HTTP_STATUS_UNAUTHORIZED": 401,
|
||||
"HTTP_STATUS_PAYMENT_REQUIRED": 402,
|
||||
"HTTP_STATUS_FORBIDDEN": 403,
|
||||
"HTTP_STATUS_NOT_FOUND": 404,
|
||||
"HTTP_STATUS_METHOD_NOT_ALLOWED": 405,
|
||||
"HTTP_STATUS_NOT_ACCEPTABLE": 406,
|
||||
"HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED": 407,
|
||||
"HTTP_STATUS_REQUEST_TIMEOUT": 408,
|
||||
"HTTP_STATUS_CONFLICT": 409,
|
||||
"HTTP_STATUS_GONE": 410,
|
||||
"HTTP_STATUS_LENGTH_REQUIRED": 411,
|
||||
"HTTP_STATUS_PRECONDITION_FAILED": 412,
|
||||
"HTTP_STATUS_PAYLOAD_TOO_LARGE": 413,
|
||||
"HTTP_STATUS_URI_TOO_LONG": 414,
|
||||
"HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE": 415,
|
||||
"HTTP_STATUS_RANGE_NOT_SATISFIABLE": 416,
|
||||
"HTTP_STATUS_EXPECTATION_FAILED": 417,
|
||||
"HTTP_STATUS_TEAPOT": 418,
|
||||
"HTTP_STATUS_MISDIRECTED_REQUEST": 421,
|
||||
"HTTP_STATUS_UNPROCESSABLE_ENTITY": 422,
|
||||
"HTTP_STATUS_LOCKED": 423,
|
||||
"HTTP_STATUS_FAILED_DEPENDENCY": 424,
|
||||
"HTTP_STATUS_TOO_EARLY": 425,
|
||||
"HTTP_STATUS_UPGRADE_REQUIRED": 426,
|
||||
"HTTP_STATUS_PRECONDITION_REQUIRED": 428,
|
||||
"HTTP_STATUS_TOO_MANY_REQUESTS": 429,
|
||||
"HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE": 431,
|
||||
"HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS": 451,
|
||||
"HTTP_STATUS_INTERNAL_SERVER_ERROR": 500,
|
||||
"HTTP_STATUS_NOT_IMPLEMENTED": 501,
|
||||
"HTTP_STATUS_BAD_GATEWAY": 502,
|
||||
"HTTP_STATUS_SERVICE_UNAVAILABLE": 503,
|
||||
"HTTP_STATUS_GATEWAY_TIMEOUT": 504,
|
||||
"HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED": 505,
|
||||
"HTTP_STATUS_VARIANT_ALSO_NEGOTIATES": 506,
|
||||
"HTTP_STATUS_INSUFFICIENT_STORAGE": 507,
|
||||
"HTTP_STATUS_LOOP_DETECTED": 508,
|
||||
"HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED": 509,
|
||||
"HTTP_STATUS_NOT_EXTENDED": 510,
|
||||
"HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED": 511,
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
/* The lib-boundary pass: the checked-coercion chokepoint between lowered
|
||||
* statements and the validator's libCall/intrinsic signature tables.
|
||||
*
|
||||
* Builtin-call lowerings assemble their IR at hundreds of sites, and most
|
||||
* lower their arguments with plain lowerExpr — no coerceToExpected against
|
||||
* the runtime signature. A TYPED argument can't go wrong there (tsc already
|
||||
* agreed with the surface), but a CHECKED-DYNAMIC one can: JS sources have
|
||||
* no annotations, so untyped helpers return dyn (`common.platformTimeout(10)`
|
||||
* feeding setTimeout's ms), and the raw dyn used to sail into the validator
|
||||
* as an ICE — or, worse, past it into the emitter as invalid C.
|
||||
*
|
||||
* This pass runs once per lowered statement (lowerStmts calls it inside the
|
||||
* per-statement poison window) and re-checks every libCall / strIntrinsic /
|
||||
* regexIntrinsic / arrIntrinsic / callValue argument against the SAME
|
||||
* signature tables the validator enforces (exported from ir/validate.ts —
|
||||
* one table, two consumers, no drift):
|
||||
*
|
||||
* - a dyn argument whose slot dynCheck can validate (JSON-safe types,
|
||||
* undefined-armed unions of those, bytes<u8>, the %Error root) is
|
||||
* WRAPPED in a dynCheck — the trust-but-verify stance coerceToExpected
|
||||
* already applies at declaration/assignment/return edges, extended to
|
||||
* the builtin-call edges that bypassed it. A dyn holding the right
|
||||
* value passes; a lying one throws the catchable TypeError.
|
||||
* - a dyn argument whose slot dynCheck can NOT validate (functions,
|
||||
* opaque handles like stats/sockets) is FENCED with the SC1100 message
|
||||
* requireExactShape would have used — in JS sources the statement
|
||||
* compiles to its runtime fence, in TS it is a compile diagnostic.
|
||||
* Either way: named, never an ICE, never invalid C.
|
||||
* - unit-typed and union-typed arguments in scalar slots fence the same
|
||||
* way (`setTimeout(cb, value)` over a number|string array), with the
|
||||
* union-mismatch wording the coercion edges use.
|
||||
* - callValue arity mismatches (calling through a checked-dynamic-typed
|
||||
* function value with more arguments than its lowered signature) fence
|
||||
* with the call shape named.
|
||||
*
|
||||
* 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 type { Lowerer } from "./lowerer.js";
|
||||
import { PoisonError } from "./lowerer.js";
|
||||
import { canAdaptDynFuncTo, IrExpr, IrType, SrcLoc, STRING, isUnitType, typeEquals } from "../../ir/nodes.js";
|
||||
import { LIB_FN_SIGS, REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "../../ir/validate.js";
|
||||
import { unionMismatchDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
|
||||
/** What a dynCheck can validate — coerceToExpected's dyn→typed domain,
|
||||
* kept in lockstep with the wrap it emits there. Adaptable FUNCTION
|
||||
* targets are in (the checked-dynamic function boundary: a dyn-wrapped
|
||||
* callback flowing into setTimeout's `() => void` slot adapts through
|
||||
* the per-target shim). */
|
||||
function dynCheckable(L: Lowerer, want: IrType): boolean {
|
||||
if (L.jsonSafe(want)) return true;
|
||||
if (want.kind === "bytes" && want.elem === "u8") return true;
|
||||
if (want.kind === "object" && want.className === "%Error") return true;
|
||||
if (want.kind === "func") {
|
||||
return canAdaptDynFuncTo(want, (id) => L.shapes.get(id), (id) => L.unions.get(id));
|
||||
}
|
||||
if (want.kind === "union") {
|
||||
const def = L.unions.get(want.unionId);
|
||||
return !!def && def.arms.every((a) => a.kind === "undefinedT" || L.jsonSafe(a));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The checked coercion for one argument slot, or the named fence. Returns
|
||||
* the (possibly wrapped) expression; throws PoisonError on a fence so the
|
||||
* statement takes the runtime-fence path in JS sources. */
|
||||
function coerceSlot(L: Lowerer, arg: IrExpr, want: IrType, what: string): IrExpr {
|
||||
if (typeEquals(arg.type, want)) return arg;
|
||||
if (arg.type.kind === "dyn" && want.kind !== "dyn") {
|
||||
if (dynCheckable(L, want)) {
|
||||
return { kind: "dynCheck", value: arg, type: want, loc: arg.loc };
|
||||
}
|
||||
fence(L, "SC1100", arg.loc, `passing 'unknown' values where '${L.fmt(want)}' is expected (${what})`);
|
||||
}
|
||||
if (isUnitType(arg.type) && want.kind !== "union" && !isUnitType(want)) {
|
||||
fence(L, "SC1090", arg.loc, `'${arg.type.kind === "undefinedT" ? "undefined" : "null"}' values where '${L.fmt(want)}' is expected (${what})`);
|
||||
}
|
||||
if (arg.type.kind === "union" && want.kind !== "union" && !containsDynOrUnit(want)) {
|
||||
L.pushDiag(unionMismatchDiag(L.fmt(want), L.fmt(arg.type), arg.loc));
|
||||
throw new PoisonError();
|
||||
}
|
||||
// Every other mismatch: leave it for the validator (the backstop). The
|
||||
// signature tables carry program-independent slots only; the shapes this
|
||||
// pass exists for are the dyn/unit/union escapes above.
|
||||
return arg;
|
||||
}
|
||||
|
||||
function containsDynOrUnit(t: IrType): boolean {
|
||||
return t.kind === "dyn" || isUnitType(t);
|
||||
}
|
||||
|
||||
function fence(L: Lowerer, code: "SC1090" | "SC1100", loc: SrcLoc, feature: string): never {
|
||||
L.pushDiag(unsupportedDiag(code, loc, feature));
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
/** Walks one lowered statement's IR in place, coercing or fencing every
|
||||
* signature-table argument slot. Idempotent: a wrapped argument matches its
|
||||
* slot on a revisit (nested statement lists are walked by their own
|
||||
* lowerStmts call first, then again inside the enclosing statement). */
|
||||
export function enforceLibBoundary(L: Lowerer, node: unknown): void {
|
||||
if (node === null || typeof node !== "object") return;
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) enforceLibBoundary(L, item);
|
||||
return;
|
||||
}
|
||||
const rec = node as Record<string, unknown>;
|
||||
for (const key of Object.keys(rec)) {
|
||||
if (key !== "loc") enforceLibBoundary(L, rec[key]);
|
||||
}
|
||||
const kind = rec["kind"];
|
||||
if (kind === "libCall") {
|
||||
const e = rec as unknown as Extract<IrExpr, { kind: "libCall" }>;
|
||||
const sig = LIB_FN_SIGS[e.fn];
|
||||
if (!sig) return;
|
||||
e.args.forEach((a, i) => {
|
||||
const want = sig.argTypes[i];
|
||||
if (want) e.args[i] = coerceSlot(L, a, want, `argument ${i + 1} of ${e.fn}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (kind === "strIntrinsic") {
|
||||
const e = rec as unknown as Extract<IrExpr, { kind: "strIntrinsic" }>;
|
||||
e.receiver = coerceSlot(L, e.receiver, STRING, `the receiver of .${e.method}`);
|
||||
const sig = STR_INTRINSIC_SIGS[e.method];
|
||||
if (!sig) return;
|
||||
e.args.forEach((a, i) => {
|
||||
const want = sig.argTypes[i];
|
||||
if (want) e.args[i] = coerceSlot(L, a, want, `argument ${i + 1} of .${e.method}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (kind === "regexIntrinsic") {
|
||||
const e = rec as unknown as Extract<IrExpr, { kind: "regexIntrinsic" }>;
|
||||
const sig = REGEX_INTRINSIC_SIGS[e.method];
|
||||
if (!sig) return;
|
||||
e.receiver = coerceSlot(L, e.receiver, sig.receiver, `the receiver of .${e.method}`);
|
||||
e.args.forEach((a, i) => {
|
||||
const want = sig.argTypes[i];
|
||||
if (want) e.args[i] = coerceSlot(L, a, want, `argument ${i + 1} of .${e.method}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (kind === "arrIntrinsic") {
|
||||
const e = rec as unknown as Extract<IrExpr, { kind: "arrIntrinsic" }>;
|
||||
if (e.receiver.type.kind === "dyn" || isUnitType(e.receiver.type)) {
|
||||
// No element type exists to validate a dyn receiver against — the
|
||||
// honest answer is the operations-on-unknown fence. Other non-array
|
||||
// receivers stay the validator's ICE (frontend breakage, not a
|
||||
// checked-dynamic escape).
|
||||
fence(L, "SC1100", e.receiver.loc, `'.${e.method}()' on '${L.fmt(e.receiver.type)}' array receivers`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kind === "callValue") {
|
||||
const e = rec as unknown as Extract<IrExpr, { kind: "callValue" }>;
|
||||
if (e.callee.type.kind === "dyn" || isUnitType(e.callee.type)) {
|
||||
fence(L, "SC1100", e.loc, `calling '${L.fmt(e.callee.type)}' values`);
|
||||
}
|
||||
if (e.callee.type.kind !== "func") return; // validator's ICE otherwise
|
||||
const params = e.callee.type.params;
|
||||
if (e.args.length !== params.length) {
|
||||
fence(
|
||||
L,
|
||||
"SC1090",
|
||||
e.loc,
|
||||
`calling a function value with ${e.args.length} argument${e.args.length === 1 ? "" : "s"} where its lowered signature takes ${params.length}`,
|
||||
);
|
||||
}
|
||||
e.args.forEach((a, i) => {
|
||||
const want = params[i];
|
||||
if (want) e.args[i] = coerceSlot(L, a, want, `argument ${i + 1} of the call`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,273 @@
|
||||
/* comptime(fn) lowering: run the closed callback under node:vm at compile
|
||||
* time (with a timeout), then bake the produced VALUE into the IR as
|
||||
* literals — records/arrays/unions included, subject to comptimeBakeable. */
|
||||
import vm from "node:vm";
|
||||
import ts5 from "typescript5";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { IrExpr, IrType } from "../../ir/nodes.js";
|
||||
import { comptimeFailedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { PoisonError } from "./lowerer.js";
|
||||
|
||||
/** Wall-clock budget for one comptime callback. Evaluation happens inside
|
||||
* the compiler (node:vm), so a runaway loop would hang the BUILD — the vm
|
||||
* timeout turns it into a diagnostic instead. */
|
||||
export const COMPTIME_TIMEOUT_MS = 2000;
|
||||
|
||||
/** Names a compile-time-computed JS value in a comptime result diagnostic
|
||||
* ("expected 'number' at $.t[2], got undefined"). */
|
||||
export function describeComptimeValue(v: unknown): string {
|
||||
if (v === undefined) return "undefined";
|
||||
if (v === null) return "null";
|
||||
if (Array.isArray(v)) return "an array";
|
||||
const t = typeof v;
|
||||
return t === "object" ? "an object" : `a ${t}`;
|
||||
}
|
||||
|
||||
/** `comptime(() => ...)`: the callback is extracted SOURCE-TEXTUALLY,
|
||||
* executed at compile time in an isolated node:vm context (real JavaScript
|
||||
* semantics — the compiler's own Node process), and the returned value is
|
||||
* lowered as if it were a literal written at the call site. No IR nodes,
|
||||
* no backend or runtime involvement — a pure frontend desugar to
|
||||
* numLit/strLit/boolLit/arrayLit/recordLit.
|
||||
*
|
||||
* The pipeline, each step with its own rejection:
|
||||
* 1. the argument must be an INLINE arrow/function-expression literal
|
||||
* (not a reference — there is no closure to extract from a value);
|
||||
* 2. not async/generator;
|
||||
* 3. no references to outer bindings (the callback must be self-contained
|
||||
* — it runs in a fresh context where module state doesn't exist) and no
|
||||
* nested comptime calls; the ambient surface (JSON, ...) is permitted
|
||||
* by the checker, though members with no vm counterpart (console,
|
||||
* process, fs) fail at evaluation;
|
||||
* 4. the call's checker type T must bake as a literal (number/string/
|
||||
* boolean/arrays/records, nested);
|
||||
* 5. evaluation runs under a wall-clock budget; a throw or timeout is a
|
||||
* SC1110 diagnostic quoting the message;
|
||||
* 6. the returned VALUE is checked against T, path-annotated
|
||||
* (dynCheck-style) — NaN/Infinity, undefined/null/functions, and shape
|
||||
* mismatches are rejected, never miscompiled. */
|
||||
export function lowerComptime(L: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// 1. Inline function literal only (tsc guarantees the arity/type; the
|
||||
// literal-ness is our requirement — source extraction needs the source).
|
||||
let cb: ts.Expression | undefined = expr.arguments[0];
|
||||
while (cb && ts.isParenthesizedExpression(cb)) cb = cb.expression;
|
||||
if (!cb || !(ts.isArrowFunction(cb) || ts.isFunctionExpression(cb))) {
|
||||
L.unsupported(
|
||||
"SC1090",
|
||||
expr.arguments[0] ?? expr,
|
||||
"comptime arguments other than inline function literals " +
|
||||
"(write the callback directly in the call: comptime(() => ...))",
|
||||
);
|
||||
}
|
||||
// 2. Async/generator callbacks have no bakeable result.
|
||||
if (
|
||||
ts.isFunctionExpression(cb) && cb.asteriskToken ||
|
||||
cb.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)
|
||||
) {
|
||||
L.unsupported("SC1090", cb, "async or generator comptime callbacks");
|
||||
}
|
||||
// 3. Self-containment.
|
||||
L.rejectComptimeCaptures(cb);
|
||||
// 4. The checker's type for the call is the bake target.
|
||||
const target = L.irTypeOf(expr);
|
||||
if (!L.comptimeBakeable(target)) {
|
||||
L.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`baking comptime results of type '${L.fmt(target)}' into the binary ` +
|
||||
`(only number, string, boolean, arrays, and records lower to literals)`,
|
||||
);
|
||||
}
|
||||
// 5. Evaluate. Types are stripped first (the extracted text is
|
||||
// TypeScript; vm runs JavaScript), then the IIFE runs in a FRESH vm
|
||||
// context: JS intrinsics (JSON, Object, ...) exist, the compiler's own
|
||||
// globals and the shipped declarations' Node-isms (console, process,
|
||||
// fs, setTimeout) do not — so a comptime island cannot leak side effects
|
||||
// into the build, and the vm timeout bounds runaway loops.
|
||||
// The TRANSPILE ISLAND: typescript@7.0.2 ships no client-side
|
||||
// transpiler, so this one call keeps 5.9.3 (adapter.ts's two-world
|
||||
// rules — only TEXT crosses this boundary, never AST/checker objects).
|
||||
const js = ts5.transpileModule(`(${cb.getText()})()`, {
|
||||
compilerOptions: { target: ts5.ScriptTarget.ESNext },
|
||||
}).outputText;
|
||||
let result: unknown;
|
||||
try {
|
||||
// `console: undefined` shadows V8's built-in per-context console (a
|
||||
// silent inspector hook) so no code path can log into the void — the
|
||||
// capture walk already rejects direct uses with a better message.
|
||||
result = vm.runInNewContext(js, { console: undefined }, { timeout: COMPTIME_TIMEOUT_MS });
|
||||
} catch (e) {
|
||||
// Errors born inside the vm context (and the timeout error itself) are
|
||||
// CROSS-REALM objects — `instanceof Error` is false — so the code and
|
||||
// message are read structurally.
|
||||
const err = typeof e === "object" && e !== null ? (e as { code?: unknown; message?: unknown }) : null;
|
||||
const detail =
|
||||
err?.code === "ERR_SCRIPT_EXECUTION_TIMEOUT"
|
||||
? `evaluation exceeded the ${COMPTIME_TIMEOUT_MS}ms compile-time budget`
|
||||
: `the callback threw: ${err && typeof err.message === "string" ? err.message : String(e)}`;
|
||||
L.pushDiag(comptimeFailedDiag(detail, locOf(cb)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
// 6. Result → literal IR, checked against T.
|
||||
return L.comptimeValueToIr(result, target, "$", cb);
|
||||
}
|
||||
|
||||
/** True when a comptime result of this type can be written as a literal
|
||||
* expression: number/string/boolean, arrays and record shapes of those,
|
||||
* nested. Closures, class instances, unions, promises, dyn, and void
|
||||
* cannot be baked. */
|
||||
export function comptimeBakeable(L: Lowerer, t: IrType): boolean {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "string":
|
||||
case "bool":
|
||||
return true;
|
||||
case "array":
|
||||
return L.comptimeBakeable(t.elem);
|
||||
case "record": {
|
||||
const shape = L.shapes.get(t.shapeId);
|
||||
return !!shape && shape.fields.every((f) => L.comptimeBakeable(f.type));
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The self-containment fence: every identifier inside the callback must
|
||||
* resolve to a declaration INSIDE the callback (its own locals/functions)
|
||||
* or to the standard library (whose JS intrinsics exist in the vm; what
|
||||
* doesn't — process, setTimeout — fails loudly at evaluation).
|
||||
* Anything else — module locals, imports, top-level functions — would be a
|
||||
* capture, and the evaluation context has no module state to capture from.
|
||||
* Type-position references (annotations, `as` targets) are exempt: they
|
||||
* are erased before evaluation. Nested comptime calls are rejected up
|
||||
* front with their own message (the vm context has no `comptime`). */
|
||||
export function rejectComptimeCaptures(L: Lowerer, cb: ts.ArrowFunction | ts.FunctionExpression): void {
|
||||
const sf = cb.getSourceFile();
|
||||
const inTypePosition = (node: ts.Node): boolean => {
|
||||
for (let p: ts.Node | undefined = node.parent; p && p !== cb; p = p.parent) {
|
||||
if (ts.isTypeNode(p)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isIdentifier(node)) {
|
||||
const parent = node.parent;
|
||||
// Property NAMES never reference outer value bindings: members of
|
||||
// ambient interfaces (arr.push) or of shapes declared in the
|
||||
// callback. Value capture happens through the receiver identifier.
|
||||
const isPropertyName =
|
||||
(ts.isPropertyAccessExpression(parent) && parent.name === node) ||
|
||||
(ts.isPropertyAssignment(parent) && parent.name === node);
|
||||
if (!isPropertyName && !inTypePosition(node)) {
|
||||
const symbol = ts.isShorthandPropertyAssignment(parent)
|
||||
? L.checker.getShorthandAssignmentValueSymbol(parent)
|
||||
: L.checker.getSymbolAtLocation(node);
|
||||
const decls = symbol ? L.checker.declarationsOf(symbol) : undefined;
|
||||
if (symbol && decls && decls.length > 0) {
|
||||
const ownLocal = decls.every(
|
||||
(d) => d.getSourceFile() === sf && d.pos >= cb.pos && d.end <= cb.end,
|
||||
);
|
||||
if (!ownLocal) {
|
||||
if (node.text === "comptime" && L.isStdlibSymbol(symbol)) {
|
||||
L.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
"comptime calls inside comptime callbacks " +
|
||||
"(the callback already runs at compile time — compute the value directly)",
|
||||
);
|
||||
}
|
||||
// console is the one ambient global V8 installs on every vm
|
||||
// context (a silent inspector hook) — a log inside the island
|
||||
// would no-op at compile time instead of printing at runtime.
|
||||
// Rejected so side effects can't silently vanish; the other
|
||||
// Node-isms (process, setTimeout) don't exist in the vm and
|
||||
// fail loudly at evaluation.
|
||||
if (node.text === "console" && L.isStdlibSymbol(symbol)) {
|
||||
L.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
"console calls inside comptime callbacks " +
|
||||
"(the callback computes a value at compile time — return it and log at runtime)",
|
||||
);
|
||||
}
|
||||
if (!L.isStdlibSymbol(symbol)) {
|
||||
L.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`comptime callbacks referencing outer bindings ` +
|
||||
`('${node.text}' — the callback runs at compile time in an isolated ` +
|
||||
`context and must be self-contained)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(cb);
|
||||
}
|
||||
|
||||
/** A JS value computed at compile time → literal IR of the expected type,
|
||||
* recursively. Mismatches are SC1110 with the offending path
|
||||
* (dynCheck-style: `$.items[2].price`) and both sides rendered. */
|
||||
export function comptimeValueToIr(L: Lowerer, value: unknown,
|
||||
expected: IrType,
|
||||
path: string,
|
||||
blame: ts.Node,): IrExpr {
|
||||
const loc = locOf(blame);
|
||||
const fail = (got: string): never => {
|
||||
L.pushDiag(
|
||||
comptimeFailedDiag(`expected '${L.fmt(expected)}' at ${path}, got ${got}`, loc),
|
||||
);
|
||||
throw new PoisonError();
|
||||
};
|
||||
switch (expected.kind) {
|
||||
case "f64": {
|
||||
if (typeof value !== "number") return fail(describeComptimeValue(value));
|
||||
if (!Number.isFinite(value)) {
|
||||
return fail(`${String(value)} (only finite numbers can be written as literals)`);
|
||||
}
|
||||
return { kind: "numLit", value, type: expected, loc };
|
||||
}
|
||||
case "string": {
|
||||
if (typeof value !== "string") return fail(describeComptimeValue(value));
|
||||
return { kind: "strLit", value, type: expected, loc };
|
||||
}
|
||||
case "bool": {
|
||||
if (typeof value !== "boolean") return fail(describeComptimeValue(value));
|
||||
return { kind: "boolLit", value, type: expected, loc };
|
||||
}
|
||||
case "array": {
|
||||
if (!Array.isArray(value)) return fail(describeComptimeValue(value));
|
||||
const elems = (value as unknown[]).map((el, i) =>
|
||||
L.comptimeValueToIr(el, expected.elem, `${path}[${i}]`, blame),
|
||||
);
|
||||
return { kind: "arrayLit", elems, type: expected, loc };
|
||||
}
|
||||
case "record": {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
return fail(describeComptimeValue(value));
|
||||
}
|
||||
const shape = L.shapes.get(expected.shapeId)!; // bakeable-checked
|
||||
const obj = value as Record<string, unknown>;
|
||||
const declared = new Set(shape.fields.map((f) => f.name));
|
||||
for (const key of Object.keys(obj)) {
|
||||
// Exact shapes, like everywhere else: comptime results get no
|
||||
// width tolerance (nothing was declared for the extra field).
|
||||
if (!declared.has(key)) fail(`an object with an unexpected field '${key}'`);
|
||||
}
|
||||
const fields = shape.fields.map((f) => ({
|
||||
name: f.name,
|
||||
value: L.comptimeValueToIr(obj[f.name], f.type, `${path}.${f.name}`, blame),
|
||||
}));
|
||||
return { kind: "recordLit", fields, type: expected, loc };
|
||||
}
|
||||
default:
|
||||
// comptimeBakeable already fenced these off.
|
||||
throw new Error(`lowerer bug: unbakeable comptime target '${expected.kind}'`);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user