// V4b / V4b-T — the runtime-panic surface seam. // // This module owns the closed theta 1.0 runtime-panic set, the `?`-operator // runtime propagation seam that panics bypass, and the runtime-defect surface // (`theta/runtime/internal-error`) for unexpected interpreter / adapter throws // (errors-and-results/error-model.md §"Runtime panics"; the registered message // templates live in diagnostics/code-registry-runtime.md). // // Five of the six closed panic sources are owned here — array index-out-of- // bounds, missing-object-key, null-index-access, null-member-access, and // `invoke`-chain depth-exceeded; the sixth (non-exhaustive `match`) is the // `MatchError` panic owned by ./match-result.ts. A panic is a thrown JS // exception, never a `Result` value, so it bypasses `?` and `match` (which // operate on `Result` values) — the bypass is intrinsic to representing panics // as thrown `ThetaPanic` instances rather than as values. // // The runtime-defect surface routes an *unexpected* throw (one that is not a // panic source) to `theta/runtime/internal-error`. The NOCEIL-3 carve-out // (errors-and-results/error-model.md §"Runtime panics"; // hard-ceilings/ceiling-invariants-and-audit.md §"No additional ceilings"): // an *uncatchable* host fatal (V8 heap-OOM via the `OOMErrorCallback` / // `abort()` path) terminates the host process before any wrap can observe it, // so it delivers no throw to a catch site and `theta/runtime/internal-error` // emits no diagnostic for it. // // V4b-T (tests-task) declares the seam — the `ThetaPanic` base and the five // panic classes, the `evaluateIndexAccess` / `evaluateMemberAccess` / // `enterInvokeFrame` accessor seams, the `evaluateQuestion` `?`-propagation // seam, the `HostFatal` NOCEIL-3 marker, and the `surfaceUnexpectedThrow` // runtime-defect surface — and stubs every behaviour-bearing function inertly // so the failing tests red on their own primary assertions (an accessor that // raises no panic, a `?` seam that neither propagates nor lets a panic through, // and a runtime-defect surface that emits a wrong-code sentinel for every // input). The paired V4b implementation leaf fills these in. import type { Diagnostic, SourceRange } from "../diagnostics/diagnostic"; import { renderInteger } from "../diagnostics/placeholder"; import type { ThetaValue, ResultValue } from "./value"; /** The registry codes carried by the five panic sources this module owns. */ export const INDEX_OUT_OF_BOUNDS_CODE = "theta/runtime/index-out-of-bounds"; export const MISSING_OBJECT_KEY_CODE = "theta/runtime/missing-object-key"; export const NULL_INDEX_ACCESS_CODE = "theta/runtime/null-index-access"; export const NULL_MEMBER_ACCESS_CODE = "theta/runtime/null-member-access"; export const INVOKE_DEPTH_EXCEEDED_CODE = "theta/runtime/invoke-depth-exceeded"; /** The runtime-defect-surface code for an unexpected interpreter / adapter throw. */ export const INTERNAL_ERROR_CODE = "theta/runtime/internal-error"; /** The `invoke`-chain depth cap (INV-4 / invocation.md §"Invocation depth bound"). */ export const INVOKE_DEPTH_CAP = 32; /** * Base class for the closed theta 1.0 runtime panics this module owns. A panic * is a thrown JS exception, never a `Result` value, so `?` and `match` (which * operate on `Result` values) cannot intercept it — it bypasses them by * construction. Each subclass carries its registered `theta/runtime/*` code. */ export abstract class ThetaPanic extends Error { abstract readonly code: string; } /** `arr[i]` with `i < 0` or `i >= arr.length` (`theta/runtime/index-out-of-bounds`). */ export class IndexOutOfBoundsPanic extends ThetaPanic { readonly code = INDEX_OUT_OF_BOUNDS_CODE; constructor(message: string) { super(message); this.name = "IndexOutOfBoundsPanic"; } } /** `obj[k]` where `k` is not a present theta-side key (`theta/runtime/missing-object-key`). */ export class MissingObjectKeyPanic extends ThetaPanic { readonly code = MISSING_OBJECT_KEY_CODE; constructor(message: string) { super(message); this.name = "MissingObjectKeyPanic"; } } /** `[i]` access on `null` (`theta/runtime/null-index-access`). */ export class NullIndexAccessPanic extends ThetaPanic { readonly code = NULL_INDEX_ACCESS_CODE; constructor(message: string) { super(message); this.name = "NullIndexAccessPanic"; } } /** `.field` access on `null` (`theta/runtime/null-member-access`). */ export class NullMemberAccessPanic extends ThetaPanic { readonly code = NULL_MEMBER_ACCESS_CODE; constructor(message: string) { super(message); this.name = "NullMemberAccessPanic"; } } /** `invoke` chain depth exceeded (`theta/runtime/invoke-depth-exceeded`). */ export class InvokeDepthExceededPanic extends ThetaPanic { readonly code = INVOKE_DEPTH_EXCEEDED_CODE; constructor(message: string) { super(message); this.name = "InvokeDepthExceededPanic"; } } /** * Runtime `[i]` indexed access (errors-and-results/error-model.md §"Runtime * panics"). `target[index]`: * - `null` target → `NullIndexAccessPanic` (`[]`); * - array, `i < 0 || i >= len` → `IndexOutOfBoundsPanic` (` not in 0..`); * - object, missing theta-side key → `MissingObjectKeyPanic` (``); * - otherwise → the indexed element / member value. * * The registered message templates are sourced from * diagnostics/code-registry-runtime.md and interpolated per the placeholder- * rendering categories (`` / `` are category-4 numerics; `` is * a category-5 source-derived identifier). */ export function evaluateIndexAccess( target: ThetaValue, index: number | string, ): ThetaValue { if (target === null) { // `[i]` access on `null` (`theta/runtime/null-index-access`). ``. const rendered = typeof index === "number" ? renderInteger(index) : index; throw new NullIndexAccessPanic(`null index access: [${rendered}]`); } if (Array.isArray(target)) { // Array indexing: bounds-check `i < 0 || i >= arr.length` // (`theta/runtime/index-out-of-bounds`). ` not in 0..`. const i = index as number; if (typeof i !== "number" || i < 0 || i >= target.length) { throw new IndexOutOfBoundsPanic( `index out of bounds: ${renderInteger(i)} not in 0..${renderInteger(target.length)}`, ); } return target[i] as ThetaValue; } // A primitive receiver (`string` / `number` / `integer` / `boolean`) is not // indexable: the type layer rejects `s[0]` at parse time // (`theta/parse/non-indexable-receiver`). Reaching here means the static check // was bypassed; surface it as a runtime defect rather than silently returning // a character (the pre-V20c behaviour, which returned `"h"` for `"hi"[0]`). // A non-panic throw is reclassified to `theta/runtime/internal-error` by the // runtime-defect surface. if (typeof target !== "object") { throw new Error( `indexed access requires an array or object receiver; got ${typeof target}`, ); } // Object indexing: a key that is not a present theta-side name on the object // is the missing-object-key panic (`theta/runtime/missing-object-key`). const key = index as string; const obj = target as { readonly [k: string]: ThetaValue }; if (!Object.prototype.hasOwnProperty.call(obj, key)) { throw new MissingObjectKeyPanic(`missing object key: ${key}`); } return obj[key] as ThetaValue; } /** * Runtime `.field` member access (errors-and-results/error-model.md §"Runtime * panics"). `target.field` on a `null` target raises `NullMemberAccessPanic` * (`.`); otherwise it returns the member value. * * The registered `null member access: .` template is sourced from * diagnostics/code-registry-runtime.md (`` is a category-5 source- * derived identifier rendered bare). */ export function evaluateMemberAccess(target: ThetaValue, field: string): ThetaValue { if (target === null) { throw new NullMemberAccessPanic(`null member access: .${field}`); } return (target as { readonly [k: string]: ThetaValue })[field] as ThetaValue; } /** * Guard the `invoke`-chain depth bound (INV-4): about to push a frame bringing * the chain count to `nextDepth`. When `nextDepth > 32` the runtime raises * `InvokeDepthExceededPanic` (`invoke chain depth exceeded: > 32`); * otherwise it returns normally. * * The registered `invoke chain depth exceeded: > 32` template is * sourced from diagnostics/code-registry-runtime.md (`` is a category-4 * numeric). */ export function enterInvokeFrame(nextDepth: number): void { if (nextDepth > INVOKE_DEPTH_CAP) { throw new InvokeDepthExceededPanic( `invoke chain depth exceeded: ${renderInteger(nextDepth)} > ${INVOKE_DEPTH_CAP}`, ); } } /** * The outcome of evaluating a `?` operand (errors-and-results/error-model.md * §"Runtime panics" — the surface panics bypass): * - `value` — the operand was `Ok(v)`; `v` flows on; * - `propagate` — the operand was `Err(e)`; the enclosing function early- * returns `Err(e)`. * A panic thrown while *producing* the operand is **not** captured here — it * propagates past `?` as a thrown `ThetaPanic`, never becoming a `propagate` * outcome. */ export type QuestionResult = | { readonly kind: "value"; readonly value: ThetaValue } | { readonly kind: "propagate"; readonly err: ThetaValue }; /** * Evaluate `operand?`: invoke `operand` (a thunk producing the `?` operand's * `Result`), then apply `?` propagation — `Ok(v)` yields `{ kind: "value" }`, * `Err(e)` yields `{ kind: "propagate" }`. A panic thrown by `operand` * propagates unchanged (the thunk is invoked without a surrounding catch), so a * panic bypasses `?`. * * Invoking `operand` *outside* any surrounding catch is the mechanism by which * a panic bypasses `?`: a thrown `ThetaPanic` (or `MatchError`) propagates from * this call unchanged, never becoming a `propagate` outcome. */ export function evaluateQuestion(operand: () => ThetaValue): QuestionResult { // A panic thrown while producing the operand propagates past this call // unchanged (no catch surrounds it), so `?` is bypassed. const result = operand() as ResultValue; return result.ok ? { kind: "value", value: result.value } : { kind: "propagate", err: result.error }; } /** * A marker for a host-fatal *uncatchable* condition (NOCEIL-3): a V8 heap-OOM * via the `OOMErrorCallback` / `abort()` path terminates the host process * before any wrap can observe it, so it never reaches a runtime catch site. The * runtime-defect surface emits **no** `theta/runtime/internal-error` for it. * Modelled as a distinct marker so the carve-out is testable without crashing * the test process. */ export class HostFatal { constructor(readonly description: string) {} } /** * The runtime-defect surface (errors-and-results/error-model.md §"Runtime * panics"). Classify a value reaching a runtime catch site: * - a `ThetaPanic` → `undefined` (already a panic; not a runtime defect, not * reclassified — the caller rethrows it so it bypasses `?`/`match`); * - a `HostFatal` → `undefined` (NOCEIL-3 carve-out: no diagnostic at all); * - any other thrown value → a `theta/runtime/internal-error` `Diagnostic` * whose `message` is the underlying `error.message` and whose `hint` is the * underlying `error.stack` (or `""` when falsy). * * The `internal error: ` template is sourced from * diagnostics/code-registry-runtime.md; `hint` carries the underlying * `error.stack` (or `""` when falsy) for operator triage. */ export function surfaceUnexpectedThrow( thrown: unknown, site: { readonly file: string; readonly range: SourceRange }, ): Diagnostic | undefined { // Already a panic (one of the six closed sources): not a runtime defect, not // reclassified — the caller rethrows it so it bypasses `?`/`match`. if (isThetaPanic(thrown)) { return undefined; } // NOCEIL-3 carve-out: an uncatchable host fatal terminates the host process // before any wrap observes it, so the runtime-defect surface emits no // diagnostic at all for it. if (thrown instanceof HostFatal) { return undefined; } const errorLike = thrown as { readonly message?: unknown; readonly stack?: unknown }; const message = typeof errorLike.message === "string" ? errorLike.message : String(thrown); const stack = typeof errorLike.stack === "string" && errorLike.stack.length > 0 ? errorLike.stack : ""; return { severity: "error", code: INTERNAL_ERROR_CODE, file: site.file, range: site.range, message: `internal error: ${message}`, hint: stack, }; } /** Whether `error` is one of the runtime panics this module owns. */ export function isThetaPanic(error: unknown): error is ThetaPanic { return error instanceof ThetaPanic; }