Keep initialization checks from refusing programs and merge main

- Fall back to unchecked same-module reads when an initialization check cannot be placed, so no previously compiled program is refused
- Merge main, including in-bounds string reads, and re-record preflight/order baselines
This commit is contained in:
Chris Tate
2026-10-08 22:12:27 -05:00
40 changed files with 1296 additions and 41 deletions
+24 -2
View File
@@ -6,6 +6,30 @@ All notable changes to scriptc will be documented in this file.
<!-- release:start -->
## 0.2.7
### Features
- **Supported import cycles initialize in module evaluation order.** Compiled modules preserve shared instances, hoisted function calls, and temporal-dead-zone checks across cyclic imports.
- **Typed returns carry more native values.** Optional values, never-returning calls, and native collection iterators retain their supported behavior across typed function boundaries.
- **Native terminal and filesystem metadata expands.** Standard terminal stream properties and filesystem error fields are available in supported static workflows.
- **An opt-in speed mode trades executable size and build time for runtime performance.** `--optimization=speed` enables runtime inlining, inline reference counting, and x86-64 runtime alignment while preserving the default release mode.
### Performance
- **Native allocation and cycle collection do less work.** A small-object allocator serves strings, arrays, and records; safe allocations and releases are emitted inline, and cycle scans back off for long-lived graphs while keeping static executables small.
- **Arrays, collections, and callbacks avoid repeated work.** Large dense arrays avoid the former sparse-storage cliff, direct element access and callback calls reduce boxing, and string-keyed Maps reuse cached hashes. Optional element reads and captured parameters avoid temporary wrappers where safe.
- **JSON, numbers, and regular expressions use more direct native paths.** Supported typed JSON parses directly into records, common number conversions use specialized paths, and regular expressions reuse compiled patterns and subject decoding with a native matcher for supported ASCII subjects.
- **Async execution and native I/O reduce overhead.** Supported await-free async functions avoid fibers, fiber switches save only required registers, HTTP responses coalesce headers and bodies, and console and process streams write directly to descriptors.
### Fixes
- **Class, record, and callback values preserve more JavaScript behavior.** Class references retain identity through unions and collections, adapted callbacks remain the same function, and record field presence stays distinct from an explicit undefined value. Data-property writes no longer incorrectly mark unrelated class methods as replaced.
- **Console line buffers account for concatenated string parts.** Long template literals and string concatenations reserve sufficient space before output.
- **Native program objects use runtime ABI v8.** External object consumers must link the matching v0.2.7 runtime pack; the versioned marker rejects incompatible runtime objects at link time.
<!-- release:end -->
## 0.2.6
### Features
@@ -26,8 +50,6 @@ All notable changes to scriptc will be documented in this file.
- **Deep reference chains clean up with bounded stack use.** Native teardown preserves ownership across generated objects and runtime containers.
- **Generic limits and diagnostics better match the failing source.** Independent concrete instantiations no longer share a global specialization cap; recursive demand remains bounded, and diagnostics retain source-file identity.
<!-- release:end -->
## 0.2.5
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-arm64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Native scriptc compiler for darwin-arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-x64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Native scriptc compiler for darwin-x64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-gnu",
"version": "0.2.6",
"version": "0.2.7",
"description": "Native scriptc compiler for linux-arm64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-musl",
"version": "0.2.6",
"version": "0.2.7",
"description": "Native scriptc compiler for linux-arm64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-gnu",
"version": "0.2.6",
"version": "0.2.7",
"description": "Native scriptc compiler for linux-x64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-musl",
"version": "0.2.6",
"version": "0.2.7",
"description": "Native scriptc compiler for linux-x64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-win32-x64-msvc",
"version": "0.2.6",
"version": "0.2.7",
"description": "Native scriptc compiler for win32-x64-msvc",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "scriptc",
"version": "0.2.6",
"version": "0.2.7",
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables \u2014 no Node, no V8, no JavaScript engine in the binary",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/compiler",
"version": "0.2.6",
"version": "0.2.7",
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -36,6 +36,10 @@ export interface CycleInitFlag {
/** The declaring module. */
module: ts.SourceFile;
settled: ReadonlySet<ts.SourceFile>;
/** True for a binding of an import cycle. A binding flagged only for its
* own module's early calls is checked opportunistically: whatever cannot
* be checked keeps the unchecked behavior instead of refusing. */
cycle: boolean;
}
/** Registers the initialization flags after global collection. Bindings
@@ -77,18 +81,25 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void {
);
continue;
}
registerInitFlag(lowerer, binding, global);
registerInitFlag(lowerer, binding, global, true);
}
// A module's own function declarations are callable before its later
// bindings are declared. Only bindings that such early-running code can
// read get a flag; the others keep their unchecked storage.
for (const binding of moduleEarlyBindings(lowerer.program, lowerer.moduleOrder)) {
const global = lowerer.globalsBySymbol.get(binding.symbol);
if (global !== undefined) registerInitFlag(lowerer, binding, global);
if (global !== undefined && binding.folded === undefined) {
registerInitFlag(lowerer, binding, global, false);
}
}
}
function registerInitFlag(lowerer: Lowerer, binding: CycleEarlyBinding, global: IrGlobal): void {
function registerInitFlag(
lowerer: Lowerer,
binding: CycleEarlyBinding,
global: IrGlobal,
cycle: boolean,
): void {
if (lowerer.cycleInitFlags.has(global.id)) return;
const flagId = `${global.id}%initialized`;
lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true });
@@ -99,6 +110,7 @@ function registerInitFlag(lowerer: Lowerer, binding: CycleEarlyBinding, global:
name: binding.name.text,
module: binding.module,
settled: binding.settled,
cycle,
};
lowerer.cycleInitFlags.set(global.id, flag);
const stmt = declaringStatement(binding.name);
@@ -171,7 +183,10 @@ export function withCycleInitFlags(
return true;
},
});
if (nested && !lowerer.remainder) {
if (nested && !flag.cycle) {
// Unmarkable store of a same-module binding: keep it unchecked.
lowerer.cycleInitFlags.delete(id);
} else if (nested && !lowerer.remainder) {
lowerer.pushDiag(
unsupportedDiag(
"SC1016",
@@ -308,17 +323,25 @@ function rewriteFunction(
/** Class layouts that read module storage directly (an evaluated heritage
* value, a symbol-keyed field) bypass the checks: refuse when that storage
* is a flagged binding. */
* is a flagged import-cycle binding, and leave a same-module binding
* unchecked. */
export function refuseUncheckedCycleReads(
lowerer: Lowerer,
classes: readonly { baseValueGlobal?: string; symbolFields?: { globalId: string }[] }[],
): void {
if (lowerer.cycleInitFlags.size === 0 || lowerer.remainder) return;
if (lowerer.cycleInitFlags.size === 0) return;
for (const cls of classes) {
const ids = [cls.baseValueGlobal, ...(cls.symbolFields ?? []).map((f) => f.globalId)];
for (const id of ids) {
const flag = id === undefined ? undefined : lowerer.cycleInitFlags.get(id);
if (flag === undefined) continue;
if (!flag.cycle) {
// A same-module binding whose storage a class layout reads keeps
// the unchecked behavior: no check, and no refusal.
lowerer.cycleInitFlags.delete(id!);
continue;
}
if (lowerer.remainder) continue;
const loc = flag.global.source?.loc ?? { file: lowerer.entry.fileName, start: 0, end: 0 };
lowerer.pushDiag(
unsupportedDiag(
@@ -6869,6 +6869,17 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
expr.argumentExpression,
);
if (index.type.kind === "f64" && recv.type.kind === "string") {
// A proven in-bounds integer index always names a code unit, where
// charAt and the property read agree.
if (lowerer.stringIndexBounds.inBounds(expr))
return {
kind: "strIntrinsic",
method: "charAt",
receiver: recv,
args: [index],
type: STRING,
loc: locOf(expr),
};
return lowerOptionalStringIndex(
lowerer,
recv,
@@ -67,6 +67,7 @@ import {
} from "./lower-builtin-values.js";
import { everyExprChild, everyStmtChild, transformStmtList } from "../../ir/traverse.js";
import { RuntimeOptionalProvenance } from "./runtime-optional-provenance.js";
import { StringIndexBounds } from "./string-index-bounds.js";
import { RuntimeOptionalLocals } from "./runtime-optional-locals.js";
import { sanitizeUnregisteredClassTypes } from "./sanitize-class-types.js";
import { UnregisteredClassTypes } from "./unregistered-class-types.js";
@@ -1570,6 +1571,18 @@ export class Lowerer {
/** Per-symbol result of the never-reassigned file scan
* (bindingNeverReassigned — object-literal generic-method receivers). */
readonly neverReassignedCache = new Map<ts.Symbol, boolean>();
private stringIndexBoundsCache: StringIndexBounds | null = null;
/** Proofs that a string element read names an existing code unit. The
* indexed-read analysis and expression lowering share one instance, so a
* read's representation and its callers' signatures always agree. */
get stringIndexBounds(): StringIndexBounds {
this.stringIndexBoundsCache ??= new StringIndexBounds(
(node) => this.checker.getSymbolAtLocation(node) ?? null,
(symbol) => this.checker.valueDeclarationOf(symbol),
(node) => (this.typeOf(node).flags & ts.TypeFlags.StringLike) !== 0,
);
return this.stringIndexBoundsCache;
}
/** Syntactic writes indexed once per JS scope. Symbol and RHS type
* checks remain contextual to each binding probe. */
readonly jsBindingWritesByOwner = new Map<ts.Node, Map<string, ts.BinaryExpression[]>>();
@@ -3842,7 +3855,8 @@ export class Lowerer {
const e = peel(node);
if (!ts.isElementAccessExpression(e)) return false;
const kind = this.mapTypeOf(this.typeOf(e.expression))?.kind;
return kind === "array" || kind === "string";
if (kind === "string") return !this.stringIndexBounds.inBounds(e);
return kind === "array";
};
const isDynamicObjectEntryRead = (node: ts.Expression): boolean => {
const read = peel(node);
@@ -3963,7 +3977,8 @@ export class Lowerer {
const t = this.mapTypeOf(this.typeOf(part));
if (t?.kind !== "string") return false;
const recv = this.mapTypeOf(this.typeOf(p.expression));
return recv?.kind === "string" || (recv?.kind === "array" && recv.elem.kind === "string");
if (recv?.kind === "string") return !this.stringIndexBounds.inBounds(p);
return recv?.kind === "array" && recv.elem.kind === "string";
};
const primitive = (part: ts.Expression): boolean => {
const t = this.mapTypeOf(this.typeOf(part));
@@ -0,0 +1,235 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule, validateModule } from "../../index.js";
import type { IrModule } from "../../ir/ir.js";
async function lower(source: string): Promise<IrModule> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-string-index-bounds-"));
try {
const entry = join(dir, "main.ts"),
output = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, {
outDir: dir,
outPath: output,
outputKind: "ir",
});
if (!result.ok)
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
const module = deserializeModule(await readFile(output, "utf8"));
expect(validateModule(module)).toEqual([]);
return module;
} finally {
await rm(dir, { recursive: true, force: true });
}
}
/** Functions whose string element reads keep the optional (checked) form. */
function optionalReads(module: IrModule): Set<string> {
return new Set(
module.functions
.filter((fn) => fn.locals.some((local) => local.name === "%indexedString"))
.map((fn) => fn.name),
);
}
const PROVEN = `
function upward(s: string): string {
let out = "";
for (let i = 0; i < s.length; i++) out += s[i];
return out;
}
function downward(s: string): string {
let out = "";
for (let i = s.length - 1; i >= 0; i--) out += s[i];
return out;
}
function stepped(s: string): string {
let out = "";
for (let i = 1; i < s.length; i += 2) out += s[i];
return out;
}
function scanned(s: string): number {
let words = 0;
let i = 0;
while (i < s.length) {
const ch = s[i];
if (ch === " ") { i++; continue; }
while (i < s.length && s[i] !== " ") i++;
words++;
}
return words;
}
function exitFirst(s: string): string {
let i = 0;
while (true) {
if (i >= s.length) return "";
if (s[i] !== " ") return s[i];
i++;
}
}
function aliased(s: string): number {
const n = s.length;
let count = 0;
for (let i = 0; i < n; i++) if (s[i] === "a") count++;
return count;
}
function inclusive(s: string): number {
let count = 0;
for (let i = 0; i <= s.length - 1; i++) if (s[i] === "b") count++;
return count;
}
function choice(s: string): string {
const i = 2;
return i < s.length ? s[i] : "-";
}
function negated(s: string): string {
let i = 0;
if (!(i < s.length)) return "-";
return s[i];
}
function trimmed(s: string): number {
let end = s.length - 1;
while (end >= 0 && s[end] === " ") end--;
return end + 1;
}
function nested(text: string): number {
const count = (): number => {
let k = 0;
for (let i = 0; i < text.length; i++) if (text[i] === "x") k++;
return k;
};
return count() + count();
}
console.log(upward("ab"), downward("ab"), stepped("abcd"), scanned("a b"), exitFirst(" q"));
console.log(aliased("aa"), inclusive("bb"), choice("abc"), negated("z"), trimmed("a "), nested("x"));
`;
const UNPROVEN = `
function bumped(s: string): string | undefined {
let i = 0;
if (i < s.length) { i++; return s[i]; }
return "";
}
function innerLoop(s: string): string {
let out = "";
let i = 0;
if (i < s.length) for (let k = 0; k < 3; k++) { out += String(s[i]); i++; }
return out;
}
function replaced(s: string): string | undefined {
let t = s;
const i = 1;
if (i < t.length) { t = ""; return t[i]; }
return "";
}
function capturedText(s: string): string {
let t = s;
const clear = (): void => { t = ""; };
let out = "";
for (let i = 0; i < t.length; i++) { clear(); out += String(t[i]); }
return out;
}
function capturedIndex(s: string): string {
let i = 0;
const skip = (): void => { i += 5; };
let out = "";
while (i < s.length) { skip(); out += String(s[i]); }
return out;
}
function fractional(s: string): string | undefined {
const i = 0.5;
return i < s.length ? s[i] : "";
}
function negative(s: string): string | undefined {
const i = -1;
return i < s.length ? s[i] : "";
}
function pastEnd(s: string): string {
let out = "";
for (let i = 0; i <= s.length; i++) out += String(s[i]);
return out;
}
function otherText(s: string, t: string): string {
let out = "";
for (let i = 0; i < s.length; i++) out += String(t[i]);
return out;
}
function parameterIndex(s: string, i: number): string | undefined {
return i < s.length ? s[i] : "";
}
function afterLoop(s: string): string | undefined {
let i = 0;
do { i++; } while (i < s.length);
return s[i];
}
function unguarded(s: string): string | undefined {
const i = 0;
return s[i];
}
console.log(bumped("a"), innerLoop("ab"), replaced("ab"), capturedText("ab"), capturedIndex("ab"));
console.log(fractional("a"), negative("a"), pastEnd("a"), otherText("abc", "a"), parameterIndex("a", 0.5));
console.log(afterLoop("ab"), unguarded(""));
`;
test("proven in-bounds string element reads lower to plain strings", async () => {
const module = await lower(PROVEN);
const names = [
"upward",
"downward",
"stepped",
"scanned",
"exitFirst",
"aliased",
"inclusive",
"choice",
"negated",
"trimmed",
"nested",
];
for (const name of names)
expect(
module.functions.some((fn) => fn.name === name),
name,
).toBe(true);
expect([...optionalReads(module)]).toEqual([]);
});
test("reads without a bounds proof keep the optional result", async () => {
const module = await lower(UNPROVEN);
const optional = optionalReads(module);
for (const name of [
"bumped",
"innerLoop",
"replaced",
"capturedText",
"capturedIndex",
"fractional",
"negative",
"pastEnd",
"otherText",
"parameterIndex",
"afterLoop",
"unguarded",
])
expect(optional.has(name), name).toBe(true);
});
test("a callee keeps a plain string parameter when every caller is proven", async () => {
const module = await lower(`
function isDigit(ch: string): boolean { return ch >= "0" && ch <= "9"; }
function isUpper(ch: string): boolean { return ch >= "A" && ch <= "Z"; }
function count(text: string): number {
let n = 0;
for (let i = 0; i < text.length; i++) if (isDigit(text[i])) n++;
return n;
}
function peekUpper(text: string, at: number): boolean { return isUpper(text[at]); }
console.log(count("a1b2"), peekUpper("Ab", 0), peekUpper("Ab", 9));
`);
const param = (name: string) => module.functions.find((fn) => fn.name === name)!.params[0]!.type;
expect(param("isDigit")).toEqual({ kind: "string" });
expect(param("isUpper").kind).toBe("union");
});
@@ -0,0 +1,598 @@
import * as ts from "../ts7/adapter.js";
import { isJsSourceFile } from "../program.js";
/* Proves that a string element read `text[i]` names an existing UTF-16 code
* unit, so the read always yields a one-character string and never
* undefined. Strings are immutable and have no holes: the read is present
* exactly when `i` is an integer with `0 <= i < text.length`.
*
* The proof is syntactic and local. Both operands must be plain bindings.
* The index binding is a block-scoped local whose every write keeps it an
* integer (literals, `x.length`, constant steps). Bounds come from the
* binding's own writes (a counter that only grows from a non-negative
* literal, or one that only shrinks from `text.length - k`) or from a guard
* that dominates the read: a loop or `if` condition, the left side of `&&`
* or `||`, a conditional, or an earlier `if (...) <exit>` statement.
*
* A guard holds at the read only when neither binding can change between
* them. Every write of either binding must be in the function that declares
* it, so calls cannot reach one; no write may sit between the guard and the
* read in source order; and no loop between them may write either binding,
* since a later iteration would observe it. Reads in a nested function only
* use guards inside that function. */
type Bound = "upper" | "lower";
interface Write {
/** Start of the written identifier (or declaration name). */
at: number;
/** A constant integer step (`i++`, `i -= 2`), an assigned value, or an
* untracked write such as destructuring. */
step: number | null;
value: ts.Expression | null;
}
interface Binding {
container: ts.Node;
declaration: ts.Node;
writes: Write[];
/** Some write sits in a function other than the declaring one. */
escapes: boolean;
/** No write beyond the declaration's own initializer. */
constant: boolean;
}
interface IndexFacts {
integer: boolean;
nonNegative: boolean;
/** Always below this string binding's length. */
belowLengthOf: ts.Symbol | null;
}
const NO_FACTS: IndexFacts = {
integer: false,
nonNegative: false,
belowLengthOf: null,
};
function peel(node: ts.Expression): ts.Expression {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
return e;
}
function isBoundary(node: ts.Node): boolean {
return (
ts.isSourceFile(node) ||
ts.isFunctionLike(node) ||
ts.isClassStaticBlockDeclaration(node) ||
ts.isPropertyDeclaration(node)
);
}
function containerOf(node: ts.Node): ts.Node {
let n = node.parent;
while (n && !isBoundary(n)) n = n.parent;
return n ?? node.getSourceFile();
}
function isLoop(node: ts.Node): boolean {
return (
ts.isForStatement(node) ||
ts.isForOfStatement(node) ||
ts.isForInStatement(node) ||
ts.isWhileStatement(node) ||
ts.isDoStatement(node)
);
}
function integerLiteral(node: ts.Expression): number | null {
const e = peel(node);
if (ts.isNumericLiteral(e)) {
const value = Number(e.text);
return Number.isSafeInteger(value) ? value : null;
}
if (
ts.isPrefixUnaryExpression(e) &&
e.operator === ts.SyntaxKind.MinusToken &&
ts.isNumericLiteral(e.operand)
) {
const value = Number(e.operand.text);
return Number.isSafeInteger(value) && value !== 0 ? -value : null;
}
return null;
}
/** An always-exiting statement: control never reaches the next sibling. */
function exits(node: ts.Statement): boolean {
if (
ts.isReturnStatement(node) ||
ts.isThrowStatement(node) ||
ts.isBreakStatement(node) ||
ts.isContinueStatement(node)
)
return true;
if (ts.isBlock(node)) {
const last = node.statements[node.statements.length - 1];
return last !== undefined && exits(last);
}
return false;
}
/** The identifier's role when it is written, or null for a read. */
function writeOf(id: ts.Identifier): Write | null {
let n: ts.Node = id;
let destructured = false;
for (;;) {
const p: ts.Node | undefined = n.parent;
if (!p) return null;
if (ts.isBinaryExpression(p)) {
const k = p.operatorToken.kind;
if (p.left !== n || k < ts.SyntaxKind.FirstAssignment || k > ts.SyntaxKind.LastAssignment)
return null;
const at = id.getStart();
if (destructured) return { at, step: null, value: null };
if (k === ts.SyntaxKind.EqualsToken) return { at, step: null, value: p.right };
const amount = integerLiteral(p.right);
if (amount !== null && k === ts.SyntaxKind.PlusEqualsToken)
return { at, step: amount, value: null };
if (amount !== null && k === ts.SyntaxKind.MinusEqualsToken)
return { at, step: -amount, value: null };
return { at, step: null, value: null };
}
if (ts.isPostfixUnaryExpression(p) || ts.isPrefixUnaryExpression(p)) {
if (
p.operand !== n ||
(p.operator !== ts.SyntaxKind.PlusPlusToken && p.operator !== ts.SyntaxKind.MinusMinusToken)
)
return null;
if (destructured) return { at: id.getStart(), step: null, value: null };
return {
at: id.getStart(),
step: p.operator === ts.SyntaxKind.PlusPlusToken ? 1 : -1,
value: null,
};
}
if (ts.isForOfStatement(p) || ts.isForInStatement(p))
return p.initializer === n ? { at: id.getStart(), step: null, value: null } : null;
if (ts.isParenthesizedExpression(p)) {
n = p;
continue;
}
if (
ts.isArrayLiteralExpression(p) ||
ts.isSpreadElement(p) ||
ts.isSpreadAssignment(p) ||
ts.isShorthandPropertyAssignment(p) ||
ts.isObjectLiteralExpression(p) ||
(ts.isPropertyAssignment(p) && p.initializer === n)
) {
destructured = true;
n = p;
continue;
}
return null;
}
}
export class StringIndexBounds {
private readonly proofs = new Map<ts.Node, boolean>();
private readonly bindings = new Map<ts.Symbol, Binding | null>();
private readonly indexFacts = new Map<ts.Symbol, IndexFacts>();
private readonly identifiers = new Map<ts.Node, Map<string, ts.Identifier[]>>();
constructor(
private readonly symbolAt: (node: ts.Node) => ts.Symbol | null,
private readonly declarationOf: (symbol: ts.Symbol) => ts.Node | undefined,
private readonly isString: (node: ts.Expression) => boolean,
) {}
/** True when `read` (an element access on a string receiver) is proven
* to name an existing code unit. */
inBounds(read: ts.ElementAccessExpression): boolean {
const cached = this.proofs.get(read);
if (cached !== undefined) return cached;
const proven = this.prove(read);
this.proofs.set(read, proven);
return proven;
}
private prove(read: ts.ElementAccessExpression): boolean {
// JavaScript declarations are not checked against callers.
if (read.questionDotToken || isJsSourceFile(read.getSourceFile())) return false;
const textNode = read.expression;
const indexNode = read.argumentExpression;
if (!ts.isIdentifier(textNode) || !ts.isIdentifier(indexNode)) return false;
if (writeOfElement(read)) return false;
const textSymbol = this.symbolAt(textNode);
const indexSymbol = this.symbolAt(indexNode);
if (!textSymbol || !indexSymbol || textSymbol === indexSymbol) return false;
const container = containerOf(read);
const text = this.binding(textSymbol);
if (!text || text.escapes || (!text.constant && text.container !== container)) return false;
// Sloppy-mode `arguments` aliases parameters.
if (ts.isParameter(text.declaration) && this.namesIn(text.container).has("arguments"))
return false;
const index = this.binding(indexSymbol);
if (!index || index.escapes || index.container !== container) return false;
const facts = this.indexFactsOf(indexSymbol, index);
if (!facts.integer) return false;
const symbols = [textSymbol, indexSymbol];
const upper =
(facts.belowLengthOf === textSymbol && text.constant) ||
this.guarded(read, "upper", indexSymbol, textSymbol, symbols);
if (!upper) return false;
return facts.nonNegative || this.guarded(read, "lower", indexSymbol, textSymbol, symbols);
}
private namesIn(container: ts.Node): Map<string, ts.Identifier[]> {
let names = this.identifiers.get(container);
if (names) return names;
names = new Map();
const found = names;
ts.walkPreorder(container, (node) => {
if (!ts.isIdentifier(node)) return;
const list = found.get(node.text);
if (list) list.push(node);
else found.set(node.text, [node]);
});
this.identifiers.set(container, names);
return names;
}
/** Every write of a block-scoped local or parameter, or null for any
* other binding (module imports, var, class members, ...). */
private binding(symbol: ts.Symbol): Binding | null {
if (this.bindings.has(symbol)) return this.bindings.get(symbol)!;
const result = this.scanBinding(symbol);
this.bindings.set(symbol, result);
return result;
}
private scanBinding(symbol: ts.Symbol): Binding | null {
const declaration = this.declarationOf(symbol);
if (!declaration) return null;
let nameNode: ts.Node;
const writes: Write[] = [];
if (ts.isVariableDeclaration(declaration)) {
const list = declaration.parent;
if (
!ts.isIdentifier(declaration.name) ||
!ts.isVariableDeclarationList(list) ||
!(list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const))
)
return null;
nameNode = declaration.name;
if (ts.isForOfStatement(list.parent) || ts.isForInStatement(list.parent)) {
// Each iteration binds a fresh constant; a `let` element may change.
if (!(list.flags & ts.NodeFlags.Const)) return null;
} else if (declaration.initializer) {
writes.push({
at: declaration.name.getStart(),
step: null,
value: declaration.initializer,
});
} else return null;
} else if (ts.isParameter(declaration)) {
if (!ts.isIdentifier(declaration.name) || declaration.dotDotDotToken) return null;
nameNode = declaration.name;
} else return null;
const container = containerOf(declaration);
let escapes = false;
let assigned = 0;
for (const id of this.namesIn(container).get(symbol.name) ?? []) {
if (id === nameNode) continue;
const write = writeOf(id);
if (!write) continue;
// A destructuring or loop target may name the binding through a
// shorthand property; count any same-named target conservatively.
if (write.step === null && write.value === null) {
writes.push(write);
assigned++;
if (containerOf(id) !== container) escapes = true;
continue;
}
if (this.symbolAt(id) !== symbol) continue;
writes.push(write);
assigned++;
if (containerOf(id) !== container) escapes = true;
}
return {
container,
declaration,
writes,
escapes,
constant: assigned === 0,
};
}
private valueFacts(value: ts.Expression): IndexFacts {
const literal = integerLiteral(value);
if (literal !== null) return { integer: true, nonNegative: literal >= 0, belowLengthOf: null };
const e = peel(value);
if (this.lengthOf(e)) return { integer: true, nonNegative: true, belowLengthOf: null };
if (
ts.isBinaryExpression(e) &&
(e.operatorToken.kind === ts.SyntaxKind.MinusToken ||
e.operatorToken.kind === ts.SyntaxKind.PlusToken)
) {
const of = this.lengthOf(peel(e.left));
const amount = integerLiteral(e.right);
if (of && amount !== null) {
const delta = e.operatorToken.kind === ts.SyntaxKind.PlusToken ? amount : -amount;
return {
integer: true,
nonNegative: delta >= 0,
belowLengthOf: delta <= -1 ? of : null,
};
}
}
return NO_FACTS;
}
/** `text.length` on a string binding: its symbol, else null. */
private lengthOf(e: ts.Expression): ts.Symbol | null {
if (
!ts.isPropertyAccessExpression(e) ||
e.questionDotToken ||
e.name.text !== "length" ||
!ts.isIdentifier(e.expression) ||
!this.isString(e.expression)
)
return null;
return this.symbolAt(e.expression);
}
private indexFactsOf(symbol: ts.Symbol, binding: Binding): IndexFacts {
const cached = this.indexFacts.get(symbol);
if (cached) return cached;
let facts: IndexFacts = NO_FACTS;
if (ts.isVariableDeclaration(binding.declaration)) {
const [initial, ...rest] = binding.writes;
facts = initial?.value ? this.valueFacts(initial.value) : NO_FACTS;
for (const write of rest) {
if (!facts.integer) break;
if (write.step !== null) {
facts = {
integer: true,
nonNegative: facts.nonNegative && write.step > 0,
belowLengthOf: write.step < 0 ? facts.belowLengthOf : null,
};
} else if (write.value) {
const value = this.valueFacts(write.value);
facts = {
integer: value.integer,
nonNegative: facts.nonNegative && value.nonNegative,
belowLengthOf: value.belowLengthOf === facts.belowLengthOf ? facts.belowLengthOf : null,
};
} else facts = NO_FACTS;
}
}
this.indexFacts.set(symbol, facts);
return facts;
}
/** `text.length`, or a const binding initialized from it while `text`
* itself is never reassigned. */
private isLength(node: ts.Expression, text: ts.Symbol): boolean {
const e = peel(node);
if (ts.isPropertyAccessExpression(e)) return this.lengthOf(e) === text;
if (!ts.isIdentifier(e)) return false;
const alias = this.symbolAt(e);
const declaration = alias ? this.declarationOf(alias) : undefined;
if (
!declaration ||
!ts.isVariableDeclaration(declaration) ||
!declaration.initializer ||
!ts.isVariableDeclarationList(declaration.parent) ||
!(declaration.parent.flags & ts.NodeFlags.Const)
)
return false;
if (this.lengthOf(peel(declaration.initializer)) !== text) return false;
return this.binding(text)?.constant === true;
}
/** `text.length - k` for an integer k >= 1. */
private isLengthMinusOne(node: ts.Expression, text: ts.Symbol): boolean {
const e = peel(node);
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.MinusToken)
return false;
const amount = integerLiteral(e.right);
return amount !== null && amount >= 1 && this.isLength(e.left, text);
}
private isIndex(node: ts.Expression, index: ts.Symbol): boolean {
const e = peel(node);
return ts.isIdentifier(e) && this.symbolAt(e) === index;
}
/** Does `cond` evaluating to `truthy` establish the bound? The index is
* an integer and lengths are numbers, so a false comparison is its
* negation. */
private holds(
node: ts.Expression,
truthy: boolean,
bound: Bound,
index: ts.Symbol,
text: ts.Symbol,
): boolean {
const e = peel(node);
if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken)
return this.holds(e.operand, !truthy, bound, index, text);
if (!ts.isBinaryExpression(e)) return false;
let op: ts.SyntaxKind = e.operatorToken.kind;
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
const conjunction = op === ts.SyntaxKind.AmpersandAmpersandToken;
if (conjunction !== truthy) return false;
return (
this.holds(e.left, truthy, bound, index, text) ||
this.holds(e.right, truthy, bound, index, text)
);
}
let left = e.left,
right = e.right;
// Normalize to `index OP other`.
if (!this.isIndex(left, index)) {
if (!this.isIndex(right, index)) return false;
[left, right] = [right, left];
op =
op === ts.SyntaxKind.LessThanToken
? ts.SyntaxKind.GreaterThanToken
: op === ts.SyntaxKind.GreaterThanToken
? ts.SyntaxKind.LessThanToken
: op === ts.SyntaxKind.LessThanEqualsToken
? ts.SyntaxKind.GreaterThanEqualsToken
: op === ts.SyntaxKind.GreaterThanEqualsToken
? ts.SyntaxKind.LessThanEqualsToken
: op;
}
if (!truthy) {
op =
op === ts.SyntaxKind.LessThanToken
? ts.SyntaxKind.GreaterThanEqualsToken
: op === ts.SyntaxKind.GreaterThanEqualsToken
? ts.SyntaxKind.LessThanToken
: op === ts.SyntaxKind.GreaterThanToken
? ts.SyntaxKind.LessThanEqualsToken
: op === ts.SyntaxKind.LessThanEqualsToken
? ts.SyntaxKind.GreaterThanToken
: ts.SyntaxKind.Unknown;
}
if (bound === "upper")
return (
(op === ts.SyntaxKind.LessThanToken && this.isLength(right, text)) ||
(op === ts.SyntaxKind.LessThanEqualsToken && this.isLengthMinusOne(right, text))
);
const limit = integerLiteral(right);
if (limit === null) return false;
return (
(op === ts.SyntaxKind.GreaterThanEqualsToken && limit >= 0) ||
(op === ts.SyntaxKind.GreaterThanToken && limit >= -1)
);
}
/** Find a guard establishing `bound` that still holds at `read`. */
private guarded(
read: ts.Node,
bound: Bound,
index: ts.Symbol,
text: ts.Symbol,
symbols: readonly ts.Symbol[],
): boolean {
const loops: ts.Node[] = [];
const end = read.pos;
const valid = (ranges: readonly (readonly [number, number])[]): boolean => {
for (const symbol of symbols) {
for (const write of this.binding(symbol)!.writes) {
if (ranges.some(([from, to]) => write.at >= from && write.at < to)) return false;
if (loops.some((loop) => write.at >= loop.pos && write.at < loop.end)) return false;
}
}
return true;
};
const holds = (cond: ts.Expression, truthy: boolean): boolean =>
this.holds(cond, truthy, bound, index, text);
let child: ts.Node = read;
for (let node = read.parent; node && !isBoundary(node); child = node, node = node.parent) {
if (ts.isBinaryExpression(node) && node.right === child) {
const op = node.operatorToken.kind;
if (
(op === ts.SyntaxKind.AmpersandAmpersandToken && holds(node.left, true)) ||
(op === ts.SyntaxKind.BarBarToken && holds(node.left, false))
) {
if (valid([[node.left.pos, end]])) return true;
}
} else if (ts.isConditionalExpression(node)) {
if (node.whenTrue === child && holds(node.condition, true)) {
if (valid([[node.condition.pos, end]])) return true;
} else if (node.whenFalse === child && holds(node.condition, false)) {
if (
valid([
[node.condition.pos, node.condition.end],
[node.whenFalse.pos, end],
])
)
return true;
}
} else if (ts.isIfStatement(node)) {
if (node.thenStatement === child && holds(node.expression, true)) {
if (valid([[node.expression.pos, end]])) return true;
} else if (node.elseStatement === child && holds(node.expression, false)) {
if (
valid([
[node.expression.pos, node.expression.end],
[node.elseStatement.pos, end],
])
)
return true;
}
} else if (ts.isWhileStatement(node)) {
if (node.statement === child && holds(node.expression, true)) {
if (valid([[node.expression.pos, end]])) return true;
}
} else if (ts.isForStatement(node)) {
// The update runs before the condition is tested again.
if (node.statement === child && node.condition && holds(node.condition, true)) {
if (
valid([
[node.condition.pos, node.condition.end],
[node.statement.pos, end],
])
)
return true;
}
} else if (ts.isBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) {
const statements = node.statements;
for (let i = statements.indexOf(child as ts.Statement) - 1; i >= 0; i--) {
const statement = statements[i]!;
if (
ts.isIfStatement(statement) &&
!statement.elseStatement &&
exits(statement.thenStatement) &&
holds(statement.expression, false) &&
valid([
[statement.expression.pos, statement.expression.end],
[statement.end, end],
])
)
return true;
}
}
if (isLoop(node)) loops.push(node);
}
return false;
}
}
/** A string element in a write position (assignment, update, or a
* destructuring/loop target), never a plain read. */
function writeOfElement(read: ts.ElementAccessExpression): boolean {
let n: ts.Node = read;
for (;;) {
const p: ts.Node | undefined = n.parent;
if (!p) return false;
if (ts.isBinaryExpression(p))
return (
p.left === n &&
p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
p.operatorToken.kind <= ts.SyntaxKind.LastAssignment
);
if (ts.isPostfixUnaryExpression(p) || ts.isPrefixUnaryExpression(p))
return (
p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken
);
if (ts.isForOfStatement(p) || ts.isForInStatement(p)) return p.initializer === n;
if (
ts.isParenthesizedExpression(p) ||
ts.isArrayLiteralExpression(p) ||
ts.isSpreadElement(p) ||
ts.isSpreadAssignment(p) ||
ts.isObjectLiteralExpression(p) ||
(ts.isPropertyAssignment(p) && p.initializer === n)
) {
n = p;
continue;
}
return false;
}
}
@@ -347,6 +347,21 @@ test.for([
["const item = { read() { return item.size; }, size: 2 }; console.log(item.read());", []],
["setTimeout(() => console.log(late), 0); const late = 'later';", []],
["let a = 1, b = () => a + 1, c = b(); console.log(c);", []],
// A wrapper that only returns closures over its callback defers it.
[
"function lazy(run: () => number) { return function () { return run(); }; } const read = lazy(() => total); const total = 3; console.log(read());",
[],
],
// A wrapper that calls its callback runs it before the declaration.
[
"function now(run: () => number) { return run(); } const read = now(() => total); const total = 3; console.log(read);",
["total"],
],
// Object methods under member-chain computed keys are stored, not run.
[
"const shelf = { [Symbol.iterator]() { return [size][Symbol.iterator](); } }; const size = 2; console.log([...shelf]);",
[],
],
] as const)("module bindings read before initialization: %s", ([source, early]) => {
const directory = mkdtempSync(
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-module-early-"),
+88 -2
View File
@@ -2747,6 +2747,7 @@ function moduleEarlyBindingsOf7(
list.push({ name, units });
storedBy.set(name.text, list);
};
const checker = program.getTypeChecker();
const storedIn = (e: ts.Expression, units: ts.Node[]): void => {
while (
ts.isParenthesizedExpression(e) ||
@@ -2764,11 +2765,26 @@ function moduleEarlyBindingsOf7(
ts.isGetAccessorDeclaration(p) ||
ts.isSetAccessorDeclaration(p)
) {
if (!ts.isComputedPropertyName(p.name)) units.push(p);
// A computed key evaluates where the literal does; a plain
// member-chain key (`[Equal.symbol]`) runs no user code.
if (!ts.isComputedPropertyName(p.name) || memberChainKey7(p.name.expression))
units.push(p);
} else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units);
}
} else if (ts.isArrayLiteralExpression(e)) {
for (const el of e.elements) if (!ts.isSpreadElement(el)) storedIn(el, units);
} else if (ts.isCallExpression(e)) {
// `const f = wrap(callback)` where wrap only returns closures over
// the callback: it runs when the stored result is called.
e.arguments.forEach((arg, index) => {
let fn: ts.Expression = arg;
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
if (
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
returnsOnlyClosuresOver7(checker, e.expression, index)
)
units.push(fn);
});
}
};
sf.statements.forEach((stmt, index) => {
@@ -2803,7 +2819,6 @@ function moduleEarlyBindingsOf7(
idents.push(node);
return undefined;
});
const checker = program.getTypeChecker();
checker.prefetchSymbolNodesExact(idents);
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
const parent = id.parent;
@@ -2935,6 +2950,77 @@ function moduleEarlyBindingsOf7(
return out;
}
/** True when the program function `callee` names reads its parameter
* `index` only inside function expressions it returns directly, and never
* through `arguments` in its own body: calling it cannot run that argument
* before the returned closure is called. */
function returnsOnlyClosuresOver7(
checker: ts.TypeChecker,
callee: ts.Expression,
index: number,
): boolean {
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
const name = ts.isIdentifier(callee)
? callee
: ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)
? callee.name
: undefined;
if (name === undefined) return false;
let symbol = checker.getSymbolAtLocation(name);
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(symbol);
}
const decls = symbol === undefined ? [] : checker.declarationsOf(symbol);
if (decls.length !== 1) return false;
const decl = decls[0]!;
if (decl.getSourceFile().isDeclarationFile) return false;
let fn: ts.Node | undefined;
if (ts.isFunctionDeclaration(decl)) fn = decl;
else if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined) {
let init: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) fn = init;
}
if (fn === undefined) return false;
const like = fn as ts.FunctionLikeDeclaration;
const body = like.body;
const param = like.parameters[index];
if (body === undefined || !ts.isBlock(body) || param === undefined) return false;
if (!ts.isIdentifier(param.name) || param.dotDotDotToken !== undefined) return false;
if (like.parameters.slice(0, index).some((p) => p.dotDotDotToken !== undefined)) return false;
const paramName = param.name.text;
let deferred = true;
ts.walkPreorder(body, (node) => {
if (!deferred) return "stop";
if (ts.isFunctionLike(node) || ts.isClassExpression(node) || ts.isClassDeclaration(node)) {
let child: ts.Node = node;
while (ts.isParenthesizedExpression(child.parent)) child = child.parent;
const returned = ts.isReturnStatement(child.parent) && child.parent.expression === child;
// A returned closure may use the parameter; any other nested
// function could escape and run before the result is called.
if (returned) return "skip";
let uses = false;
ts.walkPreorder(node, (inner) => {
if (ts.isIdentifier(inner) && inner.text === paramName) uses = true;
return uses ? "stop" : undefined;
});
if (uses) deferred = false;
return "skip";
}
if (ts.isIdentifier(node) && (node.text === paramName || node.text === "arguments")) {
if (node !== param.name) deferred = false;
}
return undefined;
});
return deferred;
}
/** An identifier or a chain of plain property reads. */
function memberChainKey7(e: ts.Expression): boolean {
while (ts.isPropertyAccessExpression(e)) e = e.expression;
return ts.isIdentifier(e);
}
/** The name a declaration node binds, if any. */
function declaredName7(node: ts.Node): ts.Node | undefined {
if (
+1 -1
View File
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"compilerVersion": "0.2.6",
"compilerVersion": "0.2.7",
"coverage": [
"Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.",
"Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.",
@@ -15175,6 +15175,18 @@
"<repo>/tests/diagnostics/unrelated-class-assertion.ts"
],
"diags": []
},
"<repo>/tests/corpus/4080-string-index-bounds.ts": {
"order": [
"<repo>/tests/corpus/4080-string-index-bounds.ts"
],
"diags": []
},
"<repo>/tests/corpus/4081-string-index-unproven.ts": {
"order": [
"<repo>/tests/corpus/4081-string-index-unproven.ts"
],
"diags": []
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/llvm-darwin-arm64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Pinned LLVM code-generation helper for scriptc on macOS arm64",
"license": "Apache-2.0 WITH LLVM-exception",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/llvm-darwin-x64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Pinned LLVM code-generation helper for scriptc on macOS x64",
"license": "Apache-2.0 WITH LLVM-exception",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/llvm-linux-arm64-gnu",
"version": "0.2.6",
"version": "0.2.7",
"description": "Pinned LLVM code-generation helper for scriptc on Linux arm64",
"license": "Apache-2.0 WITH LLVM-exception",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/llvm-linux-arm64-musl",
"version": "0.2.6",
"version": "0.2.7",
"description": "Statically linked LLVM code-generation helper for scriptc on Linux arm64 musl",
"license": "Apache-2.0 WITH LLVM-exception",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/llvm-linux-x64-gnu",
"version": "0.2.6",
"version": "0.2.7",
"description": "Pinned LLVM code-generation helper for scriptc on Linux x64",
"license": "Apache-2.0 WITH LLVM-exception",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/llvm-linux-x64-musl",
"version": "0.2.6",
"version": "0.2.7",
"description": "Statically linked LLVM code-generation helper for scriptc on Linux x64 musl",
"license": "Apache-2.0 WITH LLVM-exception",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/llvm-win32-x64-msvc",
"version": "0.2.6",
"version": "0.2.7",
"description": "Pinned LLVM code-generation helper for scriptc on Windows x64",
"license": "Apache-2.0 WITH LLVM-exception",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-android-arm64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc library runtime pack for Android arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-darwin-arm64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc runtime pack for macOS arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-darwin-x64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc runtime pack for macOS x64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-ios-arm64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc library runtime pack for iOS arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-ios-simulator-arm64",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc library runtime pack for iOS simulator arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-linux-arm64-gnu",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc runtime pack for Linux arm64 glibc",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-linux-arm64-musl",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc runtime pack for Linux arm64 musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-linux-x64-gnu",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc runtime pack for Linux x64 glibc",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-linux-x64-musl",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc runtime pack for Linux x64 musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-wasm32-wasi",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc runtime pack for WASI Preview 1",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime-win32-x64-msvc",
"version": "0.2.6",
"version": "0.2.7",
"description": "Precompiled scriptc runtime pack for Windows x64 MSVC",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/runtime",
"version": "0.2.6",
"version": "0.2.7",
"description": "scriptc native runtime sources and vendored dependencies",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+111
View File
@@ -0,0 +1,111 @@
// String element reads whose index is proven inside the string: counted
// loops in both directions, scanner loops guarded by `i < text.length`,
// early exits, length aliases, and helpers that take one character.
function isDigit(ch: string): boolean {
return ch >= "0" && ch <= "9";
}
function isLetter(ch: string): boolean {
return (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
}
function tokens(text: string): string[] {
const out: string[] = [];
let i = 0;
while (i < text.length) {
const ch = text[i];
if (ch === " ") {
i++;
continue;
}
const start = i;
if (isDigit(ch)) {
while (i < text.length && isDigit(text[i])) i++;
out.push(`num:${text.slice(start, i)}`);
} else if (isLetter(ch)) {
while (i < text.length && (isLetter(text[i]) || isDigit(text[i]))) i++;
out.push(`word:${text.slice(start, i)}`);
} else {
out.push(`sym:${ch}`);
i++;
}
}
return out;
}
function reversed(text: string): string {
let out = "";
for (let i = text.length - 1; i >= 0; i--) out += text[i];
return out;
}
function everyOther(text: string): string {
let out = "";
for (let i = 0; i <= text.length - 1; i += 2) out += text[i];
return out;
}
function firstNonSpace(text: string): string {
let i = 0;
while (true) {
if (i >= text.length) return "(none)";
if (text[i] !== " ") return text[i];
i++;
}
}
function countOf(text: string, wanted: string): number {
const n = text.length;
let count = 0;
for (let i = 0; i < n; i++) if (text[i] === wanted) count++;
return count;
}
function trimRight(text: string): string {
let end = text.length - 1;
while (end >= 0 && text[end] === " ") end--;
return text.slice(0, end + 1);
}
function middle(text: string): string {
const i = 1;
return i < text.length ? text[i] : "-";
}
function digitsInLines(lines: string[]): number[] {
const counts: number[] = [];
for (let r = 0; r < lines.length; r++) {
const line = lines[r]!;
let n = 0;
for (let i = 0; i < line.length; i++) if (isDigit(line[i])) n++;
counts.push(n);
}
return counts;
}
function codes(text: string): number[] {
const out: number[] = [];
const read = (): void => {
for (let i = 0; i < text.length; i++) out.push(text[i].charCodeAt(0));
};
read();
return out;
}
console.log(tokens("width 42 + height7 * (3)").join(" | "));
console.log(tokens("").length, tokens(" ").length, tokens("x").join(""));
console.log(
reversed("stone"),
JSON.stringify(reversed("")),
everyOther("abcdefg"),
everyOther(""),
);
console.log(firstNonSpace(" q"), firstNonSpace(" "), firstNonSpace(""));
console.log(countOf("banana", "a"), countOf("", "a"), countOf("€uro€", "€"));
console.log(
JSON.stringify(trimRight("pad ")),
JSON.stringify(trimRight(" ")),
JSON.stringify(trimRight("")),
);
console.log(middle("ok"), middle("o"), middle(""));
console.log(digitsInLines(["a1", "22b", "", "x"]).join(","));
console.log(codes("A€z").join(","));
+127
View File
@@ -0,0 +1,127 @@
// String element reads that may fall outside the string still produce
// undefined: writes between a guard and the read, writes from closures,
// fractional, negative and inclusive bounds, and guards on another string.
function show(label: string, value: string | undefined): void {
console.log(label, value === undefined ? "undefined" : JSON.stringify(value));
}
const word = "kite";
{
let i = 3;
if (i < word.length) {
i++;
show("bumped", word[i]);
}
}
{
let i = 0;
if (i < word.length) {
for (let k = 0; k < 6; k++) {
show("inner", word[i]);
i++;
}
}
}
{
let text = "abcdef";
const i = 4;
if (i < text.length) {
text = "ab";
show("shrunk", text[i]);
}
}
{
let text = "abc";
const shorten = (): void => {
text = "a";
};
for (let i = 0; i < text.length; i++) {
shorten();
show("closure-text", text[i]);
}
}
{
let i = 0;
const jump = (): void => {
i += 3;
};
while (i < word.length) {
jump();
show("closure-index", word[i]);
}
}
{
const half = 0.5;
if (half < word.length) show("half", word[half]);
const before = -1;
if (before < word.length) show("negative", word[before]);
let down = 0;
down = down - 1;
if (down < word.length) show("minus", word[down]);
}
{
const text = "abc";
for (let i = 0; i <= text.length; i++) show("inclusive", text[i]);
}
{
const long = "longer text";
const short = "s";
for (let i = 0; i < long.length; i++) if (i > 8) show("other", short[i]);
}
{
const text = "abcd";
for (let i = 0; i < text.length; i++) {
show("ahead", text[i + 1]);
i++;
show("stepped", text[i]);
}
}
{
const text = "pq";
let i = 0;
let rest = 0;
if (i < text.length) {
[i, rest] = [7, 1];
show("destructured", text[i]);
}
console.log("rest", rest);
}
function at(text: string, index: number): string | undefined {
return index < text.length ? text[index] : "-";
}
show("param-frac", at("abc", 1.5));
show("param-neg", at("abc", -2));
show("param-ok", at("abc", 2));
function afterLoop(text: string): string | undefined {
let i = 0;
do {
i++;
} while (i < text.length);
return text[i];
}
show("after-loop", afterLoop("xyz"));
let shared = 0;
function advance(): void {
shared += 10;
}
function readShared(text: string): string | undefined {
if (shared < text.length) {
advance();
return text[shared];
}
return "-";
}
show("module-index", readShared("abc"));