Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
<dd>Coverage only. Map an exact bare module specifier to a local declaration file supplied by an embedder. Repeat the option for multiple modules. The declaration supplies checker types so application coverage can continue; runtime imports and values remain explicit <code>SC1010</code> blockers. Relative paths resolve from the current working directory. Accepted files end in <code>.d.ts</code>, <code>.d.mts</code>, or <code>.d.cts</code>.</dd>

<dt><code>--tier &lt;auto|static-only&gt;</code></dt>
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today, and a checked cast whose value does not match its type throws instead of continuing with the value. Use it in CI to keep a program fully static. Library mode is always static-only. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today. Use it in CI to keep a program fully static. Library mode is always static-only. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>

<dt><code>--explain-dynamic[=&lt;name&gt;]</code></dt>
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/coverage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Code that operates on values whose types are known only at runtime, such as `any

`--explain-dynamic` lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as `--explain-dynamic=Point`, to show only the matching units. With `--print=diagnostics`, the document gains a `tiers` object with the same information.

`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. It also makes a checked cast whose value does not match its type throw, where `auto`, the default, continues with the value in the dynamic tier (see [runtime type checks](/docs/limitations#runtime-type-checks)). Otherwise `auto` behaves the same way today.
`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way.

## Diagnostic codes

Expand Down
15 changes: 8 additions & 7 deletions docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,25 @@ Programs are checked against the `es2025` standard library, scriptc's own declar

## Runtime type checks

Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. Parsed JSON that matches the type you cast it to uses the type's native layout. A value that does not match continues as the value it is, as under Node.js, which does not check the `as Config` assertion:
Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. For example, parsed JSON must match the type you cast it to:

```ts:cast.ts
type Config = { port: number };

for (const text of ['{"port": 8080}', '{"port": "eighty"}']) {
const config = JSON.parse(text) as Config;
console.log(config.port, typeof config.port);
try {
const config = JSON.parse('{"port": "eighty"}') as Config;
console.log(config.port);
} catch (error) {
if (error instanceof Error) console.log(error.message);
}
```

```console
$ scriptc run cast.ts
8080 number
eighty string
expected number at $.port, got string
```

See [runtime type checks](/docs/limitations#runtime-type-checks) for where a mismatch still throws.
Under Node.js, the same `as Config` assertion performs no check. See [runtime type checks](/docs/limitations#runtime-type-checks) for details.

## Build-time evaluation

Expand Down
9 changes: 1 addition & 8 deletions docs/content/limitations/runtime-type-checks.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
Casts from values whose type is only known at runtime validate the value.
A value that matches the type uses the type's native layout.
When the cast initializes a `const` or `let` binding declared in a function or block, and the binding is only read by the code after the cast, a value that does not match continues as the value it is, as under Node.js: with `JSON.parse(s) as Config` and `{"port": "eighty"}`, `config.port` is the string `"eighty"`, missing properties read `undefined`, and extra properties stay visible to `Object.keys`, `JSON.stringify`, and `util.inspect`.
That code then runs on dynamic values, which is slower than native records.
A statement of it that has no dynamic lowering throws a `TypeError` naming the construct and its location when such a value reaches it.

In the other cases a mismatch throws a catchable `TypeError` identifying the offending path, such as `expected number at $.port, got string`: casts that initialize module-level bindings or appear in other positions, and a mismatched value that leaves the code after its cast through a typed parameter, field, return value, or outer binding, where it is checked against that type.
A value that matches there continues, copied if it is an object.
`--tier=static-only` makes every mismatch throw at the cast.
For example, `JSON.parse(s) as Config` throws a catchable error if the data does not match `Config`, identifying the offending path, such as `expected number at $.port, got string`.
A TypeScript assertion alone does not perform this validation when run under Node.js.

Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally.
Expand Down
16 changes: 4 additions & 12 deletions packages/compiler/src/backend/llvm/dyn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1448,29 +1448,22 @@ export class LlDyn {
* differently), runs the checked-dynamic route — scr_json_parse plus the
* dynCheck builder — which reports the exact error. Returns +1, or null
* with the pending flag set. */
jsonParseHelper(t: IrType, fallback = false): string | null {
jsonParseHelper(t: IrType): string | null {
if (t.kind !== "record" && t.kind !== "array") return null;
const key = typeKey(t);
const cacheKey = fallback ? `fallback:${key}` : key;
const existing = this.jsonParsers.get(cacheKey);
const existing = this.jsonParsers.get(key);
if (existing) return existing;
if (!this.jsonSchemaSupported(t, new Set())) return null;
const host = this.host;
const name = `sc_jp_${this.jsonParsers.size}`;
this.jsonParsers.set(cacheKey, name);
this.jsonParsers.set(key, name);
const schema = this.jsonSchema(t);
const check = this.dynCheckHelper(t);
host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`);
host.declare(`declare ptr @scr_json_parse(ptr)`);
host.declare(`declare void @scr_dyn_release(ptr)`);
// A fallback site (dynCheck.fallback): a value that fails validation
// parks as the checked value (scr_dyn_check_recover) and the helper
// returns the dummy with no exception pending. Syntax errors still
// throw.
if (fallback) host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`);
const recover = fallback ? [` call zeroext i1 @scr_dyn_check_recover(ptr %d)`] : [];
this.defs.push(
`define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}${fallback ? " (fallback)" : ""}`,
`define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}`,
`entry:`,
` %fast = call ptr @scr_json_parse_schema(ptr %text, ptr @${schema})`,
` %hit = icmp ne ptr %fast, null`,
Expand All @@ -1485,7 +1478,6 @@ export class LlDyn {
` ret ptr null`,
`check:`,
` %v = call ptr @${check}(ptr %d, ptr null)`,
...recover,
` call void @scr_dyn_release(ptr %d)`,
` ret ptr %v`,
`}`,
Expand Down
8 changes: 1 addition & 7 deletions packages/compiler/src/backend/llvm/expr-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ export function emitSerializationExpr(
// falls back to exactly this route whenever the fast path declines.
const parsed = e.value;
if (!e.preserveRefs && parsed.kind === "libCall" && parsed.fn === "json.parse") {
const fused = host.dyn.jsonParseHelper(e.type, e.fallback === true);
const fused = host.dyn.jsonParseHelper(e.type);
if (fused) {
const text = emitBorrowedInput(host, parsed.args[0]!);
const r = B.tmp();
Expand All @@ -400,12 +400,6 @@ export function emitSerializationExpr(
B.line(
`${t} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr ${dynV.name}, ptr null)`,
);
// A fallback site: a value that fails validation parks as the
// checked value instead of throwing (dynCheck.fallback).
if (e.fallback) {
host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`);
B.line(`call zeroext i1 @scr_dyn_check_recover(ptr ${dynV.name})`);
}
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
return out;
Expand Down
1 change: 0 additions & 1 deletion packages/compiler/src/backend/llvm/lib-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,6 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "blob":
case "wasm":
case "record":
case "checkedCast":
case "disposable":
case "punycode":
case "constants":
Expand Down
3 changes: 0 additions & 3 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,9 +648,6 @@ export const LIB_FN_SYMS: Record<string, string> = {
"insp.circular": "scr_insp_circular",
// Undeclared source properties kept beside a checked record (scr_json.c).
"record.sourceHas": "scr_record_source_has",
// Checked-cast fallback reads (scr_json.c); take answers +1.
"checkedCast.fallbackTaken": "scr_dyn_check_fallback_taken",
"checkedCast.fallbackTake": "scr_dyn_check_fallback_take",
// WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c):
// constructions +1; url.new, the fileURLToPath pair, the win32
// pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw).
Expand Down
5 changes: 2 additions & 3 deletions packages/compiler/src/cli/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,8 @@ Options:
--tier <auto|static-only>
tier policy (default: auto). static-only keeps every
construct without a static lowering a compile error, so
the program has no dynamic-tier demotions, and makes a
checked cast whose value does not fit its type throw
instead of continuing with the value
the program has no dynamic-tier demotions. Both modes
currently compile identically
--explain-dynamic[=<name>]
coverage only: list the functions that operate on dynamic
values and the bindings, fields, functions, classes and
Expand Down
Loading
Loading