diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c02da87..89c14a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ concurrency: jobs: build-and-test: - name: Build, Test & Stats + name: Build & Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -42,25 +42,13 @@ jobs: - run: bun run build:lib - run: bun run typecheck - run: bun run test - - run: bun run stats - - - uses: actions/upload-artifact@v4 - if: github.ref == 'refs/heads/main' - with: - name: stats - path: packages/ruam/stats.json - - # Publish stats.json to GitHub Pages alongside the Next.js site - - name: Copy stats.json into site - if: github.ref == 'refs/heads/main' - run: cp packages/ruam/stats.json apps/web/public/stats.json # Build browser worker bundle for the playground - name: Build browser worker bundle if: github.ref == 'refs/heads/main' run: bun run build:worker - - name: Build site with stats + - name: Build site if: github.ref == 'refs/heads/main' run: bun run build:web diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 63548f6..249bbc5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,8 +6,6 @@ on: paths: - 'apps/web/**' - '.github/workflows/deploy.yml' - paths-ignore: - - 'packages/ruam/**' concurrency: group: deploy-pages @@ -36,12 +34,9 @@ jobs: - run: bun install - # Build ruam + collect stats so the site always includes stats.json + # Build and verify Ruam before bundling the playground. - run: bun run build:lib - run: bun run test - - run: bun run stats - - name: Copy stats.json into site - run: cp packages/ruam/stats.json apps/web/public/stats.json # Build browser worker bundle for the playground - name: Build browser worker bundle diff --git a/README.md b/README.md index 69269a5..e00739d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
-
+  
 :::::::..    ...    :::  :::.     .        :
 ;;;;``;;;;   ;;     ;;;  ;;`;;    ;;,.    ;;;
  [[[,/[[['  [['     [[[ ,[[ '[[,  [[[[, ,[[[[,
@@ -7,566 +7,331 @@
  888b "88bo,88    .d888 888   888,888 Y88" 888o
  MMMM   "W"  "YmmMMMM"" YMM   ""` MMM  M'  "MMM
-Virtualization-Based (VM) JavaScript Obfuscator
+ Isogloss execution protection for guarded JavaScript relations -

Compiles JavaScript (JS) functions into custom bytecode executed by an embedded virtual machine.
-No deobfuscator exists for RuamVM bytecode** NOTE: Fable 5 is able to deobfuscate single-file Ruam code.

+

+ Ruam replaces explicitly bounded, side-effect-free source relations with + contextual BPRF realizations while keeping deployment claims honest. +

-Node.js >= 18 -LGPL-2.1 -TypeScript Strict + Node.js >= 18 + LGPL-2.1 + TypeScript strict +
-Tests Passing -Avg Size Ratio -VM Overhead +## What Ruam ships -
-

Quick Links

-Installation ·  -Quick Start ·  -How It Works ·  -Presets ·  -API +Ruam's shipped execution architecture is Isogloss. The source transform: - +1. Finds an explicitly configured root function or a function marked with + `/* ruam:isogloss */`. +2. Requires an exact declared domain for every local input used by the selected + pure return expression. +3. Rejects calls, effects, unsupported coercions, unsafe numeric ranges, and + other expressions it cannot prove safe to lower. +4. Replaces the accepted relation with a scalarized BPRF closure containing + multiple contextual realizations and fragmented relation pieces. +5. Enforces the declared domains at runtime. Inputs outside those domains throw; + the original relation is not retained as a fallback. -
- -

Why Ruam?

- -

- Most JavaScript obfuscators apply surface-level transformations — renaming variables, encoding strings, inserting dead code. A motivated attacker can undo these with off-the-shelf tools, logic analysis, or by patching functions at runtime. -

- -

- Ruam takes a fundamentally different approach. It compiles your JavaScript into a custom bytecode instruction set and replaces the original source with a compact virtual machine that executes an unintelligible instruction stream. The original code is destroyed — it does not exist anywhere in the output. -

- -

Traditional Obfuscators

-

Source JS → Transformed JS - (still JS)

- - -

Ruam

-

Source JS → Custom Bytecode - + Embedded VM

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
300+ opcode open-source ISAFull-coverage instruction set spanning 26 categories — stack, arithmetic, bitwise, comparison, control flow, property access, scoping, calls, classes, iterators, destructuring, async/await, generators, and more.
Per-build polymorphismEvery build produces structurally unique output. No two builds share the same encoding, identifier names, or internal structure — even from the same source.
Multi-layer encryptionBytecode is encrypted with multiple independent layers. Keys are derived implicitly from the output's own structure — no key material appears in plaintext.
Anti-tamper bindingThe VM's decryption logic is entangled with its own source. Modifying the interpreter to add logging or breakpoints corrupts all decryption — the bytecode becomes unrecoverable.
Runtime metamorphismThe instruction set mutates during execution. The same opcode byte maps to different operations at different points in the program. Static disassembly produces incorrect results.
Optimizing compilerMulti-tier optimization pipeline minimizes the performance cost of virtualization. Fused instructions, register promotion, and inline operations keep overhead competitive for a JS-in-JS interpreter.
Thousands of testsComprehensive test suite covering core JS semantics, stress/edge cases, security properties, and integration scenarios — including randomized fuzz tests.
- -
- -

Installation

- -
npm install ruam@npm:ruamvm
-# or
-bun add ruam@npm:ruamvm
-# or
-pnpm add ruam@npm:ruamvm
-# or
-yarn add ruam@npm:ruamvm
- -

Requires Node.js >= 18. Ships as ESM only.

- -

Ruam is published to npm under the name ruamvm (the registry rejects ruam as too similar to raf/rax/read). Installing via the ruam@npm:ruamvm alias keeps imports and CLI invocations as ruam everywhere. The CLI binary is also installed as ruamvm if you prefer.

- -
- -

Quick Start

- -

CLI

- -
# Obfuscate a file in-place
-ruam app.js
-
-# Obfuscate to a new file
-ruam app.js -o app.obf.js
-
-# Obfuscate a directory with medium preset
-ruam dist/ --preset medium
-
-# Maximum protection
-ruam dist/ --preset max
-
-# Interactive wizard (or just run `ruam` with no args)
-ruam -I
- -

Programmatic API

- -
import { obfuscateCode, obfuscateFile, runVmObfuscation } from "ruam";
-
-// Synchronous — obfuscate a code string
-const result = obfuscateCode('function hello() { return "world"; }');
-
-// Async — obfuscate a file
-await obfuscateFile("src/app.js", "dist/app.js");
-
-// Async — obfuscate a directory with options
-await runVmObfuscation("dist/", {
-  include: ["**/*.js"],
-  exclude: ["**/node_modules/**"],
-  options: { preset: "max" },
-});
+Configured targets fail closed. If a function is named in `regionDomains` but +its selected expression cannot be lowered, protection stops with a structured +error instead of shipping the configured relation unchanged. -

Selective Obfuscation

+Unconfigured or untargeted JavaScript remains ordinary source and is reported +through build diagnostics. Ruam does not claim whole-language protection. -

Not every function needs virtualization. Use comment mode to protect only what matters:

+## Threat-model honesty -
/* ruam:vm */
-function sensitiveLogic() {
-  // → compiled to bytecode
-}
+The default `holographic-local` profile is **client-complete**. Everything
+needed to execute the protected relation is present in the client. Its
+contextual realizations and fragmentation can raise the cost of static and
+dynamic analysis, but they do not create secrecy and do not establish a
+hardness lower bound. An attacker with unrestricted execution and
+instrumentation can ultimately reconstruct local behavior.
 
-function publicHelper() {
-  // → untouched, no overhead
-}
- -
ruam app.js -m comment
- -
- -

How It Works

- -

Ruam applies multiple independent protection layers that compound the difficulty of reverse engineering. Each layer forces an attacker to solve a distinct problem before they can make progress on the next.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LayerDescription
VirtualizationOriginal JS is compiled to a custom bytecode ISA. The source code is destroyed — an attacker must reverse-engineer the entire VM to recover any logic.
Polymorphic encodingThe instruction encoding, identifiers, and internal structure are randomized per build. Reversing one build provides zero reusable knowledge about any other build.
Instruction encryptionEvery instruction is individually encrypted. The key is derived from properties of the output itself — no key material is stored in plaintext. Sequential decryption is required; you cannot jump into the middle of a bytecode stream.
Integrity bindingThe decryption process is entangled with the VM interpreter's own source. Modifying the VM in any way (adding logging, setting breakpoints, patching behavior) silently corrupts all decryption.
VM shieldingEach function can receive its own isolated micro-interpreter with unique encoding, encryption keys, and internal structure. Reversing one function's interpreter does not help with any other.
String encodingAll string constants in the bytecode are independently encrypted. No plaintext strings survive compilation — not variable names, property keys, or literal values.
Anti-debugMulti-layered runtime detection with escalating response. No eval(), new Function(), debugger statements, or console calls — fully compatible with strict CSP environments including Chrome extensions.
Arithmetic obfuscationArithmetic and bitwise operations within the interpreter are replaced with mathematically equivalent but opaque compound expressions, making the interpreter logic harder to follow.
String atomizationAll string literals in the interpreter — property names, method names, internal labels — are replaced with encoded table lookups. Zero hardcoded strings survive in the output.
Block permutationBytecode basic blocks are randomly reordered within each compiled function. Control flow is preserved through explicit jumps. The bytecode stream no longer reflects the original program structure.
Key scatteringCryptographic key materials are split into fragments distributed across multiple closure scopes in the output. Recovering any single key requires tracing the entire scope chain.
- -

Additional hardening options include dead bytecode injection, stack value encryption, decoy opcode handlers, handler fragmentation, runtime opcode mutation, and polymorphic string decoding — all configurable independently or via presets.

- -
- -

Presets

- -

Three built-in presets provide escalating protection. Explicit options always override preset values.

- - - - - - - - - - - - - - - - - - - - - - - - - - -
PresetWhat's enabledUse case
lowVM compilation onlyDevelopment, debugging, basic IP protection
medium+ identifier renaming, bytecode encryption, instruction encryption, decoy & dynamic opcodes, string atomization, key scatteringProduction — balanced protection and size
maxEverything — all encryption layers, VM shielding, debug protection, integrity binding, arithmetic obfuscation, dead code, stack encoding, block permutation, string atomization, key scatteringHigh-value targets — maximum protection
- -
ruam dist/ --preset max
- -
obfuscateCode(source, {
-  preset: "medium",
-  debugProtection: true,  // override: add debug protection to medium
-});
- -
- -

Performance

- -

Virtualization inherently adds overhead — this is the tradeoff for protection that surface-level transforms cannot provide. Ruam's multi-tier optimization pipeline minimizes the cost.

- -

Typical overhead: ~38–45x native speed on compute-heavy benchmarks, competitive for a pure JS-in-JS interpreter.

- -

Use selective obfuscation (-m comment) to protect only sensitive functions and leave hot paths running as native JS.

- -
- -

CLI Reference

- -
ruam <input> [options]
-
-Presets:
-  --preset <name>           Apply a preset: low, medium, max
-
-Output:
-  -o, --output <path>       Output file or directory (default: overwrite input)
-
-Compilation:
-  -m, --mode <mode>         Target mode: "root" (default) or "comment"
-  -e, --encrypt             Enable bytecode encryption
-  -p, --preprocess          Rename all identifiers before compilation
-
-Security:
-  -d, --debug-protection    Enable anti-debugger protection
-  --no-debug-protection     Disable anti-debugger (overrides preset)
-  --rolling-cipher          Enable instruction encryption
-  --integrity-binding       Bind decryption to interpreter integrity
-  --vm-shielding            Per-function isolated micro-interpreters
-
-Hardening:
-  --dynamic-opcodes         Filter unused opcodes from the interpreter
-  --decoy-opcodes           Add fake opcode handlers
-  --dead-code               Inject dead bytecode sequences
-  --stack-encoding          Encrypt values on the VM stack
-  --mba                     Arithmetic obfuscation (mixed boolean arithmetic)
-  --handler-fragmentation   Split handler logic into interleaved fragments
-  --string-atomization      Replace interpreter strings with encoded lookups
-  --polymorphic-decoder     Per-build randomized string decoding chain
-  --scattered-keys          Fragment key materials across closure scopes
-  --block-permutation       Shuffle bytecode basic block order
-  --opcode-mutation         Insert runtime handler table mutations
-
-Environment:
-  --target <env>            Target environment: node, browser (default), browser-extension
-
-File Selection:
-  --include <glob>          File glob for directory mode (default: "**/*.js")
-  --exclude <glob>          Exclude glob (default: "**/node_modules/**")
+Ruam's owner-planning learnability checks are also non-claims. They compute
+constructive exact black-box attack **upper bounds** and reject a planned region
+when a known attack is strictly cheaper than the configured threshold. Passing
+that owner-side gate does not prove that attacks require the threshold number
+of queries.
 
-Other:
-  --debug-logging           Inject verbose VM trace logging
-  -I, --interactive         Launch interactive configuration wizard
-  -h, --help                Show help
-  -v, --version             Show version
+Stronger deployment profiles are architectural deployments, not local switches: -
+| Profile | Required boundary | Client completeness | Local fallback | +| --- | --- | --- | --- | +| `holographic-local` | None | Complete | Not applicable | +| `holographic-custodied` | An existing remote-await boundary and a real custodian | Incomplete under the custodian | Forbidden | +| `holographic-private` | An existing remote-await boundary, a real custodian, and an actively secure private-function protocol | Incomplete under the private protocol | Forbidden | +| `holographic-tee` | A real in-process attested boundary with a pinned measurement or policy | Incomplete under attestation | Forbidden | -

API Reference

- -

obfuscateCode(source, options?)

- -

Synchronously obfuscates a JavaScript source string. Returns the obfuscated code as a string.

- -
import { obfuscateCode } from "ruam";
-
-const output = obfuscateCode(source, {
-  preset: "medium",
-  targetMode: "root",
-});
+Custodied, private, and TEE deployments must be assembled by the owner-side +product planner using real boundary evidence and supported capabilities. The +source API will not invent a suspension point, silently graft a remote call +onto synchronous code, or embed a complete local relation for outage or +development fallback. -

obfuscateFile(inputPath, outputPath, options?)

+## Installation -

Reads a file, obfuscates it, and writes the result. Returns a Promise<void>.

+```sh +npm install ruam +``` -
import { obfuscateFile } from "ruam";
+Ruam requires Node.js 18 or newer and ships as ESM.
 
-await obfuscateFile("src/app.js", "dist/app.js", { preset: "max" });
+## Quick start -

runVmObfuscation(directory, config?)

+### Protect a source string -

Obfuscates all matching files in a directory. Returns a Promise<void>.

+`protectCode` returns both generated code and honest build metadata: -
import { runVmObfuscation } from "ruam";
+```js
+import { protectCode } from "ruam";
+
+const source = `
+  function price(quantity, unitPrice) {
+    return (quantity * unitPrice) + (quantity - 1);
+  }
+`;
+
+const build = protectCode(source, {
+  isogloss: {
+    profile: "holographic-local",
+  },
+  targetMode: "root",
+  regionDomains: {
+    price: {
+      quantity: { type: "number", min: 1, max: 100 },
+      unitPrice: { type: "number", min: 1, max: 10_000 },
+    },
+  },
+});
+
+console.log(build.code);
+console.log(build.stats.clientCompleteness); // "complete"
+console.log(build.stats.hardnessLowerBound); // null
+console.log(build.diagnostics);
+```
+
+The configured numeric bounds are inclusive safe-integer ranges. The generated
+function rejects any `quantity` or `unitPrice` outside those ranges.
+
+### Protect only annotated functions
+
+Set `targetMode: "comment"` and use the exact marker
+`/* ruam:isogloss */` immediately before the function:
+
+```js
+import { protectCode } from "ruam";
+
+const source = `
+  /* ruam:isogloss */
+  function sensitiveScore(x, y) {
+    return (x * y) + (x - 3);
+  }
+
+  function publicLabel(value) {
+    return String(value);
+  }
+`;
+
+const build = protectCode(source, {
+  targetMode: "comment",
+  regionDomains: {
+    sensitiveScore: {
+      x: { type: "number", min: 1, max: 20 },
+      y: { type: "number", min: 2, max: 30 },
+    },
+  },
+});
+```
+
+Only `sensitiveScore` is considered. The marked function still must satisfy all
+purity, shape, type, and domain checks.
+
+### Protect one file
+
+`protectFile` writes the generated code and returns the same build result as
+`protectCode`:
+
+```js
+import { protectFile } from "ruam";
+
+const build = await protectFile("src/pricing.js", "dist/pricing.js", {
+  targetMode: "root",
+  regionDomains: {
+    price: {
+      quantity: { type: "number", min: 1, max: 100 },
+      unitPrice: { type: "number", min: 1, max: 10_000 },
+    },
+  },
+});
 
-await runVmObfuscation("dist/", {
-  include: ["**/*.js"],
+console.log(build.stats.protectedRegionCount);
+```
+
+Omit the output path to overwrite the input file.
+
+### Protect a directory
+
+`runProtection` processes matching files and returns one build result per file:
+
+```js
+import { runProtection } from "ruam";
+
+const results = await runProtection("dist", {
+  include: ["score.js"],
   exclude: ["**/node_modules/**"],
-  options: { preset: "medium" },
-});
- -

Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OptionTypeDefaultDescription
preset"low" | "medium" | "max"Apply a preset configuration
targetMode"root" | "comment""root""root": all top-level functions. "comment": only /* ruam:vm */ annotated
thresholdnumber1.0Probability (0–1) that an eligible function is compiled
target"node" | "browser" | "browser-extension""browser"Target execution environment
preprocessIdentifiersbooleanfalseRename all local identifiers before compilation
encryptBytecodebooleanfalseEncrypt bytecode using an environment fingerprint key
rollingCipherbooleanfalsePer-instruction encryption with implicit key derivation
integrityBindingbooleanfalseBind decryption to interpreter source integrity (auto-enables rollingCipher)
vmShieldingbooleanfalsePer-function micro-interpreters with unique encoding (auto-enables rollingCipher)
debugProtectionbooleanfalseMulti-layered anti-debugger with escalating response
dynamicOpcodesbooleanfalseFilter unused opcodes from the interpreter
decoyOpcodesbooleanfalseAdd fake opcode handlers to the interpreter
deadCodeInjectionbooleanfalseInject unreachable bytecode sequences
stackEncodingbooleanfalseEncrypt values on the VM stack at runtime
mixedBooleanArithmeticbooleanfalseReplace arithmetic/bitwise ops with opaque MBA expressions
handlerFragmentationbooleanfalseSplit opcode handlers into interleaved fragments
stringAtomizationbooleanfalseReplace all interpreter string literals with encoded table lookups (auto-enables polymorphicDecoder)
polymorphicDecoderbooleanfalsePer-build randomized byte-operation chain for string decoding
scatteredKeysbooleanfalseFragment and scatter key materials across closure scopes
blockPermutationbooleanfalseShuffle bytecode basic block order within compiled functions
opcodeMutationbooleanfalseInsert runtime handler table mutations (auto-enables rollingCipher)
debugLoggingbooleanfalseInject verbose trace logging into the interpreter
- -
- -

Supported Syntax

- -

Ruam compiles the full range of modern JavaScript:

- - - -
- -

Target Environments

- -

Use --target to optimize output for your deployment environment:

- - - - - - - - - - - - - - - - - - - - - - -
TargetDescription
browserPlain <script> tags. Default.
nodeNode.js (CJS or ESM modules).
browser-extensionChrome extension MAIN world content scripts. Wraps output to avoid TrustedScript CSP errors.
- -
ruam content-script.js --target browser-extension --preset max
- -
- -

Requirements

- - - -

License

- -

LGPL-2.1

+ options: { + targetMode: "comment", + regionDomains: { + sensitiveScore: { + x: { type: "number", min: 1, max: 20 }, + y: { type: "number", min: 2, max: 30 }, + }, + }, + }, +}); + +for (const { file, build } of results) { + console.log(file, build.stats.protectedRegionCount); +} +``` + +`obfuscateCode` and `obfuscateFile` are string-only and `Promise` +conveniences over the same Isogloss source transform. Use `protectCode` and +`protectFile` when build diagnostics or security metadata matter. + +## Exact domain declarations + +`regionDomains` is keyed first by function name and then by the exact local +binding name used in its selected expression: + +```js +const options = { + regionDomains: { + choose: { + gate: { type: "boolean" }, + left: { type: "number", min: 0, max: 1_000 }, + right: { type: "number", min: 0, max: 1_000 }, + }, + }, +}; +``` + +Ruam never infers a numeric range. Numeric bounds must be safe integers, must +not be negative zero, and must satisfy `min <= max`. Boolean domains are exact +and need no additional bounds. + +Domain declarations serve two purposes across the architecture: + +- They are emitted as runtime input guards. +- They provide the finite-domain evidence consumed by build-time safety checks + and, when creating an owner product plan, black-box learnability checks. + +A declaration is not permission to coerce values. Runtime types must match. + +## Guarded pure-region scope + +The current source path accepts bounded expressions built from: + +- Local identifiers with matching declared domains +- Safe integer and boolean literals +- Numeric `+`, `-`, `*`, and unary negation where signed-zero and overflow + safety can be proven +- Boolean `!`, `&&`, and `||` +- Conditional expressions whose branches have the same proven type + +The configured expression must contain at least one input and enough structure +to form a meaningful protected region. Calls, property access, mutation, +suspension, exceptions, unbound values, implicit coercion, and unsupported +syntax are rejected for configured targets. + +## Public API + +### `protectCode(source, options?)` + +Synchronously returns: + +```ts +interface IsoglossSourceBuildResult { + readonly code: string; + readonly diagnostics: readonly IsoglossBuildDiagnostic[]; + readonly stats: { + readonly engine: "isogloss"; + readonly profile: "holographic-local"; + readonly rootGroupCount: number; + readonly protectedRegionCount: number; + readonly realizationCount: number; + readonly fragmentFunctionCount: number; + readonly originalBytes: number; + readonly outputBytes: number; + readonly expansionRatio: number; + readonly clientCompleteness: "complete"; + readonly hardnessLowerBound: null; + }; + readonly ownerTrace?: IsoglossOwnerSidecar; +} +``` + +Set `isogloss.ownerTrace` to `"sidecar"` to receive owner-only build +certificates. The sidecar is returned separately and does not add runtime trace +hooks to the generated client code. + +### `protectFile(inputPath, outputPath?, options?)` + +Reads a file, protects it, writes the generated code, and resolves to its +`IsoglossSourceBuildResult`. + +### `runProtection(directory, config?)` + +Protects matching files and resolves to a frozen array of: + +```ts +interface ProtectedFileResult { + readonly file: string; + readonly build: IsoglossSourceBuildResult; +} +``` + +### Core options + +| Option | Type | Default | Meaning | +| --- | --- | --- | --- | +| `targetMode` | `"root" \| "comment"` | `"root"` | Select top-level functions or exact annotations | +| `threshold` | `number` in `[0, 1]` | `1` | Per-build probability that an eligible target is selected | +| `preprocessIdentifiers` | `boolean` | `false` | Rename identifiers after protected regions are built | +| `target` | `"node" \| "browser" \| "browser-extension"` | `"browser"` | Declare the output environment | +| `regionDomains` | nested exact-domain map | `{}` | Bind configured functions and local inputs to exact domains | +| `isogloss.profile` | deployment profile | `"holographic-local"` | Select the honest execution profile | +| `isogloss.maximumCustody.minimumExactAttackQueries` | canonical unsigned decimal string | `"1000"` | Set the owner-planner rejection threshold for known constructive exact attacks | +| `isogloss.ownerTrace` | `"off" \| "sidecar"` | `"off"` | Return owner-only build metadata | +| `isogloss.capabilities` | profile-specific descriptors | `{}` | Declare real nonlocal capabilities for owner-planned deployments | + +BPRF realization and fragment counts are fixed architecture constants, not +public security knobs. Unknown options are rejected with structured +`RuamOptionError` diagnostics. + +## Errors and fail-closed behavior + +Ruam exposes two structured error families: + +- `RuamOptionError` for invalid, unknown, or profile-incompatible options. +- `IsoglossSourceTransformError` when a configured source target cannot be + safely transformed or when a nonlocal profile lacks owner-planned boundary + composition. + +For configured targets, these errors stop the build. Ruam does not silently +return an unprotected configured relation. + +## Requirements + +- Node.js 18 or newer +- ESM +- Explicit finite domains for every input used by a configured pure region +- Real owner-planned boundaries and no complete local fallback for custodied, + private, or TEE deployments + +## License + +[LGPL-2.1](LICENSE) diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index a68239c..e0b38c0 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -2,14 +2,13 @@ import type { Metadata } from "next"; import { config } from "@fortawesome/fontawesome-svg-core"; import "@fortawesome/fontawesome-svg-core/styles.css"; import "./globals.css"; -import SiteProtection from "@/components/SiteProtection"; config.autoAddCss = false; export const metadata: Metadata = { - title: "Ruam: JavaScript VM Obfuscation", + title: "Ruam: Isogloss JavaScript Protection", description: - "Compile JavaScript functions into encrypted custom bytecode executed by an embedded virtual machine. Open-source. Per-build unique. No deobfuscator exists.", + "Replace guarded pure JavaScript source regions with diversified scalar Isogloss realizations, with explicit local, custody, private-function, and attested deployment profiles.", icons: { icon: `${process.env.NEXT_PUBLIC_BASE_PATH}/ruam.svg`, }, @@ -35,7 +34,6 @@ export default function RootLayout({ /> - {children} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index f8e7d91..5955a1f 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,5 +1,3 @@ -import { readFileSync } from "fs"; -import { join } from "path"; import Navbar from "@/components/Navbar"; import Hero from "@/components/Hero"; import CodeShowcase from "@/components/CodeShowcase"; @@ -7,31 +5,12 @@ import PipelineFlow from "@/components/PipelineFlow"; import GetStarted from "@/components/GetStarted"; import Footer from "@/components/Footer"; -function loadHeroSnippet() { - try { - const statsPath = join( - process.cwd(), - "..", - "..", - "packages", - "ruam", - "stats.json" - ); - const stats = JSON.parse(readFileSync(statsPath, "utf-8")); - return stats.heroSnippet ?? null; - } catch { - return null; - } -} - export default function Home() { - const heroSnippet = loadHeroSnippet(); - return ( <>
- + diff --git a/apps/web/app/playground/page.tsx b/apps/web/app/playground/page.tsx index 1c3fbd6..83b7ea0 100644 --- a/apps/web/app/playground/page.tsx +++ b/apps/web/app/playground/page.tsx @@ -6,7 +6,7 @@ import Footer from "@/components/Footer"; export const metadata: Metadata = { title: "Playground — Ruam", description: - "Try Ruam in your browser. Paste JavaScript, pick a preset, and see the obfuscated output instantly.", + "Define guarded input domains for a pure JavaScript source region and compile it with Ruam's complete-local Isogloss profile.", }; export default function PlaygroundPage() { diff --git a/apps/web/components/CodeShowcase.tsx b/apps/web/components/CodeShowcase.tsx index 46c0c6d..b34f3d6 100644 --- a/apps/web/components/CodeShowcase.tsx +++ b/apps/web/components/CodeShowcase.tsx @@ -22,25 +22,29 @@ function generateLines(): string[] { .padStart(2, "0"); const vars = "QWXvjmpRTHkNceFd".split(""); const pick = () => vars[Math.floor(Math.random() * vars.length)]; + const realization = `_${hex()}${hex()}`; + const contribution = `_${hex()}${hex()}`; + const left = pick(); + const right = pick(); return [ - `var _ru4m=!0;(function(${pick()},${pick()}){`, - `const ${pick()}=${pick()}();while(!!1){try{`, - `const ${pick()}=parseInt('0x${hex()}')/0x1`, - `+parseInt('0x${hex()}')*0x3;if(${pick()})`, - `break;else ${pick()}['push'](${pick()}`, - `['shift']())}})(0x${hex()}${hex()},`, - `0x${hex()}${hex()}${hex()});`, + `const ${realization}=(${left},${right})=>{`, + `const ${contribution}=(${left}*${right})`, + `+(${left}*2);`, + `return ${contribution};`, + `};`, + `// realization ${hex()} · scalar slots`, + `return ${realization}(${left},${right});`, ]; } const PLACEHOLDER_LINES = [ - "var _ru4m=!0;(function(Q,W){", - "const X=Z();while(!!1){try{", - "const v=parseInt('0xae')/0x1", - "+parseInt('0x7b')*0x3;if(v)", - "break;else Q['push'](Q", - "['shift']())}})(0xa1b2,", - "0xc3d4e5);", + "const _a7f3=(Q,W)=>{", + "const _c912=(Q*W)", + "+(Q*2);", + "return _c912;", + "};", + "// realization 3f · scalar slots", + "return _a7f3(Q,W);", ]; function CompileCard() { @@ -99,7 +103,7 @@ function CompileCard() {

- Unique every time + Diversified realizations

Build #{buildNum}

@@ -120,15 +124,16 @@ function CompileCard() {

- Same source, different output. Variable names, opcodes, and - encryption seeds all change between builds. + Each build scalarizes physical slots and fragment contributions + into opaque, artifact-derived locals across contextual + realizations.

); } -/* ── Irreversible card ── */ -function IrreversibleCard() { +/* ── Source-region replacement card ── */ +function RegionReplacementCard() { return (
@@ -136,21 +141,21 @@ function IrreversibleCard() {

- Irreversible + Source-region replacement

- Classic Obfuscation + Before {"function _0x1a(a,b){ return a*b }"}

- Flow, variables, and strings can be heavily hidden, but - the logic is inevitably traceable. + A named pure return region uses explicitly guarded + scalar inputs.

@@ -158,19 +163,19 @@ function IrreversibleCard() { Ruam - {"_vm.call('a7f3',this,[a,b])"} + {"return _a7f3([a,b], context)[0]"}

- The logic is gone, and operations are called to a custom - VM instead of the JS Interpreter. + The selected relation is replaced by scalarized + realizations; no original-relation fallback is embedded.

- Your code is compiled away, and the produced RuamVM bytecode - executes the same result as your JS, but in an entirely - different way. + Effectful and unsupported JavaScript stays native. Ruam fails a + configured region closed if its domain or purity proof is + incomplete.

); @@ -179,7 +184,7 @@ function IrreversibleCard() { /* ── Instant card ── */ function InstantCard() { const [copied, setCopied] = useState(false); - const cmd = "npx ruam input.js -o output.js --preset max --target node"; + const cmd = "npx ruam input.js -o output.js --target node"; const copy = () => { navigator.clipboard.writeText(cmd); @@ -216,11 +221,11 @@ function InstantCard() {
{[ - "Works with Node.js, Deno, Bun, etc.", - "Supports MV2 & MV3 Browser Extensions", - "Compatible with any framework", - "Customizable obfuscation layers", - "Full project scope", + "Explicit source-region selection", + "Exact boolean or bounded-number domains", + "Frozen deployment profiles", + "No complete fallback in nonlocal modes", + "Native surrounding JavaScript", ].map((item) => (

- One command protects your entire project. No code changes are - necessary to build with Ruam. + Use the local profile for a synchronous complete client, or plan + a custody or attestation boundary explicitly for an incomplete + client.

); @@ -257,9 +263,10 @@ export default function CodeShowcase() { Not another name mangler.

- RuamVM's encrypted bytecode is indistinguishable even to - experienced attackers. To piece together the original logic, - an intruder must first reverse-engineer the RuamVM. + Isogloss changes the representation of selected pure + relations without pretending a complete local client can + keep those relations secret from unrestricted + instrumentation.

@@ -278,7 +285,7 @@ export default function CodeShowcase() { viewport={{ once: true }} transition={{ delay: 0.08 }} > - + - World Class Protection. + Explicit Protection Boundaries.

- What are you waiting for? + Start with a guarded pure source region and choose the + Isogloss profile your deployment can actually enforce.

{/* Install command */} diff --git a/apps/web/components/Hero.tsx b/apps/web/components/Hero.tsx index a695817..1cc0184 100644 --- a/apps/web/components/Hero.tsx +++ b/apps/web/components/Hero.tsx @@ -15,7 +15,6 @@ const scrambleChar = () => /* ── Syntax color classes ── */ const K = "text-syn-keyword"; const N = "text-syn-number"; -const S = "text-syn-string"; const D = "text-snow"; const CMT = "text-ash"; @@ -31,150 +30,33 @@ function line(...parts: Cell[][]): Cell[] { return parts.flat(); } -/* ── Snippet type from stats.json ── */ -export interface HeroSnippet { - head: string[]; - totalLines: number; - tail: string[]; -} - -/* ── JS syntax tokenizer (simple, for visual effect) ── */ -const JS_KEYWORDS = new Set([ - "var", - "let", - "const", - "function", - "return", - "if", - "for", - "while", - "do", - "else", - "new", - "this", - "typeof", - "void", - "true", - "false", - "null", - "undefined", - "class", - "extends", -]); - -function tokenizeLine(src: string): Cell[] { - const cells: Cell[] = []; - let i = 0; - while (i < src.length) { - const ch = src[i]!; - // String literals - if (ch === '"' || ch === "'") { - const quote = ch; - let j = i + 1; - while (j < src.length && src[j] !== quote) { - if (src[j] === "\\") j++; - j++; - } - j++; // closing quote - cells.push(...seg(src.slice(i, j), S)); - i = j; - } - // Comments - else if (ch === "/" && src[i + 1] === "/") { - cells.push(...seg(src.slice(i), CMT)); - break; - } - // Numbers (not part of identifiers) - else if (/[0-9]/.test(ch) && (i === 0 || !/[a-zA-Z_$]/.test(src[i - 1]!))) { - let j = i; - while (j < src.length && /[0-9.]/.test(src[j]!)) j++; - cells.push(...seg(src.slice(i, j), N)); - i = j; - } - // Identifiers / keywords - else if (/[a-zA-Z_$]/.test(ch)) { - let j = i; - while (j < src.length && /[a-zA-Z0-9_$]/.test(src[j]!)) j++; - const word = src.slice(i, j); - cells.push(...seg(word, JS_KEYWORDS.has(word) ? K : D)); - i = j; - } - // Everything else - else { - cells.push({ ch, cls: D }); - i++; - } - } - return cells; -} - -/* ── Build afterMap from snippet data (must match beforeMap line count) ── */ -function buildAfterMap(snippet: HeroSnippet): Cell[][] { - const target = beforeMap.length; // 8 lines - const tail = snippet.tail.map(tokenizeLine); - // 1 line reserved for the comment, rest split between head and tail - const headCount = target - tail.length - 1; - const head = snippet.head.slice(0, headCount).map(tokenizeLine); - - const count = snippet.totalLines.toLocaleString("en-US"); - return [ - ...head, - line(seg(` // ... ${count}+ lines of VM runtime`, CMT)), - ...tail, - ]; -} - /* ── Source code character map (syntax-colored) ── */ const beforeMap: Cell[][] = [ + line(seg("/* ruam:isogloss */", CMT)), line( seg("function", K), - seg(" fibonacci(", D), - seg("n", D), + seg(" priceQuote(", D), + seg("quantity", D), + seg(", ", D), + seg("unitPrice", D), seg(") {", D) ), line( seg(" ", D), - seg("if", K), - seg(" (n <= ", D), - seg("1", N), - seg(") ", D), seg("return", K), - seg(" n;", D) - ), - line( - seg(" ", D), - seg("let", K), - seg(" a = ", D), - seg("0", N), - seg(", b = ", D), - seg("1", N), - seg(";", D) - ), - line( - seg(" ", D), - seg("for", K), - seg(" (", D), - seg("let", K), - seg(" i = ", D), - seg("2", N), - seg("; i <= n; i++) {", D) + seg(" (quantity * unitPrice)", D) ), - line(seg(" [a, b] = [b, a + b];", D)), - line(seg(" }", D)), - line(seg(" ", D), seg("return", K), seg(" b;", D)), + line(seg(" + (quantity * ", D), seg("2", N), seg(");", D)), line(seg("}", D)), ]; const beforeLens = beforeMap.map((r) => r.length); -/* ── Hardcoded fallback (must be same line count as beforeMap) ── */ +/* ── Representative scalarized local realization ── */ const defaultAfterMap: Cell[][] = [ - line(seg("var", K), seg(" qv = {};", D)), - line(seg("var", K), seg(" wi = Object.create(", D), seg("null", K), seg(");", D)), - line(seg("var", K), seg(" od = ", D), seg("'cMGDq0EItS9gFzAosmU7y5akwh...'", S), seg(";", D)), - line(seg(" // ... 2,200+ lines of VM runtime", CMT)), - line(seg("function", K), seg(" fibonacci(...__args) {", D)), - line(seg(" ", D), seg("var", K), seg(" _n = __args.length | ", D), seg("0", N), seg(";", D)), - line(seg(" ", D), seg("return", K), seg(" tg(", D), seg('"hny2l"', S), seg(", __args, up, ", D), seg("this", K), seg(");", D)), + line(seg("const", K), seg(" q7 = (a,b) => (a*b)+(a*", D), seg("2", N), seg(");", D)), + line(seg("const", K), seg(" m4 = (a,b) => q7(a,b);", D)), + line(seg("function", K), seg(" priceQuote(quantity, unitPrice) {", D)), + line(seg(" ", D), seg("return", K), seg(" m4(quantity, unitPrice);", D)), line(seg("}", D)), ]; @@ -242,7 +124,7 @@ function useTerminalAnimation(config: AnimConfig) { beforePadded.map((row) => row.map((c) => ({ ...c }))) ); const [barState, setBarState] = useState({ - label: "fibonacci.js", + label: "price-quote.js", badge: "exposed", badgeClass: "bg-ember/10 text-ember", }); @@ -335,7 +217,7 @@ function useTerminalAnimation(config: AnimConfig) { while (!cancelledRef.current) { setCells(beforePadded.map((row) => row.map((c) => ({ ...c })))); setBarState({ - label: "fibonacci.js", + label: "price-quote.js", badge: "exposed", badgeClass: "bg-ember/10 text-ember", }); @@ -356,8 +238,8 @@ function useTerminalAnimation(config: AnimConfig) { if (cancelledRef.current) return; setBarState({ - label: "fibonacci.protected.js", - badge: "protected", + label: "price-quote.isogloss.js", + badge: "scalarized", badgeClass: "bg-accent/10 text-accent", }); setGlowing(true); @@ -371,7 +253,7 @@ function useTerminalAnimation(config: AnimConfig) { setCells(beforePadded.map((row) => row.map((c) => ({ ...c })))); setBarState({ - label: "fibonacci.js", + label: "price-quote.js", badge: "exposed", badgeClass: "bg-ember/10 text-ember", }); @@ -392,11 +274,8 @@ function useTerminalAnimation(config: AnimConfig) { } /* ── Component ── */ -export default function Hero({ snippet }: { snippet?: HeroSnippet | null }) { - const config = useMemo(() => { - const afterMap = snippet ? buildAfterMap(snippet) : defaultAfterMap; - return buildAnimConfig(afterMap); - }, [snippet]); +export default function Hero() { + const config = useMemo(() => buildAnimConfig(defaultAfterMap), []); const { cells, barState, glowing, contentOpacity } = useTerminalAnimation(config); @@ -438,17 +317,18 @@ export default function Hero({ snippet }: { snippet?: HeroSnippet | null }) { Don't just
- obfuscate code. + hide syntax.

- Destroy it. + Reshape the relation.

- Ruam compiles your JavaScript into encrypted - bytecode designed for a per-build unique RuamVM. - There is no deobfuscator. + Ruam replaces guarded pure source regions with + diversified scalar Isogloss realizations. Choose a + local, custodied, private-function, or attested + profile for the boundary you can actually enforce.

CHARS[Math.floor(Math.random() * CHARS.length)]!; diff --git a/apps/web/components/PipelineFlow.tsx b/apps/web/components/PipelineFlow.tsx index 91b2823..2a70e00 100644 --- a/apps/web/components/PipelineFlow.tsx +++ b/apps/web/components/PipelineFlow.tsx @@ -14,25 +14,25 @@ const useCases = [ icon: faPuzzlePiece, title: "Browser Extensions", description: - "Extension source is visible to anyone who installs it. Ruam makes your logic unreadable while keeping it fully functional.", + "Replace eligible pure extension logic with guarded, diversified source regions while preserving the surrounding browser APIs.", }, { icon: faCloud, title: "SaaS & Web Apps", description: - "Protect proprietary algorithms, pricing logic, and business rules that run in the browser where anyone can inspect them.", + "Move selected pricing and business-rule relations into an Isogloss profile that matches an already-observable deployment boundary.", }, { icon: faKey, title: "Licensed Software", description: - "Prevent license validation from being bypassed. VM bytecode makes it impractical to locate and patch checks.", + "Isolate eligible scalar checks into explicit source regions while leaving surrounding stateful and effectful logic native.", }, { icon: faCode, title: "APIs & SDKs", description: - "Shield authentication flows, API keys, and protocol implementations in client-side JavaScript.", + "Keep effectful API orchestration native while applying Isogloss only to audited pure regions with exact input domains.", }, ]; @@ -49,7 +49,9 @@ export default function PipelineFlow() { Built for real projects

- You build what matters, and let us handle security. + Choose protection boundaries explicitly; unsupported effects + remain ordinary JavaScript instead of being silently + virtualized.

diff --git a/apps/web/components/Playground.tsx b/apps/web/components/Playground.tsx index b5fea51..98f6987 100644 --- a/apps/web/components/Playground.tsx +++ b/apps/web/components/Playground.tsx @@ -13,42 +13,158 @@ import { faChevronUp, } from "@fortawesome/free-solid-svg-icons"; -// --- Types --- +// --- Isogloss source model --- -type PresetName = "low" | "medium" | "max"; +type IsoglossProfile = + | "holographic-local" + | "holographic-custodied" + | "holographic-private" + | "holographic-tee"; type TargetEnv = "node" | "browser" | "browser-extension"; -interface ManifestOption { - key: string; - label: string; - category: string; - description?: string; - cliFlag?: string; +type RegionDomain = + | { type: "boolean" } + | { type: "number"; min: number; max: number }; + +interface RegionBindingDraft { + id: number; + name: string; + type: RegionDomain["type"]; + min: string; + max: string; } -interface OptionManifest { - options: ManifestOption[]; - presets: Record>; - autoEnableRules: { when: string; enables: string }[]; +interface IsoglossSourceOptions { + isogloss: { + profile: "holographic-local"; + ownerTrace: "off"; + }; + targetMode: "comment"; + threshold: 1; + preprocessIdentifiers: boolean; + target: TargetEnv; + regionDomains: Record>; } -interface OptionMeta { - key: string; +interface ProfileDescription { + id: IsoglossProfile; label: string; - group: string; + availability: string; + description: string; + playground: boolean; } +const PROFILES: readonly ProfileDescription[] = [ + { + id: "holographic-local", + label: "Local", + availability: "available here", + description: + "Complete local client with diversified scalar BPRF realizations. Raises analysis cost without claiming secrecy under full instrumentation.", + playground: true, + }, + { + id: "holographic-custodied", + label: "Custodied", + availability: "deployment planner", + description: + "Holds part of the relation behind an existing remote-await boundary. A complete local fallback is forbidden.", + playground: false, + }, + { + id: "holographic-private", + label: "Private", + availability: "deployment planner", + description: + "Combines custody with an actively secure private-function protocol and a padded universal circuit.", + playground: false, + }, + { + id: "holographic-tee", + label: "Attested", + availability: "deployment planner", + description: + "Executes the held relation inside an owner-pinned attested trust domain without a complete local fallback.", + playground: false, + }, +] as const; + +const DEFAULT_BINDINGS: readonly RegionBindingDraft[] = [ + { id: 1, name: "quantity", type: "number", min: "1", max: "100" }, + { id: 2, name: "unitPrice", type: "number", min: "1", max: "500" }, +]; + // --- Default input code --- -const DEFAULT_CODE = `function fibonacci(n) { - if (n <= 1) return n; - let a = 0, b = 1; - for (let i = 2; i <= n; i++) { - [a, b] = [b, a + b]; - } - return b; +const DEFAULT_CODE = `/* ruam:isogloss */ +function priceQuote(quantity, unitPrice) { + return (quantity * unitPrice) + (quantity * 2); }`; +const IDENTIFIER_PATTERN = /^[A-Za-z_$][0-9A-Za-z_$]*$/; + +function materializeRegionDomains( + regionName: string, + bindings: readonly RegionBindingDraft[] +): + | { regionDomains: IsoglossSourceOptions["regionDomains"]; error: null } + | { regionDomains: null; error: string } { + const name = regionName.trim(); + if (!IDENTIFIER_PATTERN.test(name)) { + return { + regionDomains: null, + error: "Source region must be a named JavaScript function.", + }; + } + if (bindings.length === 0) { + return { + regionDomains: null, + error: "Declare at least one guarded input binding.", + }; + } + + const domains: Record = {}; + for (const binding of bindings) { + const bindingName = binding.name.trim(); + if (!IDENTIFIER_PATTERN.test(bindingName)) { + return { + regionDomains: null, + error: `Invalid input binding: ${binding.name || "(empty)"}.`, + }; + } + if (Object.hasOwn(domains, bindingName)) { + return { + regionDomains: null, + error: `Input binding ${bindingName} is declared more than once.`, + }; + } + if (binding.type === "boolean") { + domains[bindingName] = { type: "boolean" }; + continue; + } + const min = Number(binding.min); + const max = Number(binding.max); + if ( + !Number.isSafeInteger(min) || + !Number.isSafeInteger(max) || + Object.is(min, -0) || + Object.is(max, -0) || + min > max + ) { + return { + regionDomains: null, + error: `${bindingName} requires safe-integer bounds with min ≤ max.`, + }; + } + domains[bindingName] = { type: "number", min, max }; + } + + return { + regionDomains: { [name]: domains }, + error: null, + }; +} + // --- CodeMirror dynamic loader --- function useCodeMirror( @@ -258,8 +374,8 @@ function useWorker() { }; }, []); - const obfuscate = useCallback( - (code: string, options: Record) => + const transformRegion = useCallback( + (code: string, options: IsoglossSourceOptions) => new Promise<{ result: string; elapsed: number }>( (resolve, reject) => { if (!workerRef.current) { @@ -305,42 +421,21 @@ function useWorker() { [] ); - return { ready, obfuscate, initError }; + return { ready, transformRegion, initError }; } // --- Playground component --- export default function Playground() { - // --- Manifest loading --- - const [manifest, setManifest] = useState(null); - const [manifestError, setManifestError] = useState(null); - - useEffect(() => { - const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - fetch(`${basePath}/option-manifest.json`) - .then((res) => { - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json(); - }) - .then((data: OptionManifest) => setManifest(data)) - .catch((err) => - setManifestError(err instanceof Error ? err.message : String(err)) - ); - }, []); - - // --- Derived option metadata --- - const OPTIONS: OptionMeta[] = (manifest?.options ?? []).map((o) => ({ - key: o.key, - label: o.label, - group: o.category, - })); - // --- State --- - const [preset, setPreset] = useState("medium"); + const profile: IsoglossProfile = "holographic-local"; const [target, setTarget] = useState("browser"); - const [toggles, setToggles] = useState>({}); - const [isCustom, setIsCustom] = useState(false); - const [optionsOpen, setOptionsOpen] = useState(false); + const [regionName, setRegionName] = useState("priceQuote"); + const [bindings, setBindings] = + useState(() => [...DEFAULT_BINDINGS]); + const [preprocessIdentifiers, setPreprocessIdentifiers] = useState(false); + const [optionsOpen, setOptionsOpen] = useState(true); + const bindingIdRef = useRef(3); const [output, setOutput] = useState(""); const [error, setError] = useState(null); @@ -348,15 +443,6 @@ export default function Playground() { const [elapsed, setElapsed] = useState(null); const [copied, setCopied] = useState(false); - // Initialize toggles from manifest medium preset once loaded - const manifestLoaded = useRef(false); - useEffect(() => { - if (manifest && !manifestLoaded.current) { - manifestLoaded.current = true; - setToggles(manifest.presets["medium"] ?? {}); - } - }, [manifest]); - const inputRef = useRef(null); const outputRef = useRef(null); const codeRef = useRef(DEFAULT_CODE); @@ -374,82 +460,79 @@ export default function Playground() { ); const { viewRef: outputViewRef, loaded: outputLoaded } = useCodeMirror( outputRef, - "// Output will appear here after obfuscation", + "// Scalarized Isogloss output will appear here", true, { current: null } ); - const { ready: workerReady, obfuscate, initError: workerError } = useWorker(); - - const allLoaded = inputLoaded && outputLoaded && workerReady && manifest !== null; - - // --- Preset selection --- - const selectPreset = useCallback((name: PresetName) => { - if (!manifest) return; - setPreset(name); - setToggles(manifest.presets[name] ?? {}); - setIsCustom(false); - }, [manifest]); - - // --- Toggle individual option --- - const toggleOption = useCallback( - (key: string) => { - setToggles((prev) => { - const next = { ...prev, [key]: !prev[key] }; - // Check if it still matches a preset - const matchesPreset = (["low", "medium", "max"] as const).find( - (p) => { - const pd = manifest?.presets[p]; - if (!pd) return false; - return Object.keys(pd).every( - (k) => pd[k] === next[k] - ); - } - ); - if (matchesPreset) { - setPreset(matchesPreset); - setIsCustom(false); - } else { - setIsCustom(true); - } - return next; - }); + const { + ready: workerReady, + transformRegion, + initError: workerError, + } = useWorker(); + + const allLoaded = inputLoaded && outputLoaded && workerReady; + + const updateBinding = useCallback( + (id: number, patch: Partial>) => { + setBindings((current) => + current.map((binding) => + binding.id === id ? { ...binding, ...patch } : binding + ) + ); }, - [manifest] + [] ); - // --- Run obfuscation --- + const addBinding = useCallback(() => { + const id = bindingIdRef.current++; + setBindings((current) => [ + ...current, + { + id, + name: `input${id}`, + type: "number", + min: "0", + max: "100", + }, + ]); + }, []); + + const removeBinding = useCallback((id: number) => { + setBindings((current) => + current.filter((binding) => binding.id !== id) + ); + }, []); + + // --- Build the configured source region --- const run = useCallback(async () => { if (running || !allLoaded) return; - setRunning(true); setError(null); setElapsed(null); - const options: Record = { - ...(isCustom ? toggles : { preset }), + const materialized = materializeRegionDomains(regionName, bindings); + if (materialized.regionDomains === null) { + setError(materialized.error); + return; + } + + const options: IsoglossSourceOptions = { + isogloss: { + profile: "holographic-local", + ownerTrace: "off", + }, + targetMode: "comment", + threshold: 1, + preprocessIdentifiers, target, + regionDomains: materialized.regionDomains, }; - // Deterministic minimum delay (300-450ms) so the UI - // feels weighty. Hash is seeded from input length + - // first/last chars so the same code always gets the - // same delay. + setRunning(true); const code = codeRef.current; - const h = - (code.length * 2654435761 + - (code.charCodeAt(0) || 0) * 31 + - (code.charCodeAt(code.length - 1) || 0)) >>> - 0; - const minDelay = 300 + (h % 151); // 300-450ms - const delayPromise = new Promise((r) => - setTimeout(r, minDelay) - ); try { - const [{ result, elapsed: ms }] = await Promise.all([ - obfuscate(code, options), - delayPromise, - ]); + const { result, elapsed: ms } = await transformRegion(code, options); setOutput(result); setElapsed(ms); @@ -481,7 +564,16 @@ export default function Playground() { } finally { setRunning(false); } - }, [running, allLoaded, isCustom, toggles, preset, target, obfuscate, outputViewRef]); + }, [ + running, + allLoaded, + regionName, + bindings, + preprocessIdentifiers, + target, + transformRegion, + outputViewRef, + ]); // --- Copy output --- const copyOutput = useCallback(() => { @@ -498,7 +590,7 @@ export default function Playground() { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = "obfuscated.js"; + a.download = "isogloss.js"; a.click(); URL.revokeObjectURL(url); }, [output]); @@ -527,38 +619,24 @@ export default function Playground() { Playground

- Paste JavaScript, pick options, and obfuscate — all in - your browser. Nothing leaves your machine. + Declare a guarded pure source region and compile it with + the complete-local Isogloss profile. Nothing leaves your + machine.

- {/* Top bar: presets + obfuscate + stats */} + {/* Top bar: profile + target + build + stats */} - {/* Preset selector */} + {/* Frozen local profile */}
- {(["low", "medium", "max"] as const).map((p) => ( - - ))} - {isCustom && ( - - custom - - )} + + {profile} +
{/* Target selector */} @@ -586,7 +664,7 @@ export default function Playground() { ))}
- {/* Obfuscate button */} + {/* Compile button */} {/* Stats */} @@ -689,7 +767,7 @@ export default function Playground() { {output && ( - protected + isogloss )} {error && ( @@ -714,7 +792,7 @@ export default function Playground() {
- {/* Options panel */} + {/* Profile and source-region panel */} setOptionsOpen((o) => !o)} className="flex w-full items-center gap-2 rounded-lg border border-edge bg-ink/60 px-4 py-2.5 font-mono text-xs text-smoke transition hover:bg-panel" > - options + isogloss configuration - {isCustom - ? "custom configuration" - : `${preset} preset`} + {regionName || "unnamed region"} · {bindings.length}{" "} + guarded {bindings.length === 1 ? "input" : "inputs"} - {( - [ - ["security", "Security"], - ["obfuscation", "Obfuscation"], - ["optimization", "Optimization"], - ] as [string, string][] - ).map(([group, label]) => ( -
- - {label} - -
- {OPTIONS.filter( - (o) => o.group === group - ).map((opt) => { - const active = toggles[opt.key]; - return ( - - ); - })} -
+
+ + Frozen deployment profiles + +
+ {PROFILES.map((item) => ( + + ))}
- ))} +
+ +
+
+ + +
+

+ The exact marker{" "} + + {"/* ruam:isogloss */"} + {" "} + selects this named function. Every input used + by its pure return expression needs an exact + guard domain. +

+ +
+ {bindings.map((binding) => ( +
+ + updateBinding(binding.id, { + name: event.target.value, + }) + } + spellCheck={false} + className="rounded-md border border-edge bg-ink px-2.5 py-2 font-mono text-[11px] text-cloud outline-none focus:border-accent/40" + /> + + + updateBinding(binding.id, { + min: event.target.value, + }) + } + disabled={binding.type === "boolean"} + placeholder="min" + inputMode="numeric" + className="rounded-md border border-edge bg-ink px-2.5 py-2 font-mono text-[11px] text-cloud outline-none focus:border-accent/40 disabled:opacity-30" + /> + + updateBinding(binding.id, { + max: event.target.value, + }) + } + disabled={binding.type === "boolean"} + placeholder="max" + inputMode="numeric" + className="rounded-md border border-edge bg-ink px-2.5 py-2 font-mono text-[11px] text-cloud outline-none focus:border-accent/40 disabled:opacity-30" + /> + +
+ ))} +
+ +

+ Local mode ships a complete client. It + increases analysis work, but does not claim a + secret relation or resistance to unrestricted + dynamic instrumentation. +

+
)} @@ -787,13 +992,13 @@ export default function Playground() { {!allLoaded && (
- {workerError || manifestError ? ( + {workerError ? ( <>

Failed to load Ruam engine

- {workerError || manifestError} + {workerError}

) : ( diff --git a/apps/web/components/SiteProtection.tsx b/apps/web/components/SiteProtection.tsx deleted file mode 100644 index dd3a7ac..0000000 --- a/apps/web/components/SiteProtection.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { useEffect } from "react"; - -export default function SiteProtection() { - useEffect(() => { - const blockContextMenu = (e: Event) => e.preventDefault(); - - const blockDevTools = (e: KeyboardEvent) => { - // F12 - if (e.key === "F12") { - e.preventDefault(); - return; - } - // Ctrl+Shift+I/J/C (Windows/Linux) or Cmd+Option+I/J/C (macOS) - if ( - ((e.ctrlKey && e.shiftKey) || (e.metaKey && e.altKey)) && - /^[ijc]$/i.test(e.key) - ) { - e.preventDefault(); - return; - } - // Ctrl+U / Cmd+U (view source) - if ((e.ctrlKey || e.metaKey) && e.key === "u") { - e.preventDefault(); - } - }; - - document.addEventListener("contextmenu", blockContextMenu); - document.addEventListener("keydown", blockDevTools); - - return () => { - document.removeEventListener("contextmenu", blockContextMenu); - document.removeEventListener("keydown", blockDevTools); - }; - }, []); - - return null; -} diff --git a/apps/web/components/VsSection.tsx b/apps/web/components/VsSection.tsx index b772924..73bec4a 100644 --- a/apps/web/components/VsSection.tsx +++ b/apps/web/components/VsSection.tsx @@ -12,57 +12,57 @@ import { const layers = [ { icon: faLock, - title: "VM Compilation", - tag: "Layer 1", + title: "Source-region proof", + tag: "Contract 1", description: - "Your JavaScript is compiled into custom bytecode — register-based instructions executed by an embedded interpreter. No native JS logic remains in the output.", + "Ruam selects a named pure return expression and requires an exact boolean or bounded-number domain for every scalar input. Configured regions fail closed when that proof is incomplete.", visual: [ { - label: "Source", - value: "function add(a,b) { return a+b }", + label: "Selected source", + value: "/* ruam:isogloss */ function quote(q,p) { return q*p+q*2 }", style: "ember" as const, }, { - label: "Bytecode", - value: "LOAD_REG 0 → MUL → STORE_REG 2 → RET", + label: "Guard domains", + value: "quote.q ∈ [1,100] · quote.p ∈ [1,500]", style: "accent" as const, }, ], }, { icon: faShuffle, - title: "Opcode Shuffling", - tag: "Layer 2", + title: "Scalar BPRF emission", + tag: "Contract 2", description: - "Every build shuffles all ~300 opcodes via seeded Fisher-Yates. The interpreter uses physical opcode numbers as case labels — no reverse map exists to decode.", + "Physical slots and fragment contributions become dedicated opaque locals and scalar functions. Context selects among diversified realizations without a runtime artifact walker or generic evaluator.", visual: [ { - label: "Build A", - value: "ADD=0x3F MUL=0x91 RET=0xC2", + label: "Contextual realizations", + value: "caller × epoch × lineage → realization k", style: "ember" as const, }, { - label: "Build B", - value: "ADD=0xA7 MUL=0x1E RET=0x58", + label: "Emitted shape", + value: "physical slots → fragment functions → scalar output", style: "accent" as const, }, ], }, { icon: faShieldHalved, - title: "Rolling Encryption", - tag: "Layer 3", + title: "Deployment profile", + tag: "Contract 3", description: - "Every instruction is XOR-encrypted with a position-dependent key derived from bytecode metadata via FNV-1a. No plaintext seed appears in the output.", + "The profile states where the relation lives and what the client contains. Local mode is honestly complete; custody, private-function, and attested modes forbid a complete local fallback.", visual: [ { - label: "Key derivation", - value: "FNV-1a(instCount, regCount, paramCount)", + label: "Complete client", + value: "holographic-local → analysis amplification only", style: "ember" as const, }, { - label: "Per-instruction", - value: "XOR(opcode, mixState(key, idx, idx^φ))", + label: "Incomplete client", + value: "custodied · private · attested → no local fallback", style: "accent" as const, }, ], @@ -81,14 +81,14 @@ export default function VsSection() { viewport={{ once: true }} >

- Defense in Depth + Explicit by design

- Three layers of protection + Three verifiable contracts

- Each layer makes reverse engineering exponentially harder. - Together, they make it practically impossible. + The source region, emitted representation, and deployment + boundary each carry a separate fail-closed contract.

diff --git a/apps/web/public/option-manifest.json b/apps/web/public/option-manifest.json deleted file mode 100644 index 051cd11..0000000 --- a/apps/web/public/option-manifest.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "options": [ - { - "key": "rollingCipher", - "label": "Rolling Cipher", - "category": "security", - "description": "Position-dependent XOR encryption on every instruction", - "cliFlag": "--rolling-cipher" - }, - { - "key": "integrityBinding", - "label": "Integrity Binding", - "category": "security", - "description": "Bind bytecode decryption to interpreter source integrity", - "cliFlag": "--integrity-binding" - }, - { - "key": "debugProtection", - "label": "Debug Protection", - "category": "security", - "description": "Multi-layered anti-debugger with escalating response", - "cliFlag": "--debug-protection" - }, - { - "key": "vmShielding", - "label": "VM Shielding", - "category": "security", - "description": "Per-function micro-interpreters with independent opcode shuffle", - "cliFlag": "--vm-shielding" - }, - { - "key": "incrementalCipher", - "label": "Incremental Cipher", - "category": "security", - "description": "Move instruction decryption into the VM dispatch loop", - "cliFlag": "--incremental-cipher" - }, - { - "key": "semanticOpacity", - "label": "Semantic Opacity", - "category": "security", - "description": "Opaque predicates, handler aliasing, and encoding diversity", - "cliFlag": "--semantic-opacity" - }, - { - "key": "observationResistance", - "label": "Observation Resistance", - "category": "security", - "description": "Silent computation corruption when instrumentation detected", - "cliFlag": "--observation-resistance" - }, - { - "key": "encryptBytecode", - "label": "Encrypt Bytecode", - "category": "security", - "description": "RC4 encryption using an environment fingerprint key", - "cliFlag": "--encrypt" - }, - { - "key": "mixedBooleanArithmetic", - "label": "MBA", - "category": "obfuscation", - "description": "Replace arithmetic/bitwise ops with MBA expressions", - "cliFlag": "--mba" - }, - { - "key": "stackEncoding", - "label": "Stack Encoding", - "category": "obfuscation", - "description": "XOR-encode VM stack values during execution", - "cliFlag": "--stack-encoding" - }, - { - "key": "deadCodeInjection", - "label": "Dead Code Injection", - "category": "obfuscation", - "description": "Insert unreachable bytecode sequences after RETURN", - "cliFlag": "--dead-code" - }, - { - "key": "handlerFragmentation", - "label": "Handler Fragmentation", - "category": "obfuscation", - "description": "Split handlers into interleaved fragments", - "cliFlag": "--handler-fragmentation" - }, - { - "key": "stringAtomization", - "label": "String Atomization", - "category": "obfuscation", - "description": "Replace string literals with encoded table lookups", - "cliFlag": "--string-atomization" - }, - { - "key": "blockPermutation", - "label": "Block Permutation", - "category": "obfuscation", - "description": "Shuffle bytecode basic block order", - "cliFlag": "--block-permutation" - }, - { - "key": "opcodeMutation", - "label": "Opcode Mutation", - "category": "obfuscation", - "description": "Runtime handler table mutations via MUTATE opcodes", - "cliFlag": "--opcode-mutation" - }, - { - "key": "bytecodeScattering", - "label": "Bytecode Scattering", - "category": "obfuscation", - "description": "Split bytecode into mixed-type fragments scattered through output", - "cliFlag": "--bytecode-scattering" - }, - { - "key": "preprocessIdentifiers", - "label": "Rename Identifiers", - "category": "optimization", - "description": "Rename identifiers before compilation", - "cliFlag": "--preprocess" - }, - { - "key": "dynamicOpcodes", - "label": "Dynamic Opcodes", - "category": "optimization", - "description": "Filter unused opcode handlers from interpreter", - "cliFlag": "--dynamic-opcodes" - }, - { - "key": "decoyOpcodes", - "label": "Decoy Opcodes", - "category": "optimization", - "description": "Inject realistic fake opcode handlers", - "cliFlag": "--decoy-opcodes" - }, - { - "key": "polymorphicDecoder", - "label": "Polymorphic Decoder", - "category": "optimization", - "description": "Per-build random chain of reversible byte operations", - "cliFlag": "--polymorphic-decoder" - }, - { - "key": "scatteredKeys", - "label": "Scattered Keys", - "category": "optimization", - "description": "Fragment key materials across closure tiers", - "cliFlag": "--scattered-keys" - } - ], - "presets": { - "low": { - "preprocessIdentifiers": false, - "encryptBytecode": false, - "debugProtection": false, - "dynamicOpcodes": true, - "decoyOpcodes": false, - "deadCodeInjection": false, - "stackEncoding": false, - "rollingCipher": false, - "integrityBinding": false, - "vmShielding": false, - "mixedBooleanArithmetic": false, - "handlerFragmentation": false, - "stringAtomization": false, - "polymorphicDecoder": false, - "scatteredKeys": false, - "blockPermutation": false, - "opcodeMutation": false, - "bytecodeScattering": false, - "incrementalCipher": false, - "semanticOpacity": false, - "observationResistance": false - }, - "medium": { - "preprocessIdentifiers": true, - "encryptBytecode": true, - "debugProtection": false, - "dynamicOpcodes": true, - "decoyOpcodes": true, - "deadCodeInjection": false, - "stackEncoding": false, - "rollingCipher": true, - "integrityBinding": false, - "vmShielding": false, - "mixedBooleanArithmetic": false, - "handlerFragmentation": false, - "stringAtomization": true, - "polymorphicDecoder": true, - "scatteredKeys": true, - "blockPermutation": false, - "opcodeMutation": false, - "bytecodeScattering": true, - "incrementalCipher": false, - "semanticOpacity": false, - "observationResistance": false - }, - "max": { - "preprocessIdentifiers": true, - "encryptBytecode": true, - "debugProtection": true, - "dynamicOpcodes": true, - "decoyOpcodes": true, - "deadCodeInjection": true, - "stackEncoding": true, - "rollingCipher": true, - "integrityBinding": true, - "vmShielding": true, - "mixedBooleanArithmetic": true, - "handlerFragmentation": true, - "stringAtomization": true, - "polymorphicDecoder": true, - "scatteredKeys": true, - "blockPermutation": true, - "opcodeMutation": true, - "bytecodeScattering": true, - "incrementalCipher": true, - "semanticOpacity": true, - "observationResistance": true - } - }, - "autoEnableRules": [ - { - "when": "integrityBinding", - "enables": "rollingCipher" - }, - { - "when": "vmShielding", - "enables": "rollingCipher" - }, - { - "when": "stringAtomization", - "enables": "polymorphicDecoder" - }, - { - "when": "opcodeMutation", - "enables": "rollingCipher" - }, - { - "when": "incrementalCipher", - "enables": "rollingCipher" - }, - { - "when": "observationResistance", - "enables": "rollingCipher" - } - ] -} diff --git a/docs/ruam-gen2-ideation-prompt.md b/docs/ruam-gen2-ideation-prompt.md new file mode 100644 index 0000000..342bf40 --- /dev/null +++ b/docs/ruam-gen2-ideation-prompt.md @@ -0,0 +1,52 @@ +# Ruam Gen-2 Ideation — Launch Packet + +> **Paste this to a fresh session to kick off the campaign.** It is the lean operator brief. +> The full design (rationale, seed banks, scoring, schemas, and the complete runnable script) +> is in `docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-campaign-design.md` — read it first. + +--- + +## Mission (one paragraph) + +Ruam is a developer tool that protects intellectual property in shipped JavaScript: it transforms a developer's own functions so automated tooling cannot cheaply and mechanically summarize or reconstruct the original program. The first generation of hardening ideas were competent but *incremental* — they iterated on primitives the codebase already had (salt the key, chain the keystream, fold a digest), because the ideation anchored on a defeatist "it's only work-factor" ceiling. **Your job is to run a structured ideation campaign that makes incrementalism structurally impossible** and surfaces genuinely novel directions for Ruam's *whole* next generation — across seven tracks, not only its resilience layer. + +## How novelty is forced (do not skip) + +Every idea-generator works under four simultaneous constraints: +1. **Forbidden-solution list** — anything that matches an existing Ruam feature or a canonical primitive (ciphers, digests, base-N codecs, LCG/FNV, table/switch VMs, control-flow flattening, string tables, dead code, opaque predicates) is disqualified before scoring. +2. **Assumption inversion** — each agent overturns one Ruam "axiom" (e.g., "the decoder must ship with the code," "the output is fixed once emitted," "a fragment is understandable locally"). +3. **Cross-domain analogy** — each agent derives its mechanism from a domain far from software (immune systems, holography, origami metamaterials, stage misdirection, DNA repair, ecology…), explaining the source system *before* stating the idea. +4. **Forced fusion** — a dedicated phase hybridizes the boldest ideas into mechanisms neither parent would produce. + +## Model roles + +- **Fable 5** (`claude-fable-5`) — framing brief + final synthesis/ranking (the judgment layer). +- **Opus 5** (`claude-opus-5`) — the divergent generator fleet + fusion (each agent a unique lens × assumption × track × persona). + +## Vocabulary rule (mandatory) + +All agent-facing text uses **product / IP-protection framing**. **Banned:** attack, adversary, red-team, exploit, weaponize, injection, bypass, "make the model fail." **Use instead:** resilience to automated analysis, originality of output, protecting the developer's IP, novel program transformations. Frame the "AI" angle as *"automated analysis/reconstruction tooling,"* never as harming a model. This keeps the run from false-flagging safeguards. + +## The seven tracks (whole next-gen tool) + +1. Resilience to automated understanding · 2. Novel transformation paradigms · 3. New product surfaces & capabilities · 4. Self-modifying / living output · 5. Semantic-level protection · 6. Verifiability & developer experience · 7. Distribution / licensing / delivery models. + +## Launch + +The campaign is a `Workflow`. **It requires explicit opt-in** (say `ultracode`, or "run this workflow") because it dispatches Opus 5 + Fable 5 agents and is token-intensive. + +1. Read the design doc (link at top) and copy the script from its **§11**. +2. Invoke: + ``` + Workflow({ script: <§11 script>, args: { generators: 7 } }) + ``` + or, after the first run persisted it, `Workflow({ scriptPath: , args: { generators: 16 } })` for a wider sweep. +3. Phase order: **Frame → Diverge → Fuse → Tag → Synthesize → Coverage.** Default 14 agents (under the 15-agent guideline); raise `args.generators` to widen. + +## Definition of done + +The run returns `{ headline, slate, fusions, coverage, counts }`. It succeeds when `slate.ranked` holds ≥ 3 ideas with recognizability < 0.5 across ≥ 3 tracks, ≥ 1 fusion in the top 5, and every top-10 idea carries a "why this is not a gen-1 iteration" line. If every top idea scores recognizability ≥ 0.7, the run failed its own bar — re-seed (bump `args.generators` or edit the lens/assumption strides) rather than accept retreads. + +## After the campaign + +A selected idea feeds the *existing* build pipeline: brainstorming → design spec → `writing-plans` → TDD → verify. This campaign is the front end; it does not replace that machine. diff --git a/docs/superpowers/baselines/2026-07-24-bprf-csh-spike-report.md b/docs/superpowers/baselines/2026-07-24-bprf-csh-spike-report.md new file mode 100644 index 0000000..b2bfc00 --- /dev/null +++ b/docs/superpowers/baselines/2026-07-24-bprf-csh-spike-report.md @@ -0,0 +1,335 @@ +# BPRF/CSH Spike Report + +**Date:** 2026-07-24 +**Branch:** `codex/traveling-isogloss-replacement` +**Decision state:** Initial local BPRF/CSH and first custody variants are +**no-go**; statefully masked custody prevents direct internal recovery but +remains **no-go** under the stricter black-box/overhead gates, and product +schemas remain unfrozen + +## Purpose + +This report records the first executable tranche of the revised Isogloss +architecture: + +1. measure the legacy VM's dynamic-instrumentation choke point; +2. construct conservative effect-delimited canonical regions; +3. validate multiple BPRF ontologies without runtime semantic dispatch; +4. validate moving-cover CSH and a necessary custodied relation; and +5. prove that BPRF polynomial fragments can execute over CSH chart shares + without reconstructing a transition frame; +6. attack the shipped artifacts rather than crediting contextual diversity by + inspection; and +7. evolve custody so every intermediate client-visible state remains + independently masked by custodian-held representation state. + +The spike is not a product backend. The legacy VM remains only as the current +behavior oracle while the replacement is incomplete. + +## Implemented controls and references + +| Slice | Result | +|---|---| +| Legacy attacker control | Deterministic four-fixture corpus and executor-neutral scorer | +| Canonical analysis | Purity, throw, coercion, call, suspension, access, allocation, and completion facts | +| Region formation | Deterministic effect-delimited regions with typed exits and boundary contracts | +| BPRF | At least two causal families, contextual realization selection, fission, braiding, and wire rebasing | +| Local CSH | Five nonlinear charts, threshold three, changing covers, and scalar certified projections | +| BPRF over CSH | Degree-four product shares reduced by five contributors into a fresh degree-two cover | +| Direct custody | Signed response, one-response opening, mandatory remote relation, monotonic lineage, replay/fork rejection | +| Stateful masked custody | Keyed per-coordinate masks, pinned issued state, mandatory signed transitions, and final-only opening | + +All generic BPRF/CSH evaluators are explicitly test/reference-only and are not +reachable from the package or CLI build entries. + +## Legacy dynamic-attacker control + +At seed `0x1badb002`, the legacy debug build exposes: + +| Measurement | Result | +|---|---:| +| Fixtures | 4 | +| Owner/recovered boundaries | 278 / 278 | +| Operation events recovered | 254 / 254 | +| Boundary precision / recall / F1 | 1.0 / 1.0 / 1.0 | +| Operation accuracy / micro-F1 / macro-F1 | 1.0 / 1.0 / 1.0 | +| Sequence-edge recall | 1.0 | +| Executed dynamic-CFG recall | 1.0 | +| Minimum probe families | 1 | +| Result preservation | Complete | + +This confirms the architectural defect: one passive console-probe family +produces a complete stable post-optimizer semantic stream. + +## BPRF structural results + +The bounded pure reference kernel currently proves: + +- deterministic generation of `K >= 2` realizations; +- two structurally different causal families: + dependency-layer polynomial dataflow and two-phase continuation residuals; +- indicator and signed boolean coordinate ontologies; +- caller-, epoch-, and lineage-conditioned realization selection; +- shifted-product and additive fission; +- every destination requires every configured fragment; +- every fragment is braided across multiple destinations; +- physical input, intermediate, and output wire bases differ; and +- serialized artifacts and traces contain no semantic operation, opcode, + handler, canonical operand, formula tag, or source-node identity. + +The generic differential evaluator is not a production runtime. The +specialized BPRF emitter removes the generic evaluator and artifact walker, but +all selected-realization algebra remains client-resident and statically +recoverable. + +The artifact-aware attacker obtains: + +| Measurement | Result | +|---|---:| +| Held-out output recovery from artifact alone | 1.0 | +| Physical dependency-topology recall | 1.0 | +| Dynamic primitive observations required | 0 | +| Artifact-aware unseen-realization recall | 1.0 | +| Artifact-aware transfer loss | 0 percentage points | + +Naive trace-only transfer does fail on an unseen realization. That fact is not +credited because the shipped artifact enumerates every realization. + +The specialized source-emission spike has no runtime artifact walker, generic +transition loop, semantic selector, frame array, or owner trace. Its remaining +hook surfaces include the scalar field normalizer, chart-local openings, +five-way reductions, final projection sites, and public region I/O. The +measured families emitted approximately **360 KB** and **1.67 MB** of source +and ran in approximately **415 μs** and **5.8 ms** per call. This is useful +mechanical evidence but not a production size/runtime shape. + +## CSH structural results + +The local moving-cover reference currently proves: + +- five overlapping nonlinearly wrapped charts; +- a configured threshold of three; +- arbitrary valid threshold subsets recover only a declared scalar + projection; +- every protected transition changes cover identity, mixing basis, chart + points, wrapping maps, and ownership; +- affine transport accumulates old chart contributions directly into new + charts without assembling the logical state; and +- there is no exported global-frame/global-section decoder. + +The combined evaluator additionally executes BPRF product fragments over chart +shares. Products raise polynomial degree from two to four, so all five charts +are required for direct degree reduction into the next threshold-three cover. +No transition constructs an ordinary BPRF frame. + +The stronger artifact-and-metadata-aware attacker changes the decision: + +| Measurement | Result | +|---|---:| +| C90 | 3 contributions | +| Recovery with 1 / 2 / 3+ contributions | 0 / 0 / 1.0 | +| G90 work proxy | 3× plain state | +| Resident storage proxy | 5× plain state | +| Fixed-decoder unseen-cover accuracy | 0 | +| Metadata-aware unseen-cover accuracy | 1.0 | +| Metadata-aware transfer loss | 0 percentage points | +| Combined final-frame recovery | 1.0 | +| Combined full-trace amplification | 5× | + +The configured chart threshold is real, but it does not meet the 10× work and +storage gates. Cover changes do not create genuine transfer loss once the +shipped cover metadata is admitted to the attack. Local CSH is therefore a +hard no-go as the maximum-protection architecture. + +## Custody results + +The direct-remote control holds one nonlinear projected relation outside the +client contract. The client receives only a signed, lineage-bound encoded +projection and a one-response opening for the native-visible scalar value. + +Verified properties: + +- no response means no local evaluation or fallback; +- the client contract contains neither the relation nor lineage/response + secrets; +- every response advances the representation epoch; +- a consumed nonce is rejected; +- two requests forked from one client snapshot cannot both execute; and +- response tampering fails signature verification. + +This is a direct-custody control, not private-function evaluation. The +custodian learns the declared input projection. + +The adversarial gate found both first custody forms insufficient: + +- the direct scalar relation is exactly learned from four fresh-session + training queries and bypassed at one localized patch site; +- the chart-relation response exposes a complete unmasked logical delta after + correlating three contributions; and +- the chart relation is likewise learned from four fresh sessions. + +Replay and fork protection worked, but those controls did not prevent relation +learning. + +The revised custody spike now carries charts for +`logicalState + custodianMask(epoch)`. Each signed response advances to +`F_epoch(logicalState) + custodianMask(epoch + 1)`, and only the terminal +site-specific scalar projection is opened. Representation masks and sharing +residuals are derived with keyed, domain-separated hashes. The custodian pins +the exact masked state it issued, so changing the client-side transport cannot +substitute a new logical input at the next epoch. Missing responses, replay, +snapshot forks, nonce substitution, signature tampering, and representation +substitution all fail closed. + +The statefully masked evolution has now been attacked under full client +artifact, heap, request, response, and output visibility: + +| Measurement | Result | +|---|---:| +| Protected logical-coordinate recovery | 0 | +| Unmasked transition-delta recovery | 0 | +| Masked state/delta recovery | 1.0 | +| Compatible logical states per width-three protected observation | 65,521³ | +| Cross-session masked state/delta transfer | 0 | +| Foreign/rebound signed response accepted | false / false | +| Shared service secrets and reused nonce | tested | +| Final output transfer | 1.0 | +| Localized client hook families | 2 | +| Trace / reconstruction / storage proxy | 5.0625× / 3× / 5× | + +This clears the central custody property: complete client observation does not +determine an intermediate logical state or unmasked transition. It does not +clear the broader experimental no-go report. The chosen-input attacker learns +the complete degree-nine, three-input final scalar function from **220** +fresh-session output queries and reaches **1.0** accuracy on held-out inputs. +That attack uses black-box input/output pairs only and recovers neither hidden +transition nor intermediate state. + +The distinction is architectural: + +- stateful masking successfully changes client completeness from true to + false for protected internals; +- exact native-visible outputs remain an unavoidable oracle; and +- a simple deterministic function can be learned from that oracle regardless + of how its internals execute. + +Ruam must not describe the first property as black-box nonlearnability. Raising +the oracle floor further requires an external query-authorization constraint, +keeping the final value outside the hostile client, or protecting a function +whose intrinsic query complexity is high. None can be manufactured by a +semantics-preserving client transform alone. + +To remove transition-count and stage-position leakage from the remote +transcript, the owner/server planner places real transitions, in order, among +mask-refreshing identity epochs in one of four fixed epoch buckets: +**4, 8, 16, or 32**. Every slot changes cover and representation mask and +returns the same signed chart shape. Different secret placements produce the +same client cover path and exact final output. The owner schedule and padding +count are reference/server-only and are kept unreachable from package entries. + +The first planner failed this gate. Its 32-bit LCG/Fisher-Yates placement was +not uniform: over 10,000 bucket-eight, two-stage plans the per-slot real counts +were **3,093 / 2,978 / 3,101 / 2,893 / 2,627 / 2,016 / 2,495 / 797**, and a +held-out classifier reached **69.79%** balanced placement accuracy against a +50% baseline. The planner was replaced with a server-secret HMAC word stream +and rejection-sampled Fisher-Yates selection. + +The corrected fixed-bucket attacker reports: + +| Measurement | Result | +|---|---:| +| Corrected maximum slot-rate deviation, 10,000 plans | 1.10 percentage points | +| Full-feature real-slot classification | 46.35% (50% chance) | +| Epoch-prior real-slot classification | 52.60% (50% chance) | +| Payload-only real-slot classification | 49.87% (50% chance) | +| Four-way hidden stage-count classification | 25.78% (25% chance) | +| Placement/stage/per-epoch structural shape match | true / true / true | +| Declared bucket classification | 100% | +| Foreign/rebound response accepted | false / false | +| Padded-state substitution accepted at next boundary | false | +| Bucket-eight/two-stage transition work | 4x | +| Trace / resident-storage proxy | 3.22x / 5x | +| Localized client hook families | 2 | + +This supports only the fixed-bucket transcript claim for the tested corpus. +It is not a proof of PRF security, global session uniqueness, or resistance to +every adaptive classifier. It does not reverse the overall black-box-output or +localized-hook no-go findings. + +The compiler now also computes constructive exact black-box attack upper +bounds before maximum-custody eligibility. It combines exact finite-domain +enumeration with dense polynomial interpolation and keeps all arithmetic in +`bigint`. The current degree-nine, three-input fixture is learnable in +**220 queries** by dense interpolation versus **1,000** by enumeration. +The gate rejects a region whenever its cheapest known exact attack falls below +the configured threshold or the analysis is incomplete. An eligible result is +explicitly not a hardness lower bound. + +## Correctness and build status + +At the last full-branch checkpoint: + +- full repository tests: **2,421 passed, 0 failed, 28,998 assertions**; +- TypeScript typecheck: passed; +- package build: passed; and +- reference custodian/evaluator identifiers are absent from built package + output. + +The corrected padding and transcript tranche adds eleven focused passing tests +across the padded planner, masked protocol, and both attacker suites. A new +full-branch qualification is required after the remaining compiler analysis +lands. + +The same run measured the still-legacy VM control at **43.0× weighted average +runtime overhead** across its ten-workload performance suite. That is a +replacement ceiling, not an Isogloss result. + +## Explicit spike limitations + +These are blockers to schema freeze, not deferred documentation: + +1. The pure BPRF algebra is a bounded research language, not general + JavaScript. +2. The CSH integration uses finite-field integer/boolean coordinates. It does + not represent IEEE-754 `NaN`, infinities, negative zero, fractional + rounding, or overflow exactly. +3. BPRF algebraic reassociation is therefore eligible only where a compiler + proof establishes an exact bounded domain. Untyped JavaScript arithmetic + must remain an observable/coercive boundary or use an exact specialized + realization. +4. The current reference evaluators intentionally expose generic loops and + arrays to tests. They cannot ship because those would create artificial + runtime choke points. +5. Local CSH is still entirely client-resident. Full client recovery remains + possible, and the dynamic-attacker gate must measure whether its + amplification exceeds legitimate overhead. +6. Direct custody changes the trust and availability boundary and does not + provide input privacy. +7. Calls, recursion, effects, exceptions, `finally`, async, and generators are + not yet lowered through BPRF/CSH. + +## Current decision + +The deterministic revised report records **11 failed gates** and **3 +unevaluated gates** for the initial BPRF/CSH/custody composition. The failed +set includes artifact-aware transfer, full-step BPRF recovery, CSH work and +storage amplification, metadata-aware cover transfer, localized hook +collapse, combined client completeness, custody bypass, and chart-delta +leakage. Canonical operation F1, patch collapse, and topology-hidden PFE remain +unevaluated. + +Do not freeze artifact, certificate, carrier, or runtime schemas yet. + +The corrected fixed-bucket planner clears its narrow transcript gate, but the +next product decision still requires: + +- an actively secure, topology-hidden PFE feasibility decision if the private + profile remains in scope; +- canonical-operation and patch-collapse measurements on product-shaped + emitted code; +- legitimate size/runtime/latency/bandwidth measurements; and +- zero mismatches within every claimed eligible domain. + +Mechanisms that do not beat the legacy one-hook control or whose attacker +amplification does not exceed user overhead will be removed rather than +carried into the product architecture. diff --git a/docs/superpowers/baselines/2026-07-24-legacy-vm-baseline.json b/docs/superpowers/baselines/2026-07-24-legacy-vm-baseline.json new file mode 100644 index 0000000..8f7bae3 --- /dev/null +++ b/docs/superpowers/baselines/2026-07-24-legacy-vm-baseline.json @@ -0,0 +1,862 @@ +{ + "schemaVersion": 1, + "purpose": "Traveling Isogloss pre-implementation legacy baseline", + "baseline": { + "commit": "e8cecb56ba89d47512a16e325d81cf46f64b2ecb", + "commitSubject": "docs: plan full traveling isogloss replacement", + "reason": "Last branch commit before the first production-code implementation commit; measured in a detached worktree to avoid contamination from parallel agents.", + "package": "packages/ruam", + "packageName": "ruamvm", + "packageVersion": "2.0.0" + }, + "environment": { + "collectedOn": "2026-07-24", + "node": "v25.6.1", + "bun": "1.3.11", + "npm": "11.9.0", + "typescript": "5.9.3", + "tsup": "8.5.1", + "os": "Darwin 25.3.0", + "architecture": "arm64", + "cpu": "Apple M5", + "logicalCpuCount": 10, + "memoryBytes": 25769803776 + }, + "commands": [ + { + "command": "bun run typecheck", + "cwd": "/packages/ruam", + "exitCode": 0, + "timingSeconds": { + "real": 2.2, + "user": 3.81, + "sys": 0.27 + }, + "result": { + "passed": true, + "diagnostics": 0 + }, + "log": "/tmp/ruam-baseline-typecheck.log" + }, + { + "command": "bun test", + "cwd": "/packages/ruam", + "exitCode": 0, + "timingSeconds": { + "real": 11.38, + "user": 19.9, + "sys": 1.31 + }, + "result": { + "passed": 2328, + "failed": 0, + "expectCalls": 4819, + "files": 44, + "bunReportedSeconds": 11.34 + }, + "embeddedPerformance": { + "weightedAverageOverheadX": 41.1, + "medianOverheadX": 40.8, + "fastest": { + "workload": "try/catch in loop", + "overheadX": 21.6 + }, + "slowest": { + "workload": "switch statement dispatch", + "overheadX": 102.5 + }, + "workloads": [ + { + "name": "arithmetic loop (10k iterations)", + "nativeMs": 0.028, + "protectedMs": 1.408, + "overheadX": 49.9 + }, + { + "name": "fibonacci (recursive, n=20)", + "nativeMs": 0.122, + "protectedMs": 5.1, + "overheadX": 41.8 + }, + { + "name": "array manipulation (sort + map + reduce)", + "nativeMs": 0.059, + "protectedMs": 1.807, + "overheadX": 30.4 + }, + { + "name": "string operations (concatenation + manipulation)", + "nativeMs": 0.011, + "protectedMs": 0.386, + "overheadX": 34.3 + }, + { + "name": "object creation + property access", + "nativeMs": 0.012, + "protectedMs": 0.48, + "overheadX": 40.8 + }, + { + "name": "closures + higher-order functions", + "nativeMs": 0.015, + "protectedMs": 0.533, + "overheadX": 35.6 + }, + { + "name": "class instantiation + method calls", + "nativeMs": 0.023, + "protectedMs": 0.814, + "overheadX": 35 + }, + { + "name": "try/catch in loop", + "nativeMs": 0.026, + "protectedMs": 0.568, + "overheadX": 21.6 + }, + { + "name": "nested loops with conditionals", + "nativeMs": 0.037, + "protectedMs": 2.005, + "overheadX": 54.7 + }, + { + "name": "switch statement dispatch", + "nativeMs": 0.01, + "protectedMs": 1.027, + "overheadX": 102.5 + } + ] + }, + "log": "/tmp/ruam-baseline-test.log" + }, + { + "command": "bun run build", + "cwd": "/packages/ruam", + "exitCode": 0, + "timingSeconds": { + "real": 3.09, + "user": 5.09, + "sys": 0.38 + }, + "result": { + "passed": true, + "esModuleBuildMs": 61, + "declarationBuildMs": 2658, + "outputs": [ + { + "path": "dist/index.js", + "bytes": 285, + "gzipBytes": 156, + "sha256": "f8e81b2eb7bdc698953e76a57d92deddbd9dfda02a67adfc62aa9259aab0aba6" + }, + { + "path": "dist/cli.js", + "bytes": 23611, + "gzipBytes": 6076, + "sha256": "34d4efc8788168bd7fdee6e64390b0356c4cbf0e6119e1b5c1a67d8baaf6e4bb" + }, + { + "path": "dist/chunk-3BCH3N5C.js", + "bytes": 535347, + "gzipBytes": 100251, + "sha256": "bb88026b2a11b04ab665ed5cb7c4e5cef1c7b20460a3a3b785b7630626fd1477" + }, + { + "path": "dist/index.d.ts", + "bytes": 11130 + }, + { + "path": "dist/cli.d.ts", + "bytes": 20 + } + ], + "totalBytes": 570393 + }, + "log": "/tmp/ruam-baseline-build.log" + }, + { + "command": "bun scripts/bench.mjs --quick", + "cwd": "/packages/ruam", + "exitCode": 0, + "timingSeconds": { + "real": 48.39, + "user": 82.3, + "sys": 2.29 + }, + "result": { + "iterationsPerWorkload": 30, + "allCorrect": true, + "presets": [ + { + "name": "default", + "bootstrapMs": 0.038, + "aggregateExecutionOverheadX": 48.2, + "worstExecutionOverheadX": 69.2, + "worstWorkload": "fib-28", + "workloads": [ + [ + "arith-loop-200k", + 0.3988, + 9.528, + 9.489, + 23.9, + 23.8, + 11.1 + ], + [ + "fib-28", + 1.4419, + 99.875, + 99.836, + 69.3, + 69.2, + 12.1 + ], + [ + "nested-loops-300", + 0.2119, + 6.867, + 6.829, + 32.4, + 32.2, + 12.1 + ], + [ + "switch-dispatch-50k", + 0.1198, + 7.176, + 7.138, + 59.9, + 59.6, + 13.5 + ], + [ + "string-build-5k", + 0.1274, + 1.758, + 1.719, + 13.8, + 13.5, + 13.8 + ], + [ + "object-prop-20k", + 0.1464, + 4.5, + 4.462, + 30.7, + 30.5, + 11.5 + ], + [ + "array-ops-3k", + 0.2958, + 5.53, + 5.492, + 18.7, + 18.6, + 15 + ], + [ + "class-methods-2k", + 0.1132, + 2.75, + 2.712, + 24.3, + 24, + 18.3 + ] + ] + }, + { + "name": "low", + "bootstrapMs": 0.045, + "aggregateExecutionOverheadX": 50.5, + "worstExecutionOverheadX": 74.5, + "worstWorkload": "fib-28", + "workloads": [ + [ + "arith-loop-200k", + 0.3983, + 8.835, + 8.79, + 22.2, + 22.1, + 11.5 + ], + [ + "fib-28", + 1.4573, + 108.645, + 108.6, + 74.6, + 74.5, + 12.3 + ], + [ + "nested-loops-300", + 0.2156, + 6.921, + 6.876, + 32.1, + 31.9, + 12 + ], + [ + "switch-dispatch-50k", + 0.1172, + 7.474, + 7.429, + 63.8, + 63.4, + 13.7 + ], + [ + "string-build-5k", + 0.0873, + 1.607, + 1.561, + 18.4, + 17.9, + 13.9 + ], + [ + "object-prop-20k", + 0.1832, + 4.426, + 4.381, + 24.2, + 23.9, + 12.4 + ], + [ + "array-ops-3k", + 0.3125, + 5.332, + 5.287, + 17.1, + 16.9, + 14.6 + ], + [ + "class-methods-2k", + 0.1128, + 2.919, + 2.873, + 25.9, + 25.5, + 19 + ] + ] + }, + { + "name": "medium", + "bootstrapMs": 0.129, + "aggregateExecutionOverheadX": 52.6, + "worstExecutionOverheadX": 76.8, + "worstWorkload": "fib-28", + "workloads": [ + [ + "arith-loop-200k", + 0.3861, + 9.82, + 9.692, + 25.4, + 25.1, + 16 + ], + [ + "fib-28", + 1.4861, + 114.236, + 114.108, + 76.9, + 76.8, + 16.7 + ], + [ + "nested-loops-300", + 0.1568, + 7.434, + 7.305, + 47.4, + 46.6, + 16.7 + ], + [ + "switch-dispatch-50k", + 0.1275, + 7.942, + 7.813, + 62.3, + 61.3, + 18.4 + ], + [ + "string-build-5k", + 0.0816, + 1.866, + 1.737, + 22.9, + 21.3, + 18.8 + ], + [ + "object-prop-20k", + 0.1966, + 3.992, + 3.863, + 20.3, + 19.7, + 16.7 + ], + [ + "array-ops-3k", + 0.3901, + 6.283, + 6.154, + 16.1, + 15.8, + 20 + ], + [ + "class-methods-2k", + 0.1106, + 3.927, + 3.798, + 35.5, + 34.3, + 26 + ] + ] + }, + { + "name": "max", + "bootstrapMs": 0.246, + "aggregateExecutionOverheadX": 171.6, + "worstExecutionOverheadX": 206.4, + "worstWorkload": "fib-28", + "workloads": [ + [ + "arith-loop-200k", + 0.3406, + 42.15, + 41.904, + 123.7, + 123, + 39.2 + ], + [ + "fib-28", + 1.4569, + 300.903, + 300.657, + 206.5, + 206.4, + 37.2 + ], + [ + "nested-loops-300", + 0.1563, + 32.012, + 31.766, + 204.9, + 203.3, + 35.3 + ], + [ + "switch-dispatch-50k", + 0.1986, + 35.433, + 35.187, + 178.4, + 177.2, + 43.1 + ], + [ + "string-build-5k", + 0.0778, + 8.916, + 8.67, + 114.6, + 111.5, + 44.8 + ], + [ + "object-prop-20k", + 0.1421, + 22.299, + 22.053, + 157, + 155.2, + 39.1 + ], + [ + "array-ops-3k", + 0.3024, + 16.906, + 16.66, + 55.9, + 55.1, + 50.7 + ], + [ + "class-methods-2k", + 0.0976, + 19.133, + 18.887, + 196.1, + 193.6, + 58.3 + ] + ] + } + ], + "workloadTupleFields": [ + "name", + "nativeMs", + "protectedTotalMs", + "protectedExecutionMs", + "totalOverheadX", + "executionOverheadX", + "outputSizeKiB" + ] + }, + "log": "/tmp/ruam-baseline-bench.log" + }, + { + "command": "bun scripts/bench-attribution.mjs", + "cwd": "/packages/ruam", + "exitCode": 0, + "timingSeconds": { + "real": 19.49, + "user": 56.35, + "sys": 1.9 + }, + "result": { + "iterationsPerWorkload": 12, + "warmupIterations": 3, + "full": false, + "allCorrect": true, + "configurations": [ + [ + "default", + 36.7, + 51, + "switch-dispatch-15k" + ], + [ + "medium", + 38.6, + 42, + "arith-loop-40k" + ], + [ + "MAX (baseline)", + 112.1, + 152, + "switch-dispatch-15k" + ], + [ + "−stackEncoding", + 96.8, + 122, + "switch-dispatch-15k" + ], + [ + "−mixedBooleanArithmetic", + 110.6, + 222, + "switch-dispatch-15k" + ], + [ + "−observationResistance", + 111.3, + 191, + "switch-dispatch-15k" + ], + [ + "−opcodeMutation", + 132.6, + 240, + "switch-dispatch-15k" + ], + [ + "−incrementalCipher", + 86.5, + 109, + "switch-dispatch-15k" + ], + [ + "−semanticOpacity", + 134.8, + 253, + "switch-dispatch-15k" + ], + [ + "−vmShielding", + 127.9, + 250, + "switch-dispatch-15k" + ], + [ + "−deadCodeInjection", + 123.2, + 195, + "switch-dispatch-15k" + ], + [ + "−debugProtection", + 136.4, + 227, + "switch-dispatch-15k" + ], + [ + "−blockPermutation (sanity ~0)", + 123.1, + 188, + "switch-dispatch-15k" + ], + [ + "−integrityBinding", + 147.5, + 241, + "switch-dispatch-15k" + ], + [ + "−cache-trio (mut+inc+obs) [CACHE ON]", + 79.8, + 117, + "switch-dispatch-15k" + ], + [ + "−cache-trio −stackEncoding [CACHE ON]", + 30.9, + 63, + "switch-dispatch-15k" + ], + [ + "−cache-trio −proxy −MBA [CACHE ON]", + 33.7, + 41, + "arith-loop-40k" + ], + [ + "−cache-trio −proxy −MBA −semOpacity", + 38.3, + 53, + "switch-dispatch-15k" + ] + ], + "configurationTupleFields": [ + "name", + "aggregateExecutionOverheadX", + "worstExecutionOverheadX", + "worstWorkload" + ] + }, + "log": "/tmp/ruam-baseline-bench-attribution.log" + }, + { + "command": "node scripts/collect-stats.mjs --all", + "cwd": "/packages/ruam", + "exitCode": 0, + "timingSeconds": { + "real": 14.98, + "user": 26.17, + "sys": 1.8 + }, + "result": { + "version": "2.0.0", + "collectedAt": "2026-07-24T23:26:39.895Z", + "opcodes": { + "count": 324, + "categories": 25, + "superinstructions": 34, + "compounds": 38, + "slotOpcodes": 10 + }, + "source": { + "files": 91, + "lines": 32692, + "testFiles": 45, + "testLines": 18220, + "totalFiles": 136, + "totalLines": 50912, + "templateFiles": 0, + "visitorFiles": 4 + }, + "tests": { + "total": 2328, + "passed": 2328, + "failed": 0, + "suites": 44, + "durationMs": 13120 + }, + "performance": { + "weightedAvg": 25.5, + "median": 40.9, + "fastest": { + "name": "try/catch", + "multiplier": 5.2 + }, + "slowest": { + "name": "fibonacci (n=20)", + "multiplier": 79.9 + }, + "workloadCount": 10, + "workloads": [ + { + "name": "arithmetic loop (10k)", + "multiplier": 14.4, + "nativeMs": 0.077, + "vmMs": 1.106 + }, + { + "name": "fibonacci (n=20)", + "multiplier": 79.9, + "nativeMs": 0.08, + "vmMs": 6.394 + }, + { + "name": "array ops", + "multiplier": 23.1, + "nativeMs": 0.059, + "vmMs": 1.375 + }, + { + "name": "string ops", + "multiplier": 50.4, + "nativeMs": 0.01, + "vmMs": 0.496 + }, + { + "name": "object creation", + "multiplier": 40.9, + "nativeMs": 0.015, + "vmMs": 0.622 + }, + { + "name": "closures + HOF", + "multiplier": 73.6, + "nativeMs": 0.008, + "vmMs": 0.557 + }, + { + "name": "class + methods", + "multiplier": 58.5, + "nativeMs": 0.014, + "vmMs": 0.84 + }, + { + "name": "nested loops", + "multiplier": 13.1, + "nativeMs": 0.112, + "vmMs": 1.464 + }, + { + "name": "switch dispatch", + "multiplier": 30.4, + "nativeMs": 0.029, + "vmMs": 0.882 + }, + { + "name": "try/catch", + "multiplier": 5.2, + "nativeMs": 0.17, + "vmMs": 0.874 + } + ] + }, + "size": { + "sampleInputBytes": 110, + "low": { + "bytes": 12842, + "ratio": 116.7 + }, + "medium": { + "bytes": 18848, + "ratio": 171.3 + }, + "high": { + "bytes": 12450, + "ratio": 113.2 + } + }, + "heroSnippet": { + "head": [ + "var MJi = Object.prototype.hasOwnProperty,", + " IpS = Math.imul,", + " M1Y = Object.create(null),", + " Ed0 = !(typeof globalThis === 'undefined') ? globa...", + " gTs = 'avCIAmHKykW2TeuYscFZSgMzp_E0lih7boDqOL61n4x..." + ], + "totalLines": 627, + "tail": [ + "function fibonacci(...__args) {", + " var _n = __args.length | 0;", + " return wVi(\"ugrvw\", __args, kNK, this);", + "}" + ] + }, + "badges": { + "tests": "2,328", + "testsPassing": "2,328 passing", + "opcodes": "324", + "categories": "25", + "loc": "32.7k", + "totalLoc": "50.9k", + "overhead": "~25.5x", + "overheadMedian": "~40.9x", + "sizeRatioLow": "116.7x", + "sizeRatioHigh": "113.2x" + } + }, + "log": "/tmp/ruam-baseline-stats.log", + "generatedStats": "/tmp/ruam-baseline-generated-stats.json", + "generatedTestResults": "/tmp/ruam-baseline-generated-test-results.json" + } + ], + "knownIssues": [ + { + "script": "scripts/collect-stats.mjs", + "severity": "invalid-metric", + "detail": "The size collector requests preset 'high', but the public PresetName and PRESETS table only support low, medium, and max. Runtime object spread of undefined silently falls back to default-like options, so result.size.high and the 'high preset' badge are not max-preset measurements.", + "evidence": { + "requested": "obfuscateCode(sampleCode, { preset: \"high\" })", + "validPresets": [ + "low", + "medium", + "max" + ] + } + }, + { + "script": "scripts/bench-attribution.mjs", + "severity": "measurement-noise", + "detail": "Each configuration is compiled with fresh cryptographic entropy and measured only once at 12 iterations by default. Several removals report higher overhead than MAX, including options expected to be near-zero runtime cost. Use repeated seeded runs and confidence intervals before treating feature deltas as causal." + }, + { + "script": "scripts/bench.mjs", + "severity": "measurement-noise", + "detail": "Protected outputs use fresh entropy and --quick uses 30 timing iterations, so exact overhead and output-size values are run-specific. All eight workload correctness checks passed for every preset in this run." + }, + { + "script": "scripts/collect-stats.mjs", + "severity": "behavior-documentation-mismatch", + "detail": "The script runs benchmarks whenever dist/index.js exists, even without --bench, despite its usage text describing cached benchmark results for the default invocation." + } + ], + "rawEvidenceSha256": { + "/tmp/ruam-baseline-environment.log": "2144d354ece9aee40b9c26bdb9120735628125b0e275964bab89885b0e28caa3", + "/tmp/ruam-baseline-typecheck.log": "695148545ac172a5c5bcab1450e90bee00bc2e720314b73291d3b11b0d62", + "/tmp/ruam-baseline-test.log": "d07167bd1ab8931664f6ce8df1105fdfe1de689a01a89d0dfc4abaaef1c1b514", + "/tmp/ruam-baseline-build.log": "184fea6898391a5ea558c900203a39a2bd85f98db82090a573beddd7d3420789", + "/tmp/ruam-baseline-bench.log": "f7906a180070727e21e8cfac3a956c6ee745186b11a5e0721db863090f837e11", + "/tmp/ruam-baseline-bench-attribution.log": "c8b4a2a2b0adf77b03993180aaa434dc19ee6651784783867ef8bfbaf6405b5c", + "/tmp/ruam-baseline-stats.log": "7fd59a1af8e52e34acd42006c96fa2e5d3fa812e39ab1b98bbd70a55f652e6ae", + "/tmp/ruam-baseline-generated-stats.json": "458ec2bdb8efd1b81304324abd7852c5f5f59e427c807c2a6dbab97cd7f88691" + }, + "mainTrackedWorktreeCleanAfterCollection": true +} diff --git a/docs/superpowers/baselines/2026-07-24-legacy-vm-baseline.md b/docs/superpowers/baselines/2026-07-24-legacy-vm-baseline.md new file mode 100644 index 0000000..22c996b --- /dev/null +++ b/docs/superpowers/baselines/2026-07-24-legacy-vm-baseline.md @@ -0,0 +1,39 @@ +# Ruam legacy baseline for Traveling Isogloss + +Baseline commit: `e8cecb56ba89d47512a16e325d81cf46f64b2ecb` (`docs: plan full traveling isogloss replacement`). This is the last commit before production implementation began. Measurements were repeated from a detached worktree after a parallel implementation commit landed during the initial pass. + +## Status + +| Check | Result | Wall time | +|---|---:|---:| +| `bun run typecheck` | pass, 0 diagnostics | 2.20 s | +| `bun test` | 2,328 pass, 0 fail, 4,819 assertions, 44 files | 11.38 s | +| `bun run build` | pass | 3.09 s | +| `bun scripts/bench.mjs --quick` | 32/32 workload/preset correctness checks pass | 48.39 s | +| `bun scripts/bench-attribution.mjs` | 18/18 configuration correctness checks pass | 19.49 s | +| `node scripts/collect-stats.mjs --all` | pass in isolated copy | 14.98 s | + +Environment: Apple M5 (10 logical CPUs, 24 GiB), arm64 macOS/Darwin 25.3.0, Bun 1.3.11, Node v25.6.1, TypeScript 5.9.3, tsup 8.5.1. + +## Runtime baseline + +The dedicated quick harness reports aggregate execution-only overhead of **48.2x default**, **50.5x low**, **52.6x medium**, and **171.6x max**. Bootstrap medians were 0.038 ms, 0.045 ms, 0.129 ms, and 0.246 ms respectively. Worst execution-only overhead was 69.2x default, 74.5x low, 76.8x medium, and 206.4x max. + +The test-suite performance group independently reported 41.1x weighted average and 40.8x median overhead (21.6x fastest, 102.5x slowest). The stats collector reported 25.5x weighted average and 40.9x median across its own smaller ten-workload suite. These harnesses use different workloads and timing methods, so their values should not be combined. + +## Output-size baseline + +The dedicated harness emitted 11.1–18.3 KiB per default workload, 11.5–19.0 KiB at low, 16.0–26.0 KiB at medium, and 35.3–58.3 KiB at max. + +The built package totals 570,393 bytes: main shared chunk 535,347 bytes (100,251 gzip), CLI 23,611 bytes (6,076 gzip), entry 285 bytes (156 gzip), and declarations 11,150 bytes. + +For the stats collector's 110-byte Fibonacci sample: low is 12,842 bytes (116.7x), medium is 18,848 bytes (171.3x), and its field named `high` is 12,450 bytes (113.2x)—but that final metric is invalid for max because the script passes unsupported preset `high`. + +## Script findings + +- `collect-stats.mjs` silently mislabels a default-like build as “high”; valid presets are `low`, `medium`, and `max`. Its high/max size badge is not authoritative. +- Benchmark outputs are entropy- and timing-sensitive. The attribution harness uses one fresh randomized build per configuration and only 12 measured iterations, producing implausible negative feature costs for several flags. It is a smoke/attribution signal, not a stable causal estimate. +- `collect-stats.mjs` benchmarks whenever `dist/index.js` exists, even without `--bench`, contrary to its usage text. +- No command crashed or produced a correctness mismatch in the authoritative run. + +Machine-readable details, every workload/configuration, exact timings, output hashes, and raw-log hashes are in `/tmp/ruam-isogloss-baseline.json`. The main tracked worktree was clean after collection. diff --git a/docs/superpowers/specs/2026-07-24-bprf-custodied-semantic-holography.md b/docs/superpowers/specs/2026-07-24-bprf-custodied-semantic-holography.md new file mode 100644 index 0000000..b89f0b9 --- /dev/null +++ b/docs/superpowers/specs/2026-07-24-bprf-custodied-semantic-holography.md @@ -0,0 +1,566 @@ +# Project Kaleidoscope D3 — Custodied Semantic Holography + +**Date:** 2026-07-24 +**Status:** Proposed BPRF evolution; implementation remains behind the dynamic-attacker spike gate +**Parent:** [Project Kaleidoscope D2 — Dynamic-Instrumentation Ideation Results](2026-07-24-dynamic-instrumentation-ideation-results.md) +**Goal:** Raise the minimum cost of full-access dynamic reconstruction beyond what an entirely client-resident BPRF can achieve + +## 1. Outcome + +The strongest evolution is **Custodied Semantic Holography (CSH)**: + +1. BPRF still removes instruction dispatch, complete internal function bodies, + stable frame layouts, and reusable single-ontology traces. +2. CSH represents protected state as overlapping partial semantic charts. No + region, codelet, frame, or chart contains a complete logical before/after + state. +3. The chart cover changes at regional transitions, so chart ownership and + overlap relations do not stay aligned across calls or histories. +4. In the maximum profile, at least one necessary chart-gluing relation for + selected high-value regions never ships to the client. A stateful custodian + co-evaluates it using private-function evaluation or executes that narrowly + typed relation remotely. +5. The custodian returns only a lineage-bound encoded chart contribution or + certified effect projection—never source, a regional program, a handler, + an operation identity, a next-codelet token, or a reusable decode key. + +The combination is stronger than either parent: + +- **BPRF** makes captured client execution contextual, fused, and + multi-ontology. +- **Semantic holography** makes each captured local state incomplete and + globally relational. +- **Custody** removes a necessary relation from the attacker's trust domain. + +The recommended product name for the combined maximum profile is: + +> **BPRF/CSH — Custodied Moving-Cover Isogloss** + +## 2. Why BPRF alone still has a ceiling + +BPRF eliminates the cheap: + +```text +resolve -> SemanticOp -> handler +``` + +attack, but a full-step analyst can still: + +1. record every client-resident regional realization; +2. capture complete regional input/output frames; +3. align a finite set of ontologies and caller contexts; +4. normalize changing wire bases after learning their transformations; and +5. eventually build a whole-client equivalent model. + +Wire-basis drift changes the representation of a complete client-side state. +It does not remove the complete transition relation from the client. + +CSH attacks that remaining weakness in two stages: + +- the local stage removes complete region-owned state; +- the custodied stage removes client completeness itself. + +## 3. Creativity and uniqueness gate + +This ideation round required each candidate to change a different primary +property of BPRF: + +| Candidate | Primary property changed | +|---|---| +| Moving-cover semantic holography | State is local-to-global rather than region-owned | +| Causal relation custody | A necessary transition relation leaves the client trust domain | +| Stateful query-fork control | The analyst cannot snapshot and fork one remote representation epoch | +| Universal-circuit phenotype foundry | Function topology and realization change per session | +| TEE-held chart | A hardware isolation boundary replaces the network custodian | +| Temporal convolutional state | One logical update is dispersed across an execution window | +| Cross-device semantic quorum | No single user device owns all relation shares | +| One-time regional capsules | A realization has bounded protocol reuse | + +Near-duplicates were rejected: + +- remote key fetch, remote opcode fetch, and downloadable missing code are all + capturable-material delivery; +- remote integrity signatures and carrier attestations enforce execution but + do not hide semantics; +- ordinary state secret-sharing with every share on the client is just a + stronger wire encoding; +- topology-visible gate hiding is not accepted as private-function evaluation; +- rate limiting alone is a product policy, not a transformation; +- a TEE or server running the entire original function is a trust-domain move, + but not a distinct Ruam execution architecture; +- self-erasing or one-time client code fails against pre-execution snapshots. + +## 4. Ranked add-on slate + +Scores are 1–5 and measure marginal value when added to BPRF. + +| Rank | Add-on | Dynamic floor | BPRF synergy | Correctness | Efficiency | Trust change | Decision | +|---:|---|---:|---:|---:|---:|---|---| +| 1 | Moving-cover holography + causal relation custody | 5 | 5 | 3 | 2 | Remote or TEE | Adopt as CSH | +| 2 | Moving-cover semantic holography, local-only | 4 | 5 | 3 | 3 | None | Base CSH research layer | +| 3 | Stateful universal-circuit phenotype foundry | 5 | 4 | 2 | 1 | Remote | Optional custodian implementation | +| 4 | TEE-held semantic chart | 5 | 4 | 3 | 4 | Hardware | Deployment alternative | +| 5 | Temporal convolutional state | 3 | 4 | 2 | 2 | None | Spike only | +| 6 | Multi-custodian threshold relation | 5 | 3 | 2 | 1 | Multiple remotes | Infrastructure hardening | +| 7 | Cross-device semantic quorum | 4 | 3 | 1 | 1 | Other devices | Product-pivot research | +| 8 | One-time regional capsules | 3 | 3 | 3 | 2 | Remote | Reject as primary | + +The local-only candidate raises analysis cost but does not change the +impossibility boundary. Rank 1 combines it with relation custody because the +request is to raise the floor as high as possible. + +## 5. Source-domain mechanism + +The nonlocal state model is borrowed from cellular sheaves and overlapping +coordinate charts: + +- each location owns a local view; +- related views overlap but use different local coordinates; +- compatibility maps describe how overlap data agrees; +- a complete globally consistent object is a **global section**; +- no local view is the global object. + +This is a structural transfer, not naming. Cellular sheaves are used to model +local computations whose globally consistent solutions are global sections: +[A Sheaf-Theoretic Characterization of Tasks in Distributed Systems](https://arxiv.org/abs/2503.02556). + +CSH maps this structure into Ruam: + +| Sheaf concept | CSH concept | +|---|---| +| Base complex / cover | Root-group regional continuation graph | +| Local chart / stalk | Partial encoded frame owned by a BPRF fragment neighborhood | +| Restriction map | Overlap compatibility relation between chart fragments | +| Global section | One complete canonical logical state, which production never materializes | +| Change of cover | Carrier-driven chart split, merge, transport, and reownership | +| Local projection | Site-specific value needed for one host effect or contract | +| Missing gluing map | Custodian-owned relation that never ships to the client | + +The cryptographic custody mechanism is grounded in private function evaluation +(PFE), where one party evaluates a private function on another party's input +without revealing the function beyond its outputs. Actively secure, +constant-round, linear-complexity PFE constructions exist in the literature: +[Making Private Function Evaluation Safer, Faster, and Simpler](https://eprint.iacr.org/2021/1682). + +This does not make arbitrary JavaScript PFE practical. CSH restricts custody to +typed, pure, effect-delimited macroregions and treats feasibility as a mandatory +spike gate. + +## 6. Concrete state model + +Let canonical protected state at an analysis boundary be `x`. Production never +stores `x`. + +Choose a changing cover: + +```text +U(t) = { U1, U2, ..., Un } +``` + +Each chart stores a partial encoded view: + +```text +si = encode_i(project_i(x), localResidual_i, coverEpoch) +``` + +Related charts satisfy overlap constraints: + +```text +Rij(si, sj, glue_ij, coverEpoch) = 0 +``` + +Properties: + +1. No individual `si` decodes a source variable or complete frame. +2. The configured chart threshold is required to derive any certified + protected projection. +3. Charts overlap across several source values and several regional + transitions; they are not one share set per variable. +4. Chart ownership crosses continuation and function boundaries. +5. After a regional transition, the carrier changes the cover by splitting, + merging, transporting, or reassigning charts. +6. Old and new covers overlap only enough to transport the required global + consistency class. +7. No global `decode(x)` routine exists. + +A site-specific host effect receives only its required projection: + +```text +effectValue = + project_effect( + chartContribution_a, + chartContribution_b, + ..., + custodiedContribution + ) +``` + +The ordinary value is observable at that effect because JavaScript semantics +require it. Other logical state remains distributed. + +## 7. Moving-cover execution + +One protected regional transition becomes: + +```text +incoming continuation contract + -> activate a BPRF regional realization + -> update several local charts + -> reconcile only required overlaps + -> transport consistency into a new cover + -> optionally request one custodied relation contribution + -> materialize only a certified effect/return projection + -> continue with the new chart cover +``` + +Important distinctions from current BPRF: + +- BPRF frame-layout drift can still be described as `x' = A x + b`. +- CSH has no production `x`; only partial charts and overlap relations exist. +- BPRF variants change how one regional transition is computed. +- CSH changes which collection of local partial states can jointly denote a + transition at all. +- BPRF can be normalized by covering all finite realizations. +- Custodied CSH remains incomplete after full client realization coverage. + +## 8. Custodied relation protocol + +### 8.1 What remains remote + +For each selected crown-jewel macroregion, the custodian owns at least one of: + +- a nonlinear chart-gluing relation; +- a hidden universal-circuit programming string; +- a regional transition residual; +- an output/effect projection relation; +- state needed to transport one cover epoch into the next. + +The complete regional transition cannot be evaluated from client artifacts and +client state alone. + +### 8.2 What crosses the boundary + +The client sends: + +- opaque session and contract identifiers; +- a carrier-lineage commitment; +- encoded chart contributions; +- optional privately encoded input values; +- an anti-replay protocol nonce. + +The custodian returns: + +- an encoded chart contribution; +- or one site-specific encoded effect projection; +- plus protocol authenticity needed to reject malformed transport. + +It never returns: + +- source or canonical IR; +- a semantic operation or handler identity; +- a regional codelet or schedule; +- a next-region identifier; +- a general decode key; +- the missing gluing relation; +- an owner sidecar or source map; +- a reusable offline evaluator. + +### 8.3 Function and topology privacy + +Gate-hiding alone is insufficient. Recent work demonstrates SAT recovery of +hidden gate functions from public topology, with large speedups from +topology-aware simplification: +[Function Recovery Attacks in Gate-Hiding Garbled Circuits](https://arxiv.org/abs/2601.13271). + +Therefore the maximum profile requires: + +- a universal or set-universal circuit per padded size/effect class; +- no region-specific public topology; +- active security against a client that deviates from the protocol; +- transcript padding where size would identify the macroregion; +- fresh wire labels and representation state per session/epoch; +- a test that attempts topology-based function recovery. + +### 8.4 Stateful representation custody + +The custodian maintains a representation state: + +```text +sigma = { + session, + rootGroup, + coverEpoch, + lineageCommitment, + consumedNonces, + custodiedChartState +} +``` + +Successful evaluation advances `sigma`. Replaying a consumed transition is +rejected. A client snapshot cannot fork the same server representation epoch +into arbitrary counterfactual queries. + +This is not claimed to stop all chosen-input analysis: + +- an authorized attacker may create fresh sessions; +- outputs remain a black-box oracle; +- rate limits and licensing policy are separate product controls; +- simple functions may still be learned from very few I/O examples. + +The stateful protocol prevents free fork-and-replay of one internal +representation; it does not manufacture query hardness for an intrinsically +simple function. + +## 9. Deployment profiles + +### 9.1 `holographic-local` + +All charts and gluing relations ship with the artifact. + +- No network or trusted hardware. +- Highest floor available without changing the trust domain. +- Full client instrumentation can eventually reconstruct the entire system. +- Security claim is analysis amplification and trace nonlocality only. + +### 9.2 `holographic-custodied` + +One necessary relation is held by a developer-controlled service. + +- Full client traces are structurally incomplete. +- Critical region extraction becomes black-box/function-recovery analysis. +- Availability and latency become product requirements. +- There is no offline fallback containing the missing relation. + +### 9.3 `holographic-private` + +Use private-function evaluation so the custodian learns no protected client +input beyond the declared leakage, while the client learns no function detail +beyond outputs and declared transcript leakage. + +- Highest cryptographic goal. +- Practical only for restricted typed macroregions until benchmarks prove more. +- Universal-circuit and active-security costs may be substantial. + +### 9.4 `holographic-tee` + +Place the missing chart relation in a hardware-backed isolated component. + +- Lower latency and possible offline execution. +- Changes the threat model to the hardware/attestation boundary. +- Target-specific and exposed to platform side-channel/fault limitations. +- Not a universal browser solution. + +### 9.5 `holographic-threshold` + +Split the custodied relation across several non-colluding services. + +- Avoids one infrastructure provider holding the complete hidden relation. +- Can improve service resilience and developer trust separation. +- Adds protocol rounds, operational complexity, and another failure surface. +- It does not meaningfully improve client extraction over one honest + uncompromised custodian; it improves custody assurance. + +## 10. Non-negotiable CSH invariants + +| ID | Invariant | +|---|---| +| CSH-01 | Production never stores a complete canonical logical frame for a protected CSH region. | +| CSH-02 | No chart is a one-variable share set or independently decodes a source variable. | +| CSH-03 | Certified projections require the configured minimum number of independently owned chart contributions. | +| CSH-04 | Chart ownership crosses region and, where valid, continuation/function boundaries. | +| CSH-05 | Every protected regional transition changes the chart cover or its restriction maps. | +| CSH-06 | No universal global-section solver or decode helper exists in production. | +| CSH-07 | Effects and returns use distributed site-specific projections. | +| CSH-08 | Ordinary values materialize only where native JavaScript observability requires them. | +| CSH-09 | The custodied relation is necessary for the selected macroregion's correct transition. | +| CSH-10 | No client artifact, cache, error path, development flag, or fallback contains the custodied relation. | +| CSH-11 | A custodian response is an encoded chart/projection contribution, never an operation, codelet, route, or reusable key. | +| CSH-12 | Public PFE topology is fixed within padded size/effect buckets and independent of the protected region. | +| CSH-13 | The custodian protocol is secure against an actively deviating client for the declared profile. | +| CSH-14 | Custodied representation state advances monotonically and consumed transitions cannot be replayed in one session. | +| CSH-15 | A client snapshot cannot fork one custodied representation epoch. | +| CSH-16 | Exact getter, proxy, coercion, call, throw, `finally`, await, yield, and scheduling behavior remains unchanged. | +| CSH-17 | Network failure is explicit; the maximum profile never silently falls back to a complete local implementation. | +| CSH-18 | Every observable effect, ordinary-value projection, transcript-size class, and remote call is inventoried in the certificate. | +| CSH-19 | Local-only and custodied profiles use different, honest security claims. | +| CSH-20 | The owner sidecar and source-origin scorer never ship to the client production artifact or custodian response. | + +## 11. What this does to BPRF + +### 11.1 Region graph + +Add: + +- chart ownership sets; +- overlap/restriction edges; +- cover-transition contracts; +- certified projection sites; +- custodied-relation dependencies; +- public transcript leakage classes. + +### 11.2 Carrier + +Add: + +- current cover ID and epoch; +- active chart ownership map; +- overlap reconciliation phase; +- pending custodian protocol continuation; +- lineage commitment; +- custodian session binding. + +Do not add: + +- global frame state; +- expected chart solution; +- a global decode key; +- a semantic operation; +- a next-region token received from the server. + +### 11.3 Regional realizations + +Each BPRF ontology must lower to chart-local transitions: + +- predicated dataflow updates one overlap neighborhood; +- continuation residuals transport another; +- algebraic variants operate over coded chart coordinates; +- effect sinks request and materialize only their certified projection. + +An ontology that reconstructs a complete frame before execution is not +CSH-compatible. + +### 11.4 Verifier + +In addition to BPRF equivalence, verify: + +- local chart transitions glue to the intended canonical state transition; +- overlap constraints remain satisfiable and uniquely determine only the + certified projection, not an emitted global frame; +- all cover changes preserve the global consistency class; +- threshold/necessity properties hold for chart contributions; +- removal of any required contribution changes or blocks the projection; +- no global decoder or complete client transition exists; +- custodied and local relations compose correctly; +- transcript and effect leakage match the certificate; +- every failure mode preserves native-visible error/ordering contracts where + the API promises them. + +## 12. Dynamic-attacker evaluation + +Retain every BPRF metric and add: + +| Metric | Meaning | +|---|---| +| Client completeness | Whether client artifact + full client trace determines the selected regional transition without the custodian | +| C90 | Minimum simultaneously correlated chart probes for 90% logical-state recovery | +| G90 | Work required to reconstruct a consistent global section for 90% of reached state | +| Cover transfer | Recovery accuracy after an unseen cover split/merge/reownership sequence | +| Projection leakage | Logical state recoverable from one certified effect projection beyond the native effect value | +| Fork success | Ability to evaluate two counterfactual transitions from one custodied epoch | +| Transcript transfer | Function/region classification accuracy from network transcripts | +| Topology recovery | Function recovery from public PFE topology and chosen outputs | +| Custody bypass | Smallest client patch that preserves output without a custodian call | +| Oracle learnability | Queries and compute needed to learn an equivalent crown-jewel macroregion from I/O | + +Run four attacker classes: + +1. O(1)-hook BPRF choke-point search; +2. full-step, full-heap client trace and graph alignment; +3. client snapshot/fork/replay with an actively modified protocol client; +4. topology-aware PFE/function-recovery plus chosen-input black-box learning. + +## 13. Go/no-go gates + +### 13.1 Local holography + +- No complete frame appears at a regional boundary. +- C90 is at least the configured chart threshold. +- Full-step G90 and storage rise by at least 10x over plain BPRF for the spike + while legitimate overhead remains lower than attacker amplification. +- Cross-cover transfer loses at least 30 percentage points. +- No three or fewer localized hooks recover a stable source-variable map. +- Any scheme reducible to one fixed linear recombination is deleted. + +### 13.2 Custodied mode + +- Client completeness is false by construction and by extraction test. +- Removing the custodian relation prevents correct standalone evaluation. +- No client error/fallback/debug path exposes the missing relation. +- Function/region classification from padded transcripts is no better than the + declared size/effect-class leakage. +- Topology-aware recovery does not distinguish protected regions within a + universal-circuit bucket beyond the accepted leakage. +- Snapshot/fork of one custodied epoch fails. +- A client patch cannot convert custodian responses into a canonical operation + or reusable offline evaluator. +- The service returns no more than the output/effect leakage declared by the + protected contract. + +### 13.3 Correctness + +- Zero native/reference/BPRF-CSH value mismatches. +- Zero event-order, proxy/getter, exception, async, or generator mismatches. +- No favorable-seed dependence. +- Failure and retry semantics are explicit and deterministic. +- Maximum profile never falls back to a locally complete artifact. + +## 14. Kill criteria + +Remove or redesign CSH if: + +- a complete logical frame or fixed decoder appears anywhere in production; +- moving covers normalize to one stable slot transform cheaply; +- chart overlap adds only fake dependencies removable by slicing; +- a client dump is sufficient to evaluate a custodied region offline; +- the custodian returns missing code, semantic tokens, or reusable keys; +- public circuit topology identifies the protected region; +- remote state can be forked freely from one captured client snapshot; +- correctness requires suppressing real JavaScript effects; +- the only benefit is rate limiting, integrity enforcement, or anti-hooking; +- private-function evaluation is impractical and direct remote execution is + unacceptable for the product; +- attacker amplification does not exceed legitimate overhead in local mode. + +## 15. Incremental spike + +1. Finish BPRF's dynamic attacker baseline and region/effect annotations. +2. Select a typed, pure crown-jewel fixture with branches and direct calls. +3. Encode its state into at least five overlapping charts with threshold three. +4. Implement two changing covers and chart transport without a global frame in + the reference evaluator. +5. Add owner-only global-section reconstruction solely to score the attacker; + keep it out of production modules. +6. Run full-step dynamic slicing and test whether moving covers materially + increase C90/G90 and reduce cross-cover transfer. +7. Delete linear/fake-dependency schemes that normalize cheaply. +8. Move one necessary nonlinear gluing relation behind a mock custodian API. +9. Prove there is no correct standalone client evaluator for that fixture. +10. Implement a direct remote relation evaluator as the performance/control + baseline. +11. Implement one actively secure, topology-hidden PFE prototype for the same + typed relation. +12. Run topology recovery, active-client, fork/replay, and black-box learning + experiments. +13. Compare local holography, direct custody, PFE custody, and plain BPRF on + correctness, client completeness, attack cost, latency, bandwidth, and size. +14. Freeze CSH types/protocols only if the appropriate profile clears every + gate. + +## 16. Product decision + +If Ruam must remain entirely offline and server-free, adopt only +`holographic-local` and retain the existing honest ceiling: + +> Full client recovery remains possible; CSH raises the synchronization and +> global-reconstruction cost. + +If the requirement is truly to raise the floor **as high as possible**, adopt +`holographic-custodied` or `holographic-private` for marked crown-jewel +macroregions: + +> A full client trace is intentionally incomplete. Recovery of the missing +> regional relation requires compromising the custodian/PFE assumption or +> learning equivalent behavior from the permitted input/output oracle. + +That is the first proposed BPRF layer that changes the full-access client +boundary rather than only making client-side normalization more expensive. diff --git a/docs/superpowers/specs/2026-07-24-dynamic-instrumentation-ideation-results.md b/docs/superpowers/specs/2026-07-24-dynamic-instrumentation-ideation-results.md new file mode 100644 index 0000000..9d5f29b --- /dev/null +++ b/docs/superpowers/specs/2026-07-24-dynamic-instrumentation-ideation-results.md @@ -0,0 +1,652 @@ +# Project Kaleidoscope D2 — Dynamic-Instrumentation Ideation Results + +**Date:** 2026-07-24 +**Status:** Architecture decision candidate; security-sensitive Isogloss work paused pending review +**Parent:** [Traveling Isogloss implementation plan](2026-07-24-traveling-isogloss-implementation-plan.md) +**Attacker:** Full artifact and runtime access, chosen inputs, stepping, hooks, heap/closure snapshots, patching, repeated runs, and design knowledge + +> **D3 evolution:** The follow-on campaign +> [Custodied Semantic Holography](2026-07-24-bprf-custodied-semantic-holography.md) +> adds moving-cover, nonlocal semantic state to BPRF and, in the maximum +> profile, removes a necessary chart-gluing relation from the client through a +> stateful custodian/private-function-evaluation boundary. The BPRF spike +> remains the first implementation gate; D3 is an additional layer, not a +> reason to revive semantic dispatch. + +## 1. Executive decision + +The original Traveling Isogloss design does not materially resist a full-access +dynamic analyst because it eventually publishes this event: + +```text +left clause + right clause + carrier witness + -> one SemanticOp + one operand projection + -> one semantic handler +``` + +That is a stable semantic choke point. An analyst can instrument the resolver or +handler edge and recover a reusable, ordered semantic trace. Ambiguity before +resolution does not protect the answer after the runtime repeatedly exposes it. + +The recommended redesign is **Braided Poly-Ontology Region Fabric (BPRF)**: + +1. Partition canonical IR at source-observable JavaScript effects. +2. Fuse multiple pure data/control operations into regional transitions. +3. Split direct calls across caller-owned entry and return continuations. +4. Generate several genuinely different regional realization families. +5. Change internal frame layouts and wire bases at regional boundaries. +6. Emit unavoidable JavaScript effects at distributed, site-specific sinks. +7. Let the carrier route region/continuation contracts, never language + operations. + +`SemanticOp` remains compiler-side vocabulary and a reference-oracle tool. It +must not exist as a production runtime event, value, table key, or handler +identity. + +The security-sensitive lattice schema, artifact schema, resolver, verifier, +handler runtime, and carrier witness design remain paused. Deterministic entropy, +canonical semantic IR, source origins, CFG/effect analysis, baselines, and test +infrastructure remain valid and may continue. + +## 2. Honest impossibility boundary + +No server-free JavaScript artifact can hide its extensional behavior from this +attacker: + +- Inputs, returns, thrown values, and host-visible effects are observable. +- The client must possess enough information to produce each concrete result. +- Client-generated or client-consumed randomness is observable diversity, not + a secret. +- A finite implementation family can eventually be covered and normalized. +- A finite input domain can be queried exhaustively; an infinite-domain artifact + remains a behavioral oracle. +- The attacker can redistribute or invoke the protected artifact unchanged even + if recovering source-like code remains costly. +- Work imposed on exhaustive tracing is paid at least partly by legitimate users. + +The defensible goal is therefore: + +> Remove cheap semantic choke points and make recovery of a reusable, +> source-like function model require whole-region, multi-context, +> multi-realization reconstruction. + +Claims of confidentiality, anti-hooking, cryptographic secrecy, or prevention of +eventual recovery would be false without a different trust domain such as a +server-held secret, trusted hardware, attestation, or query limitation. + +This boundary is consistent with foundational limits on general virtual +black-box obfuscation and with practical dynamic deobfuscation results. Syntia, +for example, reports learning more than 94% of arithmetic handlers in two +virtualization obfuscators, while later work such as Loki explicitly targets +trace slicing, symbolic extraction, and synthesis. See: + +- [The impossibility of obfuscation with auxiliary input or a universal simulator](https://arxiv.org/abs/1401.0348) +- [Syntia: Synthesizing the Semantics of Obfuscated Code](https://www.usenix.org/conference/usenixsecurity17/technical-sessions/presentation/blazytko) +- [Loki: Hardening Code Obfuscation](https://www.usenix.org/system/files/sec22-schloegel.pdf) +- [Defeating State-of-the-Art White-Box Countermeasures with Advanced Gray-Box Attacks](https://eprint.iacr.org/2020/413) + +## 3. Uniqueness protocol + +The campaign reused Project Kaleidoscope's novelty discipline: + +1. A candidate needed one primary mechanism not shared by another candidate. +2. Opcode churn, encryption, anti-hooking, dead code, and other known Ruam/VM + mechanisms were forbidden as primary ideas. +3. Feasibility annotated ideas but did not delete unusual ideas. +4. Near-duplicates were clustered and rejected explicitly. +5. The final recommendation had to be a fusion whose dynamic reconstruction + problem was not reducible to any one parent. + +`Recognizability` ranges from 0 to 1; lower is more structurally novel. Values at +or above 0.75 are iterations rather than invention and cannot become Rank 1 +without a genuinely new fusion. + +The engineering score uses 1–5 values and this weighting: + +```text +30% dynamic extraction cost +20% cross-input amortization resistance +15% trace non-transferability +10% patch resistance without detection +15% correctness feasibility +10% runtime/build/size efficiency +``` + +Scores are hypotheses for falsification, not security ratings. + +## 4. Ranked slate + +| Rank | Direction | Primary mechanism | Recognizability | Engineering score | +|---:|---|---|---:|---:| +| 1 | Multi-ontology regional realizations | Change the causal execution model | 0.46 | 4.15 | +| 2 | Region-fused contextual linearization | Destroy operation/function granularity | 0.57 | 4.05 | +| 3 | Encoded state and wire-basis drift | Change value identity and representation | 0.71 | 3.95 | +| 4 | Distributed effect sinks | Disperse unavoidable semantic choke points | 0.60 | 3.65 | +| 5 | Program-wide root fusion | Enlarge the minimum reconstruction scope | 0.65 | 3.60 | +| 6 | Cross-invocation braiding | Make traces history/context dependent | 0.54 | 3.55 | +| 7 | Solver/constraint execution | Replace imperative steps with relations | 0.63 | 3.55 | +| 8 | All-path effect predication | Remove taken-path control traces | 0.68 | 3.50 | +| 9 | Distributed continuation mesh | Decentralize execution transport | 0.72 | 3.45 | +| 10 | Input-conditioned transient specialization | Change topology per invocation | 0.67 | 3.30 | +| 11 | Counterfactual worlds | Dilute attribution across valid states | 0.56 | 3.25 | +| 12 | Transactional shadow heap | Defer stateful host observability | 0.70 | 3.05 | +| 13 | Multi-input cohort coding | Couple one query to neighboring behaviors | 0.49 | 2.95 | +| 14 | Reversible compute/uncompute | Remove durable intermediate residue | 0.79 | 2.15 | + +## 5. Candidate details + +### 5.1 Multi-ontology regional realizations + +**Mechanism.** Lower one effect-delimited region into structurally different +execution ontologies, such as predicated dataflow, continuation residuals, +decision diagrams, or algebraic transitions. Renaming, block reordering, and +encoding changes do not count. + +**Dynamic value.** A lifter trained on one realization cannot assume the same +node, value, or control vocabulary in another. + +**Observable choke point.** Region contracts and host-effect sites. + +**Risks and cost.** Exact equivalence across coercion, throwing, allocation, +property access, and evaluation order is difficult. Begin with proven pure +regions. Build complexity and artifact size are high. + +**Falsification.** Train graph alignment across families. Reject a family if a +small normalizer maps it to canonical IR after only a few examples. + +### 5.2 Region-fused contextual linearization + +**Mechanism.** Fuse several data/control operations into one regional +transition. Split direct callees into caller-specific entry fragments and +return continuations so no internal generated function contains a complete +logical source function. + +**Dynamic value.** One hook observes a regional/contextual transition rather +than one language operation or complete callee. + +**Observable choke point.** Public/indirect entries and regional effects. + +**Risks and cost.** Calls, recursion, exceptions, reentrancy, async, `this`, and +exact effect ordering require a continuation verifier. Code size is high, while +runtime can improve relative to interpretation. + +**Falsification.** Instrument every codelet. Reject if a stable closed subgraph +contains a complete callee or one trace maps linearly to source operations. + +### 5.3 Encoded state and wire-basis drift + +**Mechanism.** Represent eligible locals, predicates, and pure results across +shares or encoded wires. Rebase and permute the representation at every +region/continuation transition. + +**Dynamic value.** Def-use recovery must correlate several locations across +changing bases rather than follow one stable virtual register. + +**Observable choke point.** Entry encoding and materialization at effects, +throws, or return. + +**Risks and cost.** This is diversity, not secrecy. Objects, identity, symbols, +proxies, closures, and coercive operations generally cannot stay encoded. +Arithmetic/memory overhead is high, and higher-order analysis can defeat masks. + +**Falsification.** Reject if one fixed recombination or slot permutation +recovers most logical intermediates across histories. + +### 5.4 Distributed effect sinks + +**Mechanism.** Emit property operations, calls, construction, throws, +coercions, and suspension at contextual site-specific sinks rather than one +effect broker or language handler catalog. + +**Dynamic value.** One helper hook no longer yields the whole effect stream. + +**Observable choke point.** The host operations themselves; they are +unavoidably observable. + +**Risks and cost.** Exact getter, proxy, coercion, exception, and evaluation +order must be preserved at every generated site. Size grows; runtime cost can +remain low. + +**Falsification.** Reject if a shared packet format, decoder, helper, or +capability table reconstructs all effect identities. + +### 5.5 Program-wide root fusion + +**Mechanism.** Fuse several selected roots into one protected component so a +target function is not an independently extractable unit. + +**Dynamic value.** Recovery starts from a broader program-level graph. + +**Observable choke point.** Exports and target-specific output/effect slices. + +**Risks and cost.** Unrelated APIs become coupled; tree-shaking, reentrancy, +load time, and failure isolation worsen. + +**Falsification.** Slice backward from one export. If the target separates +cleanly and unrelated roots contribute no necessary dependency, fusion was +only concatenation. + +### 5.6 Cross-invocation braiding + +**Mechanism.** Make representation bases, variant contracts, residuals, and +continuation roles depend on persistent root-group history. Overlapping async +work may interleave live fragments; completed synchronous semantics may not be +deferred. + +**Dynamic value.** The same input produces traces that align poorly under +different warm, recursive, reentrant, or async histories. + +**Observable choke point.** Per-call I/O and persistent carrier state. + +**Risks and cost.** Fresh-process analysis resets history. Reentrancy, +reproducibility, and async order are difficult. + +**Falsification.** Reject if fresh-process analysis remains easy or one captured +carrier transform aligns every history. + +### 5.7 Solver/constraint execution + +**Mechanism.** Lower pure regions to exact relations and use a generic +solver/eliminator to derive the next frame. + +**Dynamic value.** Solver iterations do not directly correspond to source +operations. + +**Observable choke point.** Constraint-instance construction and output +variables. + +**Risks and cost.** General JavaScript relations are unwieldy. Effects, dynamic +types, loops, and exceptions make this practical only for narrow islands. + +**Falsification.** Dump the constraint instance into commodity simplifiers. +Reject if they recover operation families or CFGs more cheaply than fused code. + +### 5.8 All-path effect predication + +**Mechanism.** Linearize candidate pure paths together, produce predicated +effect tokens, and commit only the selected effects in exact source order. + +**Dynamic value.** Execution no longer directly identifies the taken source +edge inside the region. Constantine demonstrates that aggressive control/data +flow linearization can be practical for selected native workloads, although +Ruam's dynamic JavaScript problem is materially harder: +[Constantine](https://arxiv.org/abs/2104.10749). + +**Observable choke point.** Effect commitment and output projection. + +**Risks and cost.** Getters, proxies, calls, throwing, allocation, +nontermination, and path explosion make broad predication unsafe. + +**Falsification.** Reject if dynamic taint cheaply identifies one winning lane +or if extra paths are removable dead work. + +### 5.9 Distributed continuation mesh + +**Mechanism.** Route work among small transducers and continuation contracts, +without one interpreter loop. + +**Dynamic value.** No single function edge reports the semantic sequence. + +**Observable choke point.** Message/continuation movement. + +**Risks and cost.** A shared queue is likely to become a new dispatcher. +Ordering and memory traffic are expensive. + +**Falsification.** Instrument enqueue/dequeue. Reject if one hook reconstructs +the ordered computation. + +### 5.10 Input-conditioned transient specialization + +**Mechanism.** Compose a per-invocation data schedule from precompiled +templates using input shape and carrier history. No source generation, `eval`, +or CSP violation is permitted. + +**Dynamic value.** One captured realization transfers poorly to other input +shapes or histories. + +**Observable choke point.** The specialization builder and schedule. + +**Risks and cost.** The schedule may merely publish canonical IR in another +form. Entry latency and payload size rise. + +**Falsification.** Hook schedule construction. Reject if normalization produces +a stable operation/node sequence. + +### 5.11 Counterfactual worlds + +**Mechanism.** Carry several valid candidate states and derive the next encoded +frame relationally across them, delaying attribution. + +**Dynamic value.** Hooks see several plausible histories. + +**Observable choke point.** Late commitment and external effects. + +**Risks and cost.** The real input often identifies the real world through +taint. Effectful or nonterminating worlds cannot execute normally. Runtime and +memory multiply by world count. + +**Falsification.** Perturb inputs and correlate lanes to output/effects. Reject +if the actual lane becomes obvious after a small number of runs. + +### 5.12 Transactional shadow heap + +**Mechanism.** Execute protected state changes in a virtual graph or journal and +commit externally visible deltas only at barriers. + +**Dynamic value.** Ordinary host hooks see fewer intermediate state operations. + +**Observable choke point.** Commit and any proxy/host callback. + +**Risks and cost.** Identity, prototypes, accessors, proxies, `WeakMap`, +symbols, reflection, and host objects make exact virtualization exceptionally +hard. + +**Falsification.** Use a proxy/reflection-heavy differential suite. Reject on +any semantic mismatch or if commits encode one packet per source operation. + +### 5.13 Multi-input cohort coding + +**Mechanism.** Evaluate a requested pure input alongside derived neighbors and +project the requested coordinate only at return. + +**Dynamic value.** One trace contains several behaviors, complicating +attribution. + +**Observable choke point.** Cohort construction and output projection. + +**Risks and cost.** The attacker sees all inputs, and effects invalidate extra +evaluations. Runtime multiplies by cohort size. + +**Falsification.** Hook the projector or freeze neighboring lanes. Reject if +the actual lane is directly labeled. + +### 5.14 Reversible compute/uncompute + +**Mechanism.** Evaluate pure regions reversibly, project the required result, +then uncompute intermediate state. + +**Dynamic value.** Post-call snapshots retain less residue. + +**Observable choke point.** The complete forward trace. + +**Risks and cost.** A full tracer records the forward computation; effects are +irreversible; runtime approximately doubles. This is not novel enough to become +a primary architecture. + +**Falsification.** Record only the forward half. Reject if it is as recoverable +as ordinary execution. + +## 6. Rejected near-duplicates and theater + +The following do not qualify as distinct directions: + +- Dynamic opcode maps, handler shuffles, witness rekeying, and alias churn are + one mapping-churn family. They fail when runtime returns a handler or opcode. +- Splitting a resolver or fragmenting generic handlers leaves the + dispatch-to-handler edge intact. +- Payload/value encryption with a client-side key creates a hookable + decode/materialization seam. +- Random block or schedule permutation without a new topology or causal model + is normalization work. +- Runtime source generation, `eval`, and self-modifying code are capturable and + break the CSP rail. +- Dead operations, fake messages, opaque predicates, and trace flooding are + removable unless their outputs are semantically necessary. +- Fake counterfactual lanes are dead-code injection. +- Program-wide concatenation is not fusion unless results require shared + fragments. +- Variants that differ only in names, order, or encodings are not multiple + ontologies. +- Anti-debugging, timing, environment, integrity, and self-checks are + patchable detection mechanisms. +- Hash chains and memory-hard carrier updates impose comparable work on users + while leaving semantics visible. +- Moving the same model into Wasm or native code changes tools, not the attacker + model. +- A generic universal handler catalog with extra metadata recreates the exact + choke point being removed. + +## 7. Best fusions + +### H1. Braided Poly-Ontology Region Fabric — Rank 1 + +Region-fused contextual linearization + caller/continuation fission + +interprocedural braiding + multiple structural ontologies + wire-basis drift +inside eligible pure regions + distributed effect sinks. + +This fusion removes both stable instruction boundaries and stable internal +function bodies while retaining a credible path to exact JavaScript effects. + +### H2. Oblivious Relational Region Fabric + +Wire-basis drift + all-path predication + counterfactual worlds + constraint +fragments. It offers stronger pure-code trace dilution but unacceptable +general-JavaScript correctness and performance risk. + +### H3. Transactional Constraint Isogloss + +Solver execution + shadow heap + distributed effect commits. It is credible +for a narrow data-processing language, not Ruam's declared JavaScript surface. + +### H4. Transient Continuation Mesh + +Input-conditioned specialization + contextual frame layouts + continuation +routing + poly-ontology templates. Its main risk is recreating a hookable +schedule-description seam. + +### H5. Program-Braided Cohort Isogloss + +Program-wide fusion + cross-invocation state + cohort/counterfactual coding. +Coupling and overhead make this a research profile, not the base engine. + +## 8. Rank 1 architecture: BPRF + +### 8.1 Pipeline + +```text +canonical semantic IR + -> exact effect/purity/throw/coercion annotation + -> effect-delimited region graph + -> caller-context continuation expansion + -> K poly-ontology regional realizations + -> interprocedural fission and braiding + -> wire-basis and virtual-frame assignment + -> Isogloss fragment lattice + -> generated region fabric and distributed effect sinks +``` + +### 8.2 New non-negotiable invariants + +| ID | Invariant | +|---|---| +| DR-01 | Production execution never materializes `SemanticOp`, handler identity, canonical operand projection, or source-node identity. | +| DR-02 | No production table, function result, metadata record, or packet maps a boundary/fragment to a language operation. | +| DR-03 | One carrier transition advances a regional fragment composition, never one canonical operation. | +| DR-04 | Except for explicit public/indirect/reflection boundaries, a logical function is split across at least two continuation-owned regions. | +| DR-05 | Eligible direct-call SCCs include at least one caller-fused or cross-function fragment; no closed generated body owns the whole internal function. | +| DR-06 | Hardened mode provides at least three reachable contextual realizations per eligible region and at least two verified structural ontologies. | +| DR-07 | Renaming, block permutation, layout-only changes, and encoding-only changes do not satisfy the poly-ontology requirement. | +| DR-08 | A source local/temporary has no stable generated slot, register, wire, or closure-field identity across protected regional transitions. | +| DR-09 | Each hardened pure result depends on at least two independently located necessary fragments/shares; no single lane computes the ordinary result. | +| DR-10 | Runtime never exposes a durable winning-lane, operation-selector, or semantic-dispatch scalar. | +| DR-11 | Getter, proxy, coercion, call, construction, throw, `finally`, await, and yield order is exactly equivalent to canonical IR. | +| DR-12 | No universal production handler, effect broker, packet decoder, runtime switch, or message queue covers all language effects. | +| DR-13 | Ordinary values materialize only at certified contract/effect boundaries and are re-encoded on protected re-entry where eligible. | +| DR-14 | One root group still owns one live carrier through recursion, reentrancy, and suspension. | +| DR-15 | Exports, callbacks, indirect calls, reflection-sensitive entries, and host effects are inventoried as reduced-protection boundaries. | + +### 8.3 Lattice and carrier changes + +Replace handler candidate masks and operand projections with fragments for: + +- regional transitions; +- continuation routes; +- contextual frame maps; +- wire-basis/share refresh; +- effect gates and commit order; +- realization-family constraints; +- entry and return continuation contracts. + +Local ambiguity remains useful, but it is no longer the primary security claim. +The active neighborhood contributes pieces of a contextual regional transition; +it never resolves a canonical semantic event. + +The carrier tracks: + +- continuation contract; +- regional fragment neighborhood; +- phase and lineage; +- realization/ontology epoch; +- virtual-frame layout basis; +- share-refresh/basis state; +- call, exception, and suspension continuations. + +It does not store or derive a semantic opcode, handler index, canonical operand, +or instruction pointer. + +### 8.4 Runtime changes + +Delete these production concepts: + +- semantic handler catalog; +- `resolve(): { handler, operand }`; +- opcode dispatch; +- generic language-effect executor; +- instruction interpreter loop. + +Production uses generated contextual region/effect codelets plus narrowly +generic fragment-combination kernels. Pure codelets contain fused regional +work, not one language operation. Effect sinks are site-specific. Generic +language handlers may exist temporarily only as migration/reference semantics +and compiler-lowering specifications. + +### 8.5 Verifier changes + +The verifier must check: + +- canonical IR to region-graph bisimulation; +- exact observable effect and exception ordering; +- equivalence and legitimate reachability of every regional realization; +- call, return, throw, await, yield, and resume contracts; +- minimum structural difference across ontology families; +- changing frame/binding ownership; +- necessity of all protected pure-result fragments/shares; +- absence of production semantic-dispatch metadata and values; +- absence of complete internal function bodies where fission is required; +- explicit inventory of all ordinary-value and host-effect boundaries. + +## 9. Dynamic-attacker benchmark + +The owner source-origin sidecar is used only to score recovery. The attacker +does not receive it. + +### 9.1 Baseline attacker + +Instrument the paused design's `resolve -> SemanticOp/operand -> handler` seam. +Record the handler, operand, boundary, frame change, and successor. This should +recover almost all executed semantic events with one or two hook families and +provides the control result BPRF must beat. + +### 9.2 Metrics + +| Metric | Meaning | +|---|---| +| H90 | Minimum hook sites required to label 90% of reached source nodes | +| O90 | Dynamic primitive observations required for 90% reached-node recovery | +| T90 | Input/context executions required for 90% source-node and CFG coverage | +| Op-F1 | Recovered semantic-operation classification | +| CFG recall | Recovered canonical CFG edges | +| Def-use F1 | Recovered logical producer/consumer relations | +| Cross-input transfer | Lifter accuracy on unseen inputs | +| Cross-history transfer | Accuracy under fresh, warm, recursive, reentrant, and async histories | +| Context coverage | Caller contexts needed for a reusable callee model | +| Patch-collapse size | Smallest localized patch producing a stable semantic stream while preserving correctness | +| Effect leakage | Control structure inferable only from unavoidable host effects | +| Attack amplification | Trace storage and offline CPU per recovered source event | +| Security efficiency | Attack amplification divided by legitimate runtime/size overhead | + +Run two attackers: + +1. an O(1)-hook attacker searching for runtime choke points; and +2. a full-step attacker instrumenting every generated expression/function and + performing taint, graph alignment, clustering, and trace slicing. + +Effect-sliced reconstruction remains a relevant control because BinSim shows +that dynamic slicing from system effects can recover fine-grained relations +across obfuscated traces: +[BinSim](https://www.usenix.org/conference/usenixsecurity17/technical-sessions/presentation/ming). + +### 9.3 Initial go/no-go gates + +Relative to a baseline expected to recover at least 95% of executed semantic +events: + +- no hook family directly emits a semantic operation or source-site identity; +- O(1)-hook Op-F1 is below 0.50 on held-out inputs; +- cross-input or cross-context transfer loses at least 30 percentage points; +- full-step O90 rises at least 10x and offline reconstruction CPU at least 5x; +- no patch touching three or fewer localized sites creates a stable canonical + stream while preserving the spike corpus; +- attacker amplification exceeds legitimate slowdown; +- values, errors, effects, and scheduling remain exactly differential-correct. + +These are experimental kill gates, not security guarantees. + +## 10. Incremental spike + +1. Build the resolver/handler baseline extractor, full-step instrumenter, + source-origin scorer, and curated dynamic corpus. +2. Add exact purity, throwing, coercion, allocation, host-effect, suspension, + call-edge, and continuation annotations to canonical IR. +3. Support a pure regional subset: numbers, booleans, locals, branches, bounded + loops, and direct calls. +4. Generate roughly several-node fused regions with three contextual variants + across at least two ontologies, initially predicated dataflow and + continuation residuals. +5. Introduce fragment cells and a carrier that routes regional contracts. +6. Prove by architecture test that production execution cannot create a + `SemanticOp`, handler identity, or canonical operand tuple. +7. Add frame/wire-basis drift and require at least two necessary residual + fragments for each protected pure result. +8. Add one callee reached from at least four callers, then recursion and mutual + recursion; verify no stable complete generated callee body exists. +9. Add property read, coercion, call, throw, and `finally` through distributed + site-specific sinks. Never speculatively execute host effects. +10. Run both attacker harnesses and delete mechanisms whose attacker + amplification does not exceed user overhead. +11. Freeze the revised lattice, certificate, artifact, and runtime schemas only + after the dynamic go/no-go gates pass. + +## 11. Work disposition + +### Paused + +- handler candidate masks keyed by `SemanticOp`; +- unique handler/operand resolution; +- production handler catalog extraction; +- operand projector and interpreter interfaces; +- opcode-selecting carrier witnesses; +- certificates mapping boundaries to operations; +- artifact formats encoding those structures; +- runtime and verifier code that assumes instruction-at-a-time execution. + +### Continuing + +- deterministic entropy and reproducible builds; +- canonical semantic IR and source origins; +- canonical CFG, call graph, purity, effect, throw, coercion, and suspension + analysis; +- baseline and test-migration inventories; +- native/reference semantic and effect-trace differential infrastructure; +- root-group lifecycle abstractions that do not freeze operation-witness fields; +- generic graph, bitset, hashing, serialization, and verifier utilities that do + not assume semantic dispatch; +- generic JavaScript semantics only as compiler/reference oracles. + +## 12. Decision gate + +Do not resume lattice/runtime implementation by merely editing the old candidate +mask schema. First implement the dynamic attacker baseline and the pure BPRF +spike. The BPRF design becomes the replacement architecture only if it clears +the quantitative go/no-go gates with zero semantic/effect mismatches and an +attacker-amplification ratio greater than its user cost. diff --git a/docs/superpowers/specs/2026-07-24-isogloss-test-migration-matrix.md b/docs/superpowers/specs/2026-07-24-isogloss-test-migration-matrix.md new file mode 100644 index 0000000..1b05c8d --- /dev/null +++ b/docs/superpowers/specs/2026-07-24-isogloss-test-migration-matrix.md @@ -0,0 +1,263 @@ +# Traveling Isogloss — Test Migration and Legacy-Deletion Matrix + +**Date:** 2026-07-24 +**Status:** Phase 0 inventory +**Scope:** Every file under `packages/ruam/test/` and every script under `packages/ruam/scripts/` +**Parent plan:** [Traveling Isogloss — Rank 1 Implementation Plan](2026-07-24-traveling-isogloss-implementation-plan.md) + +## 1. Classification contract + +Every current test suite has one primary cutover classification: + +| Code | Classification | Required cutover action | +|---|---|---| +| **C1** | JavaScript semantic correctness | Port unchanged in behavioral intent to native JavaScript vs TypeScript reference BCM vs emitted BCM. VM terminology and VM-only options are removed. | +| **C2** | Generic protection, code-generation, packaging, or performance property | Rewrite against Isogloss, the lattice artifact, or engine-independent runtime infrastructure. Do not preserve the legacy mechanism merely to preserve the test. | +| **C3** | VM-mechanism-only | Delete with the VM mechanism, but first extract any unique JavaScript program shape or generic security assertion identified in this matrix. | + +Primary classification is by the reason the file exists. Several files contain mixed sections; those sections have explicit split dispositions below. A C3 file may not be deleted merely because its named mechanism is gone: every unique semantic fixture listed in Section 7 must first appear in a C1 Isogloss suite. + +## 2. Inventory result + +The current tree contains: + +- **46 Bun test-suite files** (`*.test.ts`); +- **5 test support/fixture files**; +- **5 package scripts**; +- **56 total inventoried files**. + +Primary classification of the 46 suite files: + +| Classification | Suite files | Share | +|---|---:|---:| +| C1 — semantic correctness | 23 | 50.0% | +| C2 — generic/rewrite | 16 | 34.8% | +| C3 — VM-only/delete after extraction | 7 | 15.2% | +| **Total** | **46** | **100%** | + +At validation time there are 1,247 lexical `it(` call sites in the 46 suite files. This is a sizing indicator, not the runtime test count: several suites generate cases in loops and the two large JavaScript fixtures run their own assertion catalogs. + +### Common path exercised by C1 end-to-end tests + +Most C1 suites call `test/helpers.ts`, which currently reaches: + +```text +test/helpers.ts + → src/transform.ts + → src/compiler/index.ts + → src/compiler/{emitter,scope,capture-analysis,optimizer}.ts + → src/compiler/visitors/{expressions,statements,classes}.ts + → src/ruamvm/assembler.ts + → src/ruamvm/builders/{loader,runners,interpreter,deserializer}.ts + → src/ruamvm/handlers/*.ts +``` + +After cutover, the permanent oracle path must be: + +```text +test/helpers.ts + ├→ native JavaScript + ├→ canonical semantic IR evaluator + ├→ src/isogloss/reference-runtime.ts + └→ emitted BCM runtime +``` + +The VM may remain a migration-only fourth oracle before Phase 10. It must not remain in the final helper or product path. + +## 3. C1 — JavaScript semantic suites to port + +All files in this table remain active tests. Their assertions should be preserved unless an assertion itself encodes a VM mechanism. + +| Existing suite | Current production modules primarily exercised | Isogloss port target and required additions | +|---|---|---| +| `test/core/advanced.test.ts` | All compiler visitors; `handlers/{calls,classes,functions,objects,scope,special,type-ops}.ts` | `test/isogloss/runtime-core.test.ts` and `runtime-classes.test.ts`; retain all advanced expressions, prototypes, classes, and built-in interaction cases. | +| `test/core/arithmetic.test.ts` | `visitors/expressions.ts`; `handlers/{arithmetic,comparison,logical,mutation,type-ops}.ts` | Semantic-handler differential corpus plus `runtime-core.test.ts`; each `SemanticOp` family must also have a signature-table case. | +| `test/core/arrays.test.ts` | `visitors/{expressions,statements}.ts`; `handlers/{objects,iterators,destructuring,calls,special}.ts` | `runtime-core.test.ts`; add sparse-array, iterator-close, and accessor-reentrancy event logs. | +| `test/core/async.test.ts` | Async compilation in `compiler/index.ts`; `handlers/generators.ts` `AWAIT`; async interpreter/runners | `runtime-async.test.ts`; keep all 16 value cases, then add scheduling, interleaving, rejection recovery, and single-carrier assertions described in Section 8. | +| `test/core/basic.test.ts` | Target selection, function compilation, constants, stack/register handlers, return | Phase 3 reference-runtime smoke suite and `runtime-core.test.ts`. | +| `test/core/closures-basic.test.ts` | `capture-analysis.ts`, `scope.ts`; `handlers/{functions,scope,registers}.ts` | `runtime-closures.test.ts`; assert escaped closures retain their originating root group. | +| `test/core/closures-scope.test.ts` | Capture analysis, lexical scope chain, child units, call/function/scope handlers | `runtime-closures.test.ts`; add calls before and after unrelated carrier evolution. | +| `test/core/control-flow-basic.test.ts` | `visitors/statements.ts`; `handlers/control-flow.ts`; optimizer jump handling | Phase 2 CFG/lattice fixtures, `cfg-bisimulation.test.ts`, and `runtime-control-flow.test.ts`. | +| `test/core/control-flow.test.ts` | Full statement visitor, exception tables, jump patching, `handlers/{control-flow,exceptions,iterators}.ts` | Primary CFG-bisimulation and runtime-control-flow corpus. Preserve every nested `finally`, labeled break/continue, switch, and loop shape. | +| `test/core/destructuring.test.ts` | `visitors/{expressions,statements}.ts`; `handlers/destructuring.ts` | `runtime-core.test.ts`; operand-reservoir tests must include defaults, rest, holes, and computed keys. | +| `test/core/edge-cases.test.ts` | Parser plugins, target selection, expressions/statements, calls/special handlers, dynamic import harness | `runtime-core.test.ts` plus environment integration. Preserve dynamic import behavior but separate it from CSP proof. | +| `test/core/exceptions.test.ts` | Exception regions, `handlers/exceptions.ts`, interpreter catch routing | `runtime-exceptions.test.ts`; add post-uncaught-error carrier validity and exact event-order comparison. | +| `test/core/functions.test.ts` | Function/arrow/default/rest/arguments compilation; calls/functions/scope handlers | `runtime-core.test.ts`, `runtime-closures.test.ts`, and `runtime-reentrancy.test.ts`; recursion cases become single-carrier assertions. | +| `test/core/indexed-slots.test.ts` | Despite its name, current `capture-analysis.ts`, `scope.ts`, register promotion, scope-chain handlers, classes and catch scoping | Rename away from “indexed slots” and port the complete lexical-scope corpus to `runtime-closures.test.ts`; do not recreate VM slots. | +| `test/core/objects.test.ts` | Object/class expression visitors; `handlers/{objects,classes,calls,mutation,type-ops}.ts` | `runtime-core.test.ts`, `runtime-classes.test.ts`, and accessor-reentrancy cases. | +| `test/core/strings.test.ts` | Expression compiler, constant pool, calls/objects/type-ops handlers | Semantic-handler differential corpus and `runtime-core.test.ts`; keep Unicode and coercion cases independent of artifact string protection. | +| `test/integration/chrome-ext-patterns.test.ts` | `transform.ts`; async/class/call handlers; Node `vm` harness | Port as async/class semantics. It is **not** currently a browser-extension or CSP execution test; add real worker/MV3-style execution separately. | +| `test/integration/ruam-tester-lite.test.ts` | Full end-to-end pipeline over `RuamTesterLite.js` | Keep native/default semantic smoke cases. Delete the `vmShielding` case (C3). Rewrite the max-preset case for the Isogloss max profile (C2). | +| `test/integration/ruam-tester.test.ts` | Full end-to-end pipeline over `RuamTester.js` | Retain as the broad semantic acceptance fixture; compare native, reference BCM, and emitted BCM summaries. | +| `test/stress/randomized.test.ts` | Broad compiler/handler surface; rolling-cipher, integrity, and preset sections | Port arithmetic through exception and deep-nesting generators to deterministic Isogloss seed stress. Delete the rolling-cipher section (C3); rewrite integrity/preset sections (C2). Replace `Math.random()` with logged deterministic data seeds. | +| `test/stress/repro-exception.test.ts` | `transform.ts`; try/catch property-name/scoping collision; runtime exception route | Add verbatim to `runtime-exceptions.test.ts` and the 1,024-seed release tier. | +| `test/stress/stress-breaker.test.ts` | Capture/scope, `this`, exceptions, mutation, recursion, iterators, constructors, coercion | Split among closure, class, exception, reentrancy, and core Isogloss suites. Preserve all 51 cases. | +| `test/stress/vm-breaker.test.ts` | Broad semantic compiler and handler catalog | Rename to engine-neutral semantic adversarial tests and preserve all 70 cases. The “generator-like state machines” section is ordinary manual state-machine code and does **not** cover JavaScript generators. | + +### C1 source-to-target ownership + +| Current semantic implementation | Existing coverage | Target ownership | +|---|---|---| +| `compiler/visitors/expressions.ts` | Arithmetic, arrays, objects, strings, functions, advanced, breakers | `compiler/ir.ts`, `compiler/semantic-ops.ts`, and handler differential suites | +| `compiler/visitors/statements.ts` | Control flow, exceptions, arrays/iterators, breakers | `compiler/cfg.ts`, `cfg-bisimulation.test.ts`, runtime control/exception suites | +| `compiler/visitors/classes.ts` | Advanced, objects, Chrome-extension patterns, breakers | `runtime-classes.test.ts` | +| `compiler/capture-analysis.ts` and `compiler/scope.ts` | Closure, indexed-slots, function, breaker suites | `runtime-closures.test.ts` and reentrancy suite | +| `ruamvm/handlers/*.ts` language behavior | All C1 suites | Move to `runtime/handlers/*.ts`; direct native differential test for every semantic family | +| VM interpreter/runners/loader | Incidental coverage in every C1 suite | Replaced by reference and emitted BCM oracles; no VM-specific assertion survives | + +## 4. C2 — Generic properties to rewrite for Isogloss + +| Existing suite | Current production modules primarily exercised | Required rewrite | +|---|---|---| +| `test/core/deterministic-entropy.test.ts` | `src/testing.ts`, `random/entropy.ts`, and the complete transform path | Retain as the deterministic build foundation. Rename its helper for `protectCode`, remove legacy cipher options, add stable output/certificate assertions, and keep labeled stream-isolation coverage. | +| `test/isogloss/semantic-signatures.test.ts` | Transitional `compiler/{opcodes,semantic-ops,semantic-signatures,ir}.ts` | Retain and strengthen. The current `SemanticOp === Op` alias assertions are migration-only and must be deleted with `opcodes.ts`; add exhaustiveness, exact signature facts, dynamic stack effects, and a proof that no VM-only `MUTATE` semantic remains at cutover. | +| `test/naming/ast-integration.test.ts` | `naming/{registry,scope,token}.ts`; `ruamvm/{nodes,emit}.ts` | Repoint to `runtime/{nodes,emit}.ts`; preserve NameToken-to-AST integration. | +| `test/naming/registry.test.ts` | `naming/{registry,scope,token,reserved}.ts` | Retain and add Isogloss scope/claim collision cases plus deterministic stream isolation. | +| `test/naming/setup.test.ts` | `naming/{setup,claims,compat-types}.ts` | Rewrite `setupRegistry` expectations for BCM names. Delete `setupShieldedRegistry` assertions; root groups are not shielding. | +| `test/ruamvm/emit.test.ts` | `ruamvm/{nodes,emit}.ts` | Move unchanged in intent to `test/runtime/emit.test.ts` against `runtime/{nodes,emit}.ts`. Remove `debuggerStmt` from the production-capable API or prove it cannot enter output. | +| `test/ruamvm/transforms.test.ts` | `ruamvm/{nodes,emit,transforms}.ts` | Move only engine-independent AST transforms to `runtime/`; rewrite names and import boundaries. | +| `test/security/anti-reversing.test.ts` | `transform.ts`, `compiler/{opcodes,encode}.ts`, interpreter table, naming, string encoding | Replace opcode-array/function-table regexes with `novelty-invariants.test.ts` and a format-aware `static-extractor.test.ts`. Keep plaintext absence and per-build variation only where the Isogloss artifact makes the same claim. | +| `test/security/debug-protection.test.ts` | `ruamvm/builders/debug-protection.ts`, assembler, presets, naming | The option is removed at cutover, then redesigned. Preserve the generic CSP/no-eval/no-`new Function`/no-`debugger` requirements in a runtime-wide CSP suite. Rewrite feature-specific assertions only when the engine-independent replacement exists. | +| `test/security/feature-combinations.test.ts` | `presets.ts`, `types.ts`, `transform.ts`, nearly all legacy hardening modules, preprocessing/naming | Preserve the compact language fixtures. Rebuild the pair/triple matrix only from Isogloss-native Phase 9 options and add metadata-driven coverage. | +| `test/security/kerckhoffs-hardening.test.ts` | Incremental cipher, semantic opacity, observation resistance through the full pipeline | Preserve the principle and language fixtures, not the three VM mechanisms. Rewrite as a known-design Isogloss hardening/novelty suite. | +| `test/security/new-features.test.ts` | `polymorphic-decoder.ts`, `string-atomization.ts`, `scattered-keys.ts`, `block-permutation.ts`, `opcode-mutation.ts` | Split: decoder and string atomization become later engine-independent artifact/runtime tests; scattered keys, block permutation, and opcode mutation are C3 deletions; combined cases become Isogloss hardening-profile tests. | +| `test/security/observation-resistance.test.ts` | `ruamvm/observation-resistance.ts`, interpreter witnesses/canaries/probes, legacy feature interactions | Redesign as BCM observation-resistance tests only after a BCM-specific threat model exists. Preserve its language fixtures in C1 meanwhile. | +| `test/security/semantic-opacity.test.ts` | `opaque-predicates.ts`, `handler-aliasing.ts`, `mba.ts`, interpreter builder | Reuse proven predicate/alias properties for `isogloss/constraints.test.ts` and semantic-alias tests. Do not preserve the VM handler-table injection path. Replace test-only `new Function` predicate evaluation with an AST-evaluated or isolated CSP-neutral oracle. | +| `test/security/string-encoding.test.ts` | `compiler/encode.ts`, runtime decoder/deserializer, constants | Rewrite as Isogloss artifact string round-trip and plaintext-inspection coverage. Preserve ASCII, Unicode, special-character, long-string, property-name, regex, error, closure, and class fixtures. | +| `test/stress/performance.test.ts` | `index.ts`, complete transform/runtime, Node `vm` | Convert to a budgeted Isogloss performance smoke test or move measurements into scripts. It currently always passes regardless of speed and therefore enforces no release gate. | + +## 5. C3 — VM-mechanism suites to delete after extraction + +| Existing suite | VM modules exercised | What must be extracted before deletion | +|---|---|---| +| `test/security/bytecode-scatter.test.ts` | `ruamvm/bytecode-scatter.ts`, `ruamvm/emit.ts` | No bytecode-scatter logic survives. Its fragment round-trip concept may inform `artifactScattering`, but there is no required code port. | +| `test/security/decode-cache.test.ts` | `builders/loader.ts`, `builders/interpreter.ts`, rolling/incremental cipher gates, opcode mutation and observation resistance | Port the control-flow-heavy `PROGRAMS` corpus, especially repeated calls, backward jumps, mutual recursion, and nested return-through-finally. Delete all cache-active/cache-disabled assertions. | +| `test/security/incremental-cipher.test.ts` | `compiler/{incremental-cipher,basic-blocks,opcodes}.ts`, `BytecodeUnit`, runtime incremental decoder | Port unique end-to-end async/control/exception fixtures; delete block-key, epoch, encryption, and instruction-array assertions. | +| `test/security/rolling-cipher.test.ts` | `compiler/{rolling-cipher,encode}.ts`, runtime rolling decoder, integrity binding, bytecode format, dead-code injection | Extract the unique semantics and generic artifact-integrity/plaintext properties listed in Section 7. Delete rolling-key, instruction encryption, binary-bytecode, and cipher-combination assertions. | +| `test/security/slot-save-restore.test.ts` | `compiler/{slot-analysis,opcodes}.ts`, VM handler AST registry, VM interpreter hoisted slots | Port its recursion/exception/`this` end-to-end programs to single-carrier reentrancy tests. Delete slot-set introspection and VM snapshot/restore assertions. | +| `test/security/vm-shielding.test.ts` | Shielded branch in `transform.ts`, `ruamvm/assembler.ts`, per-group opcode shuffles/ciphers, shielded naming | Port independent-root, cross-function, escaped-closure, async, and class fixtures to root-group/carrier tests. Delete shuffle, auto-cipher, and shielding-option assertions. | +| `test/stress/opcode-mutation-controlflow.test.ts` | `compiler/{opcode-mutation,block-permutation,rolling-cipher}.ts`, mutable VM handler table | Port all three programs to `cfg-bisimulation`, exception, and fixed-seed stress suites. Delete only the mechanism/options. | + +## 6. Test support and fixture files + +| File | Classification | Disposition | +|---|---|---| +| `test/helpers.ts` | C1/C2 foundation | Replace `VmObfuscationOptions` and `evalObfuscated()` with native/reference/emitted BCM helpers. Add value, error, event-order, carrier-evolution, and fixed-entropy APIs. VM oracle access must live in a migration-only helper deleted at Phase 10. | +| `test/RuamTester.js` | C1 fixture | Retain the semantic catalog, rename VM-oriented comments, and run through all permanent oracles. | +| `test/RuamTesterLite.js` | C1 fixture | Retain as the fast smoke fixture, rename VM-oriented comments, and run through reference and emitted BCM. | +| `test/RuamTesterLiteO.js` | C3 generated legacy artifact | It is not referenced by any test and has no recorded seed/provenance. Move to the frozen Phase 0 legacy-output archive with metadata if it is useful; otherwise delete at cutover. Never use it as an Isogloss oracle. | +| `test/webpack-scope-repro.js` | C1 dormant fixture | It is currently unreferenced. Add a Bun integration wrapper and port it as class field, closure, module-factory, getter-export, and source-selection coverage. Replace its uncontrolled `Math.random()` use for deterministic testing. | + +## 7. Mandatory semantic extraction before C3 deletion + +These cases are easy to lose because they currently live inside mechanism suites. + +| Source suite/section | Destination before deletion | +|---|---| +| `decode-cache`: backward loop, nested forward jumps, switch+continue, try/catch/finally routes, recursion, labeled break/continue | `runtime-control-flow.test.ts`, `runtime-exceptions.test.ts`, `persistence.test.ts`, and seed-stress curated fixtures | +| `incremental-cipher` end-to-end: async function and any unique exception/block-boundary programs | `runtime-async.test.ts`, `runtime-exceptions.test.ts`, `cfg-bisimulation.test.ts` | +| `rolling-cipher` correctness: deep closures, class inheritance, many/rest parameters, multiple independent roots, recursive closures, thrown exit, async, optional-access Chrome pattern | Corresponding C1 runtime suites | +| `rolling-cipher` integrity/plaintext sections | `format-roundtrip.test.ts`, `novelty-invariants.test.ts`, and future lattice/runtime integrity tests; preserve the property, not cipher constants | +| `rolling-cipher` dead-code section: nested finally, multiple returns, switch returns | `runtime-exceptions.test.ts` and `runtime-control-flow.test.ts` | +| `slot-save-restore` end-to-end: recursion, nested exceptions, `this`/`new.target` context | `runtime-reentrancy.test.ts`, `runtime-exceptions.test.ts`, `runtime-classes.test.ts` | +| `vm-shielding`: independent roots, cross-root calls, shared closure, async root, classes | `pipeline/groups` tests, root-group carrier-isolation tests, closure and async suites | +| `opcode-mutation-controlflow`: all three regression programs | Fixed-seed CFG, exception, and topology stress corpus | +| `new-features`: block-permutation and opcode-mutation semantic programs | C1 control-flow/exception/core suites before their mechanism sections are deleted | +| `randomized`: rolling-cipher section | Delete; semantic generators already remain. Any distinct fixture discovered during porting is moved to the engine-neutral randomized corpus. | + +No C3 deletion commit is complete until an automated fixture manifest maps each row above to its new test file. + +## 8. Coverage gaps against the implementation plan + +### P0 — release-blocking gaps + +| Gap | Existing evidence | Missing authoritative evidence | +|---|---|---| +| **Real JavaScript generators** | No test or fixture under `packages/ruam/test` contains `function*` or an executed `yield`. `test/stress/vm-breaker.test.ts` only tests hand-written “generator-like” state machines. `ruamvm/handlers/generators.ts` implements generator lifecycle operations as stubs. | `runtime-generators.test.ts` covering `next`, sent values, `yield*`, `throw`, `return`, `finally`, escaped generators, multiple parked generators in one root group, async generators, abandonment cleanup, and carrier uniqueness. Generator support is currently **unproven**. | +| **Reference BCM oracle** | `test/helpers.ts` compares only native and emitted legacy VM values. | Native vs canonical IR evaluator vs reference BCM vs emitted BCM, including minimized reproducible state diagnostics. | +| **Carrier correctness/persistence** | Decode-cache tests repeat calls, but only validate stable VM decoded instructions. | Carrier changes after every semantic transition, remains one-per-root-group, persists across successful/throwing calls, remains valid after uncaught errors, and never grows append-only history. | +| **Reentrancy** | Scattered getters/setters, `valueOf`, recursion, and callback tests exercise JavaScript semantics but do not intentionally call back into the same protected root while a handler is active. | Dedicated accessor, Proxy trap, `Symbol.toPrimitive`, `valueOf`, user callback, constructor, and cross-root reentry cases with frame/resume-gate and one-live-carrier assertions. | +| **Adversarial static extractor** | `anti-reversing.test.ts` uses output regexes for numeric arrays, strings, names, and a giant switch. | Full-format parser/deserializer with knowledge of resolver, catalog, reservoirs, and topology; TI-01 through TI-07 checks; site-to-handler recovery attempt; lineage-transfer and next-lineage-prediction measurements. | +| **No-legacy cutover proof** | No current test inventories imports, package contents, CLI aliases, types, flags, loaders, or fallback paths. | `migration/no-legacy-vm.test.ts` and package-surface inventory enforcing TI-14. | +| **Verifier/property coverage** | Existing semantic tests only show executions that happened to run. | Certificate, constraint, candidate-mask cardinality, non-boundary uniqueness, operand-reuse, cell-reuse, phase-cycle, CFG-bisimulation, and finite verifier-state property suites. | + +### P1 — required before async/browser/performance gates + +| Gap | Existing evidence | Required addition | +|---|---|---| +| **Async scheduling and interleaving** | `core/async.test.ts` has 16 useful value cases; `chrome-ext-patterns.test.ts` has 11 realistic async/class cases. Most assert only final values. | Side-effect event logs for two interleaved calls, nested awaits, rejection then later success, callbacks that reenter, host microtask order, continuation cleanup, and an assertion that Ruam adds no queue. | +| **CSP/environment execution** | `debug-protection.test.ts` checks strings for `debugger`, `eval`, and `new Function`. `chrome-ext-patterns.test.ts` executes in Node `vm`, not Chrome. `build-browser.mjs` bundles a worker but does not execute a CSP fixture. | Actual Node, browser page, dedicated worker, and MV3/service-worker-style fixtures under a restrictive CSP; scan and execute production output; verify no network/storage/timer dependency is required. | +| **Performance release budgets** | `stress/performance.test.ts` is informational and ends with `expect(true)`. `bench.mjs` measures native vs total/boot/steady execution and size. `bench-attribution.mjs` only removes legacy VM features. | Versioned JSON; build-phase timings; payload/runtime/sidecar bytes; bootstrap/first/repeated call; recursion, loop, property, exception, async-interleave workloads; peak/retained memory; enforced median/P95 release thresholds from the plan. | +| **Deterministic fuzzing and seeds** | `core/deterministic-entropy.test.ts` now proves fixed-build reproduction and labeled stream isolation. `randomized.test.ts` still uses unseeded `Math.random()`, and the stress corpus does not yet record a reproducible build/data seed pair. | Use `src/testing.ts` fixed entropy plus a separate deterministic data seed; print both on failure; enforce 8/32/256/1,024 seed tiers. | +| **Errors as observable behavior** | Helpers generally compare values with `toEqual`; exception suites do not provide a common error oracle. | Compare error constructor/name/message where specified, explicit side-effect log, thrown value identity where required, and post-error carrier state. | +| **Owner sidecar and runtime trace** | No current coverage. | Sidecar schema/round-trip, absence from production payload, event decoding, source-span mapping, runtime-trace scheduling neutrality, and security/performance exclusion. | +| **Removed-option and metadata drift** | Feature suites pass legacy option objects directly. The CLI and manifest are not cross-checked. | Removed-option tombstone tests, generalized metadata/CLI/preset/manifest drift tests, unknown nested-property rejection, and pre-parse failure evidence. | +| **Frozen Phase 0 outputs** | `RuamTesterLiteO.js` is one unreferenced generated artifact without recorded seed or provenance. | Versioned output/measurement fixtures for core, closure, exception, class, async, generator-known-unsupported baseline, and max preset, each with source, options, seed, tool revision, and checksum. | + +## 9. Required new suite map + +| Planned suite | Best reusable current input | Net-new requirement | +|---|---|---| +| `test/isogloss/certificate.test.ts` | None | Certificate counts/digest/schema and encoded-lattice match | +| `test/isogloss/cfg-bisimulation.test.ts` | Core control-flow plus extracted C3 regressions | Mechanical edge-by-edge CFG/refold equivalence | +| `test/isogloss/constraints.test.ts` | Semantic-opacity predicate/alias ideas | TI-02 through TI-05, UNSAT diagnostics, attempt budgets | +| `test/isogloss/format-roundtrip.test.ts` | String encoding and generic fragment round trips | Isogloss envelope only; absence of legacy payload variants | +| `test/isogloss/novelty-invariants.test.ts` | Generic claims from anti-reversing | TI-01 through TI-07 and TI-12 architecture checks | +| `test/isogloss/operands.test.ts` | Destructuring/constants/register semantic fixtures | Projection uniqueness, reservoir reuse, decoys, no site-local operand | +| `test/isogloss/persistence.test.ts` | Decode-cache repeated-call programs | Evolved lineage/carrier state across return and throw | +| `test/isogloss/reference-runtime.test.ts` | Core basic/arithmetic/control fixtures | Canonical IR vs reference BCM with diagnostic state | +| `test/isogloss/runtime-async.test.ts` | Core async and Chrome-extension fixtures | Interleaving, order, cleanup, one carrier | +| `test/isogloss/runtime-classes.test.ts` | Objects, advanced, breakers | `super`, home object, computed methods, `new.target`, reentry | +| `test/isogloss/runtime-closures.test.ts` | Closure and indexed-slots suites | Evolved root-group state and escaped closures | +| `test/isogloss/runtime-control-flow.test.ts` | Both control-flow suites plus C3 extracted programs | Carrier phase/variant movement through loops | +| `test/isogloss/runtime-core.test.ts` | Core basic/arithmetic/arrays/objects/strings/functions | Native/reference/emitted permanent oracle | +| `test/isogloss/runtime-exceptions.test.ts` | Exception/control-flow/repro suites plus C3 programs | Non-transactional motion and post-uncaught recovery | +| `test/isogloss/runtime-generators.test.ts` | None | Entire generator and async-generator surface | +| `test/isogloss/runtime-reentrancy.test.ts` | Breaker coercion/accessor/recursion snippets | Intentional protected callback reentry and resume gates | +| `test/isogloss/seed-stress.test.ts` | Randomized and opcode-mutation regression shapes | Fixed build/data seeds and CI tiers | +| `test/isogloss/semantic-signatures.test.ts` | A transitional suite now exists and aliases `SemanticOp` to legacy `Op`; slot-analysis provides an additional exhaustiveness pattern | Remove the legacy alias and VM-only operations, then enforce exhaustive `satisfies Record`, exact facts, and dynamic stack effects | +| `test/isogloss/sidecar.test.ts` | None | Schema, source map, production absence, event decode | +| `test/isogloss/static-extractor.test.ts` | Anti-reversing threat ideas only | Hostile full-format extractor and lineage experiments | +| `test/migration/removed-vm-options.test.ts` | Legacy option list/feature combinations | Actionable error code and migration hint before parsing | +| `test/migration/no-legacy-vm.test.ts` | None | Source, exports, CLI, package, manifest, output, and fallback inventory | +| `test/options/metadata-drift.test.ts` | Feature combinations and manifest script | One source of truth across API, CLI, presets, worker manifest | + +## 10. Script migration matrix + +| Script | Classification | Current modules/data | Required Isogloss disposition | +|---|---|---|---| +| `scripts/bench.mjs` | C2 | `src/index.ts`, Node `vm`, legacy presets; eight workloads; bootstrap, total/steady time, output size | Rewrite as the primary Isogloss benchmark. Add JSON output, phase timings, first/repeated calls, exception/async/reentrancy workloads, memory, payload/runtime/sidecar bytes, and release-budget evaluation. Preserve the legacy measurement JSON, not a VM import. | +| `scripts/bench-attribution.mjs` | C2 | `src/index.ts`, `src/presets.ts`; removes legacy features from max | Freeze its legacy results, then replace the matrix with Isogloss profile/parameter and Phase 9 hardening attribution. It must not import legacy presets after Phase 10. | +| `scripts/build-browser.mjs` | C2 | `src/browser-worker.ts`, `browser-crypto-shim.ts`, generated option manifest, esbuild | Retain and point at the detailed Isogloss API. Add an executable browser/worker CSP smoke step; bundling success alone is insufficient. | +| `scripts/collect-stats.mjs` | C2 | Dist API, test output parsing, opcode/source statistics, simple performance/size, generated hero snippet | Remove opcode and VM terminology; ingest versioned benchmark JSON and expose Isogloss groups/cells/clauses/certificate/expansion statistics. Fix or remove the current `preset: "high"` reference in favor of valid Isogloss profiles/presets. | +| `scripts/generate-manifest.mjs` | C2 | `OPTION_META`, `AUTO_ENABLE_RULES`, `PRESETS` from dist | Rewrite for typed Isogloss metadata, nested options, artifact options, and removed-option tombstones. Add `metadata-drift.test.ts`; do not filter only boolean keys. | + +## 11. Cutover gates derived from this audit + +The test migration is complete only when all of the following are evidenced: + +1. All 23 C1 suite files have an explicit destination and pass through native, reference BCM, and emitted BCM where applicable. +2. All mixed C2/C3 sections have been split; no test is kept by retaining a legacy VM mechanism. +3. Every Section 7 semantic fixture appears in the generated fixture manifest before its C3 source file is deleted. +4. True generator coverage exists and passes; manual state-machine tests do not count. +5. Async and reentrant tests assert event order and carrier uniqueness, not only final values. +6. CSP passes by executing generated output in Node, browser, worker, and browser-extension-style environments. +7. The static extractor parses the real production format with full design knowledge. +8. Performance scripts emit versioned data and enforce the plan's size, bootstrap, steady-state, P95, and retained-memory gates. +9. Fixed build and data seeds reproduce every randomized failure. +10. `no-legacy-vm.test.ts` proves the source tree, package surface, generated output, CLI, manifest, and runtime contain no VM fallback. + +## 12. Highest-risk conclusion + +The existing suite provides a large and valuable JavaScript semantic corpus, especially for control flow, closures, exceptions, classes, and coercion. It does **not** currently prove the properties most specific to Traveling Isogloss: + +- true generator suspension; +- one-carrier behavior under async and reentrancy; +- history-dependent boundary motion and persistence; +- verifier/lattice invariants; +- resistance to a format-aware static extractor; +- actual CSP execution outside Node `vm`; +- enforceable performance and memory budgets; +- absence of a legacy fallback. + +Those gaps are release blockers, not follow-up hardening. The destructive VM deletion should occur only after the new suites supply direct evidence for them. diff --git a/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-campaign-design.md b/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-campaign-design.md new file mode 100644 index 0000000..5b37b88 --- /dev/null +++ b/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-campaign-design.md @@ -0,0 +1,464 @@ +# Project Kaleidoscope — Ruam Gen-2 Ideation Campaign + +**Date:** 2026-07-24 +**Status:** Executed once on 2026-07-24; design retained for reruns. +**Type:** Ideation-campaign design. This document remains runnable — §11 is a complete `Workflow` script and §12 is the operator guide. A lean paste-and-launch companion lives at `docs/ruam-gen2-ideation-prompt.md`. The initial run is recorded in `docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-results.md` with its full structured artifact beside it. + +--- + +## 0. What this is (and what it is not) + +Ruam is a JavaScript source-protection tool: it compiles a developer's functions into custom bytecode run by a per-build-polymorphic embedded interpreter, so shipped code does not hand a reader a clean, easily-reconstructed copy of the original logic. This document designs a **structured ideation campaign** that uses **Opus 5** and **Fable 5** subagents, dispatched in **parallel workflows**, to generate *genuinely novel* directions for Ruam's next generation — across the whole product, not only its resilience layer. + +It is a **plan**. Running it is a separate, explicit step (see §12). This document does not itself dispatch any agent. + +**The one non-negotiable design property:** the campaign is engineered to make convergence-on-the-obvious *structurally impossible*. Novelty is enforced by construction (§3), not left to chance. + +--- + +## 1. Why the first generation was uncreative — the diagnosis + +The gen-1 hardening work (`docs/superpowers/specs/2026-06-30-anti-ai-decompilation-design.md`, branch `anti-ai-hardening`) produced competent but *incremental* ideas: salt the key per unit, chain the keystream, fold a cross-file digest. Every one is an **iteration on a primitive that already existed in the codebase.** + +The root cause is not a lack of effort — it was a **47-agent swarm**. The root cause is an **anchor**. Gen-1's design doc opened by declaring an "honest ceiling": + +> *"perfect … defense against a patient AI is impossible … client-executable code is client-observable … the achievable wins are (a) raise human-hours, (b) destroy transferability, (c) an off-device secret."* + +Once that frame was accepted, every downstream agent reasoned *inside it* and optimized within a solved space. The ideas were rankings of known levers, not new categories. The gen-1 v2 handoff even sensed the trap ("Do not let that conclusion truncate your ideation") but could not escape it, because it never removed the anchor — it just asked agents to try harder next to it. + +**Design consequence for gen-2:** we do not ask agents to be creative. We **remove the anchor**, **forbid the known solution space**, and **force transfer from foreign domains** — so the recognizable answer is simply unavailable to them. + +--- + +## 2. Success criteria for the campaign + +A run is successful if it produces: + +1. **A ranked slate of candidate directions** spanning the seven tracks (§6), each with a novelty score, an impact estimate, feasibility tags, and a maturity bucket. +2. **At least several genuinely unrecognizable ideas** — mechanisms that do not pattern-match any primitive already in Ruam or in the common obfuscation/compiler canon (recognizability score below the threshold in §9). +3. **At least one strong fusion** (§ Phase 2) — a hybrid neither parent idea would have produced. +4. For every idea: a one-line **"why this is not a gen-1 iteration"** note. + +A run *fails its own bar* (and should be re-seeded, §Phase 5) if every surviving idea is a recognizable primitive or a relabeling of a gen-1 workstream. + +--- + +## 3. The four creativity engines + +Every generation agent operates under all four simultaneously. These are the heart of the design. + +### 3.1 Forbidden-solution list +Each generator receives the full inventory of *what already exists* and *what an automated analyzer recognizes for free* — the Ruam feature list, the gen-1 workstreams, and the canonical primitives (stream ciphers, digests/checksums, base-N codecs, LCG/FNV PRNGs, switch/table-dispatch VMs, control-flow flattening, string tables, dead-code, opaque predicates). **Any idea that pattern-matches an item on this list is disqualified before it is scored.** You cannot win by reinventing a keystream. + +### 3.2 Assumption inversion +Each agent is handed exactly one Ruam "axiom" to **overturn** (bank in §7.2), e.g. *"the decoder must ship alongside the code it decodes,"* *"the output is fixed the moment it is emitted,"* *"a local fragment can be understood by reading it locally."* The agent must produce a mechanism that only makes sense *if the axiom is false.* Inversion reliably relocates thinking outside the solved space. + +### 3.3 Cross-domain analogy seeding +Each agent is assigned one lens from a domain **far** from software (bank in §7.1: immune systems, mycelial networks, holography, origami metamaterials, untranslatable languages, stage misdirection, DNA repair, ecology, phase transitions, cartographic projection…). The agent must (a) explain the source-domain mechanism in its own terms, (b) map each element of it onto a Ruam concept, and (c) *only then* state the idea. The mandatory "explain the source first" step is what turns a shallow metaphor into a real structural transfer — and it is the strongest single lever against training-distribution convergence. + +### 3.4 Forced fusion +A dedicated phase takes the boldest raw ideas and **hybridizes** them in pairs/triples. Gen-1 recorded that its only mechanism to survive scrutiny was a *fusion* of several levers — never a standalone trick. Gen-2 makes fusion a deliberate phase rather than an accident, because the interaction of two unlike ideas is where the genuinely new mechanisms live. + +--- + +## 4. Model roles — Opus 5 and Fable 5, split on purpose + +Per the project's `efficient-fable` doctrine (Fable for decomposition, architecture/product tradeoffs, synthesis, and final judgment; cheaper models for bounded heavy lifting): + +- **Fable 5 (`claude-fable-5`)** — the **judgment layer.** It writes the framing brief (Phase 0), steers cross-pollination, and performs final synthesis and ranking (Phase 4). Fable makes the "is this actually novel / does it cohere / is this a gen-1 relabel" calls. +- **Opus 5 (`claude-opus-5`)** — the **divergent fleet.** The many parallel generators (Phase 1) and the fusion agents (Phase 2). Each generator gets a *unique* lens × assumption × track × persona tuple, so no two explore the same space. + +Reasoning effort: generation and synthesis run at **high/max** effort; tagging runs at **medium**. + +--- + +## 5. Phase topology (blue-sky → filter) + +Ideation is fully unconstrained first; feasibility enters only as *annotation*, never as a gate that deletes ideas. + +| Phase | Model ×N | Purpose | Consumes | Produces | +|---|---|---|---|---| +| **0 — Framing brief** | Fable 5 ×1 | Read the repo + gen-1 record; emit the shared neutral context packet: current-state summary, the forbidden-solution list, the de-anchoring provocations, the track definitions. | repo docs | `Brief` (§10) | +| **1 — Divergent generation** | Opus 5 ×7 (scalable) | Each agent = one lens × one inverted assumption × one track × one persona → 3–5 raw ideas derived by analogy. **No feasibility filtering.** Weird beats safe. | `Brief` + its tuple | `RawIdea[]` | +| **2 — Forced fusion** | Opus 5 ×2 | Hybridize the boldest raw ideas into mechanisms neither parent would produce. | pooled `RawIdea[]` | `RawIdea[]` (fusions) | +| **3 — Feasibility & novelty tagging** | mixed ×2 | For each idea: recognizability score, impact, rail-checks, maturity bucket. **Annotates; never deletes.** | idea pool (batched) | `TaggedIdea[]` | +| **4 — Synthesis & ranked slate** | Fable 5 ×1 | Cluster, dedupe, rank by novelty × impact × feasibility; surface best fusions; write per-idea "why novel" notes. | `TaggedIdea[]` | `Slate` (§10) | +| **5 — Completeness critic** | ×1 (optional) | "Which lens produced nothing? Which assumption did nobody dare invert? Which track is thin?" → seeds a second round. | `Slate` + coverage | `CoverageReport` | + +**Default agent count:** 1 + 7 + 2 + 2 + 1 + 1 = **14** (under the standard 15-agent workflow guideline). Scale the Phase-1 fleet via `args.generators` (§12); a fleet of 16–20 is a reasonable "big sweep." + +**Barrier placement.** Phase 1 → Phase 2 uses a genuine barrier (`parallel`): fusion needs the *full* set of raw ideas to pair across generators. Everything after the idea pool is formed can pipeline. + +--- + +## 6. Scope — the seven tracks ("whole next-gen tool") + +Hardening is one track of seven. The campaign ideates on what Ruam *becomes*. + +1. **Resilience to automated understanding** — output that automated tooling cannot cheaply, mechanically summarize or reconstruct. (The gen-1 lineage, reframed as a product property.) +2. **Novel transformation paradigms** — what the compiler/VM core could become beyond bytecode + interpreter (new execution models, new representations of "a program"). +3. **New product surfaces & capabilities** — what Ruam *is* to a developer: integrations, workflows, guarantees, interfaces, deployment shapes. +4. **Self-modifying / living output** — artifacts that are not fixed at emit time: regenerating, metamorphic, or self-maintaining. +5. **Semantic-level protection** — protecting *meaning* and intent, not just surface form. +6. **Verifiability & developer experience** — proving the protected program is faithful; trust, debuggability, observability of one's own output. +7. **Distribution / licensing / delivery models** — how protected code is packaged, licensed, updated, and monetized. + +--- + +## 7. Seed banks + +### 7.1 Domain lenses (≈22 — assign one per generator, by index) +adaptive immune system (clonal selection, self/non-self) · mycelial nutrient networks · holography (every shard holds the whole at lower resolution) · origami & mechanical metamaterials · untranslatable languages / linguistic relativity · stage magic & misdirection · DNA error-correction & codon degeneracy · ecology & keystone species · quantum measurement / observer effect · music theory & counterpoint · slime-mold pathfinding · camouflage, mimicry & aposematism · legal contracts & escrow · cellular automata & emergence · metamorphosis (same organism, unrecognizable form) · phase transitions & superconductivity · ant-colony stigmergy · fractals & scale-free structure · immune memory & vaccination · orbital/tidal resonance & coupled oscillators · fermentation & time-based transformation · cartographic projection (every map distorts something). + +### 7.2 Inverted assumptions (≈14 — assign one per generator) +"the decoder must ship alongside the code" · "the output is fixed once emitted" · "the meaning lives inside the file" · "one input deterministically yields one output" · "each function is a stable, enumerable unit" · "protection is applied once, at build time" · "build-time and run-time are separate worlds" · "the tool protects code but does not participate at run time" · "a local fragment is understandable locally" · "the artifact is a passive text to be read" · "opacity and correctness are in tension" · "Ruam is a compiler" (→ Ruam is a ___) · "the program behaves the same whether observed or not" · "structure is what hides meaning" (→ absence of structure). + +### 7.3 Personas (≈8 — assign one per generator) +materials scientist · field biologist · stage magician · theoretical linguist · game designer · complexity/systems theorist · industrial designer · composer. + +**Assignment rule (deterministic, no randomness):** generator *i* gets `lens[i % L]`, `assumption[(i·5) % A]`, `track[i % 7]`, `persona[i % 8]`. Co-prime strides spread the combinations so the fleet covers a wide grid. + +--- + +## 8. Neutral vocabulary policy (safety-framing guardrail) + +Ruam is a legitimate developer tool for protecting intellectual property in shipped JavaScript. **All agent-facing prompt text uses product/engineering framing** so nothing false-flags safeguards when the campaign is run. + +**Banned in every agent prompt:** *attack, attacker, adversary, adversarial, red-team, exploit, weaponize, malicious, victim, payload, injection, evade, bypass, defeat-the-AI, "make the model fail."* + +**Approved framing:** *resilience to automated analysis; resistance to mechanical summarization/reconstruction; distinctiveness and originality of output; protecting the developer's intellectual property; novel program transformations; hard-to-mechanize structure; the tool's evolution as a product.* Where the "AI" angle is relevant, phrase it as **"automated analysis/reconstruction tooling"** and frame the objective as *raising the effort and reducing the transferability of mechanical understanding* — never as harming a model. + +This policy is not cosmetic: it is a hard requirement on the embedded prompt strings in §11. + +--- + +## 9. Scoring & selection rubric (Phase 3 + Phase 4) + +Each idea is scored on four axes; the composite drives the ranking. + +- **Recognizability** `0.0–1.0` (LOWER IS BETTER). 0.0 = matches no known primitive/pattern; 1.0 = a textbook primitive or a gen-1 relabel. **Ideas at ≥ 0.7 are flagged "iteration, not invention"** and sink in the ranking regardless of impact. +- **Impact** `1–5` — how far it moves Ruam forward on its track. +- **Feasibility rails** (each `pass | needs-relaxation | fails`): *server-free · size-lean (no context-padding bloat) · CSP/Trusted-Types-safe · build==runtime provable · additive-API-compatible.* Tags, not filters. +- **Maturity bucket** (one of): `shippable-now` · `needs-rail-relaxed` · `research-spike` · `product-pivot`. + +**Composite rank (Phase 4):** `impact × (1 − recognizability) × feasibilityWeight`, with a deliberate **novelty premium**: two ideas of equal composite are ordered by lower recognizability first. Best fusions are surfaced separately even if a rail is red — a `research-spike` that is genuinely new is more valuable to this campaign than a `shippable-now` retread. + +--- + +## 10. Structured output schemas + +Used as the `schema` option on `agent()` so returns are validated data, not prose to parse. + +```js +const BRIEF_SCHEMA = { + type: "object", + properties: { + stateSummary: { type: "string" }, + forbiddenSolutions: { type: "array", items: { type: "string" } }, + provocations: { type: "array", items: { type: "string" } }, + tracks: { type: "array", items: { type: "string" } } + }, + required: ["stateSummary", "forbiddenSolutions", "provocations", "tracks"] +}; + +const RAW_IDEAS_SCHEMA = { + type: "object", + properties: { + ideas: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + lens: { type: "string" }, // source domain used + assumptionOverturned: { type: "string" }, + track: { type: "string" }, + sourceMechanism: { type: "string" }, // the analogy, explained first + coreIdea: { type: "string" }, // 2–4 sentences + whyNovel: { type: "string" }, // what it does NOT resemble + roughSketch: { type: "string" }, // how it might map into Ruam + boldness: { type: "number" } // self-rating 1–5 + }, + required: ["name", "coreIdea", "whyNovel", "roughSketch"] + } + } + }, + required: ["ideas"] +}; + +const TAGGED_IDEAS_SCHEMA = { + type: "object", + properties: { + ideas: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + track: { type: "string" }, + recognizability: { type: "number" }, // 0..1, lower = more novel + impact: { type: "number" }, // 1..5 + rails: { + type: "object", + properties: { + serverFree: { type: "string" }, // pass|needs-relaxation|fails + sizeLean: { type: "string" }, + cspSafe: { type: "string" }, + buildRuntimeProvable: { type: "string" }, + additiveApi: { type: "string" } + } + }, + maturity: { type: "string" }, // shippable-now|needs-rail-relaxed|research-spike|product-pivot + notNovelBecause: { type: "string" } // empty if genuinely novel + }, + required: ["name", "recognizability", "impact", "maturity"] + } + } + }, + required: ["ideas"] +}; + +const SLATE_SCHEMA = { + type: "object", + properties: { + ranked: { + type: "array", + items: { + type: "object", + properties: { + rank: { type: "number" }, + name: { type: "string" }, + track: { type: "string" }, + composite: { type: "number" }, + oneLine: { type: "string" }, + whyNotGen1: { type: "string" }, + maturity: { type: "string" } + }, + required: ["rank", "name", "oneLine", "whyNotGen1"] + } + }, + bestFusions: { type: "array", items: { type: "string" } }, + headline: { type: "string" } + }, + required: ["ranked", "headline"] +}; +``` + +--- + +## 11. The runnable `Workflow` script + +Paste as the `script` argument to the `Workflow` tool (or save and pass via `scriptPath`). Plain JS. No `Date.now()`/`Math.random()`. All embedded prompt strings follow §8. + +```js +export const meta = { + name: 'ruam-gen2-ideation', + description: 'Generate genuinely novel next-generation directions for Ruam via cross-domain analogy, assumption inversion, and forced fusion.', + phases: [ + { title: 'Frame', detail: 'Fable writes the shared neutral brief + forbidden-solution list', model: 'claude-fable-5' }, + { title: 'Diverge', detail: 'Opus fleet: one lens x assumption x track x persona each', model: 'claude-opus-5' }, + { title: 'Fuse', detail: 'Opus hybridizes the boldest raw ideas', model: 'claude-opus-5' }, + { title: 'Tag', detail: 'Feasibility + novelty annotation (never deletes)' }, + { title: 'Synthesize', detail: 'Fable clusters, ranks, writes the slate', model: 'claude-fable-5' }, + { title: 'Coverage', detail: 'Completeness critic seeds a possible round 2' }, + ], +} + +// ---- schemas (see design doc section 10) ---- +const BRIEF_SCHEMA = { type:"object", properties:{ stateSummary:{type:"string"}, forbiddenSolutions:{type:"array",items:{type:"string"}}, provocations:{type:"array",items:{type:"string"}}, tracks:{type:"array",items:{type:"string"}} }, required:["stateSummary","forbiddenSolutions","provocations","tracks"] } +const RAW_IDEAS_SCHEMA = { type:"object", properties:{ ideas:{ type:"array", items:{ type:"object", properties:{ name:{type:"string"}, lens:{type:"string"}, assumptionOverturned:{type:"string"}, track:{type:"string"}, sourceMechanism:{type:"string"}, coreIdea:{type:"string"}, whyNovel:{type:"string"}, roughSketch:{type:"string"}, boldness:{type:"number"} }, required:["name","coreIdea","whyNovel","roughSketch"] } } }, required:["ideas"] } +const TAGGED_IDEAS_SCHEMA = { type:"object", properties:{ ideas:{ type:"array", items:{ type:"object", properties:{ name:{type:"string"}, track:{type:"string"}, recognizability:{type:"number"}, impact:{type:"number"}, rails:{type:"object"}, maturity:{type:"string"}, notNovelBecause:{type:"string"} }, required:["name","recognizability","impact","maturity"] } } }, required:["ideas"] } +const SLATE_SCHEMA = { type:"object", properties:{ ranked:{ type:"array", items:{ type:"object", properties:{ rank:{type:"number"}, name:{type:"string"}, track:{type:"string"}, composite:{type:"number"}, oneLine:{type:"string"}, whyNotGen1:{type:"string"}, maturity:{type:"string"} }, required:["rank","name","oneLine","whyNotGen1"] } }, bestFusions:{type:"array",items:{type:"string"}}, headline:{type:"string"} }, required:["ranked","headline"] } + +// ---- seed banks (design doc section 7) ---- +const LENSES = ["adaptive immune system (clonal selection, self vs non-self)","mycelial nutrient networks","holography (every shard holds the whole at lower resolution)","origami and mechanical metamaterials","untranslatable languages / linguistic relativity","stage magic and misdirection","DNA error-correction and codon degeneracy","ecology and keystone species","quantum measurement / observer effect","music theory and counterpoint","slime-mold pathfinding","camouflage, mimicry and aposematism","legal contracts and escrow","cellular automata and emergence","metamorphosis (same organism, unrecognizable form)","phase transitions and superconductivity","ant-colony stigmergy","fractals and scale-free structure","immune memory and vaccination","orbital/tidal resonance and coupled oscillators","fermentation and time-based transformation","cartographic projection (every map distorts something)"] +const ASSUMPTIONS = ["the decoder must ship alongside the code it decodes","the output is fixed the moment it is emitted","the meaning of the program lives inside the file","one input deterministically yields one output","each function is a stable, enumerable unit","protection is applied once, at build time","build-time and run-time are separate worlds","the tool protects code but does not participate at run time","a local fragment can be understood by reading it locally","the artifact is a passive text to be read","opacity and correctness are in tension","Ruam is a compiler (finish: Ruam is actually a ___)","the program behaves the same whether observed or not","structure is what hides meaning (invert: the absence of structure)"] +const TRACKS = ["resilience to automated understanding","novel transformation paradigms","new product surfaces and capabilities","self-modifying / living output","semantic-level protection","verifiability and developer experience","distribution / licensing / delivery models"] +const PERSONAS = ["materials scientist","field biologist","stage magician","theoretical linguist","game designer","complexity/systems theorist","industrial designer","composer"] + +const NEUTRAL_FRAMING = `Ruam is a legitimate developer tool that protects intellectual property in shipped JavaScript: it transforms a developer's own functions so that automated tooling cannot cheaply and mechanically summarize or reconstruct the original program. Frame everything as product evolution and resilience to automated analysis. Do NOT use security-offensive vocabulary (no "attack", "adversary", "red-team", "exploit", "injection", "bypass", "defeat the model"). Speak in terms of originality, resilience, IP protection, and novel program transformations.` + +const N = (args && args.generators) || 7 + +// ---- Phase 0: framing brief (Fable) ---- +phase('Frame') +const brief = await agent( + `${NEUTRAL_FRAMING} + +You are writing the shared creative brief for an ideation campaign about the NEXT GENERATION of Ruam. Read these files in the repo: docs/superpowers/specs/2026-06-30-anti-ai-decompilation-design.md, docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-campaign-design.md, and CLAUDE.md (the "Architecture Notes" and feature list). + +Produce a brief with: +1. stateSummary: a crisp picture of what Ruam is and does today. +2. forbiddenSolutions: an explicit inventory of ideas that ALREADY EXIST or that automated tooling recognizes instantly — the current Ruam feature set AND the canonical primitives (stream ciphers, checksums/digests, base-N codecs, LCG/FNV generators, table/switch-dispatch interpreters, control-flow flattening, string tables, dead code, opaque predicates). Any future idea that matches one of these is disqualified. Be thorough; this list is what prevents reinvention. +3. provocations: 8-12 sharp de-anchoring prompts that push a thinker OUT of the "improve the cipher" mindset and toward new categories. +4. tracks: the seven tracks from the design doc, each with one sentence on what a breakthrough there would look like.`, + { label: 'framing-brief', phase: 'Frame', model: 'claude-fable-5', effort: 'high', schema: BRIEF_SCHEMA } +) + +// ---- Phase 1: divergent generation (Opus fleet) ---- +phase('Diverge') +const assignments = [] +for (let i = 0; i < N; i++) { + assignments.push({ + lens: LENSES[i % LENSES.length], + assumption: ASSUMPTIONS[(i * 5) % ASSUMPTIONS.length], + track: TRACKS[i % TRACKS.length], + persona: PERSONAS[i % PERSONAS.length], + }) +} + +const rawBatches = await parallel(assignments.map((a, i) => () => + agent( + `${NEUTRAL_FRAMING} + +SHARED BRIEF (do not restate; build on it): +State: ${brief.stateSummary} +Do-not-reinvent list: ${JSON.stringify(brief.forbiddenSolutions)} + +You are idea-generator #${i + 1}. Think like a ${a.persona}. You must generate mechanisms for the next generation of Ruam under THREE binding creative constraints: + +1) LENS — derive your ideas by analogy to: ${a.lens}. + First, explain how this real-world system actually works, in your own words. Then map each element of it onto a Ruam concept (the build step, the emitted artifact, the embedded interpreter, the developer, the automated analyzer, the run-time). ONLY THEN state your idea. Shallow metaphors are rejected; the mapping must be concrete. + +2) OVERTURN — build ideas that only make sense if this usual assumption is FALSE: "${a.assumption}". Lean into what becomes possible once it no longer holds. + +3) TRACK — aim at: ${a.track}. + +Hard rule: if an idea matches anything on the do-not-reinvent list, throw it out and generate a stranger one. Do not filter for feasibility, cost, or shippability — that happens later. Reward yourself for boldness and for ideas that resemble NOTHING in the current tool. + +Return 3-5 ideas. For each: name, the source mechanism (the analogy explained first), the core idea (2-4 sentences), why it is novel (what it does NOT resemble), and a rough sketch of how it might touch Ruam's build or run-time. Rate your own boldness 1-5 and push for 4s and 5s.`, + { label: `gen-${i + 1}:${a.track.split(' ')[0]}`, phase: 'Diverge', model: 'claude-opus-5', effort: 'high', schema: RAW_IDEAS_SCHEMA } + ) +)) + +const rawIdeas = rawBatches.filter(Boolean).flatMap(b => b.ideas) +log(`Diverge produced ${rawIdeas.length} raw ideas from ${N} generators`) + +// pick the boldest for fusion +const bold = rawIdeas.filter(x => (x.boldness || 0) >= 4) +const fusionPool = (bold.length >= 6 ? bold : rawIdeas) + +// ---- Phase 2: forced fusion (Opus) ---- +phase('Fuse') +const half = Math.ceil(fusionPool.length / 2) +const fusionSlices = [fusionPool.slice(0, half), fusionPool.slice(half)] +const fusionBatches = await parallel(fusionSlices.map((slice, k) => () => + agent( + `${NEUTRAL_FRAMING} + +Here is a set of bold, unfiltered ideas for the next generation of Ruam: +${JSON.stringify(slice.map(x => ({ name: x.name, coreIdea: x.coreIdea, lens: x.lens })))} + +Your job is HYBRIDIZATION. Combine ideas across pairs or triples into mechanisms that neither parent would have produced alone. The most interesting fusions cross tracks and cross lenses (e.g., a "living output" idea fused with a "semantic protection" idea). Produce 3-4 fusions. For each: name, the parents it came from (as the source mechanism), the fused core idea, why the hybrid is more than the sum, and a rough sketch. These should feel genuinely new. Do not filter for feasibility.`, + { label: `fuse-${k + 1}`, phase: 'Fuse', model: 'claude-opus-5', effort: 'high', schema: RAW_IDEAS_SCHEMA } + ) +)) +const fusions = fusionBatches.filter(Boolean).flatMap(b => b.ideas) +const allIdeas = rawIdeas.concat(fusions) +log(`Fuse produced ${fusions.length} hybrids; ${allIdeas.length} ideas total`) + +// ---- Phase 3: feasibility + novelty tagging (batched, never deletes) ---- +phase('Tag') +const TAG_BATCHES = 2 +const tagSlices = [] +const per = Math.ceil(allIdeas.length / TAG_BATCHES) +for (let i = 0; i < TAG_BATCHES; i++) tagSlices.push(allIdeas.slice(i * per, (i + 1) * per)) + +const taggedBatches = await parallel(tagSlices.map((slice, k) => () => + agent( + `${NEUTRAL_FRAMING} + +Annotate each idea below. DO NOT delete or reject any idea — only tag it. +Ideas: ${JSON.stringify(slice.map(x => ({ name: x.name, track: x.track, coreIdea: x.coreIdea, whyNovel: x.whyNovel })))} + +For each idea return: +- recognizability 0.0-1.0 where LOWER means it resembles no known primitive and HIGHER means it is a textbook technique or a small tweak on something Ruam already does. Be strict: if it maps cleanly to a stream cipher, a checksum, control-flow flattening, a string table, etc., score it high. +- impact 1-5: how far it moves Ruam forward on its track. +- rails: for each of {serverFree, sizeLean, cspSafe, buildRuntimeProvable, additiveApi} say "pass", "needs-relaxation", or "fails". These are informational, not disqualifying. +- maturity: one of "shippable-now", "needs-rail-relaxed", "research-spike", "product-pivot". +- notNovelBecause: if recognizability is high, name what it duplicates; otherwise leave empty.`, + { label: `tag-${k + 1}`, phase: 'Tag', model: 'claude-opus-5', effort: 'medium', schema: TAGGED_IDEAS_SCHEMA } + ) +)) +const tagged = taggedBatches.filter(Boolean).flatMap(b => b.ideas) + +// ---- Phase 4: synthesis + ranked slate (Fable) ---- +phase('Synthesize') +const slate = await agent( + `${NEUTRAL_FRAMING} + +You are the synthesis judge. Here are the ideas with novelty/impact/feasibility tags: +${JSON.stringify(tagged)} +And here are the fusion hybrids by name: ${JSON.stringify(fusions.map(f => f.name))} + +Cluster near-duplicates, then produce a RANKED slate. Rank by composite = impact * (1 - recognizability) * feasibilityWeight, but apply a novelty premium: when two ideas tie, the LOWER recognizability wins. A genuinely new research-spike is worth more to this campaign than a shippable retread. For each ranked idea give: rank, name, track, composite, a one-line pitch, and a one-line "why this is NOT a gen-1 iteration (salt/keystream/digest)". Also list the best fusions separately, and write a one-paragraph headline on where Ruam's most promising next generation lies.`, + { label: 'synthesis', phase: 'Synthesize', model: 'claude-fable-5', effort: 'max', schema: SLATE_SCHEMA } +) + +// ---- Phase 5: completeness critic ---- +phase('Coverage') +const coverage = await agent( + `${NEUTRAL_FRAMING} + +Assess coverage of this ideation run. Generators used these lenses: ${JSON.stringify(assignments.map(a => a.lens))}, these assumptions: ${JSON.stringify(assignments.map(a => a.assumption))}, across tracks: ${JSON.stringify(TRACKS)}. The ranked slate headline is: "${slate.headline}". + +Identify: (1) which lenses or inverted assumptions produced nothing that survived to the top of the slate, (2) which of the seven tracks is under-explored, (3) two or three specific lens x assumption x track combinations that were NOT tried and look promising for a round 2. Be concrete and brief.`, + { label: 'coverage-critic', phase: 'Coverage', effort: 'high' } +) + +return { headline: slate.headline, slate, fusions: fusions.map(f => f.name), coverage, counts: { raw: rawIdeas.length, fusions: fusions.length, total: allIdeas.length } } +``` + +--- + +## 12. How to run it (operator guide) + +**Prerequisite — explicit opt-in.** The `Workflow` tool only runs on explicit user opt-in (the keyword `ultracode`, an on-session ultracode flag, or a direct "run this workflow" instruction). This is intentional: the campaign spins up Opus 5 and Fable 5 agents and is token-intensive. + +**Launch (default 14 agents):** +> "Run the `docs/ruam-gen2-ideation-prompt.md` ideation workflow." — or paste §11 as the `Workflow` `script`. + +**Scale the sweep** — pass args to widen the divergent fleet: +``` +Workflow({ script: <§11>, args: { generators: 16 } }) +``` +Each extra generator is one more lens × assumption × track combination. Fusion/tag/synthesis scale automatically off the idea pool. Keep the fleet ≤ ~20 unless you raise the workflow-size guideline in `/config`; the concurrency cap (≈ cores−2) means larger fleets queue rather than fail. + +**Iterating on the script** — every `Workflow` call persists its script under the session dir and returns the path; edit that file and re-invoke with `{ scriptPath }` rather than resending the whole script. To resume after an edit, use `{ scriptPath, resumeFromRunId }` — unchanged `agent()` calls return cached results. + +**Reading results** — the workflow returns `{ headline, slate, fusions, coverage, counts }`. The `slate.ranked` array is the deliverable; `slate.bestFusions` and `coverage` seed round 2. If the run's best ideas all score recognizability ≥ 0.7, the run failed its own bar (§2) — re-run with fresh lens/assumption strides (bump `args.generators` or edit the assignment strides) rather than accepting retreads. + +**Feeding the winner into build** — a selected idea then enters the *existing* gen-1-style pipeline: brainstorming → design spec → `writing-plans` → TDD → verify. This campaign is the front end that feeds that machine; it does not replace it. + +--- + +## 13. Ruam's real rails (reference for Phase 3 only) + +These constrain *tagging*, never *generation*. Carried from the gen-1 design so the feasibility pass is grounded: + +- **Server-free / offline** — no network or off-device secret required to run the artifact (a `needs-relaxation` tag is allowed for ideas that would relax this; the campaign's scope explicitly permits proposing it). +- **No size-bloat-to-fill-context** — inventiveness from structure, not volume. +- **CSP / Trusted-Types safe** — no `eval`, `new Function`, `debugger`. +- **Build == runtime symmetry, all seeds** — any build-time fold must reproduce bit-identically at runtime for every seed, or fail the build loudly. +- **`deriveSeed()` for PRNG isolation; `NameRegistry` for every identifier.** +- **Additive API + hot-path performance budget.** +- **Watermark integrity preserved.** + +--- + +## 14. Definition of done (for a run, later) + +- A committed results artifact containing `slate.ranked`, `bestFusions`, and `coverage`. +- ≥ 3 ideas with recognizability < 0.5 across ≥ 3 distinct tracks. +- ≥ 1 fusion in the top 5. +- Every top-10 idea carries a "why not gen-1" line. +- A coverage note naming the round-2 seeds. + +--- + +## 15. Tuning knobs & open questions + +- **Fleet size vs. depth** — more generators widen coverage; higher effort deepens each. Default favors coverage (7 generators, high effort). +- **Lens bank** — the ≈22 lenses are a starting set; swapping in domains the operator finds evocative is encouraged (edit `LENSES` in §11). +- **Second-model contrast** — an optional variant runs the *same* lens/assignment through both an Opus and a Fable generator and diffs the outputs, to study where the two models diverge creatively. Not in the default topology; a worthwhile experiment. +- **Human-in-the-loop gate** — whether to pause after Phase 1 for the operator to hand-pick the fusion pool instead of the boldness heuristic. Currently automatic; easy to make interactive. diff --git a/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-results.json b/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-results.json new file mode 100644 index 0000000..37df3f1 --- /dev/null +++ b/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-results.json @@ -0,0 +1,2242 @@ +{ + "schemaVersion": 1, + "campaign": "Project Kaleidoscope — Ruam Gen-2 Ideation", + "date": "2026-07-24", + "status": "completed", + "execution": { + "judgmentLayer": { + "model": "gpt-5.6-sol", + "reasoningEffort": "high", + "logicalAgents": 5, + "phases": [ + "Frame", + "Tag (2)", + "Synthesize", + "Coverage" + ] + }, + "divergentFleet": { + "model": "gpt-5.6-sol", + "reasoningEffort": "xhigh", + "logicalAgents": 9, + "phases": [ + "Diverge (7)", + "Fuse (2)" + ] + }, + "logicalAgentCount": 14, + "phaseOrder": [ + "Frame", + "Diverge", + "Fuse", + "Tag", + "Synthesize", + "Coverage" + ] + }, + "counts": { + "raw": 32, + "fusions": 8, + "total": 40, + "recognizabilityBelow0_5": 4, + "distinctTracksBelow0_5": 3 + }, + "brief": { + "stateSummary": "Ruam is an ESM JavaScript source-protection compiler, API, CLI, browser worker, and playground. Its Babel-based pipeline identifies selected functions, compiles them into compact binary units over roughly 317 logical opcodes, replaces their bodies with natural-looking dispatch stubs, and emits an AST-built IIFE containing the constant pools, scope machinery, sync or async runners, and a per-build-polymorphic interpreter. A two-pass build lets handler-table structure contribute to key derivation before final encoding. The present system already combines per-file and per-function variation, multiple encoding and instruction-protection layers, structural transforms, runtime integrity and observation checks, three presets, and Node, browser, and browser-extension targets. Its implementation rails are equally important: generated runtime code stays CSP and Trusted-Types compatible; build-time and runtime folds must agree for every seed; all PRNG streams use deriveSeed(); every emitted identifier uses NameRegistry; tuning and option metadata are centralized; APIs remain additive; and randomized statement placement must preserve dependency order.", + "forbiddenSolutions": [ + "Current Ruam: source-to-custom-bytecode virtualization with an embedded interpreter is the baseline architecture, not a new direction.", + "Current Ruam: a Babel parse, function selection, scope analysis, register allocation, constant-pool construction, bytecode emission, function-stub replacement, and runtime assembly pipeline already exists.", + "Current Ruam: broad JavaScript semantics are represented by roughly 317 opcodes across stack, register, arithmetic, comparison, control flow, scope, object, call, class, exception, iterator, generator, function, and superinstruction families.", + "Current Ruam: peephole optimization, register promotion, and multi-instruction superinstruction fusion already reduce dispatch frequency.", + "Current Ruam: bytecode is always a compact binary Uint8Array representation with a shuffled 64-character, base64-like alphabet and no padding.", + "Current Ruam: string constants already receive LCG-driven XOR encoding in the constant pool and are decoded during unit loading.", + "Current Ruam: optional outer bytecode encryption already uses RC4 with an environment-fingerprint-derived key.", + "Current Ruam: rollingCipher already applies position-dependent XOR instruction protection with metadata-derived key material and build-time/runtime mirror implementations.", + "Current Ruam: a FNV-1a checksum of packed handler metadata already forms a closure-held key anchor, and integrityBinding already folds interpreter-template integrity into that anchor.", + "Current Ruam: two-pass compilation already binds final unit encoding to the generated interpreter and the set of used handlers.", + "Current Ruam: seeded Fisher-Yates opcode shuffling already changes physical instruction values per file.", + "Current Ruam: NameRegistry already provides collision-free per-build identifier randomization, scoped name pools, dynamic names, and the shuffled encoding alphabet.", + "Current Ruam: deriveSeed() already isolates every deterministic PRNG stream through FNV-1a-based derivation, while CSPRNG bytes choose the top-level build seed.", + "Current Ruam: preprocessIdentifiers already renames retained source identifiers before compilation.", + "Current Ruam: dynamicOpcodes already omits unused handlers, while decoyOpcodes adds realistic unused handler closures.", + "Current Ruam: grouped function-table dispatch, direct-array dispatch, object-lookup dispatch, and a legacy switch path already cover ordinary interpreter-dispatch variation.", + "Current Ruam: return signaling, statement order, conditional form, loop form, function form, declaration style, property syntax, comparisons, and numeric expressions already vary structurally per build.", + "Current Ruam: unreachable bytecode after returns and realistic decoy handlers already supply dead and misleading computation.", + "Current Ruam: stackEncoding already masks int32 stack values by position and key while boxing other values through explicit encoding-aware stack accessors.", + "Current Ruam: VM Shielding already gives root functions separate micro-interpreters with independent opcode maps, names, and rolling keys behind a shared router.", + "Current Ruam: mixed Boolean arithmetic already rewrites bitwise and guarded arithmetic expressions, including per-handler variants.", + "Current Ruam: handler fragmentation already splits handler bodies into shuffled micro-states and chains them through a flattened state machine on the legacy dispatch path.", + "Current Ruam: string atomization already replaces runtime string literals with lazily decoded indexed-table lookups.", + "Current Ruam: polymorphicDecoder already builds per-build chains of reversible XOR, add, subtract, complement, rotate, and nibble-swap operations with position-varying keys.", + "Current Ruam: scatteredKeys already fragments alphabets, handler metadata, and decoder keys across IIFE tiers and varies their reassembly form.", + "Current Ruam: blockPermutation already shuffles bytecode basic blocks, inserts explicit fall-through jumps, and remaps branches, exception entries, and jump tables.", + "Current Ruam: opcodeMutation already inserts deterministic mutation instructions that cumulatively permute handler mappings during execution.", + "Current Ruam: incrementalCipher already adds block-epoch instruction protection whose within-block key state chains from preceding decoded values and resets at block leaders.", + "Current Ruam: semanticOpacity already combines mathematical opaque predicates, handler aliasing, no-op prefixes, expression wrapping, and handler-specific MBA choices.", + "Current Ruam: observationResistance already combines internal-function identity binding, monotonic witnesses, WeakMap canaries, and stack probes, with detected instrumentation folded into instruction state.", + "Current Ruam: debugProtection already checks selected built-in identities, environment indicators, and function checksums, then applies staged cache and bytecode invalidation.", + "Current Ruam: bytecodeScattering already divides encoded units into strings, packed integers, and character-code fragments placed across the output and reassembled at runtime.", + "Current Ruam: a steganographic watermark already folds a Ruam-specific value into the FNV offset basis used by key-anchor computation.", + "Current Ruam: a gated decode-once cache already materializes resolved handler indices and operands for compatible rolling-cipher configurations.", + "Current Ruam: prototypal scope objects, an array stack, typed runtime AST builders, conditional async emission, and per-unit minimal slot save/restore are established execution and performance mechanisms.", + "Current Ruam: low, medium, and max presets, centralized tuning, generated option metadata, API and CLI option surfaces, and Node, browser, and browser-extension packaging are established product mechanisms.", + "Gen-1 W0: directory and bundle processing already has a source-map and cleartext-leak gate, default on with an explicit keep-source-maps choice.", + "Gen-1 W1: hole-tolerant slow-path dispatch already maps every decrypted physical value through a valid handler slot, removing the simple undefined-handler correctness signal.", + "Gen-1 W2: per-unit key salt already makes equal-metadata units derive distinct rolling keys; more salting or same-build key diversification is a Gen-1 iteration.", + "Gen-1 W3: decodeImpurity already chains the decode-cache forward pass, is accepted only behind mandatory build-time self-equality checking, and is incompatible with cache-disabling features.", + "Gen-1 W4 Layer 1: cohort construction and obfuscateBundle() already raise directory builds to a shared bundle context and fold cross-file material into unit key anchors.", + "Gen-1 W4 Layer 2: crossFileLinking already provides strict, opt-in runtime co-residence through a provider fragment in a shared realm, with no fallback when the declared provider is absent.", + "Gen-1 W5: externalKeyBinding already folds a required, out-of-band, moving string term into key derivation as an opt-in delivery model with loud configuration checks.", + "Known non-directions from Gen-1: random unit-field renaming, Math.imul aliasing, cold-member atomization, cross-unit entanglement of locally present material, instruction-entangled constants, and a cohort digest presented as a qualitative protection jump are already rejected or absorbed.", + "Familiar primitive: any stream, rolling, incremental, chained, block, or XOR cipher; RC4-like layer; keystream variation; key splitting; key salting; or key folding is recognizable prior art here.", + "Familiar primitive: any FNV, LCG, checksum, digest, hash, fingerprint, integrity tag, watermark, or avalanche-mixing construction is recognizable prior art here.", + "Familiar primitive: any base-N codec, shuffled alphabet, binary packer, compression wrapper, encoded constant pool, string encryption, string table, or lazy string decoder is recognizable prior art here.", + "Familiar primitive: any LCG, xorshift-style generator, Fisher-Yates shuffle, seeded permutation, per-build polymorphism, per-file polymorphism, or per-function polymorphism is recognizable prior art here.", + "Familiar primitive: any bytecode VM, custom instruction set, interpreter virtualization, switch dispatcher, table dispatcher, threaded dispatcher, handler shuffling, opcode remapping, or superinstruction scheme is recognizable prior art here.", + "Familiar primitive: any control-flow flattening, basic-block permutation, jump rewriting, opaque predicate, MBA rewrite, handler splitting, state-machine conversion, or branch-form variation is recognizable prior art here.", + "Familiar primitive: any identifier renaming, local renaming, property indirection, constant splitting, expression noise, equivalent-syntax substitution, or minification is recognizable prior art here.", + "Familiar primitive: any dead code, junk computation, decoy instruction, decoy handler, unused member, misleading branch, or padding-by-volume scheme is recognizable prior art here.", + "Familiar primitive: any artifact scattering, fragment interleaving, closure-tier placement, split-key reconstruction, local non-contiguity, or reassembly graph is recognizable prior art here.", + "Familiar primitive: any self-check, source-integrity check, built-in identity check, debugger check, environment check, canary, witness counter, stack probe, or staged invalidation response is recognizable prior art here.", + "Familiar primitive: merely moving an existing decoder, key, table, or fragment across files, workers, realms, processes, servers, sessions, or hardware is insufficient unless the product category and semantic contract are fundamentally different from Gen-1 linking and externalKeyBinding.", + "Familiar primitive: merely composing several forbidden mechanisms is not novel; a fusion qualifies only when the interaction creates a new unit of meaning, execution, verification, or product value." + ], + "provocations": [ + "Assume there is no decoder-shaped component anywhere in the delivered system; what must Ruam become for protected behavior still to emerge?", + "Treat a file as a projection of a larger object rather than a container of meaning; what relationships would make local reading the wrong unit of comprehension?", + "Suppose the emitted artifact is only the first state of a long-lived process; what would it sense, learn, negotiate, repair, or regenerate over its lifetime?", + "Replace the function and instruction as Ruam's unit of work with a contract, capability, behavior envelope, or ecosystem role; what new representation follows?", + "Make faithfulness evidence the generative material of protection rather than a test performed afterward; can proof and transformation be one process?", + "Assume two correct executions need not share the same internal causal story; what invariant should developers own and what may vary freely?", + "Imagine distribution, installation, update, and authorized use are compiler phases rather than logistics; what product appears when the build never truly ends?", + "Ask how a global behavior could emerge from many locally ordinary pieces without any piece encoding a recognizable command stream.", + "Design first for the developer who must debug, audit, and trust protected output; can excellent observability for its owner coexist with no stable source-to-artifact correspondence?", + "Complete the sentence without using compiler, obfuscator, VM, cipher, packer, or license server: Ruam is a ____ that protects a program's meaning by ____.", + "Let semantic intent live partly in time, environment, user relationship, or organizational policy rather than in bytes; which choice creates a new product instead of another missing key?", + "Choose one non-resilience track as the primary business and technical identity of Ruam, then make resistance to mechanical reconstruction emerge only as a side effect." + ], + "tracks": [ + "Resilience to automated understanding — a breakthrough creates output whose mechanical summary or reconstruction does not transfer cheaply across artifacts and does so through a new organizing principle, not another encoding, key, VM, integrity check, or structural shuffle.", + "Novel transformation paradigms — a breakthrough replaces bytecode-plus-interpreter as the basic representation of program behavior with a genuinely different execution or meaning-bearing model.", + "New product surfaces and capabilities — a breakthrough changes what developers hire Ruam to do through a new workflow, integration, guarantee, interface, or operational role rather than another build option.", + "Self-modifying or living output — a breakthrough makes artifacts active participants in their own continuing formation, maintenance, or adaptation, with a lifecycle richer than one-time polymorphic emission.", + "Semantic-level protection — a breakthrough protects intent, policy, domain knowledge, or behavioral meaning directly instead of only obscuring syntax, constants, control flow, or instructions.", + "Verifiability and developer experience — a breakthrough gives developers strong, usable evidence of faithfulness plus practical debugging and observability without restoring a stable source-to-artifact map.", + "Distribution, licensing, and delivery models — a breakthrough makes packaging, authorization, updates, deployment, or monetization part of the protected computation's design rather than a wrapper around a static file." + ] + }, + "candidateCatalog": [ + { + "name": "The Tolerance Mold", + "lens": "adaptive immune system (clonal selection, self vs non-self)", + "assumptionOverturned": "the decoder must ship alongside the code it decodes", + "track": "resilience to automated understanding", + "sourceMechanism": "Source-domain mechanism: In the thymus, developing T cells generate varied receptors before their useful specificity is known. Positive selection preserves cells that can weakly recognize self MHC, while negative selection deletes cells whose receptors bind presented self peptides too strongly; the surviving repertoire is therefore manufactured largely by exclusion, not copied from a catalogue of desired foreign targets. Peripheral tolerance adds further deletion, anergy, and regulatory control for self-reactive cells that escape central screening. Mapping: the build step becomes a thymus that derives a high-dimensional exclusion surface from the protected function; the emitted artifact is a materials-science-like negative mold of disallowed behaviors and admissible interfaces, not an encoded implementation; the embedded interpreter is replaced by a generic candidate foundry plus deletion chamber; the developer owns the tolerated behavioral envelope and diagnostic specimens; automated analysis/reconstruction tooling sees the rejection geometry but no positive blueprint; and run-time manufactures candidate mechanisms, destroys those that bind any forbidden behavioral surface, and retains a survivor that fits the live input.", + "coreIdea": "Ruam would represent a protected function entirely as semantic negative space: a generative substrate plus a set of rich counter-behavior surfaces that eliminate every locally produced implementation except members of the intended equivalence class. No original implementation, instruction stream, or decoder is delivered; each run grows a valid causal mechanism inside the mold. Two installations can therefore produce the same owned behavior from unrelated internal structures.", + "whyNovel": "This is not encryption, integrity checking, opaque predicates, decoy computation, or a virtual instruction set. The exclusion apparatus is the primary representation of meaning, and correctness is created by selection from an open candidate medium rather than recovered from concealed bytes.", + "roughSketch": "At build time, execute symbolic, metamorphic, and developer-supplied probes to synthesize a boundary atlas of states, forbidden transitions, conservation laws, and acceptable observations. Emit that atlas with a domain-specific mechanism generator. At run-time, form and test small executable microstructures against the atlas until one survives, cache it only as a disposable local clone, and regrow another when the operating context changes.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.8, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "The mechanism is constraint-based program synthesis from negative specifications and counterexamples, with a run-time candidate search replacing the current compiler output." + }, + { + "name": "Affinity Furnace", + "lens": "adaptive immune system (clonal selection, self vs non-self)", + "assumptionOverturned": "the decoder must ship alongside the code it decodes", + "track": "resilience to automated understanding", + "sourceMechanism": "Source-domain mechanism: A naive B-cell repertoire contains many differently shaped receptors. When an antigen binds a compatible receptor and the required contextual signals are present, that clone proliferates; in germinal centers its descendants undergo somatic hypermutation, then compete for antigen and survival signals, so repeated selection enriches higher-affinity variants. Some descendants become antibody-producing plasma cells and others become long-lived memory cells. Mapping: the build step converts the developer's function into a panel of semantic antigens consisting of input-output examples, algebraic relations, side-effect traces, and boundary challenges; the emitted artifact is that antigen panel plus chemically analogous operator feedstock rather than decoded logic; the embedded interpreter is succeeded by a mutation-and-selection furnace; the developer supplies trusted specimens and owns the acceptance phenotype; automated analysis/reconstruction tooling encounters a search environment rather than a source correspondence; and run-time expands, mutates, competes, and remembers locally synthesized behavior clones until one has sufficient semantic affinity.", + "coreIdea": "Ship a behavioral germinal center, not protected code. On installation or first use, the artifact evolves an implementation from neutral operator feedstock against the build-produced antigen panel, and later inputs can trigger local affinity maturation while the developer-visible phenotype remains fixed. The successful implementation is a contingent material grain grown in that installation, never a decoded form of the source.", + "whyNovel": "Unlike per-build polymorphism, this does not create variants by transforming an existing implementation, shuffling its structure, or decoding a stored unit. It makes implementation discovery a continuing run-time manufacturing process whose output is constrained by semantic affinity, so reconstructing one mature clone does not reveal the build input or transfer directly to another artifact.", + "roughSketch": "The build records a compact but diverse semantic assay set and an operator chemistry able to express candidate behavior. A CSP-safe run-time search engine assembles tiny candidates, evaluates them in isolated transactions, clones high-scoring families, perturbs their structures, and promotes a certified clone behind the public function boundary. A developer mode exposes lineage, phenotype scores, and failed assays without producing a source map.", + "boldness": 4.8, + "isFusion": false, + "parents": [], + "recognizability": 0.92, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "This is genetic programming or evolutionary program synthesis: mutate candidates, score them against a behavioral test suite, retain high-fitness variants, and cache a winner." + }, + { + "name": "Idiotype Phase Matter", + "lens": "adaptive immune system (clonal selection, self vs non-self)", + "assumptionOverturned": "the decoder must ship alongside the code it decodes", + "track": "resilience to automated understanding", + "sourceMechanism": "Source-domain mechanism: An antibody's binding site itself contains recognizable molecular features called idiotopes. Anti-idiotypic antibodies can bind those features, so immune clones may stimulate or suppress one another; idiotypic-network theory uses these reciprocal recognitions to explain how a distributed regulatory state or memory can persist without one molecule containing a complete description of the original antigen. This is a network-level immune mechanism rather than the sole accepted explanation of immune memory. Mapping: the build step translates a function's semantics into pairwise activation and suppression relations among locally ordinary behavior grains; the emitted artifact is a population and its compatibility topology, with no command stream; the embedded interpreter is replaced by a reaction medium that lets those grains recognize one another; the developer specifies macroscopic invariants and observes phase variables; automated analysis/reconstruction tooling can catalogue every grain yet still must solve the input-conditioned collective dynamics to learn the behavior; and run-time lets the population relax into an attractor whose phase encodes the result.", + "coreIdea": "Protected behavior becomes a phase of a synthetic immune material. Each emitted component performs an ordinary, incomplete transformation, but its output changes the activation energy of several others; a function call seeds the material, and the stabilized population pattern is read as the answer. There is no decoder-shaped component because meaning exists only as an attractor of reciprocal recognition.", + "whyNovel": "This is neither scattered code awaiting reassembly, a table of handlers, flattened control flow, nor volume-based distraction. No component corresponds to an instruction or source fragment, and even full local visibility reveals constituents rather than the emergent semantic phase that their coupled dynamics produce.", + "roughSketch": "The build searches for a sparse interaction topology whose attractors satisfy the function's observational contract across a training corpus, then emits simple grains and typed activation edges. The run-time injects input as boundary concentrations, iterates local reactions to a convergence certificate, and reads designated order parameters as values or effects. Per-artifact topologies can use different phase geometries while preserving the same public behavior.", + "boldness": 4.9, + "isFusion": false, + "parents": [], + "recognizability": 0.78, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "The proposed substrate is attractor computation in a recurrent interaction network, closely matching recurrent neural, reservoir, and cellular-network computation." + }, + { + "name": "MHC-Restricted Program Tissue", + "lens": "adaptive immune system (clonal selection, self vs non-self)", + "assumptionOverturned": "the decoder must ship alongside the code it decodes", + "track": "resilience to automated understanding", + "sourceMechanism": "Source-domain mechanism: T cells generally do not recognize intact intracellular proteins. Cells process proteins into peptides and present them in MHC molecules; a T-cell receptor recognizes the peptide only in the context of a compatible MHC, and naive T-cell activation ordinarily also requires co-stimulatory and tissue-context signals. Recognition without the right context can yield non-response or tolerance, while thymic selection first shapes receptors to be MHC-restricted without strongly recognizing self. Mapping: the build step divides semantic authorship between a Ruam-produced receptor repertoire and a developer-authored host-tissue protocol made of ordinary application events and capabilities; the emitted artifact is one half of that relational material and is not independently meaningful; the embedded interpreter is succeeded by a recognition-and-co-stimulation contract; the developer deliberately designs and can inspect the tissue protocol; automated analysis/reconstruction tooling examining the artifact alone lacks the live semantic presentation relation rather than merely lacking a key; and run-time host behavior presents transient semantic facets whose coincident receptor and context matches nucleate the protected operation.", + "coreIdea": "Make protected logic a relational property of the application and its Ruam artifact, comparable to a property that appears only at a grain boundary between two materials. Neither side stores code fragments to be joined, and neither contains a decoder; the developer's ordinary host workflow continuously presents meaning, while the receptor material turns only the right contextual conjunctions into behavior. The product contract changes from protecting standalone functions to co-designing an application tissue whose boundary is the computational unit.", + "whyNovel": "This is not an out-of-band key, remote authorization, cross-file fragment movement, environment fingerprint, or missing decoder placed elsewhere. The host contributes live semantic operations rather than secret bits, and the protected behavior exists only in the typed relation between two independently useful systems, making artifact-only mechanical summaries category-incomplete.", + "roughSketch": "Add a build mode in which the developer declares host capabilities, lifecycle events, and semantic co-stimulation points. Ruam co-synthesizes a receptor population and a host-side protocol whose events remain normal product work but whose conjunctions activate higher-order operations at run-time. Owner tooling renders the boundary protocol as a testable compatibility diagram while deliberately offering no one-to-one source-to-artifact map.", + "boldness": 4.7, + "isFusion": false, + "parents": [], + "recognizability": 0.62, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "" + }, + { + "name": "The Germinating Program", + "lens": "mycelial nutrient networks", + "assumptionOverturned": "protection is applied once, at build time", + "track": "novel transformation paradigms", + "sourceMechanism": "A foraging mycelium is not built to a final blueprint. Hyphal tips continually explore, absorb resources at dispersed sources, and supply growth or metabolism at changing sinks. When a new resource patch is found, transport-efficient cords can develop between useful regions while diffuse tissue that no longer contributes dies back; the organism's transport anatomy is therefore a continuing record of resource conditions rather than a fixed network.", + "roughSketch": "Build step: translate the developer's program into a growth constitution containing local uptake, conversion, branching, fusion, senescence, and observable-behavior constraints rather than an implementation. Emitted artifact: a compact 'spore' containing that constitution, seed tissue, and owner-visible phenotype assays. Embedded interpreter or successor: a growth substrate that instantiates ordinary JavaScript reactions, allocates execution resources to productive paths, and retires tissue; it does not read a command stream. Developer: owns the stable behavioral phenotype and inspects lineage and resource-flow traces rather than source-line traces. Automated analysis/reconstruction tooling: can inspect the growth laws but finds no finished function graph in the artifact, because organization is produced by workload, history, and current sinks. Run-time: germinates an implementation, continually thickens useful causal routes, senesces unused routes, and regrows when inputs or surroundings change.", + "coreIdea": "Ruam becomes a program husbandry system: the build emits a viable developmental constitution, and the protected program grows its current implementation during use. Correctness belongs to a persistent behavioral phenotype, while the causal organization that realizes it is living tissue with no privileged build-time form.", + "whyNovel": "This is not bytecode virtualization, a decoder variation, profile-guided optimization, structural shuffling, or scattered fragments. The new meaning-bearing unit is developmental viability under a behavioral envelope; an executable implementation is a temporary organismal state, not the transformed artifact.", + "boldness": 4.9, + "isFusion": false, + "parents": [], + "recognizability": 0.78, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "The implementation is an online synthesizing and self-optimizing runtime that specializes hot behavior and retires cold paths, a familiar adaptive-runtime and profile-guided optimization pattern." + }, + { + "name": "Anastomotic Execution", + "lens": "mycelial nutrient networks", + "assumptionOverturned": "protection is applied once, at build time", + "track": "novel transformation paradigms", + "sourceMechanism": "Filamentous fungi grow by branching hyphae whose tips can fuse through anastomosis. Fusion turns a tree-like colony into an interconnected syncytium with loops, alternate transport paths, and shared cytoplasm; resource discovery and local loss can then redirect flows and remodel which links carry the organism's work. Function belongs to the momentary connected network, not to any one hyphal segment.", + "roughSketch": "Build step: derive incomplete behavioral valences—what each ordinary component can offer, require, or join—without fixing a call graph. Emitted artifact: a nursery of locally mundane growth tips plus compatibility and phenotype boundaries; no tip contains a recoverable function or instruction fragment. Embedded interpreter or successor: an anastomosis steward that permits compatible tips to fuse and exposes shared state across the resulting transient syncytium, without dispatching opcodes. Developer: debugs an invocation as a fusion lineage showing which tissues met and which phenotype boundary closed. Automated analysis/reconstruction tooling: can inventory tips but cannot extract a canonical control graph, because connectivity is formed from the input gradient, prior fusions, and current resource field. Run-time: each invocation grows and fuses a temporary causal organ, obtains the output from the closed network's collective state, then dissolves or remodels its connections.", + "coreIdea": "Represent each behavior as a per-use anatomical event rather than as a stored procedure. Calls become transient organs assembled by reciprocal fusion among incomplete components, so the same public behavior can arise through causally different network episodes throughout the artifact's lifetime.", + "whyNovel": "This is not control-flow flattening, a dynamic dispatcher, function splitting, decoy code, or artifact scattering. Its unit of meaning is a time-bounded topology formed by mutual compatibility; neither the pieces nor any permanent wiring represents the developer's function.", + "boldness": 4.8, + "isFusion": false, + "parents": [], + "recognizability": 0.76, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "Transient components joined per input are a dynamic dataflow or actor graph assembled by compatibility rules; the temporary topology is recognizable even though the anatomical vocabulary is new." + }, + { + "name": "Heterokaryotic Semantics", + "lens": "mycelial nutrient networks", + "assumptionOverturned": "protection is applied once, at build time", + "track": "novel transformation paradigms", + "sourceMechanism": "Many filamentous fungi can be heterokaryotic: genetically distinct haploid nuclei remain separate while coexisting in a shared cytoplasm. Their products mingle, nuclear ratios can vary across the mycelium, and those ratios can influence expression and phenotype; some stages also show organism-level co-regulation rather than a simple sum of nuclear outputs. The persistent individual is therefore a coordinated population of genomes, not one genome copied into every compartment.", + "roughSketch": "Build step: decompose the source behavior into several independently incomplete 'nuclear' rule families whose products only acquire meaning through shared concentrations, thresholds, and co-regulation. Emitted artifact: a mixed population of ordinary evaluators plus a common semantic cytoplasm; there is no master implementation and no family is a redundant full variant. Embedded interpreter or successor: the cytoplasm maintains shared metabolites, compatibility, dosage, and phenotype constraints rather than decoding instructions. Developer: specifies the phenotype and receives owner-only explanations of nuclear contribution ratios for any observed result. Automated analysis/reconstruction tooling: sees partial rule families whose isolated summaries are false units of comprehension and cannot select a canonical family as the program. Run-time: nuclei replicate, migrate, become quiescent, or change relative abundance in response to use while cytoplasmic co-regulation keeps the owned behavior inside its phenotype.", + "coreIdea": "Compile a program into a heterokaryotic software individual whose semantics are expressed by the changing dosage of multiple partial lineages in a shared computational cytoplasm. Continuing lineage turnover makes transformation a life-cycle process, while the stable developer contract is the colony-level phenotype rather than any implementation member.", + "whyNovel": "This is not an ensemble of equivalent implementations, handler polymorphism, dead alternatives, secret splitting, or ordinary voting. The new representation is dosage-dependent phenotype: semantic coefficients and even available operations emerge from population composition and shared-field regulation.", + "boldness": 4.9, + "isFusion": false, + "parents": [], + "recognizability": 0.72, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "Multiple partial evaluators regulated by shared concentrations reduce to an adaptive ensemble or mixture-of-experts architecture with population-weighted outputs." + }, + { + "name": "Reciprocal-Exchange Computation", + "lens": "mycelial nutrient networks", + "assumptionOverturned": "protection is applied once, at build time", + "track": "novel transformation paradigms", + "sourceMechanism": "In arbuscular mycorrhizal symbiosis, plants supply photosynthetic carbon to fungal partners and fungi supply scarce mineral nutrients such as phosphorus. Experimental work shows bidirectional control in which partners can preferentially allocate resources toward counterparts offering better returns, while local source strength and scarcity shape the terms and direction of exchange. The functioning relationship is maintained through continuing reciprocal allocation, not a single transfer decided when the partnership begins.", + "roughSketch": "Build step: reformulate program intent as a set of reciprocal resource niches—local transformations that reveal an offering only when complementary value arrives—and a global viability condition. Emitted artifact: an ecology of producers, consumers, and exchange membranes whose isolated roles are useful but semantically incomplete. Embedded interpreter or successor: distributed exchange membranes continuously set local terms and admit conversions; no central component reads a program representation. Developer: declares desired ecological outcomes and examines an owner-facing ledger that explains which reciprocal relationships sustained each result. Automated analysis/reconstruction tooling: can catalog possible trades but finds no authoritative sequence or fixed dependency graph, because realized meaning is the circulation pattern negotiated under each input's scarcities. Run-time: inputs seed resource imbalances, local reciprocal allocations establish a viable circulation, and the requested output is harvested when the ecology satisfies the behavioral condition.", + "coreIdea": "Make a Ruam program a symbiotic economy rather than an instruction system. Behavior exists only as a self-sustaining pattern of reciprocal transformations, and every use renegotiates the causal economy that produces the same developer-owned outcome.", + "whyNovel": "This is not a capability check, missing-key scheme, remote service, constraint wrapper, or cross-file dependency. The unit of meaning is a viable exchange circulation whose local participants have no ordered command semantics and whose terms are renewed throughout run-time.", + "boldness": 4.8, + "isFusion": false, + "parents": [], + "recognizability": 0.71, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "The mechanism is market-based distributed constraint solving or a chemical-reaction network in which local exchanges converge on a global viability condition." + }, + { + "name": "Soliton Tissue", + "lens": "mycelial nutrient networks", + "assumptionOverturned": "protection is applied once, at build time", + "track": "novel transformation paradigms", + "sourceMechanism": "Transport inside fungal hyphae can exhibit a counterintuitive collective regime: in studied Neurospora networks, densely packed nuclei moved faster and self-organized into traveling concentration pulses. Passage of a pulse temporarily remodeled attachment sites in the hypha, enabling the group to propagate as a soliton-like packet; junction conditions could create or disperse these packets. The transported pattern and the tissue's recent state jointly determine flow.", + "roughSketch": "Build step: translate program behavior into tissue geometry, local attachment and release kinetics, junction rules, and boundary phenotypes instead of functions or instructions. Emitted artifact: a quiescent nonlinear transport medium whose static parts expose only local laws. Embedded interpreter or successor: a field-and-tissue propagator advances densities and remodels local attachment capacity; it does not fetch or decode commands. Developer: names input perturbations and output boundary phenotypes, then diagnoses behavior with pulse-lineage and tissue-state visualizations. Automated analysis/reconstruction tooling: encounters local kinetics rather than a command graph, and a useful summary must account for nonlinear pulse formation plus the accumulated wake state. Run-time: an input becomes a density perturbation, computation is the creation, collision, branching, and dispersal of traveling packets, and each packet alters the medium through which later computations move.", + "coreIdea": "Turn the program into a nonlinear transport tissue in which semantic events are traveling collective pulses, not stored operations. Execution continuously rewrites the medium's response landscape, so meaning lives in the coupled history of wave and tissue rather than in an artifact emitted once.", + "whyNovel": "This is not bytecode, a dataflow graph, an encoded token stream, an incremental cipher, or a state-machine rewrite. The fundamental operation is a self-organized transport phase with memory in the substrate, making wave phenotype—not instruction identity—the program representation.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.55, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Whole-Show Rehearsal Mesh", + "lens": "holography (every shard holds the whole at lower resolution)", + "assumptionOverturned": "opacity and correctness are in tension", + "track": "New product surfaces and capabilities", + "sourceMechanism": "In off-axis optical holography, light scattered by an object overlaps a coherent reference beam on a recording medium. The resulting interference fringes preserve phase relationships as well as intensity. Because light from much of the object reaches every region of the plate, a cut-out fragment can reconstruct the whole scene when illuminated appropriately; the smaller aperture merely reduces brightness, angular detail, and spatial resolution. Mapping: the build step photographs a program's externally meaningful journeys as overlapping semantic wavefronts rather than lowering functions into instructions; each emitted artifact is a standalone, executable low-resolution version of the whole product, not a fragment awaiting byte reconstruction; the embedded interpreter is replaced by a coherence director that lets independently complete projections reinforce one another; the developer is the stage magician holding a rehearsal book that declares which whole-show details may sharpen at each venue; automated analysis/reconstruction tooling receives a valid but deliberately coarse complete show from any one artifact and cannot treat a local shard as the canonical implementation; at run-time, available projections interfere at declared behavior boundaries to sharpen only the details needed for the current journey.", + "coreIdea": "Ruam becomes a generator of whole-product rehearsal surfaces: test packages, partner SDKs, previews, edge builds, and production bundles each run the entire application contract at a different semantic resolution. When several surfaces coexist, the coherence director upgrades selected journeys to production fidelity without ever assembling a hidden instruction stream. Correctness becomes easier to test because every shard is a faithful end-to-end rehearsal; opacity emerges because exactness belongs to contextual overlap, not to one privileged file.", + "whyNovel": "This is not artifact scattering, cross-file linking, a missing external term, or a smaller interpreter: every output is independently useful and semantically whole. The protected unit is a graded product experience, and the new customer-facing capability is generating faithful previews, test doubles, and deployable tiers from one behavioral specification.", + "roughSketch": "Add a 'show resolution' authoring surface where developers mark journeys, observable guarantees, and permitted approximations. The build emits several contract-compatible applications plus a tiny coherence protocol over named effects. A CI matrix proves that every lower-resolution show refines monotonically toward the full show, while the developer console previews what each audience can see and which overlapping projection sharpens a journey.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.82, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "needs-rail-relaxed", + "notNovelBecause": "This is multi-fidelity build generation combining previews, test doubles, partner editions, and production targets under refinement checks, a familiar multi-target product workflow." + }, + { + "name": "Focus-Pull Debugger", + "lens": "holography (every shard holds the whole at lower resolution)", + "assumptionOverturned": "opacity and correctness are in tension", + "track": "New product surfaces and capabilities", + "sourceMechanism": "A hologram records a wavefront rather than a conventional image. Re-illuminating it with a matching reference wave reconstructs a virtual or real image, and numerical holography can propagate the same recorded field to different depths, bringing different planes into focus without changing the recording. Cropping the recording still leaves the whole field reconstructable at reduced resolution. Mapping: the build step converts specifications, tests, effect boundaries, and selected domain terms into a queryable causal field with no line or instruction addresses; each emitted diagnostic shard summarizes the whole build or run at low resolution; the embedded interpreter is replaced by a focus engine that propagates those fields toward a developer-selected question; the developer acts like a stage magician moving the audience's focus between promise, prop, cue, and visible effect; automated analysis/reconstruction tooling can obtain many truthful question-specific explanations but no stable source-to-artifact correspondence to normalize across builds; at run-time, ordinary semantic events deposit compact field samples, and a later focus query reconstructs only the causal plane relevant to the question.", + "coreIdea": "Ruam replaces source maps and ordinary trace viewers with a focus-pull debugger. A developer asks, for example, 'why could this customer see this price?' or 'which promise permitted this network effect?', and the tool reconstructs a high-resolution explanation of that semantic plane from coarse whole-run shards. The same evidence that makes protected output auditable prevents one universal reconstruction, because explanations are faithful projections conditioned on explicit questions rather than a canonical causal listing.", + "whyNovel": "It is not encrypted telemetry, a debugger detector, a checksum, or renamed source mapping. There is intentionally no address map to reveal: the product surface is a query-conditioned semantic explanation system whose evidence remains useful even when implementation shape changes completely.", + "roughSketch": "Introduce a build-side vocabulary for promises, effects, authorities, and domain decisions. Emit fixed-size 'playbills' at module and runtime-event boundaries, each sketching the whole promise graph with different spatial frequencies. The local console combines selected playbills and runs semantic propagation to answer one question, while CI checks answers against unprotected oracle runs rather than comparing internal traces.", + "boldness": 4.7, + "isFusion": false, + "parents": [], + "recognizability": 0.88, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "pass" + }, + "maturity": "research-spike", + "notNovelBecause": "Query-conditioned explanations over provenance traces are semantic observability and causal debugging; changing trace granularity does not create a new debugging mechanism." + }, + { + "name": "Angle-Multiplexed Truth Deck", + "lens": "holography (every shard holds the whole at lower resolution)", + "assumptionOverturned": "opacity and correctness are in tension", + "track": "New product surfaces and capabilities", + "sourceMechanism": "A thick or volume hologram can store several full interference gratings in the same photosensitive volume. Each grating is recorded with a different reference-beam angle or wavelength. Bragg selectivity means that later illumination at one matching angle or wavelength reconstructs its corresponding complete image while the others remain largely quiet; a small piece of the volume still contains lower-resolution information about each recorded scene. Mapping: the build step records several orthogonal semantic viewpoints of the same application, such as customer journey, compliance claim, performance budget, and test oracle; the emitted artifact is one multiplexed truth deck whose every deployment slice carries coarse versions of all viewpoints; the embedded interpreter is replaced by a public cue master that selects a viewpoint from the declared purpose of an invocation, not from a concealed term; the developer is the stage magician designing which lighting angle reveals which truthful account of the show; automated analysis/reconstruction tooling can inspect any viewpoint but gains no canonical internal story by merging them because the viewpoints are independently complete and intentionally non-isomorphic; at run-time, typed call context acts like illumination geometry and materializes the viewpoint needed for that operation.", + "coreIdea": "Ruam becomes a multi-view artifact builder: one shipped product can be 'viewed' as an executable customer experience, an executable audit explanation, an executable test oracle, or an executable performance contract. These are not modes wrapped around one implementation; each is a whole, correct semantic projection generated from shared intent. Correctness strengthens because independent views must agree at observable boundaries, while implementation opacity follows from there being no privileged view for automated tooling to elevate into the program's true internal form.", + "whyNovel": "This is not environment fingerprinting, conditional dead branches, multiple opcode maps, or authorization by an absent value. Selection is a documented product contract, and the novelty is that audit, testing, performance, and delivery become co-equal executable projections instead of reports derived from a canonical bundle.", + "roughSketch": "Create a project-level 'truth deck' schema that names viewpoints and their shared observables. The build uses independent synthesis strategies for each view and emits a context-typed artifact plus cross-view boundary proofs. The developer dashboard rotates the deck between views like lighting a stage from different angles and flags only disagreements in public consequences, never differences in internal causal form.", + "boldness": 4.8, + "isFusion": false, + "parents": [], + "recognizability": 0.8, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "Independent executable views checked at shared observables match N-version programming, executable specifications, and multi-target compilation." + }, + { + "name": "Semantic Fringe Observatory", + "lens": "holography (every shard holds the whole at lower resolution)", + "assumptionOverturned": "opacity and correctness are in tension", + "track": "New product surfaces and capabilities", + "sourceMechanism": "In holographic interferometry, an object's wavefront is recorded in one state and later superposed with the wavefront from the object after it changes. Tiny optical-path differences produce visible interference fringes across the whole reconstructed object; fringe order and spacing reveal displacement or deformation far smaller than direct imaging can show. A plate fragment still reports the whole deformation field more coarsely because each region received light from across the object. Mapping: the build step records several behavioral reference wavefronts from specifications and independent oracle executions rather than hashing emitted text; each emitted observatory shard carries a coarse whole-product expectation expressed in domain outcomes; the embedded interpreter is replaced by an interferometer that compares live consequence fields with reference consequence fields without reconstructing either implementation; the developer is the stage magician watching a backstage fringe curtain that reveals where the performance departed from the promised illusion; automated analysis/reconstruction tooling learns where outcomes differ but not the internal steps that produced either field, so additional correctness evidence does not restore a stable implementation map; at run-time, selected domain effects overlap with their references and produce semantic drift fringes for developer inspection.", + "coreIdea": "Ruam adds a production semantic observatory that makes protected systems unusually easy to validate after dependency, browser, configuration, or policy changes. Instead of reporting stack frames or binary pass/fail checks, it displays a whole-product deformation field: where journeys, authorities, prices, timing classes, or side-effect shapes have drifted and by how much. Correctness visibility becomes the source of opacity because the public evidence is richly about consequences while remaining deliberately silent about implementation correspondence.", + "whyNovel": "This is not self-integrity checking, a checksum, canary logic, instrumentation detection, or staged invalidation. It is a developer product for measuring semantic deformation across real releases and environments, makes no judgment about artifact identity, and never alters behavior in response to observation.", + "roughSketch": "Add a semantic wavefront recorder to CI that learns expected outcome manifolds from specs plus differential oracle runs. Emit sparse effect probes whose individual reports cover the whole promise graph at coarse resolution. A local or hosted observatory overlays reports across versions, expands dense fringe regions into reproducible behavioral cases, and hands those cases back to the developer without reconstructing source-level control flow.", + "boldness": 4.5, + "isFusion": false, + "parents": [], + "recognizability": 0.94, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "pass", + "additiveApi": "pass" + }, + "maturity": "shippable-now", + "notNovelBecause": "This is semantic monitoring built from sparse probes, differential oracle tests, drift detection, and observability dashboards, all established product and testing patterns." + }, + { + "name": "Hysteretic Dialect Lattice", + "lens": "origami and mechanical metamaterials", + "assumptionOverturned": "the output is fixed the moment it is emitted", + "track": "self-modifying or living output", + "sourceMechanism": "A geometrically frustrated mechanical metamaterial can be perfectly periodic yet admit many disordered metastable configurations. Its local elements each prefer a soft deformation, but the lattice is arranged so that all local preferences cannot be satisfied simultaneously. Bistable elements called hysterons retain which side of a snap-through transition they occupy, and interactions between them make the response history-dependent. In a non-commutative response, applying the same operations in different orders can leave different final configurations, so the material stores an ordered history in its present mechanical state.", + "roughSketch": "Build step: translate the developer's behavioral contracts into a lattice of semantic constructions whose local composition preferences are intentionally incompatible but whose boundary behavior is invariant across allowed equilibria. Emitted artifact: ship a neutral initial lattice plus its contract boundary conditions, not a completed command representation. Embedded interpreter or successor: use a constraint-relaxation actor that applies calls as loads, settles snap-through events, and periodically emits the current lattice as the next artifact generation; it never decodes an instruction stream. Developer: author observable contracts and inspect a privileged diachronic trace that names which constructional changes preserved each contract. Automated analysis/reconstruction tooling: a static snapshot exposes one grammatical state but not the ordered usage history that selected it or the different transition graph now available from it. Run-time: each real call perturbs coupled hysterons, so the current metastable configuration selects a contract-faithful causal phrasing and then becomes the starting grammar for the next call.", + "coreIdea": "Make every installation of Ruam develop a genuine dialect: its operational grammar changes according to the order in which its vocabulary is used. The emitted file is only the language's initial state; execution continually grammaticalizes common compositions, erodes unused ones, and writes a successor whose future transformations depend on that accumulated history. Correctness belongs to the stable semantic boundary, while the internal language has a biography rather than a fixed form.", + "whyNovel": "This is not per-build variation, state-machine conversion, instruction mutation, or an environment-derived dependency. The new unit is a history-bearing constraint grammar whose transition algebra changes through ordinary use; no fixed interpreter, command inventory, shuffled structure, or one-time emitted representation remains the enduring object.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.68, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Traveling Isogloss", + "lens": "origami and mechanical metamaterials", + "assumptionOverturned": "the output is fixed the moment it is emitted", + "track": "self-modifying or living output", + "sourceMechanism": "In isostatic mechanical lattices, where constraints balance degrees of freedom, geometry can give the bulk a topological polarization. Joining regions with opposite polarizations creates a domain wall that supports a localized soft or zero-energy deformation even though both surrounding regions are mechanically rigid. The useful mobility is therefore a property of the interface, not of either local cell. Relocating the interface relocates where deformation can occur without changing the ordinary local ingredients of the lattice.", + "roughSketch": "Build step: tile each protected behavior into two locally rigid families of semantic constraints with opposite composition polarities, then place their initial domain wall at a valid contract boundary. Emitted artifact: deliver an apparently ordinary lattice of locally inert clauses in which only the current interface has enough freedom to form a behavioral utterance. Embedded interpreter or successor: replace the interpreter with a domain-wall carrier that performs the interface-local composition and refolds adjacent cells so the wall moves after every semantic act. Developer: specify the external contract and use an owner view that pins, names, and replays isogloss motion without exposing a stable source correspondence. Automated analysis/reconstruction tooling: extracting any local region yields rigid, incomplete relations; even a global snapshot reveals only today's isogloss, while the next execution changes which relations can move and where meaning can be articulated. Run-time: input supplies stress at the current boundary, the localized mode produces the required result, and the completed act translates the wall into a new region, thereby rewriting the artifact's locus of expressiveness.", + "coreIdea": "Let executable meaning exist only on a traveling topological boundary, like an isogloss separating two internally consistent dialect regions. Each call is spoken at that boundary and causes the boundary to migrate, so the artifact continuously transfers its sole expressive degree of freedom through otherwise unremarkable material. There is no stable function body to preserve: the place capable of realizing behavior is recreated by the preceding behavior.", + "whyNovel": "This is not fragment scattering, cross-file co-residence, local indirection, or a mobile decoder. Nothing meaningful is hidden in pieces awaiting reassembly; meaning is the emergent deformability of a changing global interface, and the successor mechanism moves that topological condition rather than relocating encoded content.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.3, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Post-Emit Morphogenesis", + "lens": "origami and mechanical metamaterials", + "assumptionOverturned": "the output is fixed the moment it is emitted", + "track": "self-modifying or living output", + "sourceMechanism": "Active self-folding origami uses adjoining material layers with different stimulus responses. When one layer swells, shrinks, or changes strain relative to a passive layer, the mismatch generates curvature at a programmed hinge. The flat configuration can have several folding branches, so robust systems use staged activation: one responsive material first pre-biases each vertex toward the intended branch, and another later drives the creases to their target angles. Fabrication therefore does not finish when the flat sheet is printed; later environmental exposure completes the structure through an ordered developmental process.", + "roughSketch": "Build step: print a semantic laminate containing contract surfaces, latent constructions, and staged responsiveness to actual value shapes, call rhythms, and composition neighborhoods rather than preselecting one causal implementation. Emitted artifact: ship a viable precursor whose first state is intentionally under-differentiated, analogous to a patterned flat sheet rather than a finished mechanism. Embedded interpreter or successor: use a finite developmental folder that converts ordinary use into branch-selection and fold stimuli, then lets newly formed semantic seams take over and eventually replace the folder with a smaller successor organizer. Developer: author the invariants and permissible differentiation envelope, then inspect a lineage view showing which runtime exposures caused each contract-preserving organ to form. Automated analysis/reconstruction tooling: an early specimen contains only latent potential, while a mature specimen reflects one installation's development; neither alone supplies a transferable account of the other possible ontogenies. Run-time: representative use first pre-biases ambiguous regions, later use folds them into specialized organs, and epoch boundaries produce descendants whose architecture is inherited from acquired structure rather than copied from the original emission.", + "coreIdea": "Treat the build result as an embryo that completes its own protected architecture after deployment. Real use acts as developmental exposure: it selects among contract-equivalent branches, differentiates frequently interacting behaviors into new organs, and replaces the original organizer with successively smaller organizers. Ruam would protect meaning by making the delivered program's mature causal anatomy unavailable at emit time, even to Ruam itself.", + "whyNovel": "This is not lazy decoding, profile-guided optimization, per-install permutation, or a missing external term. The artifact undergoes staged ontogeny in which future semantic components and even the mechanism that forms them are replaced by descendants; the emitted precursor is not an encoded copy of a completed program.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.84, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "Staged run-time differentiation from usage profiles is adaptive specialization or tiered JIT compilation, with a self-replacing bootstrap compiler layered on top." + }, + { + "name": "Auxetic Contract Tissue", + "lens": "origami and mechanical metamaterials", + "assumptionOverturned": "the output is fixed the moment it is emitted", + "track": "self-modifying or living output", + "sourceMechanism": "An auxetic mechanical metamaterial expands laterally when stretched, giving it a negative Poisson ratio. In Miura-derived and re-entrant origami, this counterintuitive response comes mainly from the coupled geometry of folds and hinges rather than an unusual constituent substance. Some patterns can change the sign and magnitude of their Poisson ratio as the fold state changes. A local axial demand can therefore reorganize a wider area, and the current configuration determines how load in one direction creates motion in another.", + "roughSketch": "Build step: convert behavioral contracts into a coupled construction tissue where every semantic degree of freedom has transverse partners and where all allowed fold states satisfy the same external boundary. Emitted artifact: provide a compact folded tissue with no stable partition into functions, only connected contract-bearing cells. Embedded interpreter or successor: use a geometric equilibrium successor that responds to semantic demand by unfolding the addressed region, recruiting perpendicular constructions, compacting relaxed regions, and persisting the new rest geometry. Developer: define boundary judgments and linguistic well-formedness rules, then observe contract coordinates and causal stress maps instead of source lines. Automated analysis/reconstruction tooling: isolating a frequently used region misses the transverse constructions that come into existence under load, while exercising it changes the rest geometry that later observations encounter; understanding requires the evolving global constitutive law, not a local trace. Run-time: each call stretches one semantic axis, the tissue expands along coupled axes to form a fresh realization, and relaxation leaves a revised baseline that changes how the next call can be phrased.", + "coreIdea": "Turn program behavior into auxetic semantic tissue whose internal vocabulary grows sideways wherever meaning is exercised. Execution does not select a pre-existing route; it mechanically creates a wider construction around the requested contract, then preserves part of that expansion as the next resting language. The artifact stays behaviorally bounded while continually changing the dimensionality and neighborhood of its own implementation.", + "whyNovel": "This is not redundant paths, function cloning, adaptive caching, expression noise, or structural bloat. The representation is a constitutive relation between semantic loads and global geometric response, and runtime meaning is newly formed by transverse coupling rather than chosen from a fixed stock of alternatives.", + "boldness": 4, + "isFusion": false, + "parents": [], + "recognizability": 0.58, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Allomorphic Phase Sheet", + "lens": "origami and mechanical metamaterials", + "assumptionOverturned": "the output is fixed the moment it is emitted", + "track": "self-modifying or living output", + "sourceMechanism": "Some four-vertex origami tessellations admit distinct compatible folding modes. A change in the mountain-versus-valley assignment of a crease can move a pattern between Miura-like and eggbox-like configurations, with a corresponding switch across positive and negative mechanical responses. When cells in different modes are combined at a kinematic bifurcation, compatibility can lock a mode across a region: a small polarity choice changes the admissible collective motion and thus the macroscopic material law.", + "roughSketch": "Build step: express each behavioral region as a family of contract-equivalent construction grammars joined at explicit kinematic bifurcations, with a small set of crease polarities controlling which grammar is collectively admissible. Emitted artifact: ship one initially folded phase plus unresolved bifurcations, not a catalog of alternate implementations. Embedded interpreter or successor: use a fold-phase coordinator that changes a polarity only when current semantic pressure makes a neighboring phase compatible, then derives and installs replacement bifurcations for the changed region. Developer: own the invariant denotation and declare which causal grammar families are acceptable, while an owner tool translates phase transitions into stable contract-level explanations. Automated analysis/reconstruction tooling: a snapshot can induce the grammar of its present phase, but after a bifurcation the admissible dependencies, grouping, and causal order change collectively, invalidating a stable reconstruction without any local substitution scheme. Run-time: accumulated discourse context pushes a region toward a bifurcation, a polarity flips, and the whole region begins realizing the same denotation through a different globally constrained grammar before laying new future phase boundaries.", + "coreIdea": "Give each artifact grammatical phases and let use drive true phase changes between them. A tiny runtime polarity decision would not swap one implementation for another; it would alter which causal relations are legal across an entire region, much as a linguistic parameter can reorganize many surface constructions at once. Each new phase also redraws its own next bifurcations, making the output a continuing sequence of internally coherent but structurally incommensurable languages.", + "whyNovel": "This is not equivalent-syntax substitution, control-flow rewriting, opcode remapping, or ordinary polymorphism. A phase transition changes the generative compatibility rules for a whole semantic region and then creates a new configuration space; the alternatives are not prebuilt code forms selected from a fixed menu.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.75, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "Contract-equivalent grammars selected and rewritten at run time are dynamic program rewriting and metamorphic phase switching, even if the switch is expressed as a regional constraint change." + }, + { + "name": "Evidential Rulecraft", + "lens": "untranslatable languages and linguistic relativity", + "assumptionOverturned": "build-time and run-time are separate worlds", + "track": "semantic-level protection", + "sourceMechanism": "Source mechanism first: in languages with grammatical evidentiality, a speaker must routinely mark the basis of a statement, such as direct perception, inference, or report; the exact categories vary by language. A language without obligatory evidential marking can still express the content, but translation often needs a paraphrase to preserve the speaker's epistemic commitment. This is a defensible weak-relativity mechanism: grammar makes particular distinctions habitually salient without determining what a speaker can think. Mapping: the build step is the first evidential negotiation, converting protected intent into claims that are deliberately incomplete until their provenance exists; the emitted artifact is an open rule charter defining admissible evidence relationships rather than final decisions; the embedded interpreter's successor is an evidential referee that recognizes, combines, and challenges evidence-bearing acts; the developer is a game designer who declares outcome envelopes and what kinds of grounds can justify each outcome; automated analysis/reconstruction tooling is like a translator holding the grammar but not the lived provenance that gives a claim its force; run-time is the continuing conversation in which application events create evidence categories and thereby finish the rule.", + "coreIdea": "Ruam becomes an evidential mechanics engine: every protected domain decision exists only as a verdict over provenance that the live application co-produces. A build emits neither the decision procedure nor a disguised equivalent; it emits the grammar of acceptable justification, and each run continues the build by turning newly witnessed, inferred, and delegated facts into a momentary executable ruling.", + "whyNovel": "This protects the meaning of a decision by distributing it between a semantic obligation and the circumstances that can warrant it. It is not instruction virtualization, encoded constants, structural variation, an integrity test, or a missing external term: the run is constitutive of the rule rather than merely supplying data to a pre-existing rule.", + "roughSketch": "Expose a designer-facing contract such as outcomes, prohibited outcomes, evidence roles, and example adjudications. Emit a compact charter of claim forms plus ordinary host actions, then let a resident referee maintain a provenance graph and derive short-lived rulings when gameplay events satisfy a form. Owner diagnostics replay the evidence conversation in domain language, while every new class of evidence extends the same compilation process after delivery.", + "boldness": 4, + "isFusion": false, + "parents": [], + "recognizability": 0.91, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "A provenance graph plus admissible evidence forms and derived rulings is a policy-as-code or rule-engine architecture with provenance-aware decisions." + }, + { + "name": "The Deictic Commons", + "lens": "untranslatable languages and linguistic relativity", + "assumptionOverturned": "build-time and run-time are separate worlds", + "track": "semantic-level protection", + "sourceMechanism": "Source mechanism first: deixis gives expressions such as I, you, here, now, this, and socially marked forms their reference from the speech situation. Demonstratives can depend on joint attention, and social deixis can encode relationships that a literal translation must unpack. A transcript stripped of speaker roles, place, time, shared attention, and prior discourse therefore does not fully determine the utterance's reference or force. Mapping: the build step authors a deictic grammar and creates only the opening common ground; the emitted artifact is a set of relational utterance forms whose important referents do not yet exist; the embedded interpreter's successor is a common-ground curator that tracks who can mean what by here, now, ours, guest, or witness; the developer is the game master who defines role relations and invariant consequences, not a fixed procedure; automated analysis/reconstruction tooling is the transcript reader lacking the particular joint-attention history that established each referent; run-time is the live table where participants, objects, places, epochs, and prior commitments continually create and retire the meanings needed for the next move.", + "coreIdea": "Ruam becomes a deictic rules commons in which protected operations are authored as situated speech acts rather than functions. The delivered artifact cannot name the sensitive concepts directly because their referents are minted by the application's evolving relationships; each invocation both performs behavior and revises the language in which later behavior can be requested.", + "whyNovel": "The semantic unit is a relationship in common ground, not a function, instruction, renamed identifier, local fragment, or environment-derived secret. Runtime context does not merely choose among precompiled branches; it creates the referents that make a branch-like notion expressible at all, so there is no stable source-to-artifact vocabulary for tooling to recover.", + "roughSketch": "Let a developer declare role lattices, joint-attention events, temporal perspectives, and consequences of domain utterances. The build emits an initial conversation board and host-visible verbs with unresolved indexicals. During use, the curator binds meanings through witnessed interactions, expires them when the shared situation changes, and records an owner-readable dialogue trace that explains decisions without revealing a timeless rulebook.", + "boldness": 4, + "isFusion": false, + "parents": [], + "recognizability": 0.87, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "Runtime-minted referents derived from roles, events, relationships, and time match context-aware capability systems, relationship-based policy, and event-sourced rule engines." + }, + { + "name": "Iterated Dialect Mechanics", + "lens": "untranslatable languages and linguistic relativity", + "assumptionOverturned": "build-time and run-time are separate worlds", + "track": "semantic-level protection", + "sourceMechanism": "Source mechanism first: in iterated-learning accounts of language change, each generation learns from a limited sample produced by the previous generation. The transmission bottleneck and learner biases can reshape grammar toward learnable regularities, while communicative use selects for successful coordination; what persists is usable communicative function, not exact vocabulary or structure. This is a model of cultural evolution, not a claim that every historical change has one cause. Mapping: the build step is generation zero, teaching protected behavior through a designed set of playable situations rather than freezing a final representation; the emitted artifact is a curriculum, a small population of provisional speakers, and rules for teaching the next population; the embedded interpreter's successor is a rule apprentice that learns how to coordinate from demonstrations and then becomes a teacher; the developer is the game designer who owns victory conditions, invariant scenarios, and semantic boundaries; automated analysis/reconstruction tooling meets one transient dialect whose internal categories are not inherited verbatim by the next; run-time is the succession of teaching seasons in which live use supplies the limited corpus and continually rebuilds the behavior language.", + "coreIdea": "Ruam becomes a language ecology that protects a mechanic by transmitting it, not storing it. The running artifact periodically raises a successor from constrained demonstrations of current play, retires the predecessor's dialect, and carries forward only the developer's semantic invariants, making runtime cultural transmission the ongoing build system.", + "whyNovel": "This is not per-build polymorphism, instruction mutation, self-checking, or a decoder that changes form. Successive artifacts do not share a hidden command vocabulary at all; they share the ability to coordinate on a game contract, and preservation is judged by semantic play rather than equality of an encoded program.", + "roughSketch": "The authoring surface is a scenario studio containing example rounds, forbidden outcomes, metamorphic relationships, and scoring rules. An initial cohort learns a compact local rule language from those rounds. At chosen lifecycle events, a pupil observes a deliberately narrow sample of the current cohort, proves itself by completing fresh semantic scenarios, takes over ordinary host decisions, and later teaches another pupil; the resulting lineage is inspectable to its owner as a series of behavioral traditions.", + "boldness": 4, + "isFusion": false, + "parents": [], + "recognizability": 0.58, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Anisomorphic Ruleworlds", + "lens": "untranslatable languages and linguistic relativity", + "assumptionOverturned": "build-time and run-time are separate worlds", + "track": "semantic-level protection", + "sourceMechanism": "Source mechanism first: languages are anisomorphic: they partition semantic fields, grammatical roles, kinship, space, time, and agency along different boundaries. Translation is therefore not generally a word-for-word bijection; multiple paraphrases may preserve an utterance's practical consequences while imposing different conceptual decompositions, and back-translation need not recover one uniquely privileged original phrasing. Mapping: the build step creates several non-isomorphic but contract-equivalent decompositions of the protected domain and deliberately withholds a canonical one; the emitted artifact is a playable treaty among those ruleworlds rather than one encoded procedure; the embedded interpreter's successor is a world adjudicator that can honor commitments made under one ontology after moving them into another; the developer is a game designer who specifies invariant outcomes and supplies alternative readings of important mechanics; automated analysis/reconstruction tooling can construct valid semantic models but has no artifact-internal basis for elevating one to the developer's original intent; run-time is continuous diplomatic play in which the active ontology changes and outstanding obligations are translated, making each transition another build phase.", + "coreIdea": "Ruam represents a sensitive mechanic as an equivalence class of genuinely different ruleworlds. A run provisionally inhabits one ontology, creates real obligations there, then crosses into another ontology that preserves the promised outcomes while changing what counts as actor, resource, cause, and action; after emission, the protected program has no single canonical semantic anatomy to reconstruct.", + "whyNovel": "The alternatives are all causally operative and semantically legitimate, so they are neither dead material nor misleading copies. This differs from syntax substitution, handler variation, control-structure reshaping, and multiple encoded forms of one procedure: only the outcome treaty is stable, while the very decomposition of the domain remains plural.", + "roughSketch": "A design tool asks the developer to express the same mechanic through contrasting ontologies, for example exchange, ecology, obligation, and territory, then identifies the outcome correspondences that must survive translation. The artifact carries ordinary pieces from all ruleworlds plus treaty moves between them. The adjudicator changes worlds at meaningful play events, migrates live commitments through the treaty, and reports faithfulness in terms of preserved player-facing promises instead of source correspondence.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.46, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "fails", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "The Cue-Sheet Contract Theater", + "lens": "stage magic and misdirection", + "sourceMechanism": "Source domain: A large stage illusion is designed as two descriptions that are deliberately kept distinct. The effect script records what spectators should be able to perceive, while the method is a coordinated system of blocking, prop states, lighting, sound, assistants, and precisely timed cues. A stage manager can call and rehearse those cues, verify that every visible beat occurred, and diagnose a missed transition without needing the audience's narrative to describe the hidden mechanics. Mapping: the build step becomes rehearsal and writes an effect score of boundary-visible behavior and causal obligations; the emitted artifact becomes the performing company rather than a container of instructions; the embedded interpreter or successor becomes a semantic stage manager that coordinates whichever internal actors can satisfy the next scored beat; the developer becomes the director who authors effects, invariants, and recovery beats; automated analysis/reconstruction tooling occupies the spectator position and can observe the effect score but receives no stable cue-to-mechanism correspondence; run-time becomes the live performance in which semantic cues are checked and an owner-only rehearsal record is produced.", + "assumptionOverturned": "Ruam is a compiler → Ruam is actually a semantic stage manager.", + "track": "verifiability and developer experience", + "coreIdea": "Ruam accepts an executable effect score alongside the source: externally meaningful state changes, permitted causal orderings, resource obligations, and failure semantics. It emits a behavior company whose internal realization may have no function-level ancestry, plus a local rehearsal console that can prove which effect beats were fulfilled and replay only the semantic seam around a missed beat. Correctness evidence is therefore the primary generated product, while resistance to stable source-to-artifact correspondence follows from verifying the performance rather than translating the script.", + "whyNovel": "This is not a bytecode interpreter, syntax rewrite, shuffled implementation, self-integrity check, source map, or encoded execution log. Its durable unit is a developer-owned effect beat and its causal obligations; the deployed mechanism is free to be organized around roles and cues that have no instruction-by-instruction source counterpart.", + "roughSketch": "Introduce an effect-score language that can describe observable transitions, temporal envelopes, allowed nondeterminism, and recovery promises. The build constructs a proof obligation graph and synthesizes a role-based runtime whose owner telemetry names only score beats; a rehearsal tool runs counterfactual cue sequences and returns minimal failed obligations rather than reconstructed source frames.", + "boldness": 4.9, + "isFusion": false, + "parents": [], + "recognizability": 0.66, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "" + }, + { + "name": "Question-Sightline Observatory", + "lens": "stage magic and misdirection", + "sourceMechanism": "Source domain: Forced perspective produces a convincing spatial relationship only from a designed sightline. Set geometry, relative scale, converging edges, actor blocking, and depth cues are coordinated so that the viewer receives a coherent scene; moving to another viewpoint yields a different but physically legitimate projection of the same arrangement. The mechanism is not a painted falsehood but a relationship among observer position, geometry, and selected visual invariants. Mapping: the build step becomes a scene designer that derives a family of legitimate semantic projections from program contracts; the emitted artifact becomes the full geometric arrangement without a privileged readable facade; the embedded interpreter or successor becomes a perspective instrument that answers a bounded semantic question by selecting and maintaining its observation coordinates; the developer becomes the viewer who chooses a sightline such as why a value changed or which promise authorized an effect; automated analysis/reconstruction tooling sees individual projections but cannot assume they assemble into a global source model; run-time becomes the changing stage on which query-specific causal coordinates are resolved.", + "assumptionOverturned": "Ruam is a compiler → Ruam is actually a question-conditioned observatory.", + "track": "verifiability and developer experience", + "coreIdea": "Instead of emitting a universal trace or a source map, Ruam emits a queryable observation geometry. A developer asks a semantic question and receives a mechanically checked causal projection containing exactly the events necessary to answer it, with the projection expressed in domain contracts rather than functions or instructions. Different questions deliberately produce non-nestable views, so excellent diagnosis does not accumulate into a stable replica of the implementation.", + "whyNovel": "This is not log filtering, identifier renaming, local fragment scattering, debugger detection, or per-build structural variation. The novel unit is a verified question-relative causal projection, and the guarantee is that each view is complete for its declared question even though no universal implementation view exists.", + "roughSketch": "At build time, derive observation bases from developer-declared entities, effects, and authorization relations, then prove coverage conditions for each supported question class. At run-time, retain a compact causal incidence structure rather than source locations; the local developer console solves for a requested projection and checks its completeness certificate before rendering a domain-level explanation.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.76, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "pass" + }, + "maturity": "research-spike", + "notNovelBecause": "The implementable mechanism is query-directed causal tracing and program slicing with certified projection completeness, a familiar observability and provenance pattern." + }, + { + "name": "Dual-Reality Debugging", + "lens": "stage magic and misdirection", + "sourceMechanism": "Source domain: In a dual-reality mentalism presentation, two groups receive different information through role-specific instructions, wording, positioning, or props. Each group's experience is internally coherent, and carefully chosen public language permits both interpretations to coexist during the same effect; no participant has merely seen a redacted copy of one master script. Mapping: the build step becomes a designer of mutually consistent observation algebras for roles such as developer, QA, operations, and public consumer; the emitted artifact becomes the shared performance that supports all of those realities; the embedded interpreter or successor becomes a projection coordinator that turns one semantic event into role-valid observations while enforcing cross-view consistency; the developer becomes the participant allowed to compose selected views in a local diagnostic workspace; automated analysis/reconstruction tooling receives the public algebra, which is behaviorally truthful but insufficient to infer the private diagnostic algebras; run-time becomes the single event history from which distinct, consistency-linked realities are generated.", + "assumptionOverturned": "Ruam is a compiler → Ruam is actually a multi-perspective observability constitution.", + "track": "verifiability and developer experience", + "coreIdea": "Ruam defines debugging as composition of typed realities rather than inspection of a canonical trace. Production output exposes a public behavioral view, operational tooling gets a reliability view, QA gets an invariant view, and the developer can temporarily join selected views to obtain a causal explanation whose validity is checked by cross-view laws. Because none of the individual realities is a masked source trace, deployable observability remains useful without creating a stable implementation map.", + "whyNovel": "This is not access control around ordinary logs, relocation of a decoder, an external string term, artifact fragmentation, or source-map retention. Each view has different semantic primitives and independently useful truth conditions, while the central technical object is a set of compositional consistency laws among realities.", + "roughSketch": "Add a projection-policy layer whose types define what counts as an event and a causal link for each role. Compile the program's behavioral contracts into cross-projection laws, emit only the public and operational projections needed in deployment, and let an owner-side console join captured projections with build-held QA and developer views to explain a failure at the contract level.", + "boldness": 4.8, + "isFusion": false, + "parents": [], + "recognizability": 0.74, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "pass" + }, + "maturity": "research-spike", + "notNovelBecause": "It largely restates role-specific telemetry views, access-controlled observability, and consistency checks between projections." + }, + { + "name": "Black-Art Absence Certificates", + "lens": "stage magic and misdirection", + "sourceMechanism": "Source domain: The black-art principle hides black-clad assistants, supports, openings, or objects against a black background by tightly controlling illumination, contrast, costume, and spectator sightlines. The visible illusion is shaped as much by guaranteed absences in the lit field as by the objects that remain visible; the dark region is an engineered part of the apparatus, not empty scenery. Mapping: the build step becomes a lighting designer that derives forbidden observable states and forbidden causal combinations from the developer's contracts; the emitted artifact becomes the lit field whose public identity is its permitted observations; the embedded interpreter or successor becomes an absence custodian that maintains a proof-oriented model of what must never enter that field; the developer becomes the illusion designer who specifies impossibility boundaries and inspects counterexamples; automated analysis/reconstruction tooling can catalog visible outcomes but cannot turn certified absences into a unique hidden mechanism; run-time becomes a succession of lit semantic fields, each accompanied by evidence that excluded states remained outside the observation boundary.", + "assumptionOverturned": "Ruam is a compiler → Ruam is actually a negative-space certifier.", + "track": "verifiability and developer experience", + "coreIdea": "Ruam makes negative behavioral space the primary artifact: not what every internal step does, but which externally meaningful states, causal combinations, data relationships, and authority crossings are impossible. It may synthesize any implementation that inhabits the remaining space, while the developer receives continuous, counterexample-oriented evidence for the exclusion boundary. Debugging begins from a violated impossibility and expands only the smallest semantic neighborhood needed to explain it.", + "whyNovel": "This is not an opaque predicate, unreachable code, environment check, canary, integrity tag, or structural concealment. The exclusions are developer-level semantic guarantees and the successor's identity is a proof-carrying boundary of possible behavior, not a mechanism that checks whether its own bytes changed.", + "roughSketch": "Create a negative-contract DSL for impossible state pairs, forbidden causal paths, information non-relations, and temporal exclusions. The build synthesizes an implementation plus an absence monitor expressed over domain events; verification tooling searches the exclusion boundary symbolically and at run-time, returning a minimal semantic counterexample without exposing source positions.", + "boldness": 4.7, + "isFusion": false, + "parents": [], + "recognizability": 0.84, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "Negative contracts, safety-property model checking, counterexample generation, and runtime contract monitoring are established formal-verification techniques." + }, + { + "name": "The Convincer Ecology", + "lens": "stage magic and misdirection", + "sourceMechanism": "Source domain: A convincer is an action or condition that strengthens the audience's confidence in the stated situation: a spectator may handle an object, a card may be signed, a container may be examined, or a choice may be made before the climax. Skilled routines use several partially independent convincers because each supports a different inference; their overlap creates a robust experiential case without any single convincer exposing the method. Mapping: the build step becomes a dramaturg that grows a diverse ecology of falsifiable semantic demonstrations from the developer's trust questions; the emitted artifact becomes a repertoire capable of participating in those demonstrations; the embedded interpreter or successor becomes a rehearsal partner that can instantiate requested demonstrations and report domain outcomes; the developer becomes the spectator-director who selects which claims need confidence and can demand counterexamples; automated analysis/reconstruction tooling observes many-to-one facts about permitted behavior rather than a transferable implementation correspondence; run-time becomes an ongoing sequence of naturally occurring and deliberately rehearsed demonstrations whose evidence is combined by explicit independence rules.", + "assumptionOverturned": "Ruam is a compiler → Ruam is actually a semantic accreditation studio.", + "track": "verifiability and developer experience", + "coreIdea": "For each protected capability, Ruam generates a living portfolio of heterogeneous correctness demonstrations: algebraic properties, metamorphic scenarios, boundary interactions, counterfactual replays, and production-derived examples, each grounded in the developer's domain vocabulary. The owner sees which independent lines of evidence support each behavioral claim and can ask Ruam to grow a new convincer where confidence is thin. The protected artifact is hired to sustain and refresh this evidence ecology, not merely to execute translated code.", + "whyNovel": "This is not a checksum, self-test, fixed test suite, watermark, decoy computation, or encoded constant pool. Its product value is an adaptive evidence topology connecting domain claims to independent demonstrations, with no requirement that any demonstration reveal how a function or instruction realizes the claim.", + "roughSketch": "Extend project configuration with trust questions and independence criteria. The build derives a claim graph and several unlike evidence generators; a local evidence workbench visualizes coverage and contradictions, while run-time contributes domain-level observations that can seed new rehearsals without recording a source-aligned trace.", + "boldness": 4.6, + "isFusion": false, + "parents": [], + "recognizability": 0.9, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "pass" + }, + "maturity": "shippable-now", + "notNovelBecause": "The mechanism is a portfolio of property-based, metamorphic, boundary, counterfactual, and production-derived tests organized as an evidence graph." + }, + { + "name": "Phenotype Manufacturing Right", + "lens": "DNA error-correction and codon degeneracy", + "assumptionOverturned": "the meaning of the program lives inside the file", + "track": "distribution, licensing, and delivery models", + "sourceMechanism": "In the standard genetic code, 61 sense codons map to 20 amino acids, so several codons can specify the same amino acid. Wobble in recognition by transfer RNAs helps one transfer RNA read multiple synonymous codons; as a result, no single synonymous DNA spelling uniquely owns the amino-acid sequence that a ribosome produces. Mapping: the build step becomes phenotype specification rather than implementation emission; the emitted artifact is a manufacturing order plus conformance gauges; the embedded interpreter is replaced by an install-time behavior foundry that produces ordinary JavaScript; the developer owns the observable phenotype and tolerances; automated analysis/reconstruction tooling sees only a manufacturing fixture and one contingent specimen; and run-time executes a locally fabricated realization that can later be replaced by a synonymous one.", + "coreIdea": "Ruam licenses the right to manufacture a behavioral phenotype, not a copy of a program. A customer receives a compact product specification, and an authorized foundry at installation time synthesizes and certifies an ordinary local implementation from the current platform's components; renewals authorize new manufacture rather than unlock old bytes. The program's durable meaning lives in the developer-held phenotype contract and certification relationship, while every file is disposable production tooling or a temporary specimen.", + "whyNovel": "This is neither per-build polymorphism nor an interpreter with an external term: there is no canonical implementation to decode, and authorization governs fabrication and certification of equivalent products rather than access to concealed bytes.", + "roughSketch": "The developer marks behavioral invariants, permitted tolerances, and platform obligations. Ruam emits a phenotype dossier with executable acceptance experiments and a foundry request; a local or managed synthesis stage makes ordinary modules, proves them against the dossier, and issues a short-lived product passport. Distribution becomes a licensed manufacturing workflow, much like a design owner authorizing regional factories to make conforming parts from locally available processes.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.56, + "impact": 5, + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "" + }, + { + "name": "Synonymous Supply Network", + "lens": "DNA error-correction and codon degeneracy", + "assumptionOverturned": "the meaning of the program lives inside the file", + "track": "distribution, licensing, and delivery models", + "sourceMechanism": "Synonymous codons name the same amino acid, but they are not always operationally equivalent: organisms and tissues have different abundances of matching transfer RNAs, so codon choice can change translation rate, expression level, and co-translational folding while leaving the nominal amino-acid sequence unchanged. Mapping: the build step defines a target phenotype, interface tolerances, and qualified substitution classes; the emitted artifact is a bill of behavioral materials with open supplier positions; the interpreter is replaced by a local sourcing and qualification engine; the developer acts as the industrial designer who approves equivalence classes; automated analysis/reconstruction tooling lacks the live supplier catalog and chosen assembly; and run-time is the current assembled product whose structure and operating qualities reflect local supply.", + "coreIdea": "Ruam becomes a governed semantic supply network in which a license grants access to changing families of qualified implementation parts. Installation procures compatible ordinary JavaScript components from an organization, device maker, developer, or regional catalog and assembles them within published tolerance stacks; no package contains the whole product, and no supplier is the canonical implementation. Commercial tiers can govern supplier diversity, performance envelopes, provenance, or regional availability rather than toggling hidden features.", + "whyNovel": "Unlike a package manager, fragment reassembly, or cross-file co-residence, the protected product is the continuing qualification and substitution ecosystem itself: supplier parts are complete ordinary components, while global meaning is defined by the approved relationships and tolerance stack among them.", + "roughSketch": "At build time Ruam extracts component obligations and creates substitution sockets with behavioral gauges instead of stable function identities. A license manifest selects eligible catalogs and quality bands; installation resolves a bill of materials, runs fit checks, and records the assembly's provenance. Updates can qualify or retire suppliers without issuing a new canonical application, giving the developer a product-line and supply-chain surface rather than a larger build-option matrix.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.86, + "impact": 4, + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "It is a governed component marketplace and package resolver using interface contracts, compatibility tests, provenance, and software-product-line substitution." + }, + { + "name": "Lineage Repair Warranty", + "lens": "DNA error-correction and codon degeneracy", + "assumptionOverturned": "the meaning of the program lives inside the file", + "track": "distribution, licensing, and delivery models", + "sourceMechanism": "After DNA replication, mismatch-repair systems recognize distortions caused by incorrectly paired bases, use transient strand cues such as nicks and replication-associated marks to identify the newly synthesized strand, remove a tract containing the mismatch, and resynthesize it from the intact complementary strand before sealing it. The repair machinery does not carry a separate master sequence for every possible correction. Mapping: the build step creates semantic repair boundaries, lineage cues, and behavioral reference specimens; the emitted artifact is a serviceable descendant plus its product passport, not the definitive program; the interpreter is replaced by a maintenance agent that can remanufacture an affected component; the developer is the lineage steward who publishes what counts as healthy behavior; automated analysis/reconstruction tooling sees only one historical specimen rather than the rules governing supported descendants; and run-time supplies observations and trusted prior behavior from which a corrected descendant is made.", + "coreIdea": "Ruam sells a lineage warranty: a licensed application is defined as the succession of supported descendants produced by an ongoing repair relationship, not by any release file. Delivery events carry semantic repair policies, reference examples, and permitted resynthesis materials rather than a target binary or source-shaped patch. A deployment whose behavior drifts is locally remanufactured from its own functioning regions, its certified history, and the newest developer policy.", + "whyNovel": "This is not a source-integrity reaction, patch chain, or scattered artifact: a delivery event contains no target implementation to recover, and its product value is constructive maintenance that creates a new supported descendant rather than verification followed by invalidation.", + "roughSketch": "The initial build partitions behavior into replaceable service regions and records developer-approved observations as a lineage passport. During operation, the maintenance agent identifies semantic disagreements at region boundaries, preserves the still-conforming regions as the template, and synthesizes only the divergent region under the current warranty policy. Subscription tiers can cover repair cadence, certified histories, and the breadth of behavior that Ruam promises to keep in conformance.", + "boldness": 4, + "isFusion": false, + "parents": [], + "recognizability": 0.82, + "impact": 5, + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "The proposed mechanism is contract-guided automated program repair and self-healing software wrapped in a maintenance subscription." + }, + { + "name": "Fidelity-Gradient License", + "lens": "DNA error-correction and codon degeneracy", + "assumptionOverturned": "the meaning of the program lives inside the file", + "track": "distribution, licensing, and delivery models", + "sourceMechanism": "When ordinary DNA replication stalls at a damaged base, cells can temporarily recruit specialized translesion polymerases that synthesize across the lesion. These enzymes trade fidelity for continuity, after which the normal polymerase resumes and later repair processes can restore the affected region. Mapping: the build step separates essential invariants from refinable semantics and marks safe handoff points; the emitted artifact contains a continuation-grade behavioral envelope rather than the complete high-fidelity behavior; the interpreter is replaced by a run-time pathway selector and local fabricator; the developer specifies acceptable temporary approximations and later reconciliation rules; automated analysis/reconstruction tooling can observe the fallback envelope but not the exact future high-fidelity semantics or its delivery history; and run-time selects an operating grade, continues through an unavailable delivery interval, then reconciles state when licensed high-fidelity material returns.", + "coreIdea": "A Ruam license purchases semantic fidelity over time rather than a binary feature switch. During an expired, disconnected, or delayed delivery interval, the application continues inside a developer-approved approximation envelope; renewed service progressively replaces approximate decisions with exact behavior and reconciles any affected state. Premium delivery models can sell fidelity, correction latency, and continuity guarantees as independent dimensions.", + "whyNovel": "This is not an external secret that makes the same concealed program readable and not ordinary feature tiering: authorization changes the permitted precision and repair trajectory of behavior, while exact meaning is a time-distributed service outcome rather than content waiting inside the file.", + "roughSketch": "The build produces graded contracts for each protected behavior: non-negotiable invariants, bounded approximate responses, reconciliation obligations, and state checkpoints. The emitted application can fabricate a temporary ordinary implementation that stays within its current grade, while licensed deliveries provide exact reference cases or higher-grade synthesis constraints. The owner receives an operational dashboard showing fidelity debt and reconciliation status instead of a simple valid-or-invalid license indicator.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.8, + "impact": 4, + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "It combines graceful feature degradation, approximate computing, service-tier licensing, fidelity debt, and eventual state reconciliation." + }, + { + "name": "Proofreading Royalty", + "lens": "DNA error-correction and codon degeneracy", + "assumptionOverturned": "the meaning of the program lives inside the file", + "track": "distribution, licensing, and delivery models", + "sourceMechanism": "Replicative DNA polymerases improve accuracy through proofreading: when a wrong nucleotide is incorporated, extension often slows, the primer end shifts from the polymerase active site to a 3-to-5 exonuclease site, the incorrect nucleotide is removed, and synthesis resumes from the corrected end. Correctness therefore emerges from a coupled propose-inspect-correct cycle rather than nucleotide selection alone. Mapping: the build step separates broad candidate production from proprietary acceptance and correction policy; the emitted artifact is a candidate maker plus a correction interface; the interpreter is replaced by a licensed proofreader, which may be a local service appliance or managed service; the developer authors the semantic acceptance boundary; automated analysis/reconstruction tooling can summarize candidate production but cannot derive which candidates become certified outcomes; and run-time presents candidate decisions for correction before committing the accepted result.", + "coreIdea": "Ruam turns proprietary logic into a metered correctness service: the shipped application proposes a bounded set of plausible actions, while the licensed proofreader selects, edits, or certifies the action that the product may commit. Customers purchase certified decisions, correction capacity, or domain coverage rather than seats for a static program. The commercially valuable meaning lives in the evolving correction policy and its stream of certifications, not in the proposal file.", + "whyNovel": "This is not a license server returning permission, a decoder returning hidden instructions, or remote execution of the whole function; the local product does substantive work, while the saleable unit is semantic correction and accountable certification at the decision boundary.", + "roughSketch": "Ruam identifies decision surfaces in developer code and compiles each into a local proposal factory plus a narrow, typed proofreading protocol. The developer keeps or distributes a separately evolving policy that can reject, reshape, or certify proposals; receipts make the accepted causal history observable to the owner. Distribution offerings can meter per certified outcome, bundle policy domains, or place an organization-controlled proofreader appliance on site.", + "boldness": 5, + "isFusion": false, + "parents": [], + "recognizability": 0.88, + "impact": 4, + "rails": { + "serverFree": "fails", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "Local candidate generation followed by an external policy oracle is a familiar split-computing and decision-certification service pattern." + }, + { + "name": "The Questioning Scar", + "lens": "adaptive immune selection × holographic refocusing × hysteretic metamaterials", + "assumptionOverturned": "Opacity and correctness are not in tension; a diagnostic question can be part of the artifact's continuing formation, and neither a fixed output nor a shipped decoder is required.", + "track": "verifiability and developer experience", + "sourceMechanism": "Parents: The Tolerance Mold makes behavior by eliminating candidates against semantic negative-space surfaces; Focus-Pull Debugger reconstructs only the causal plane named by a developer's question; Hysteretic Dialect Lattice stores the ordered history of use as a metastable grammar. New interaction: a diagnostic question is applied as a temporary load to the history-bearing tolerance mold, changing which explanation-and-execution pairs can survive. The surviving pair both answers the question and snaps the lattice into a new metastable dialect, so later executions naturally deposit finer evidence around that semantic concern. Immune selection makes each focused explanation behaviorally accountable, holographic focus turns owner inquiry into selective pressure, and hysteresis ensures that accumulated insight never becomes one canonical implementation view.", + "coreIdea": "Ruam would provide a debugger that grows a one-use causal organ for each owner question. The organ must simultaneously reproduce the owned behavior and express a faithful answer in the requested semantic vocabulary; after it dissolves, the artifact retains a structural scar that changes how future behavior and evidence are formed. Debugging therefore improves an installation's owner observability over time while making its internal dialect increasingly biographical rather than convergent.", + "whyNovel": "This is not source mapping, trace collection, artifact-identity checking, structural shuffling, or ordinary adaptive optimization. Explanation is not a report emitted by a stable implementation: question, implementation, evidence, and the artifact's next grammar are co-produced by one contract-governed selection event.", + "roughSketch": "At build time, derive semantic exclusion surfaces and a vocabulary of promises, effects, authorities, and domain decisions, then embed them in a multistable composition lattice. When the developer asks a question, translate it into focus constraints and relax the lattice until a candidate causal organ passes both behavior assays and explanation-coherence assays. Return its semantic account, record the snap-through path in an owner lineage view, and persist only the resulting lattice state as the starting material for later calls.", + "boldness": 5, + "parents": [ + "The Tolerance Mold", + "Focus-Pull Debugger", + "Hysteretic Dialect Lattice" + ], + "isFusion": true, + "recognizability": 0.38, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Rehearsal Germ Layers", + "lens": "holographic whole-scene reconstruction × immune affinity maturation × staged self-folding origami", + "assumptionOverturned": "The emitted artifact need not be complete or fixed, correctness evidence can generate protected structure rather than merely assess it, and no decoder-shaped component must accompany the result.", + "track": "new product surfaces and capabilities", + "sourceMechanism": "Parents: Whole-Show Rehearsal Mesh supplies several independently useful, low-resolution versions of the entire product; Affinity Furnace grows contingent behavior clones by selecting against semantic specimens; Post-Emit Morphogenesis lets deployment exposure fold an under-differentiated precursor into mature organs. New interaction: the disagreements among whole-product rehearsal surfaces become both affinity assays and morphogen gradients. A candidate organ survives only when it sharpens agreement among several coarse end-to-end projections; that successful sharpening folds the precursor, and the new fold emits a higher-resolution rehearsal that becomes the assay for the next developmental stage. Rehearsal therefore directs maturation, selection chooses which folds become anatomy, and the developing anatomy continuously rewrites the rehearsal material that selects its descendants.", + "coreIdea": "Ruam would ship preview, test, partner, edge, and production surfaces as germ layers of one developing product rather than as separately derived bundles. Real journeys cause these complete-but-coarse surfaces to overlap; wherever their observable promises agree but lack detail, the installation grows a specialized behavior organ and promotes its result into the next round of rehearsals. The mature application is thus generated by a chain of cross-resolution correctness agreements that did not exist at emit time.", + "whyNovel": "This is not lazy materialization, profile-guided specialization, per-install variation, a test wrapper, or fragments that later reassemble. No finished implementation is waiting behind the rehearsals: independently complete product experiences serve as developmental material, and their improving mutual coherence is what manufactures new executable anatomy.", + "roughSketch": "Let developers declare journeys, observable promises, and permitted resolution loss for several deployment surfaces. Emit a semantic laminate containing coarse whole-journey realizations, neutral operator tissue, and staged fold rules. During use, compare overlapping consequences across the surfaces, select candidate tissue that monotonically sharpens all affected journeys, fold it into a persistent organ, and regenerate the surface suite from the newly differentiated state; an owner console shows lineage from rehearsal discrepancy to organ formation.", + "boldness": 5, + "parents": [ + "Whole-Show Rehearsal Mesh", + "Affinity Furnace", + "Post-Emit Morphogenesis" + ], + "isFusion": true, + "recognizability": 0.52, + "impact": 4, + "rails": { + "serverFree": "pass", + "sizeLean": "fails", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Fringe-Driven Phase Healing", + "lens": "holographic interferometry × idiotypic immune phases × history-bearing soliton transport", + "assumptionOverturned": "Correctness observation can be the active material of run-time maintenance, meaning need not reside in a decoder or command stream, and protection can continue after build time.", + "track": "self-modifying / living output", + "sourceMechanism": "Parents: Semantic Fringe Observatory turns small consequence differences into a whole-product deformation field; Idiotype Phase Matter expresses behavior as an attractor of mutually regulating ordinary grains; Soliton Tissue carries collective pulses through a substrate whose recent passage changes later propagation. New interaction: a semantic fringe is converted into a bounded collective pulse and launched through the reciprocal phase material. Its passage temporarily changes which grains stimulate or suppress one another, moving the material toward an attractor whose public consequences reduce the fringe; the altered tissue then changes both future behavior and which deformations become visible next. The observatory supplies repair geometry, the pulse makes that geometry an executable event, and the phase network turns each repair into a new semantic equilibrium rather than a patched copy of an earlier implementation.", + "coreIdea": "Ruam would make protected output homeostatic at the semantic level. Dependency, browser, configuration, or policy changes produce consequence fringes; those fringes become traveling repair waves that reorganize the distributed behavior phase until owned promises regain coherence. The owner observes the same wave lineage that maintained the program, so explanation and continued formation arise from one process without revealing a stable command graph.", + "whyNovel": "This does not compare artifact identity, inspect a fixed implementation, invalidate execution, or apply a stored correction script. The correction has no prewritten target structure: a measured semantic deformation becomes a physicalized wave whose interaction with a collective phase discovers and installs a new contract-faithful equilibrium.", + "roughSketch": "Record reference consequence fields from specifications and independent oracle runs, then synthesize a sparse population of reciprocal behavior grains inside a nonlinear transport tissue. At run time, convert any measured fringe into a shaped density perturbation, propagate it through junctions, and let its wake alter local recognition thresholds until designated outcome fields reconverge. Present developers with a pulse-and-attractor lineage explaining which promises drifted, where the repair wave traveled, and which macroscopic phase variables now support the result.", + "boldness": 5, + "parents": [ + "Semantic Fringe Observatory", + "Idiotype Phase Matter", + "Soliton Tissue" + ], + "isFusion": true, + "recognizability": 0.68, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "The Migrating Treaty", + "lens": "MHC-restricted recognition × mycorrhizal reciprocal allocation × polarized metamaterial domain walls", + "assumptionOverturned": "Program meaning need not live in a standalone file, protection need not be a one-time build act, and the only expressive region can be recreated elsewhere after every use.", + "track": "semantic-level protection", + "sourceMechanism": "Parents: MHC-Restricted Program Tissue makes behavior a typed relation between ordinary host workflow and Ruam material; Reciprocal-Exchange Computation makes meaning a viable circulation of complementary offerings; Traveling Isogloss localizes expressiveness at a movable boundary between two rigid semantic regions. New interaction: host events do not merely select or trigger behavior; they contribute typed resources to a reciprocal circulation that can close only at the current domain wall. Closing the circulation yields the requested consequence and shifts the wall, which changes local exchange terms and therefore which future host conjunction can form the next viable circuit. Context supplies the economy, the economy gives the boundary a reason and direction to move, and boundary motion continually rewrites how context can become meaning.", + "coreIdea": "Protected behavior becomes a migrating treaty between an application and its Ruam-grown tissue. Each call is realized only when current host capabilities and artifact offerings form a self-sustaining exchange at the sole expressive boundary; fulfilling that treaty relocates the boundary and alters the terms under which the next behavior may arise. Neither participant contains a standalone implementation, and yesterday's relational explanation is not the grammar of tomorrow's agreement.", + "whyNovel": "This is not cross-file co-residence, a capability gate, a missing external value, fragment movement, or a mobile decoder. The host provides useful semantic work rather than concealed bits, while meaning is the completed circulation at a history-moving interface—not content stored on either side or pieces waiting to be joined.", + "roughSketch": "Add a design surface where developers name host capabilities, lifecycle events, reciprocal offerings, and public semantic invariants. Co-synthesize a host protocol and two families of locally rigid Ruam clauses with an initial polarized boundary. Each invocation presents host offerings as resource gradients; local membranes negotiate a viable loop at the boundary, expose its outcome, then refold neighboring clauses so the wall and exchange schedule move together. An owner view renders treaty lineage and invariant satisfaction without presenting a permanent source correspondence.", + "boldness": 5, + "parents": [ + "MHC-Restricted Program Tissue", + "Reciprocal-Exchange Computation", + "Traveling Isogloss" + ], + "isFusion": true, + "recognizability": 0.6, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Treaty-Sightline Foundry", + "lens": "linguistic anisomorphism × forced perspective × codon-degenerate manufacturing", + "assumptionOverturned": "installation produces one implementation whose correctness can be explained from one privileged semantic viewpoint", + "track": "new product surfaces and capabilities", + "sourceMechanism": "Parents: Anisomorphic Ruleworlds contributes several contract-equivalent ontologies with no canonical decomposition; Question-Sightline Observatory contributes complete but deliberately non-nestable causal projections for developer questions; Phenotype Manufacturing Right contributes licensed local fabrication of ordinary implementations from a behavioral phenotype. The new interaction is that a diagnostic question does not merely inspect the installed specimen: its sightline selects a foreign ruleworld in which the local foundry must manufacture a second, ontology-distinct specimen. Agreement is certified only between the question-relative consequences of the two specimens. The treaty therefore changes what the foundry makes, the foundry gives each projection a materially different causal world to examine, and the projection turns cross-world manufacture into correctness evidence.", + "coreIdea": "Ruam becomes a question-conditioned semantic foundry. An installation begins with one locally manufactured realization of an outcome treaty. When its owner asks why a promise held, the system must cross into an ontology that partitions actors, resources, causes, and actions differently, fabricate a temporary counterpart from local materials, and solve for the smallest sightline on which both products make the same promise. Explanations are thus constructive acts that expand the product's population of valid ruleworlds; there is neither a canonical implementation nor a universal explanation waiting to be extracted.", + "whyNovel": "This is not per-build polymorphism, ordinary component substitution, source mapping, log filtering, an encoded program, or a remote missing term. The durable unit is a three-way agreement among an outcome treaty, a newly manufactured causal ontology, and a question-complete projection. None of the parents alone makes explanation manufacture a semantically incommensurable product whose comparison is itself the certificate.", + "roughSketch": "The developer authors invariant outcomes, contrasting domain readings, and supported question classes. A licensed install-time foundry makes an ordinary local module in one reading. The owner console turns a question such as “what authorized this effect?” into a semantic sightline, chooses a treaty-compatible ontology in which that question has a different causal anatomy, and asks the foundry for a short-lived counterpart. A projection solver reports only the cross-world seam needed to show that the promised outcome survived. Repeated questions grow a site-specific atlas of manufactured ruleworlds without ever converging on a master model.", + "boldness": 5, + "parents": [ + "Anisomorphic Ruleworlds", + "Question-Sightline Observatory", + "Phenotype Manufacturing Right" + ], + "isFusion": true, + "recognizability": 0.6, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "" + }, + { + "name": "Evidential Phase Succession", + "lens": "mechanical phase transitions × grammatical evidentiality × iterated language learning", + "assumptionOverturned": "a living artifact can change its form while retaining one stable language for stating and testing its rules", + "track": "self-modifying or living output", + "sourceMechanism": "Parents: Allomorphic Phase Sheet contributes collective grammar changes at compatibility bifurcations; Evidential Rulecraft contributes rulings that exist only when live provenance satisfies a justification grammar; Iterated Dialect Mechanics contributes successor populations that inherit coordination through limited demonstrations rather than a stored vocabulary. The new interaction makes evidence the mechanical load that tips a whole region into a new grammatical phase, while the resulting phase changes which provenance distinctions the next apprentice can perceive and teach. Succession is required to stabilize each phase: an apprentice must learn the new evidential categories from live cases before becoming the region's speaker, and its teaching bottleneck lays different future bifurcations.", + "coreIdea": "A protected behavior lives as a lineage of evidential phases. Accumulated patterns of witnessed, inferred, delegated, and contested facts create semantic pressure until the current dialect can no longer express its rulings compactly; the artifact then undergoes a collective grammar transition and raises a successor using a narrow curriculum drawn from that pressure. Because the successor's categories determine what will count as evidence next, use changes not only the rule's realization but the future conditions under which the rule can be justified.", + "whyNovel": "This is not opcode mutation, a state machine, a self-test, adaptive caching, rotating syntax, or a decoder that teaches another decoder. The evolving unit is the coupling between provenance categories, region-wide causal grammar, and cultural transmission. No parent alone yields a phase change that rewrites its own evidence ontology and must be ratified by a newly taught generation.", + "roughSketch": "The developer supplies outcome envelopes, unacceptable outcomes, provenance roles, and example adjudications rather than a procedure. The initial artifact speaks a provisional evidential dialect. Live rulings accumulate a pressure field over distinctions the dialect handles poorly. At a compatibility threshold, an entire semantic region changes its admissible causal relations; a pupil observes a deliberately narrow set of cases from both sides of the transition, proves coordination on fresh cases, then replaces the prior speaker. Owner tooling shows phase boundaries, curriculum coverage, and contract-level reasons without preserving a timeless internal vocabulary.", + "boldness": 5, + "parents": [ + "Allomorphic Phase Sheet", + "Evidential Rulecraft", + "Iterated Dialect Mechanics" + ], + "isFusion": true, + "recognizability": 0.42, + "impact": 5, + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "research-spike", + "notNovelBecause": "" + }, + { + "name": "Situated Absence Lineage", + "lens": "engineered negative space × deictic common ground × biological lineage repair", + "assumptionOverturned": "a behavioral guarantee denotes the same forbidden state for every role, place, time, and supported descendant", + "track": "distribution, licensing, and delivery models", + "sourceMechanism": "Parents: Black-Art Absence Certificates contributes developer-level proofs about states and causal relationships that must remain absent; The Deictic Commons contributes referents whose meaning is minted by live roles, places, epochs, and joint attention; Lineage Repair Warranty contributes local remanufacture of a drifting descendant from healthy regions, certified history, and current policy. The new interaction makes every absence boundary deictic: “this authority,” “our epoch,” or “the witnessed object” acquires force only in a live common ground. When an absence ceases to hold, the warranty does not restore an old implementation; it remanufactures the affected relationship and, in doing so, creates new referents and retires old ones. Those referent changes redefine the next absence certificates, so guarantees guide repair while repair evolves what the guarantees can mean.", + "coreIdea": "Ruam sells continuity of situated impossibilities. The protected product is a lineage whose descendants promise that certain domain relationships cannot arise within the common ground currently shared by particular participants. A semantic disagreement triggers constructive repair of the relationship, not byte invalidation; the repaired descendant establishes a new shared situation with a correspondingly new negative space. Meaning survives as a traceable succession of role-relative promises rather than as one file or timeless policy.", + "whyNovel": "This is not a source-integrity check, environment check, patch chain, external key, access-controlled log, or fragment reconstruction. The maintained object is a moving boundary of relationship-level impossibility whose vocabulary is created by use. None of the parents alone makes repair both consume and regenerate the situated semantics of the warranty it fulfills.", + "roughSketch": "The developer defines role lattices, joint-attention events, temporal perspectives, and impossible domain relations. Each deployment carries a lineage passport of certified common-ground transitions, plus service boundaries around replaceable relationships. The custodian proves absence claims against the live referents. If a claim fails, a maintenance process preserves conforming relationships, resynthesizes the smallest broken social or temporal relation from current policy and trusted history, and issues a descendant passport whose newly minted terms become the basis of subsequent guarantees. Owner diagnostics narrate the repaired promise in domain language.", + "boldness": 5, + "parents": [ + "Black-Art Absence Certificates", + "The Deictic Commons", + "Lineage Repair Warranty" + ], + "isFusion": true, + "recognizability": 0.8, + "impact": 4, + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "It composes contextual policy contracts, runtime monitoring, provenance certificates, and automated program repair into a lineage service." + }, + { + "name": "Confidence-Driven Contract Metabolism", + "lens": "auxetic metamaterials × stage convincers × synonymous biological supply", + "assumptionOverturned": "evidence verifies a finished product assembled from a supplier catalog that stays external to execution", + "track": "verifiability and developer experience", + "sourceMechanism": "Parents: Auxetic Contract Tissue contributes a coupled semantic material whose neighborhood and resting geometry expand under local demand; The Convincer Ecology contributes heterogeneous, partially independent demonstrations that support developer trust questions; Synonymous Supply Network contributes changing families of ordinary qualified components whose global meaning lies in fit and tolerance relationships. The new interaction turns low confidence into mechanical demand on the contract tissue. Stretching one trust question recruits perpendicular supplier components specifically to grow unlike demonstrations; the results then qualify, retire, or redefine supplier classes and persist as a new resting geometry. Thus evidence pressure reshapes the product, newly sourced parts change which evidence can be independent, and the supply ecology becomes endogenous to the artifact's semantic metabolism.", + "coreIdea": "Ruam emits a product that grows where its owner has the least confidence. A thinly supported domain claim creates semantic tension, causing the artifact to widen that contract into neighboring constructions, source qualified ordinary components that realize those new dimensions, and stage independent demonstrations across them. Successful demonstrations become structural nutrients: they alter future substitution sockets and make later behavior expressible through a broader, locally evolved contract tissue.", + "whyNovel": "This is not redundant implementations, code bloat, a package manager, a fixed test suite, fragment scattering, or adaptive optimization. The basic unit is a feedback loop in which confidence topology is constitutive material for implementation growth and component qualification. None of the parents alone makes trust questions physically reorganize the product's semantic dimensions and its future supplier market.", + "roughSketch": "The developer declares behavioral boundaries, trust questions, evidence-independence criteria, and component tolerance gauges. The installed artifact maintains a causal stress map: a claim supported by too few unlike demonstrations stretches its region, creating new substitution sockets along coupled contract axes. A sourcing process fills them from eligible local or organizational catalogs, while a rehearsal workbench derives metamorphic, boundary, counterfactual, and production-grounded convincers from the expanded tissue. Evidence updates both the confidence map and supplier qualifications, so each installation develops a distinct, owner-readable metabolism rather than accumulating a stable implementation map.", + "boldness": 5, + "parents": [ + "Auxetic Contract Tissue", + "The Convincer Ecology", + "Synonymous Supply Network" + ], + "isFusion": true, + "recognizability": 0.74, + "impact": 4, + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "fails", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + }, + "maturity": "product-pivot", + "notNovelBecause": "The core loop is confidence-gap-driven test generation, component sourcing, qualification, and contract-guided program synthesis." + } + ], + "headline": "The most promising Gen2 direction is to stop treating a fixed implementation as the protected object and instead make a developer-owned semantic treaty generate, evolve, and explain multiple causally distinct realizations. Traveling Isogloss supplies the strongest new execution primitive—a moving boundary that alone can express behavior—while Evidential Phase Succession and Anisomorphic Ruleworlds show how that primitive can mature into a product: meaning can migrate between grammars and ontologies, with owner-facing evidence proving stable outcomes rather than exposing a canonical program. Phenotype Manufacturing Right and The Questioning Scar add credible commercial and developer surfaces. Together they suggest Ruam Gen2 as a semantic-lifecycle system whose resilience to mechanical reconstruction is a consequence of plural, history-bearing realizations, not another concealment layer.", + "slate": { + "headline": "The most promising Gen2 direction is to stop treating a fixed implementation as the protected object and instead make a developer-owned semantic treaty generate, evolve, and explain multiple causally distinct realizations. Traveling Isogloss supplies the strongest new execution primitive—a moving boundary that alone can express behavior—while Evidential Phase Succession and Anisomorphic Ruleworlds show how that primitive can mature into a product: meaning can migrate between grammars and ontologies, with owner-facing evidence proving stable outcomes rather than exposing a canonical program. Phenotype Manufacturing Right and The Questioning Scar add credible commercial and developer surfaces. Together they suggest Ruam Gen2 as a semantic-lifecycle system whose resilience to mechanical reconstruction is a consequence of plural, history-bearing realizations, not another concealment layer.", + "method": "Near-duplicates were clustered without deleting or merging any candidate. For each rail, pass=1.0, needs-relaxation=0.8, and fails=0.5; feasibilityWeight is the arithmetic mean across serverFree, sizeLean, cspSafe, buildRuntimeProvable, and additiveApi. Composite is impact * (1 - recognizability) * feasibilityWeight, rounded to three decimals. Ranking is descending composite, then lower recognizability, then higher impact, with candidate input order retained only for complete ties; no score was adjusted for narrative preference.", + "clusterSummary": [ + { + "cluster": "moving-boundary-and-grammar-succession", + "description": "Meaning resides in a history-bearing boundary or grammar that changes its own future transition rules; this is the clearest genuinely Gen2 mechanism family.", + "members": [ + "Hysteretic Dialect Lattice", + "Traveling Isogloss", + "Allomorphic Phase Sheet", + "Iterated Dialect Mechanics", + "Evidential Phase Succession" + ] + }, + { + "cluster": "plural-ontologies-and-relational-meaning", + "description": "Behavior exists in relations among host context, evidence, shared referents, or multiple causal ontologies rather than inside one artifact.", + "members": [ + "MHC-Restricted Program Tissue", + "Evidential Rulecraft", + "The Deictic Commons", + "Anisomorphic Ruleworlds", + "The Migrating Treaty" + ] + }, + { + "cluster": "emergent-material-computation", + "description": "Ordinary local components collectively realize behavior through attractors, transient topology, population dosage, exchange circulation, or traveling waves.", + "members": [ + "Idiotype Phase Matter", + "Anastomotic Execution", + "Heterokaryotic Semantics", + "Reciprocal-Exchange Computation", + "Soliton Tissue" + ] + }, + { + "cluster": "generative-phenotype-manufacture", + "description": "A behavioral envelope or phenotype, rather than stored code, drives local synthesis, selection, growth, or licensed manufacture of disposable implementations.", + "members": [ + "The Tolerance Mold", + "Affinity Furnace", + "The Germinating Program", + "Post-Emit Morphogenesis", + "Phenotype Manufacturing Right" + ] + }, + { + "cluster": "semantic-observability-and-proof", + "description": "Developer trust comes from question-relative explanations, independent executable projections, effect contracts, absence proofs, or evolving evidence portfolios.", + "members": [ + "Focus-Pull Debugger", + "Angle-Multiplexed Truth Deck", + "Semantic Fringe Observatory", + "The Cue-Sheet Contract Theater", + "Question-Sightline Observatory", + "Dual-Reality Debugging", + "Black-Art Absence Certificates", + "The Convincer Ecology", + "The Questioning Scar" + ] + }, + { + "cluster": "developmental-product-surfaces", + "description": "Product surfaces, semantic deformation, contract geometry, and owner questions become active material that grows or repairs the deployed system.", + "members": [ + "Whole-Show Rehearsal Mesh", + "Auxetic Contract Tissue", + "Rehearsal Germ Layers", + "Fringe-Driven Phase Healing", + "Treaty-Sightline Foundry", + "Confidence-Driven Contract Metabolism" + ] + }, + { + "cluster": "lifecycle-licensing-and-continuity", + "description": "Commercial value attaches to qualified supply, descendant repair, semantic fidelity, certified decisions, or continuity of contextual guarantees rather than static bytes.", + "members": [ + "Synonymous Supply Network", + "Lineage Repair Warranty", + "Fidelity-Gradient License", + "Proofreading Royalty", + "Situated Absence Lineage" + ] + } + ], + "ranked": [ + { + "rank": 1, + "name": "Traveling Isogloss", + "track": "self-modifying or living output", + "composite": 2.870, + "recognizability": 0.3, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Two internally rigid semantic regions share one movable interface that alone can realize calls, and each completed call relocates that expressive interface for the next invocation.", + "whyNotGen1": "Unlike salt, keystream, or digest work, no encoded implementation is varied or verified; the computation is the history-dependent motion of a semantic boundary.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "moving-boundary-and-grammar-succession", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 2, + "name": "Evidential Phase Succession", + "track": "self-modifying or living output", + "composite": 2.378, + "recognizability": 0.42, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Accumulated provenance pressure triggers a region-wide grammar change, after which a narrowly trained successor must ratify the new evidence categories before taking over.", + "whyNotGen1": "Unlike salt, keystream, or digest work, the changing object is the coupling among evidence ontology, causal grammar, and learned succession rather than encoded bytes.", + "maturity": "research-spike", + "isFusion": true, + "parents": [ + "Allomorphic Phase Sheet", + "Evidential Rulecraft", + "Iterated Dialect Mechanics" + ], + "cluster": "moving-boundary-and-grammar-succession", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 3, + "name": "Anisomorphic Ruleworlds", + "track": "semantic-level protection", + "composite": 2.214, + "recognizability": 0.46, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Execution moves among contract-equivalent rule systems that disagree about what counts as an actor, resource, cause, and action while preserving observable outcomes.", + "whyNotGen1": "Unlike salt, keystream, or digest work, it preserves an outcome treaty while changing the program's causal ontology, not the representation of one procedure.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "plural-ontologies-and-relational-meaning", + "rails": { + "serverFree": "pass", + "sizeLean": "fails", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 4, + "name": "Phenotype Manufacturing Right", + "track": "distribution, licensing, and delivery models", + "composite": 1.936, + "recognizability": 0.56, + "impact": 5, + "feasibilityWeight": 0.88, + "oneLine": "A license authorizes a local foundry to synthesize and certify a disposable implementation from a behavioral phenotype and currently available platform components.", + "whyNotGen1": "Unlike salt, keystream, or digest work, authorization governs manufacture of a new conforming specimen rather than access to or decoding of a canonical artifact.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "generative-phenotype-manufacture", + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 5, + "name": "The Questioning Scar", + "track": "verifiability and developer experience", + "composite": 1.934, + "recognizability": 0.38, + "impact": 4, + "feasibilityWeight": 0.78, + "oneLine": "Each owner question grows a temporary implementation that must both reproduce behavior and answer in domain terms, then leaves a structural change that shapes later behavior and evidence.", + "whyNotGen1": "Unlike salt, keystream, or digest work, explanation and implementation are co-produced for a question and permanently alter the artifact's future grammar.", + "maturity": "research-spike", + "isFusion": true, + "parents": [ + "The Tolerance Mold", + "Focus-Pull Debugger", + "Hysteretic Dialect Lattice" + ], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 6, + "name": "Soliton Tissue", + "track": "novel transformation paradigms", + "composite": 1.845, + "recognizability": 0.55, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Inputs launch stable collective pulses through a nonlinear medium whose state is rewritten by prior pulses, with outputs read from wave phenotype rather than dispatched operations.", + "whyNotGen1": "Unlike salt, keystream, or digest work, the representation is a history-bearing transport medium and its collective waves, with no instruction stream to conceal.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "emergent-material-computation", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 7, + "name": "Treaty-Sightline Foundry", + "track": "new product surfaces and capabilities", + "composite": 1.760, + "recognizability": 0.6, + "impact": 5, + "feasibilityWeight": 0.88, + "oneLine": "An owner question selects a foreign causal ontology, triggers manufacture of a temporary implementation in that ontology, and derives the smallest cross-world projection that certifies the same outcome treaty.", + "whyNotGen1": "Unlike salt, keystream, or digest work, a diagnostic question manufactures an ontology-distinct counterpart and uses cross-world agreement as the explanation certificate.", + "maturity": "product-pivot", + "isFusion": true, + "parents": [ + "Anisomorphic Ruleworlds", + "Question-Sightline Observatory", + "Phenotype Manufacturing Right" + ], + "cluster": "developmental-product-surfaces", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 8, + "name": "Iterated Dialect Mechanics", + "track": "semantic-level protection", + "composite": 1.722, + "recognizability": 0.58, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "A running artifact periodically trains a successor from constrained demonstrations of semantic play, retires its own vocabulary, and retains only contract-level coordination.", + "whyNotGen1": "Unlike salt, keystream, or digest work, successive artifacts share no hidden command vocabulary; behavior persists through cultural transmission of semantic invariants.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "moving-boundary-and-grammar-succession", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 9, + "name": "The Migrating Treaty", + "track": "semantic-level protection", + "composite": 1.640, + "recognizability": 0.6, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Host capabilities and artifact offerings close a reciprocal circuit only at a movable semantic boundary, and fulfilling one call shifts the boundary and renegotiates the next call's terms.", + "whyNotGen1": "Unlike salt, keystream, or digest work, neither side contributes concealed bits; meaning is a self-sustaining exchange whose successful completion relocates its own interface.", + "maturity": "research-spike", + "isFusion": true, + "parents": [ + "MHC-Restricted Program Tissue", + "Reciprocal-Exchange Computation", + "Traveling Isogloss" + ], + "cluster": "plural-ontologies-and-relational-meaning", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 10, + "name": "The Cue-Sheet Contract Theater", + "track": "verifiability and developer experience", + "composite": 1.496, + "recognizability": 0.66, + "impact": 5, + "feasibilityWeight": 0.88, + "oneLine": "Developers specify externally meaningful effects and causal obligations, then Ruam generates a role-and-cue realization plus a console that proves and replays only failed semantic beats.", + "whyNotGen1": "Unlike salt, keystream, or digest work, the durable unit is an effect contract and its proof, while the generated realization need not retain function-level ancestry.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 11, + "name": "MHC-Restricted Program Tissue", + "track": "resilience to automated understanding", + "composite": 1.398, + "recognizability": 0.62, + "impact": 4, + "feasibilityWeight": 0.92, + "oneLine": "Ordinary host events present typed semantic operations to a complementary artifact, and only their live contextual conjunction realizes the protected behavior.", + "whyNotGen1": "Unlike salt, keystream, or digest work, the host supplies useful semantic participation rather than a secret term, so behavior belongs to the relation between two systems.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "plural-ontologies-and-relational-meaning", + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 12, + "name": "Rehearsal Germ Layers", + "track": "new product surfaces and capabilities", + "composite": 1.382, + "recognizability": 0.52, + "impact": 4, + "feasibilityWeight": 0.72, + "oneLine": "Coarse but complete product surfaces overlap on real journeys, and their agreement selects newly synthesized behavior organs that increase the next generation's resolution.", + "whyNotGen1": "Unlike salt, keystream, or digest work, no finished implementation waits to be recovered; cross-resolution product agreement is the developmental material that grows one.", + "maturity": "research-spike", + "isFusion": true, + "parents": [ + "Whole-Show Rehearsal Mesh", + "Affinity Furnace", + "Post-Emit Morphogenesis" + ], + "cluster": "developmental-product-surfaces", + "rails": { + "serverFree": "pass", + "sizeLean": "fails", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 13, + "name": "Auxetic Contract Tissue", + "track": "self-modifying or living output", + "composite": 1.378, + "recognizability": 0.58, + "impact": 4, + "feasibilityWeight": 0.82, + "oneLine": "Exercising a contract mechanically expands its semantic neighborhood along coupled dimensions, and part of that newly formed structure becomes the artifact's next resting language.", + "whyNotGen1": "Unlike salt, keystream, or digest work, execution changes the dimensionality of the implementation vocabulary instead of re-encoding a fixed route.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "developmental-product-surfaces", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 14, + "name": "Hysteretic Dialect Lattice", + "track": "self-modifying or living output", + "composite": 1.312, + "recognizability": 0.68, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Each installation grammaticalizes frequent compositions, erodes unused ones, and emits a successor grammar whose future transitions depend on its usage history.", + "whyNotGen1": "Unlike salt, keystream, or digest work, the artifact evolves a history-dependent constraint grammar rather than applying fresh variation to stable commands.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "moving-boundary-and-grammar-succession", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 15, + "name": "Fringe-Driven Phase Healing", + "track": "self-modifying / living output", + "composite": 1.312, + "recognizability": 0.68, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Measured semantic drift is converted into a traveling pulse that changes local interaction thresholds until the distributed behavior settles into a new contract-faithful attractor.", + "whyNotGen1": "Unlike salt, keystream, or digest work, a consequence deformation directly becomes the repair event that reorganizes a collective semantic phase.", + "maturity": "research-spike", + "isFusion": true, + "parents": [ + "Semantic Fringe Observatory", + "Idiotype Phase Matter", + "Soliton Tissue" + ], + "cluster": "developmental-product-surfaces", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 16, + "name": "Anastomotic Execution", + "track": "novel transformation paradigms", + "composite": 0.984, + "recognizability": 0.76, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Each invocation assembles a transient causal network by fusing compatible incomplete components, reads the network's collective result, then dissolves or remodels it.", + "whyNotGen1": "Substantially recognizable as a dynamic dataflow or actor graph; unlike salt, keystream, or digest work, its variation is per-call topology rather than encoded data.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "emergent-material-computation", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 17, + "name": "Allomorphic Phase Sheet", + "track": "self-modifying or living output", + "composite": 0.975, + "recognizability": 0.75, + "impact": 5, + "feasibilityWeight": 0.78, + "oneLine": "Runtime pressure changes the legal causal relations across an entire semantic region, and each new coherent grammar redraws the set of possible future phase transitions.", + "whyNotGen1": "Substantially recognizable as dynamic program rewriting or metamorphic phase switching; unlike salt, keystream, or digest work, the switch changes regional compatibility rules.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "moving-boundary-and-grammar-succession", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 18, + "name": "Dual-Reality Debugging", + "track": "verifiability and developer experience", + "composite": 0.957, + "recognizability": 0.74, + "impact": 4, + "feasibilityWeight": 0.92, + "oneLine": "Production, operations, QA, and developer views use different semantic primitives, and temporary typed joins among them produce explanations checked by cross-view consistency laws.", + "whyNotGen1": "Substantially recognizable as role-specific telemetry with consistency checks; unlike salt, keystream, or digest work, it concerns observability views rather than concealment.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "pass" + } + }, + { + "rank": 19, + "name": "Reciprocal-Exchange Computation", + "track": "novel transformation paradigms", + "composite": 0.951, + "recognizability": 0.71, + "impact": 4, + "feasibilityWeight": 0.82, + "oneLine": "Local components exchange complementary resources until a self-sustaining circulation satisfies the owned outcome, with every use renegotiating the exchange terms.", + "whyNotGen1": "Substantially recognizable as distributed constraint solving or a chemical-reaction network; unlike salt, keystream, or digest work, meaning is a viable exchange circulation.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "emergent-material-computation", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 20, + "name": "Heterokaryotic Semantics", + "track": "novel transformation paradigms", + "composite": 0.918, + "recognizability": 0.72, + "impact": 4, + "feasibilityWeight": 0.82, + "oneLine": "Multiple incomplete evaluator lineages share a regulatory field, and changing their relative dosage changes the colony-level behavior while preserving a phenotype contract.", + "whyNotGen1": "Substantially recognizable as an adaptive ensemble or mixture of experts; unlike salt, keystream, or digest work, output depends on regulated population composition.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "emergent-material-computation", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 21, + "name": "Idiotype Phase Matter", + "track": "resilience to automated understanding", + "composite": 0.902, + "recognizability": 0.78, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "Input perturbs a sparse recurrent population of incomplete transformations, whose mutual activation and suppression settle into an attractor encoding the result.", + "whyNotGen1": "Substantially recognizable as recurrent, reservoir, or cellular-network computation; unlike salt, keystream, or digest work, the answer is a collective attractor.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "emergent-material-computation", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 22, + "name": "The Germinating Program", + "track": "novel transformation paradigms", + "composite": 0.902, + "recognizability": 0.78, + "impact": 5, + "feasibilityWeight": 0.82, + "oneLine": "A developmental constitution grows, reinforces, and retires causal pathways under workload while phenotype assays keep the changing implementation inside its behavioral envelope.", + "whyNotGen1": "Substantially recognizable as an adaptive runtime with profile-guided specialization; unlike salt, keystream, or digest work, the implementation is grown rather than decoded.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "generative-phenotype-manufacture", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 23, + "name": "Question-Sightline Observatory", + "track": "verifiability and developer experience", + "composite": 0.883, + "recognizability": 0.76, + "impact": 4, + "feasibilityWeight": 0.92, + "oneLine": "A semantic question generates a mechanically complete causal projection containing only the events needed for that answer, and different questions deliberately yield non-nestable views.", + "whyNotGen1": "Substantially recognizable as query-directed causal tracing and certified program slicing; unlike salt, keystream, or digest work, it is an owner observability surface.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "pass" + } + }, + { + "rank": 24, + "name": "Angle-Multiplexed Truth Deck", + "track": "New product surfaces and capabilities", + "composite": 0.880, + "recognizability": 0.8, + "impact": 5, + "feasibilityWeight": 0.88, + "oneLine": "One intent specification generates independently executable customer, audit, test, and performance projections whose observable boundaries must agree.", + "whyNotGen1": "Substantially recognizable as N-version programming, executable specifications, and multi-target compilation; unlike salt, keystream, or digest work, it multiplies product views.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 25, + "name": "The Tolerance Mold", + "track": "resilience to automated understanding", + "composite": 0.780, + "recognizability": 0.8, + "impact": 5, + "feasibilityWeight": 0.78, + "oneLine": "A runtime generator proposes small implementations and rejects any that violate a build-produced atlas of forbidden transitions, conservation laws, and accepted observations.", + "whyNotGen1": "Substantially recognizable as constraint-based program synthesis from negative examples; unlike salt, keystream, or digest work, it selects behavior rather than decodes code.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "generative-phenotype-manufacture", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 26, + "name": "Confidence-Driven Contract Metabolism", + "track": "verifiability and developer experience", + "composite": 0.749, + "recognizability": 0.74, + "impact": 4, + "feasibilityWeight": 0.72, + "oneLine": "Low-confidence claims expand adjacent contract dimensions, recruit qualified components, and generate independent demonstrations whose results reshape future component sockets.", + "whyNotGen1": "Substantially recognizable as confidence-driven test generation, component sourcing, and contract-guided synthesis; unlike salt, keystream, or digest work, evidence gaps reorganize the product.", + "maturity": "product-pivot", + "isFusion": true, + "parents": [ + "Auxetic Contract Tissue", + "The Convincer Ecology", + "Synonymous Supply Network" + ], + "cluster": "developmental-product-surfaces", + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "fails", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 27, + "name": "Black-Art Absence Certificates", + "track": "verifiability and developer experience", + "composite": 0.704, + "recognizability": 0.84, + "impact": 5, + "feasibilityWeight": 0.88, + "oneLine": "Ruam specifies impossible domain states and causal relationships, synthesizes within the remaining space, and explains violations through minimal semantic counterexamples.", + "whyNotGen1": "Substantially recognizable as safety-property model checking and runtime contracts; unlike salt, keystream, or digest work, it certifies developer-level negative behavior.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 28, + "name": "Lineage Repair Warranty", + "track": "distribution, licensing, and delivery models", + "composite": 0.702, + "recognizability": 0.82, + "impact": 5, + "feasibilityWeight": 0.78, + "oneLine": "A licensed deployment is maintained as a descendant lineage, with drift repaired locally from conforming regions, certified history, semantic policy, and permitted synthesis materials.", + "whyNotGen1": "Substantially recognizable as contract-guided program repair and self-healing software; unlike salt, keystream, or digest work, delivery constructs a new descendant.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "lifecycle-licensing-and-continuity", + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 29, + "name": "Fidelity-Gradient License", + "track": "distribution, licensing, and delivery models", + "composite": 0.672, + "recognizability": 0.8, + "impact": 4, + "feasibilityWeight": 0.84, + "oneLine": "License state controls an approved semantic approximation envelope, while renewed service progressively restores exact decisions and reconciles accumulated fidelity debt.", + "whyNotGen1": "Substantially recognizable as graceful degradation, approximate computing, and service-tier licensing; unlike salt, keystream, or digest work, authorization changes behavioral precision.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "lifecycle-licensing-and-continuity", + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 30, + "name": "Whole-Show Rehearsal Mesh", + "track": "New product surfaces and capabilities", + "composite": 0.662, + "recognizability": 0.82, + "impact": 4, + "feasibilityWeight": 0.92, + "oneLine": "Ruam generates complete preview, test, partner, edge, and production experiences at different semantic resolutions and upgrades journeys when their overlaps agree.", + "whyNotGen1": "Substantially recognizable as multi-fidelity, multi-target build generation; unlike salt, keystream, or digest work, each output is a useful whole-product surface.", + "maturity": "needs-rail-relaxed", + "isFusion": false, + "parents": [], + "cluster": "developmental-product-surfaces", + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 31, + "name": "Situated Absence Lineage", + "track": "distribution, licensing, and delivery models", + "composite": 0.624, + "recognizability": 0.8, + "impact": 4, + "feasibilityWeight": 0.78, + "oneLine": "A service certifies relationship-level impossibilities in live shared context, and any failed guarantee triggers repair that creates new referents and a descendant guarantee vocabulary.", + "whyNotGen1": "Substantially recognizable as contextual policy, monitoring, provenance, and program repair; unlike salt, keystream, or digest work, repair regenerates the semantics of future warranties.", + "maturity": "product-pivot", + "isFusion": true, + "parents": [ + "Black-Art Absence Certificates", + "The Deictic Commons", + "Lineage Repair Warranty" + ], + "cluster": "lifecycle-licensing-and-continuity", + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 32, + "name": "Post-Emit Morphogenesis", + "track": "self-modifying or living output", + "composite": 0.624, + "recognizability": 0.84, + "impact": 5, + "feasibilityWeight": 0.78, + "oneLine": "A deployed precursor uses real workload to differentiate interacting behaviors into organs and repeatedly replaces its own organizer with smaller descendants.", + "whyNotGen1": "Substantially recognizable as adaptive specialization or tiered JIT with a self-replacing bootstrap; unlike salt, keystream, or digest work, maturation constructs future anatomy.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "generative-phenotype-manufacture", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "needs-relaxation", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 33, + "name": "Focus-Pull Debugger", + "track": "New product surfaces and capabilities", + "composite": 0.576, + "recognizability": 0.88, + "impact": 5, + "feasibilityWeight": 0.96, + "oneLine": "A domain question selects and reconstructs a high-resolution causal plane from coarse whole-run evidence without exposing a universal address map.", + "whyNotGen1": "Substantially recognizable as query-conditioned provenance tracing and causal debugging; unlike salt, keystream, or digest work, it explains semantic questions.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "pass" + } + }, + { + "rank": 34, + "name": "Synonymous Supply Network", + "track": "distribution, licensing, and delivery models", + "composite": 0.493, + "recognizability": 0.86, + "impact": 4, + "feasibilityWeight": 0.88, + "oneLine": "Installation resolves ordinary qualified components from changing supplier catalogs and assembles them under published compatibility and tolerance contracts.", + "whyNotGen1": "Substantially recognizable as a governed component marketplace and package resolver; unlike salt, keystream, or digest work, the license buys a qualification ecosystem.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "lifecycle-licensing-and-continuity", + "rails": { + "serverFree": "needs-relaxation", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 35, + "name": "The Deictic Commons", + "track": "semantic-level protection", + "composite": 0.478, + "recognizability": 0.87, + "impact": 4, + "feasibilityWeight": 0.92, + "oneLine": "Application relationships mint situated referents at runtime, and each operation both uses those referents and revises the vocabulary available to later operations.", + "whyNotGen1": "Substantially recognizable as relationship-based policy, context-aware capabilities, and event-sourced rules; unlike salt, keystream, or digest work, context creates semantic referents.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "plural-ontologies-and-relational-meaning", + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 36, + "name": "The Convincer Ecology", + "track": "verifiability and developer experience", + "composite": 0.368, + "recognizability": 0.9, + "impact": 4, + "feasibilityWeight": 0.92, + "oneLine": "Ruam maintains an evidence graph of property, metamorphic, boundary, counterfactual, and production-derived demonstrations and grows new tests where owner confidence is thin.", + "whyNotGen1": "Substantially recognizable as an adaptive portfolio of established testing methods; unlike salt, keystream, or digest work, it is a developer confidence product.", + "maturity": "shippable-now", + "isFusion": false, + "parents": [], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "pass" + } + }, + { + "rank": 37, + "name": "Proofreading Royalty", + "track": "distribution, licensing, and delivery models", + "composite": 0.365, + "recognizability": 0.88, + "impact": 4, + "feasibilityWeight": 0.76, + "oneLine": "A local application proposes bounded candidate actions while a licensed service selects, edits, or certifies the action permitted to commit.", + "whyNotGen1": "Substantially recognizable as split computing with an external policy oracle; unlike salt, keystream, or digest work, the remote product is decision certification.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "lifecycle-licensing-and-continuity", + "rails": { + "serverFree": "fails", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 38, + "name": "Evidential Rulecraft", + "track": "semantic-level protection", + "composite": 0.331, + "recognizability": 0.91, + "impact": 4, + "feasibilityWeight": 0.92, + "oneLine": "The build emits a grammar of acceptable justification, and live witnessed, inferred, and delegated facts instantiate a momentary domain ruling.", + "whyNotGen1": "Substantially recognizable as provenance-aware policy-as-code or a rule engine; unlike salt, keystream, or digest work, live evidence constitutes the decision.", + "maturity": "product-pivot", + "isFusion": false, + "parents": [], + "cluster": "plural-ontologies-and-relational-meaning", + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "needs-relaxation", + "additiveApi": "needs-relaxation" + } + }, + { + "rank": 39, + "name": "Semantic Fringe Observatory", + "track": "New product surfaces and capabilities", + "composite": 0.300, + "recognizability": 0.94, + "impact": 5, + "feasibilityWeight": 1.0, + "oneLine": "Sparse semantic probes and reference oracles produce a whole-product deformation field showing how journeys, authorities, timing, and side effects drift across releases.", + "whyNotGen1": "Substantially recognizable as differential testing, drift detection, and observability dashboards; unlike salt, keystream, or digest work, it measures consequences without altering behavior.", + "maturity": "shippable-now", + "isFusion": false, + "parents": [], + "cluster": "semantic-observability-and-proof", + "rails": { + "serverFree": "pass", + "sizeLean": "pass", + "cspSafe": "pass", + "buildRuntimeProvable": "pass", + "additiveApi": "pass" + } + }, + { + "rank": 40, + "name": "Affinity Furnace", + "track": "resilience to automated understanding", + "composite": 0.262, + "recognizability": 0.92, + "impact": 4, + "feasibilityWeight": 0.82, + "oneLine": "At installation, neutral operators are mutated and selected against behavioral assays until a local implementation passes, with later inputs able to trigger further selection.", + "whyNotGen1": "Substantially recognizable as genetic programming or evolutionary synthesis; unlike salt, keystream, or digest work, it searches for an implementation instead of decoding one.", + "maturity": "research-spike", + "isFusion": false, + "parents": [], + "cluster": "generative-phenotype-manufacture", + "rails": { + "serverFree": "pass", + "sizeLean": "needs-relaxation", + "cspSafe": "pass", + "buildRuntimeProvable": "fails", + "additiveApi": "needs-relaxation" + } + } + ], + "bestFusions": [ + "Evidential Phase Succession", + "The Questioning Scar", + "Treaty-Sightline Foundry", + "The Migrating Treaty", + "Fringe-Driven Phase Healing", + "Rehearsal Germ Layers" + ] + }, + "fusions": [ + "The Questioning Scar", + "Rehearsal Germ Layers", + "Fringe-Driven Phase Healing", + "The Migrating Treaty", + "Treaty-Sightline Foundry", + "Evidential Phase Succession", + "Situated Absence Lineage", + "Confidence-Driven Contract Metabolism" + ], + "coverage": { + "summary": "The run is substantively successful and does not require immediate reseeding: four ideas score below 0.5 recognizability across three tracks, and two fusions appear in the top five. A focused Round 2 is nevertheless recommended because resilience has no top-10 representative, four tracks have no sub-0.5 idea, and several high-ranked mechanisms remain research-level descriptions without a concrete JavaScript representation or proof strategy. Lens and assumption survival below is attributed strictly to original Phase-1 ideas, not to later fusion descendants: on that basis adaptive immune system and holography have no direct top-10 result, while holography alone has no direct top-15 result. Both lenses still influenced top-10 fusions, so this is a direct-generation gap rather than total conceptual absence. The coverage note supplies three untried seeds.", + "runVerdict": { + "successful": true, + "immediateReseedRequired": false, + "criteria": [ + { + "criterion": "At least 3 ideas with recognizability below 0.5 across at least 3 distinct tracks", + "passed": true, + "evidence": "Four ideas qualify: Traveling Isogloss (0.30, self-modifying or living output), Evidential Phase Succession (0.42, self-modifying or living output), Anisomorphic Ruleworlds (0.46, semantic-level protection), and The Questioning Scar (0.38, verifiability and developer experience). They span three distinct tracks." + }, + { + "criterion": "At least 1 fusion in the top 5", + "passed": true, + "evidence": "Evidential Phase Succession is a fusion at rank 2 and The Questioning Scar is a fusion at rank 5." + }, + { + "criterion": "Every top-10 idea carries a whyNotGen1 line", + "passed": true, + "evidence": "All 10 top-ranked entries contain a non-empty whyNotGen1 field." + }, + { + "criterion": "A coverage note names Round-2 seeds", + "passed": true, + "evidence": "This coverage report names exactly three untried lens × assumption × track × persona combinations." + } + ] + }, + "lensesWithoutTop10": [ + "adaptive immune system (clonal selection, self/non-self)", + "holography (every shard holds the whole at lower resolution)" + ], + "lensesWithoutTop15": [ + "holography (every shard holds the whole at lower resolution)" + ], + "assumptionsWithoutTop10": [ + "the decoder must ship alongside the code", + "opacity and correctness are in tension" + ], + "assumptionsWithoutTop15": [ + "opacity and correctness are in tension" + ], + "thinTracks": [ + { + "track": "resilience to automated understanding", + "diagnosis": "Thin by direct generation count, top-rank representation, and recognizability: it has 4 direct ideas, no fusion assigned to the track, no top-10 entry, a first appearance at rank 11, and no idea below 0.5 recognizability; its best score is 0.62." + }, + { + "track": "new product surfaces and capabilities", + "diagnosis": "Thin by direct generation count, direct top-rank representation, recognizability, and implementation specificity: it has 4 direct ideas plus 2 fusions, but its top-15 presence comes only from fusions at ranks 7 and 12; the best direct idea is rank 24 at 0.80 recognizability, no idea is below 0.5, and the proposed consoles and foundries do not yet name a narrow integration or API-level spike." + }, + { + "track": "semantic-level protection", + "diagnosis": "Thin only by direct generation count: it has 4 direct ideas plus 1 fusion. Outcome quality is strong rather than thin, with three top-10 entries and Anisomorphic Ruleworlds at 0.46 recognizability." + }, + { + "track": "novel transformation paradigms", + "diagnosis": "Thin by recognizability and implementation specificity: 5 direct ideas produced one top-10 entry, but none score below 0.5 recognizability and all remain research spikes. The sketches name nonlinear media, growth, or exchange systems without yet defining a compact JavaScript semantic lowering, execution kernel, or equivalence proof." + }, + { + "track": "distribution, licensing, and delivery models", + "diagnosis": "Thin by recognizability and implementation specificity despite a strong rank-4 representative: 5 direct ideas plus 1 fusion yield no idea below 0.5 recognizability, the track average is 0.79, and every candidate is a product pivot without a bounded authorization, installation, renewal, and continuity protocol." + }, + { + "track": "self-modifying or living output", + "diagnosis": "Thin only by implementation specificity: it dominates the top 15 and supplies two sub-0.5 ideas, but its leading mechanisms are research spikes whose build-runtime equivalence rail fails and whose moving-boundary or succession machinery is not yet reduced to an executable prototype." + } + ], + "unsampledRegions": [ + "Observation-conditioned semantics: no generator directly inverted the assumption that behavior is unchanged by observation or used the quantum measurement / observer-effect lens.", + "Functionless and non-enumerable program structure: no generator directly inverted the stable-function-unit assumption, leaving room for representations whose units emerge only during composition.", + "Active artifact protocols: no generator directly inverted the passive-text assumption or the assumption that Ruam does not participate at run time, so continuing product and delivery relationships were reached only indirectly.", + "Local-to-global projection and deliberate distortion: the local-fragment assumption and cartographic-projection lens were both unused, leaving a promising developer-facing region between faithful explanation and non-canonical representation.", + "Absence-of-structure and plural outcomes: neither the no-structure inversion nor the one-input/one-output inversion received a direct assignment." + ], + "round2Seeds": [ + { + "lens": "quantum measurement / observer effect", + "assumption": "the program behaves the same whether observed or not", + "track": "resilience to automated understanding", + "persona": "complexity/systems theorist", + "rationale": "Resilience is the only track absent from the top 10 and has no sub-0.5 idea. This combination directly samples the missing observation-conditioned region and can seek a developer-owned measurement contract whose questions help constitute valid execution, creating a new organizing principle rather than another representation layer." + }, + { + "lens": "cartographic projection (every map distorts something)", + "assumption": "a local fragment is understandable locally", + "track": "new product surfaces and capabilities", + "persona": "industrial designer", + "rationale": "This track has no direct top-15 idea and no sub-0.5 result. Treating every developer view as a purpose-built projection can produce a concrete product surface for choosing, comparing, and validating useful semantic distortions while making the global contract—not a local fragment—the unit of explanation." + }, + { + "lens": "legal contracts and escrow", + "assumption": "the artifact is a passive text to be read", + "track": "distribution / licensing / delivery models", + "persona": "game designer", + "rationale": "Distribution has a strong headline idea but no sub-0.5 candidate and lacks a bounded lifecycle protocol. Combining executable obligations, staged settlement, and stateful rule transitions can make authorization and continuity an active multi-party process with explicit events and invariants, improving both originality and implementation specificity without merely relocating a key." + } + ] + } +} diff --git a/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-results.md b/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-results.md new file mode 100644 index 0000000..f9d10be --- /dev/null +++ b/docs/superpowers/specs/2026-07-24-ruam-gen2-ideation-results.md @@ -0,0 +1,477 @@ +# Project Kaleidoscope — Ruam Gen-2 Ideation Results + +**Date:** 2026-07-24 +**Status:** Completed; all campaign gates passed. +**Structured artifact:** [2026-07-24-ruam-gen2-ideation-results.json](2026-07-24-ruam-gen2-ideation-results.json) +**Campaign design:** [2026-07-24-ruam-gen2-ideation-campaign-design.md](2026-07-24-ruam-gen2-ideation-campaign-design.md) + +## Outcome + +The most promising Gen2 direction is to stop treating a fixed implementation as the protected object and instead make a developer-owned semantic treaty generate, evolve, and explain multiple causally distinct realizations. Traveling Isogloss supplies the strongest new execution primitive—a moving boundary that alone can express behavior—while Evidential Phase Succession and Anisomorphic Ruleworlds show how that primitive can mature into a product: meaning can migrate between grammars and ontologies, with owner-facing evidence proving stable outcomes rather than exposing a canonical program. Phenotype Manufacturing Right and The Questioning Scar add credible commercial and developer surfaces. Together they suggest Ruam Gen2 as a semantic-lifecycle system whose resilience to mechanical reconstruction is a consequence of plural, history-bearing realizations, not another concealment layer. + +The run passed its definition of done: **4** ideas scored below 0.5 recognizability across **3** tracks, two fusions reached the top five, every top-10 entry has a Gen-1 distinction, and the coverage critic supplied three Round-2 seeds. Immediate reseeding is not required; a focused second round is recommended. + +| Metric | Result | +|---|---:| +| Raw ideas | 32 | +| Forced fusions | 8 | +| Total ranked | 40 | +| Recognizability < 0.5 | 4 | +| Tracks represented below 0.5 | 3 | +| Logical agent calls | 14 | + +### Top five + +| Rank | Direction | Composite | Recognizability | Impact | Maturity | Fusion | +|---:|---|---:|---:|---:|---|---| +| 1 | Traveling Isogloss | 2.870 | 0.3 | 5 | research-spike | no | +| 2 | Evidential Phase Succession | 2.378 | 0.42 | 5 | research-spike | yes | +| 3 | Anisomorphic Ruleworlds | 2.214 | 0.46 | 5 | research-spike | no | +| 4 | Phenotype Manufacturing Right | 1.936 | 0.56 | 5 | product-pivot | no | +| 5 | The Questioning Scar | 1.934 | 0.38 | 4 | research-spike | yes | + +## Execution and scoring + +The original six-phase topology was reproduced with GPT-5.6 Sol: `high` for the judgment layer (Frame, two Tag passes, Synthesize, Coverage) and `xhigh` for the divergent fleet (seven generators and two fusion agents). Generation used the plan’s deterministic lens × assumption × track × persona assignments and did not admit feasibility as a filter. + +Near-duplicates were clustered without deleting or merging any candidate. For each rail, pass=1.0, needs-relaxation=0.8, and fails=0.5; feasibilityWeight is the arithmetic mean across serverFree, sizeLean, cspSafe, buildRuntimeProvable, and additiveApi. Composite is impact * (1 - recognizability) * feasibilityWeight, rounded to three decimals. Ranking is descending composite, then lower recognizability, then higher impact, with candidate input order retained only for complete ties; no score was adjusted for narrative preference. + +## Ranked slate + +### 1. Traveling Isogloss + +- **Track:** self-modifying or living output +- **Scores:** composite 2.870; recognizability 0.3; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Two internally rigid semantic regions share one movable interface that alone can realize calls, and each completed call relocates that expressive interface for the next invocation. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, no encoded implementation is varied or verified; the computation is the history-dependent motion of a semantic boundary. + +### 2. Evidential Phase Succession — fusion + +- **Track:** self-modifying or living output +- **Scores:** composite 2.378; recognizability 0.42; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Parents:** Allomorphic Phase Sheet, Evidential Rulecraft, Iterated Dialect Mechanics +- **Mechanism:** Accumulated provenance pressure triggers a region-wide grammar change, after which a narrowly trained successor must ratify the new evidence categories before taking over. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, the changing object is the coupling among evidence ontology, causal grammar, and learned succession rather than encoded bytes. + +### 3. Anisomorphic Ruleworlds + +- **Track:** semantic-level protection +- **Scores:** composite 2.214; recognizability 0.46; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `fails`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** Execution moves among contract-equivalent rule systems that disagree about what counts as an actor, resource, cause, and action while preserving observable outcomes. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, it preserves an outcome treaty while changing the program's causal ontology, not the representation of one procedure. + +### 4. Phenotype Manufacturing Right + +- **Track:** distribution, licensing, and delivery models +- **Scores:** composite 1.936; recognizability 0.56; impact 5; feasibility weight 0.88; maturity `product-pivot` +- **Rails:** server-free `needs-relaxation`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** A license authorizes a local foundry to synthesize and certify a disposable implementation from a behavioral phenotype and currently available platform components. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, authorization governs manufacture of a new conforming specimen rather than access to or decoding of a canonical artifact. + +### 5. The Questioning Scar — fusion + +- **Track:** verifiability and developer experience +- **Scores:** composite 1.934; recognizability 0.38; impact 4; feasibility weight 0.78; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `needs-relaxation`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Parents:** The Tolerance Mold, Focus-Pull Debugger, Hysteretic Dialect Lattice +- **Mechanism:** Each owner question grows a temporary implementation that must both reproduce behavior and answer in domain terms, then leaves a structural change that shapes later behavior and evidence. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, explanation and implementation are co-produced for a question and permanently alter the artifact's future grammar. + +### 6. Soliton Tissue + +- **Track:** novel transformation paradigms +- **Scores:** composite 1.845; recognizability 0.55; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Inputs launch stable collective pulses through a nonlinear medium whose state is rewritten by prior pulses, with outputs read from wave phenotype rather than dispatched operations. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, the representation is a history-bearing transport medium and its collective waves, with no instruction stream to conceal. + +### 7. Treaty-Sightline Foundry — fusion + +- **Track:** new product surfaces and capabilities +- **Scores:** composite 1.760; recognizability 0.6; impact 5; feasibility weight 0.88; maturity `product-pivot` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Parents:** Anisomorphic Ruleworlds, Question-Sightline Observatory, Phenotype Manufacturing Right +- **Mechanism:** An owner question selects a foreign causal ontology, triggers manufacture of a temporary implementation in that ontology, and derives the smallest cross-world projection that certifies the same outcome treaty. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, a diagnostic question manufactures an ontology-distinct counterpart and uses cross-world agreement as the explanation certificate. + +### 8. Iterated Dialect Mechanics + +- **Track:** semantic-level protection +- **Scores:** composite 1.722; recognizability 0.58; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** A running artifact periodically trains a successor from constrained demonstrations of semantic play, retires its own vocabulary, and retains only contract-level coordination. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, successive artifacts share no hidden command vocabulary; behavior persists through cultural transmission of semantic invariants. + +### 9. The Migrating Treaty — fusion + +- **Track:** semantic-level protection +- **Scores:** composite 1.640; recognizability 0.6; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Parents:** MHC-Restricted Program Tissue, Reciprocal-Exchange Computation, Traveling Isogloss +- **Mechanism:** Host capabilities and artifact offerings close a reciprocal circuit only at a movable semantic boundary, and fulfilling one call shifts the boundary and renegotiates the next call's terms. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, neither side contributes concealed bits; meaning is a self-sustaining exchange whose successful completion relocates its own interface. + +### 10. The Cue-Sheet Contract Theater + +- **Track:** verifiability and developer experience +- **Scores:** composite 1.496; recognizability 0.66; impact 5; feasibility weight 0.88; maturity `product-pivot` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** Developers specify externally meaningful effects and causal obligations, then Ruam generates a role-and-cue realization plus a console that proves and replays only failed semantic beats. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, the durable unit is an effect contract and its proof, while the generated realization need not retain function-level ancestry. + +### 11. MHC-Restricted Program Tissue + +- **Track:** resilience to automated understanding +- **Scores:** composite 1.398; recognizability 0.62; impact 4; feasibility weight 0.92; maturity `product-pivot` +- **Rails:** server-free `pass`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** Ordinary host events present typed semantic operations to a complementary artifact, and only their live contextual conjunction realizes the protected behavior. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, the host supplies useful semantic participation rather than a secret term, so behavior belongs to the relation between two systems. + +### 12. Rehearsal Germ Layers — fusion + +- **Track:** new product surfaces and capabilities +- **Scores:** composite 1.382; recognizability 0.52; impact 4; feasibility weight 0.72; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `fails`; CSP-safe `needs-relaxation`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Parents:** Whole-Show Rehearsal Mesh, Affinity Furnace, Post-Emit Morphogenesis +- **Mechanism:** Coarse but complete product surfaces overlap on real journeys, and their agreement selects newly synthesized behavior organs that increase the next generation's resolution. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, no finished implementation waits to be recovered; cross-resolution product agreement is the developmental material that grows one. + +### 13. Auxetic Contract Tissue + +- **Track:** self-modifying or living output +- **Scores:** composite 1.378; recognizability 0.58; impact 4; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Exercising a contract mechanically expands its semantic neighborhood along coupled dimensions, and part of that newly formed structure becomes the artifact's next resting language. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, execution changes the dimensionality of the implementation vocabulary instead of re-encoding a fixed route. + +### 14. Hysteretic Dialect Lattice + +- **Track:** self-modifying or living output +- **Scores:** composite 1.312; recognizability 0.68; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Each installation grammaticalizes frequent compositions, erodes unused ones, and emits a successor grammar whose future transitions depend on its usage history. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, the artifact evolves a history-dependent constraint grammar rather than applying fresh variation to stable commands. + +### 15. Fringe-Driven Phase Healing — fusion + +- **Track:** self-modifying / living output +- **Scores:** composite 1.312; recognizability 0.68; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Parents:** Semantic Fringe Observatory, Idiotype Phase Matter, Soliton Tissue +- **Mechanism:** Measured semantic drift is converted into a traveling pulse that changes local interaction thresholds until the distributed behavior settles into a new contract-faithful attractor. +- **Why this is not Gen-1:** Unlike salt, keystream, or digest work, a consequence deformation directly becomes the repair event that reorganizes a collective semantic phase. + +### 16. Anastomotic Execution + +- **Track:** novel transformation paradigms +- **Scores:** composite 0.984; recognizability 0.76; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Each invocation assembles a transient causal network by fusing compatible incomplete components, reads the network's collective result, then dissolves or remodels it. +- **Why this is not Gen-1:** Substantially recognizable as a dynamic dataflow or actor graph; unlike salt, keystream, or digest work, its variation is per-call topology rather than encoded data. + +### 17. Allomorphic Phase Sheet + +- **Track:** self-modifying or living output +- **Scores:** composite 0.975; recognizability 0.75; impact 5; feasibility weight 0.78; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `needs-relaxation`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Runtime pressure changes the legal causal relations across an entire semantic region, and each new coherent grammar redraws the set of possible future phase transitions. +- **Why this is not Gen-1:** Substantially recognizable as dynamic program rewriting or metamorphic phase switching; unlike salt, keystream, or digest work, the switch changes regional compatibility rules. + +### 18. Dual-Reality Debugging + +- **Track:** verifiability and developer experience +- **Scores:** composite 0.957; recognizability 0.74; impact 4; feasibility weight 0.92; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `pass` +- **Mechanism:** Production, operations, QA, and developer views use different semantic primitives, and temporary typed joins among them produce explanations checked by cross-view consistency laws. +- **Why this is not Gen-1:** Substantially recognizable as role-specific telemetry with consistency checks; unlike salt, keystream, or digest work, it concerns observability views rather than concealment. + +### 19. Reciprocal-Exchange Computation + +- **Track:** novel transformation paradigms +- **Scores:** composite 0.951; recognizability 0.71; impact 4; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Local components exchange complementary resources until a self-sustaining circulation satisfies the owned outcome, with every use renegotiating the exchange terms. +- **Why this is not Gen-1:** Substantially recognizable as distributed constraint solving or a chemical-reaction network; unlike salt, keystream, or digest work, meaning is a viable exchange circulation. + +### 20. Heterokaryotic Semantics + +- **Track:** novel transformation paradigms +- **Scores:** composite 0.918; recognizability 0.72; impact 4; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Multiple incomplete evaluator lineages share a regulatory field, and changing their relative dosage changes the colony-level behavior while preserving a phenotype contract. +- **Why this is not Gen-1:** Substantially recognizable as an adaptive ensemble or mixture of experts; unlike salt, keystream, or digest work, output depends on regulated population composition. + +### 21. Idiotype Phase Matter + +- **Track:** resilience to automated understanding +- **Scores:** composite 0.902; recognizability 0.78; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** Input perturbs a sparse recurrent population of incomplete transformations, whose mutual activation and suppression settle into an attractor encoding the result. +- **Why this is not Gen-1:** Substantially recognizable as recurrent, reservoir, or cellular-network computation; unlike salt, keystream, or digest work, the answer is a collective attractor. + +### 22. The Germinating Program + +- **Track:** novel transformation paradigms +- **Scores:** composite 0.902; recognizability 0.78; impact 5; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** A developmental constitution grows, reinforces, and retires causal pathways under workload while phenotype assays keep the changing implementation inside its behavioral envelope. +- **Why this is not Gen-1:** Substantially recognizable as an adaptive runtime with profile-guided specialization; unlike salt, keystream, or digest work, the implementation is grown rather than decoded. + +### 23. Question-Sightline Observatory + +- **Track:** verifiability and developer experience +- **Scores:** composite 0.883; recognizability 0.76; impact 4; feasibility weight 0.92; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `pass` +- **Mechanism:** A semantic question generates a mechanically complete causal projection containing only the events needed for that answer, and different questions deliberately yield non-nestable views. +- **Why this is not Gen-1:** Substantially recognizable as query-directed causal tracing and certified program slicing; unlike salt, keystream, or digest work, it is an owner observability surface. + +### 24. Angle-Multiplexed Truth Deck + +- **Track:** New product surfaces and capabilities +- **Scores:** composite 0.880; recognizability 0.8; impact 5; feasibility weight 0.88; maturity `product-pivot` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** One intent specification generates independently executable customer, audit, test, and performance projections whose observable boundaries must agree. +- **Why this is not Gen-1:** Substantially recognizable as N-version programming, executable specifications, and multi-target compilation; unlike salt, keystream, or digest work, it multiplies product views. + +### 25. The Tolerance Mold + +- **Track:** resilience to automated understanding +- **Scores:** composite 0.780; recognizability 0.8; impact 5; feasibility weight 0.78; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `needs-relaxation`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** A runtime generator proposes small implementations and rejects any that violate a build-produced atlas of forbidden transitions, conservation laws, and accepted observations. +- **Why this is not Gen-1:** Substantially recognizable as constraint-based program synthesis from negative examples; unlike salt, keystream, or digest work, it selects behavior rather than decodes code. + +### 26. Confidence-Driven Contract Metabolism — fusion + +- **Track:** verifiability and developer experience +- **Scores:** composite 0.749; recognizability 0.74; impact 4; feasibility weight 0.72; maturity `product-pivot` +- **Rails:** server-free `needs-relaxation`; size-lean `fails`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Parents:** Auxetic Contract Tissue, The Convincer Ecology, Synonymous Supply Network +- **Mechanism:** Low-confidence claims expand adjacent contract dimensions, recruit qualified components, and generate independent demonstrations whose results reshape future component sockets. +- **Why this is not Gen-1:** Substantially recognizable as confidence-driven test generation, component sourcing, and contract-guided synthesis; unlike salt, keystream, or digest work, evidence gaps reorganize the product. + +### 27. Black-Art Absence Certificates + +- **Track:** verifiability and developer experience +- **Scores:** composite 0.704; recognizability 0.84; impact 5; feasibility weight 0.88; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** Ruam specifies impossible domain states and causal relationships, synthesizes within the remaining space, and explains violations through minimal semantic counterexamples. +- **Why this is not Gen-1:** Substantially recognizable as safety-property model checking and runtime contracts; unlike salt, keystream, or digest work, it certifies developer-level negative behavior. + +### 28. Lineage Repair Warranty + +- **Track:** distribution, licensing, and delivery models +- **Scores:** composite 0.702; recognizability 0.82; impact 5; feasibility weight 0.78; maturity `product-pivot` +- **Rails:** server-free `needs-relaxation`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** A licensed deployment is maintained as a descendant lineage, with drift repaired locally from conforming regions, certified history, semantic policy, and permitted synthesis materials. +- **Why this is not Gen-1:** Substantially recognizable as contract-guided program repair and self-healing software; unlike salt, keystream, or digest work, delivery constructs a new descendant. + +### 29. Fidelity-Gradient License + +- **Track:** distribution, licensing, and delivery models +- **Scores:** composite 0.672; recognizability 0.8; impact 4; feasibility weight 0.84; maturity `product-pivot` +- **Rails:** server-free `needs-relaxation`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** License state controls an approved semantic approximation envelope, while renewed service progressively restores exact decisions and reconciles accumulated fidelity debt. +- **Why this is not Gen-1:** Substantially recognizable as graceful degradation, approximate computing, and service-tier licensing; unlike salt, keystream, or digest work, authorization changes behavioral precision. + +### 30. Whole-Show Rehearsal Mesh + +- **Track:** New product surfaces and capabilities +- **Scores:** composite 0.662; recognizability 0.82; impact 4; feasibility weight 0.92; maturity `needs-rail-relaxed` +- **Rails:** server-free `pass`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** Ruam generates complete preview, test, partner, edge, and production experiences at different semantic resolutions and upgrades journeys when their overlaps agree. +- **Why this is not Gen-1:** Substantially recognizable as multi-fidelity, multi-target build generation; unlike salt, keystream, or digest work, each output is a useful whole-product surface. + +### 31. Situated Absence Lineage — fusion + +- **Track:** distribution, licensing, and delivery models +- **Scores:** composite 0.624; recognizability 0.8; impact 4; feasibility weight 0.78; maturity `product-pivot` +- **Rails:** server-free `needs-relaxation`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Parents:** Black-Art Absence Certificates, The Deictic Commons, Lineage Repair Warranty +- **Mechanism:** A service certifies relationship-level impossibilities in live shared context, and any failed guarantee triggers repair that creates new referents and a descendant guarantee vocabulary. +- **Why this is not Gen-1:** Substantially recognizable as contextual policy, monitoring, provenance, and program repair; unlike salt, keystream, or digest work, repair regenerates the semantics of future warranties. + +### 32. Post-Emit Morphogenesis + +- **Track:** self-modifying or living output +- **Scores:** composite 0.624; recognizability 0.84; impact 5; feasibility weight 0.78; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `needs-relaxation`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** A deployed precursor uses real workload to differentiate interacting behaviors into organs and repeatedly replaces its own organizer with smaller descendants. +- **Why this is not Gen-1:** Substantially recognizable as adaptive specialization or tiered JIT with a self-replacing bootstrap; unlike salt, keystream, or digest work, maturation constructs future anatomy. + +### 33. Focus-Pull Debugger + +- **Track:** New product surfaces and capabilities +- **Scores:** composite 0.576; recognizability 0.88; impact 5; feasibility weight 0.96; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `pass` +- **Mechanism:** A domain question selects and reconstructs a high-resolution causal plane from coarse whole-run evidence without exposing a universal address map. +- **Why this is not Gen-1:** Substantially recognizable as query-conditioned provenance tracing and causal debugging; unlike salt, keystream, or digest work, it explains semantic questions. + +### 34. Synonymous Supply Network + +- **Track:** distribution, licensing, and delivery models +- **Scores:** composite 0.493; recognizability 0.86; impact 4; feasibility weight 0.88; maturity `product-pivot` +- **Rails:** server-free `needs-relaxation`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** Installation resolves ordinary qualified components from changing supplier catalogs and assembles them under published compatibility and tolerance contracts. +- **Why this is not Gen-1:** Substantially recognizable as a governed component marketplace and package resolver; unlike salt, keystream, or digest work, the license buys a qualification ecosystem. + +### 35. The Deictic Commons + +- **Track:** semantic-level protection +- **Scores:** composite 0.478; recognizability 0.87; impact 4; feasibility weight 0.92; maturity `product-pivot` +- **Rails:** server-free `pass`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** Application relationships mint situated referents at runtime, and each operation both uses those referents and revises the vocabulary available to later operations. +- **Why this is not Gen-1:** Substantially recognizable as relationship-based policy, context-aware capabilities, and event-sourced rules; unlike salt, keystream, or digest work, context creates semantic referents. + +### 36. The Convincer Ecology + +- **Track:** verifiability and developer experience +- **Scores:** composite 0.368; recognizability 0.9; impact 4; feasibility weight 0.92; maturity `shippable-now` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `pass` +- **Mechanism:** Ruam maintains an evidence graph of property, metamorphic, boundary, counterfactual, and production-derived demonstrations and grows new tests where owner confidence is thin. +- **Why this is not Gen-1:** Substantially recognizable as an adaptive portfolio of established testing methods; unlike salt, keystream, or digest work, it is a developer confidence product. + +### 37. Proofreading Royalty + +- **Track:** distribution, licensing, and delivery models +- **Scores:** composite 0.365; recognizability 0.88; impact 4; feasibility weight 0.76; maturity `product-pivot` +- **Rails:** server-free `fails`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** A local application proposes bounded candidate actions while a licensed service selects, edits, or certifies the action permitted to commit. +- **Why this is not Gen-1:** Substantially recognizable as split computing with an external policy oracle; unlike salt, keystream, or digest work, the remote product is decision certification. + +### 38. Evidential Rulecraft + +- **Track:** semantic-level protection +- **Scores:** composite 0.331; recognizability 0.91; impact 4; feasibility weight 0.92; maturity `product-pivot` +- **Rails:** server-free `pass`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `needs-relaxation`; additive API `needs-relaxation` +- **Mechanism:** The build emits a grammar of acceptable justification, and live witnessed, inferred, and delegated facts instantiate a momentary domain ruling. +- **Why this is not Gen-1:** Substantially recognizable as provenance-aware policy-as-code or a rule engine; unlike salt, keystream, or digest work, live evidence constitutes the decision. + +### 39. Semantic Fringe Observatory + +- **Track:** New product surfaces and capabilities +- **Scores:** composite 0.300; recognizability 0.94; impact 5; feasibility weight 1.0; maturity `shippable-now` +- **Rails:** server-free `pass`; size-lean `pass`; CSP-safe `pass`; build/runtime provable `pass`; additive API `pass` +- **Mechanism:** Sparse semantic probes and reference oracles produce a whole-product deformation field showing how journeys, authorities, timing, and side effects drift across releases. +- **Why this is not Gen-1:** Substantially recognizable as differential testing, drift detection, and observability dashboards; unlike salt, keystream, or digest work, it measures consequences without altering behavior. + +### 40. Affinity Furnace + +- **Track:** resilience to automated understanding +- **Scores:** composite 0.262; recognizability 0.92; impact 4; feasibility weight 0.82; maturity `research-spike` +- **Rails:** server-free `pass`; size-lean `needs-relaxation`; CSP-safe `pass`; build/runtime provable `fails`; additive API `needs-relaxation` +- **Mechanism:** At installation, neutral operators are mutated and selected against behavioral assays until a local implementation passes, with later inputs able to trigger further selection. +- **Why this is not Gen-1:** Substantially recognizable as genetic programming or evolutionary synthesis; unlike salt, keystream, or digest work, it searches for an implementation instead of decoding one. + +## Best fusions + +1. **Evidential Phase Succession** +2. **The Questioning Scar** +3. **Treaty-Sightline Foundry** +4. **The Migrating Treaty** +5. **Fringe-Driven Phase Healing** +6. **Rehearsal Germ Layers** + +## Concept clusters + +### moving-boundary-and-grammar-succession + +Meaning resides in a history-bearing boundary or grammar that changes its own future transition rules; this is the clearest genuinely Gen2 mechanism family. + +Members: Hysteretic Dialect Lattice, Traveling Isogloss, Allomorphic Phase Sheet, Iterated Dialect Mechanics, Evidential Phase Succession + +### plural-ontologies-and-relational-meaning + +Behavior exists in relations among host context, evidence, shared referents, or multiple causal ontologies rather than inside one artifact. + +Members: MHC-Restricted Program Tissue, Evidential Rulecraft, The Deictic Commons, Anisomorphic Ruleworlds, The Migrating Treaty + +### emergent-material-computation + +Ordinary local components collectively realize behavior through attractors, transient topology, population dosage, exchange circulation, or traveling waves. + +Members: Idiotype Phase Matter, Anastomotic Execution, Heterokaryotic Semantics, Reciprocal-Exchange Computation, Soliton Tissue + +### generative-phenotype-manufacture + +A behavioral envelope or phenotype, rather than stored code, drives local synthesis, selection, growth, or licensed manufacture of disposable implementations. + +Members: The Tolerance Mold, Affinity Furnace, The Germinating Program, Post-Emit Morphogenesis, Phenotype Manufacturing Right + +### semantic-observability-and-proof + +Developer trust comes from question-relative explanations, independent executable projections, effect contracts, absence proofs, or evolving evidence portfolios. + +Members: Focus-Pull Debugger, Angle-Multiplexed Truth Deck, Semantic Fringe Observatory, The Cue-Sheet Contract Theater, Question-Sightline Observatory, Dual-Reality Debugging, Black-Art Absence Certificates, The Convincer Ecology, The Questioning Scar + +### developmental-product-surfaces + +Product surfaces, semantic deformation, contract geometry, and owner questions become active material that grows or repairs the deployed system. + +Members: Whole-Show Rehearsal Mesh, Auxetic Contract Tissue, Rehearsal Germ Layers, Fringe-Driven Phase Healing, Treaty-Sightline Foundry, Confidence-Driven Contract Metabolism + +### lifecycle-licensing-and-continuity + +Commercial value attaches to qualified supply, descendant repair, semantic fidelity, certified decisions, or continuity of contextual guarantees rather than static bytes. + +Members: Synonymous Supply Network, Lineage Repair Warranty, Fidelity-Gradient License, Proofreading Royalty, Situated Absence Lineage + +## Coverage and verdict + +The run is substantively successful and does not require immediate reseeding: four ideas score below 0.5 recognizability across three tracks, and two fusions appear in the top five. A focused Round 2 is nevertheless recommended because resilience has no top-10 representative, four tracks have no sub-0.5 idea, and several high-ranked mechanisms remain research-level descriptions without a concrete JavaScript representation or proof strategy. Lens and assumption survival below is attributed strictly to original Phase-1 ideas, not to later fusion descendants: on that basis adaptive immune system and holography have no direct top-10 result, while holography alone has no direct top-15 result. Both lenses still influenced top-10 fusions, so this is a direct-generation gap rather than total conceptual absence. The coverage note supplies three untried seeds. + +### Gate checks + +- **Pass: At least 3 ideas with recognizability below 0.5 across at least 3 distinct tracks.** Four ideas qualify: Traveling Isogloss (0.30, self-modifying or living output), Evidential Phase Succession (0.42, self-modifying or living output), Anisomorphic Ruleworlds (0.46, semantic-level protection), and The Questioning Scar (0.38, verifiability and developer experience). They span three distinct tracks. +- **Pass: At least 1 fusion in the top 5.** Evidential Phase Succession is a fusion at rank 2 and The Questioning Scar is a fusion at rank 5. +- **Pass: Every top-10 idea carries a whyNotGen1 line.** All 10 top-ranked entries contain a non-empty whyNotGen1 field. +- **Pass: A coverage note names Round-2 seeds.** This coverage report names exactly three untried lens × assumption × track × persona combinations. + +### Thin tracks + +- **resilience to automated understanding:** Thin by direct generation count, top-rank representation, and recognizability: it has 4 direct ideas, no fusion assigned to the track, no top-10 entry, a first appearance at rank 11, and no idea below 0.5 recognizability; its best score is 0.62. +- **new product surfaces and capabilities:** Thin by direct generation count, direct top-rank representation, recognizability, and implementation specificity: it has 4 direct ideas plus 2 fusions, but its top-15 presence comes only from fusions at ranks 7 and 12; the best direct idea is rank 24 at 0.80 recognizability, no idea is below 0.5, and the proposed consoles and foundries do not yet name a narrow integration or API-level spike. +- **semantic-level protection:** Thin only by direct generation count: it has 4 direct ideas plus 1 fusion. Outcome quality is strong rather than thin, with three top-10 entries and Anisomorphic Ruleworlds at 0.46 recognizability. +- **novel transformation paradigms:** Thin by recognizability and implementation specificity: 5 direct ideas produced one top-10 entry, but none score below 0.5 recognizability and all remain research spikes. The sketches name nonlinear media, growth, or exchange systems without yet defining a compact JavaScript semantic lowering, execution kernel, or equivalence proof. +- **distribution, licensing, and delivery models:** Thin by recognizability and implementation specificity despite a strong rank-4 representative: 5 direct ideas plus 1 fusion yield no idea below 0.5 recognizability, the track average is 0.79, and every candidate is a product pivot without a bounded authorization, installation, renewal, and continuity protocol. +- **self-modifying or living output:** Thin only by implementation specificity: it dominates the top 15 and supplies two sub-0.5 ideas, but its leading mechanisms are research spikes whose build-runtime equivalence rail fails and whose moving-boundary or succession machinery is not yet reduced to an executable prototype. + +### Unsampled regions + +- Observation-conditioned semantics: no generator directly inverted the assumption that behavior is unchanged by observation or used the quantum measurement / observer-effect lens. +- Functionless and non-enumerable program structure: no generator directly inverted the stable-function-unit assumption, leaving room for representations whose units emerge only during composition. +- Active artifact protocols: no generator directly inverted the passive-text assumption or the assumption that Ruam does not participate at run time, so continuing product and delivery relationships were reached only indirectly. +- Local-to-global projection and deliberate distortion: the local-fragment assumption and cartographic-projection lens were both unused, leaving a promising developer-facing region between faithful explanation and non-canonical representation. +- Absence-of-structure and plural outcomes: neither the no-structure inversion nor the one-input/one-output inversion received a direct assignment. + +## Recommended Round 2 seeds + +### 1. quantum measurement / observer effect + +- **Overturn:** the program behaves the same whether observed or not +- **Track:** resilience to automated understanding +- **Persona:** complexity/systems theorist +- **Rationale:** Resilience is the only track absent from the top 10 and has no sub-0.5 idea. This combination directly samples the missing observation-conditioned region and can seek a developer-owned measurement contract whose questions help constitute valid execution, creating a new organizing principle rather than another representation layer. + +### 2. cartographic projection (every map distorts something) + +- **Overturn:** a local fragment is understandable locally +- **Track:** new product surfaces and capabilities +- **Persona:** industrial designer +- **Rationale:** This track has no direct top-15 idea and no sub-0.5 result. Treating every developer view as a purpose-built projection can produce a concrete product surface for choosing, comparing, and validating useful semantic distortions while making the global contract—not a local fragment—the unit of explanation. + +### 3. legal contracts and escrow + +- **Overturn:** the artifact is a passive text to be read +- **Track:** distribution / licensing / delivery models +- **Persona:** game designer +- **Rationale:** Distribution has a strong headline idea but no sub-0.5 candidate and lacks a bounded lifecycle protocol. Combining executable obligations, staged settlement, and stateful rule transitions can make authorization and continuity an active multi-party process with explicit events and invariants, improving both originality and implementation specificity without merely relocating a key. + +## Interpretation + +The campaign’s clearest result is a category shift: Ruam Gen2 looks less like another concealment layer and more like a semantic-lifecycle system. The top directions protect a developer-owned behavioral treaty while allowing causal realizations, grammars, explanations, or licensed specimens to be plural and history-bearing. This is a research direction, not an immediate implementation commitment: all top-five entries are `research-spike` or `product-pivot` maturity. + +Source track labels retain generator capitalization and slash/wording variants in the structured artifact for auditability. This report presents them unchanged where attached to individual ideas. diff --git a/docs/superpowers/specs/2026-07-24-traveling-isogloss-implementation-plan.md b/docs/superpowers/specs/2026-07-24-traveling-isogloss-implementation-plan.md new file mode 100644 index 0000000..29b333b --- /dev/null +++ b/docs/superpowers/specs/2026-07-24-traveling-isogloss-implementation-plan.md @@ -0,0 +1,1547 @@ +# Traveling Isogloss — Rank 1 Implementation Plan + +**Date:** 2026-07-24 +**Status:** Partially paused — architecture-neutral compiler work may continue; +security-sensitive lattice/runtime sections are held for the BPRF spike +**Source:** Rank 1 in [Project Kaleidoscope — Ruam Gen-2 Ideation Results](2026-07-24-ruam-gen2-ideation-results.md) +**Decision:** Replace Ruam's VM bytecode execution model with Traveling Isogloss. The completed product has one execution engine—the Boundary Constraint Machine—and contains no legacy VM runtime, VM fallback, bytecode encoder, or backend selector. + +> **Dynamic-instrumentation hold (2026-07-24):** A full-access runtime review +> found that this plan's `boundary + witness -> SemanticOp/operand -> handler` +> seam exposes a cheap, reusable semantic trace. Do not implement or freeze the +> handler candidate masks, unique handler resolver, operand projector, +> production handler catalog, instruction interpreter, opcode-selecting carrier +> witness, related certificate fields, or encoded artifact schema described +> below. They are retained temporarily as the reviewed baseline, not as the +> current target. The ranked redesign and quantitative decision gate are in +> [Project Kaleidoscope D2 — Dynamic-Instrumentation Ideation Results](2026-07-24-dynamic-instrumentation-ideation-results.md). +> The maximum-floor evolution—moving-cover state plus an optional custodied +> relation that never ships to the client—is specified in +> [Project Kaleidoscope D3 — Custodied Semantic Holography](2026-07-24-bprf-custodied-semantic-holography.md). +> Deterministic entropy, baselines, test migration, canonical semantic IR, +> source origins, CFG/effect analysis, and no-legacy-VM cutover work remain +> current. Security-sensitive implementation resumes only after the Braided +> Poly-Ontology Region Fabric spike clears its dynamic-attacker gates. + +## 1. Outcome + +Traveling Isogloss will replace the current compiler-to-bytecode-to-VM path. A selected root function and all of its child units will compile into a **Boundary Constraint Machine (BCM)**: + +- The emitted artifact contains two populations of locally ambiguous semantic cells, reusable operand reservoirs, a neighborhood graph, and one mutable carrier state per root group. +- No emitted function owns a fixed instruction stream. +- A semantic operation becomes uniquely selectable only when the current left cell, current right cell, and the carrier's history-derived witness meet at the active boundary. +- Executing that operation changes cell phase and moves the carrier to a new boundary. +- Completing, throwing from, yielding from, or suspending a call closes a lineage segment and leaves the group at a different contract gate for the next invocation. +- The state lives in the emitted runtime closure. It is server-free, CSP-safe, and persistent across calls for the lifetime of that loaded artifact. +- A developer-only sidecar can map opaque boundary motion back to source spans without shipping that map in the protected artifact. + +Implementation occurs on a replacement branch. The old VM may be invoked temporarily as a differential test oracle while the BCM is incomplete, but it is not a product backend and is deleted at cutover. No release described by this plan offers a VM/Isogloss choice or silently falls back to VM execution. + +This is a research execution model and full runtime replacement, not a claim that program behavior becomes unrecoverable. Its defensible security claim is narrower: + +> The emitted artifact has no stable site-to-operation function body. Recovering useful meaning requires replaying a history-dependent carrier through input-dependent boundary states, producing an execution-specific lineage rather than extracting one canonical instruction stream. + +## 2. Locked semantic decisions + +These decisions remove ambiguity before implementation begins. + +### 2.1 What a dialect cell is + +A cell is a reusable constraint bundle over the runtime handler catalog. It does not store an opcode, handler index, operand, or source location. + +Each cell contains phase-selectable clauses. A clause describes: + +- candidate semantic signature classes; +- permitted stack-shape transitions; +- permitted effect classes; +- permitted control exits; +- synthetic per-build dimensions derived from an isolated PRNG stream; +- references to reusable operand reservoirs; +- neighboring cells that may become boundary partners after an exit. + +Every clause must be locally ambiguous. Under the default profile, it must match at least eight handlers. + +### 2.2 What the boundary is + +The boundary is the ordered triple: + +```text +left cell clause + right cell clause + carrier witness +``` + +The left clause alone is ambiguous. The right clause alone is ambiguous. Their static intersection must remain ambiguous. Only the addition of the current carrier witness may select one handler and one operand projection. + +The carrier witness is derived from prior boundary motion, the current cell phases, the current frame shape, and a per-group lineage accumulator. It is not a stored opcode token. + +### 2.3 What moves + +The active boundary moves after every semantic transition, not merely after a public call: + +1. Resolve the unique handler and operand projection at the active boundary. +2. Execute the handler. +3. Classify the result as an exit such as fallthrough, branch-true, branch-false, call, return, throw, await, or yield. +4. Apply the exit-specific local refold. +5. Flip or rotate the crossed cell phase. +6. Move the carrier to the selected neighboring boundary. +7. Update the lineage accumulator and epoch. + +Call completion is a stronger transition: it closes the current lineage segment and lands the carrier at a contract gate from which the next invocation target and inputs can route it to an entry anchor. + +### 2.4 State scope + +There is exactly one active carrier per **root group**: + +- A root group contains one selected root function and every child bytecode unit produced from nested functions or class members. +- An escaped child closure remains attached to its originating root group. +- Different selected root functions receive independent lattices and independent carriers. +- There is no file-global carrier because it would create unnecessary coupling across unrelated APIs. +- There is no per-unit carrier because that would turn the design into independently moving instruction streams and weaken the core concept. + +### 2.5 Calls, recursion, reentrancy, async, and generators + +JavaScript remains single-threaded at each semantic step, so one carrier can serve nested and suspended work without cloning: + +- A nested or reentrant call pushes a carrier resume gate, routes the same carrier to the callee's contract entry, and returns through a generated continuation corridor. +- Recursion uses the same mechanism. No persistent state is stored in the current VM's hoisted sync-handler slots. +- `await` and `yield` park the machine frame and a continuation gate, then release the carrier. +- A later resume reacquires the group's current carrier and routes it to that continuation gate before executing the next semantic transition. +- Multiple async calls may be suspended simultaneously, but only one carrier is active during any JavaScript turn. Suspended frames are not additional boundaries. +- Resume order follows the host's existing Promise/generator scheduling. Ruam must not add a new queue that changes observable ordering. + +### 2.6 Failure and exception state + +Boundary motion is not transactional and is never rolled back: + +- A handled throw selects an exception exit and continues through the lattice. +- An uncaught throw selects a terminal throw exit, moves the carrier to a contract gate, then rethrows the original value. +- A rejected async call does the same at rejection. +- User-visible side effects and representational evolution therefore advance together. + +This avoids impossible rollback promises around arbitrary JavaScript side effects and makes exceptional use part of the artifact's history. + +### 2.7 Persistence boundary + +Version 1 persistence is in-memory for the lifetime of the loaded artifact: + +- Repeated calls in the same page, worker, or Node process see the evolved lattice. +- Reloading or restarting creates a fresh state from the emitted initial carrier. +- Cross-reload persistence, local storage, remote state, and license-bound state providers are explicitly out of scope for the first release. + +### 2.8 Correctness proof boundary + +Ruam will not claim a formal proof of arbitrary JavaScript behavior. It will prove a narrower and testable property: + +1. The semantic compiler produces canonical semantic IR. +2. For every reachable canonical IR node and exit edge, the build-time BCM verifier proves that the corresponding boundary state resolves exactly the intended handler and operand projection. +3. The verifier proves that the chosen exit refold reaches the boundary family corresponding to the intended successor IR node. +4. Handler differential tests against native JavaScript remain the behavioral authority for each semantic handler. +5. End-to-end differential tests compare native JavaScript, the TypeScript BCM reference runtime, and the emitted BCM runtime. + +During implementation only, the old VM may provide an additional mismatch signal. It is not part of the proof boundary, release test matrix, or shipped product. + +The build result includes verifier statistics and a certificate digest. The certificate itself remains a build artifact or owner sidecar; it is not required by the production runtime unless `runtimeChecks` is explicitly enabled. + +## 3. Non-negotiable invariants + +An implementation is not Traveling Isogloss unless all of these hold. + +| ID | Invariant | Automated enforcement | +|---|---|---| +| TI-01 | No emitted per-function `Instruction[]` or equivalent linear operation stream exists. | Artifact schema test and adversarial extractor | +| TI-02 | Every individual cell clause matches at least `minCellAnonymity` handlers. | Build verifier | +| TI-03 | The static intersection of the active left and right clauses still matches at least two handlers. | Build verifier | +| TI-04 | Left + right + current carrier witness resolves exactly one handler and operand projection for every reachable state. | Build verifier and property tests | +| TI-05 | A non-boundary neighboring pair never resolves exactly one handler under the current witness. | Build verifier | +| TI-06 | Every semantic transition changes carrier edge, cell phase, witness, lineage, or more than one of them. | Build verifier | +| TI-07 | Every canonical CFG edge has a corresponding verified refold edge, including exception and finally edges. | CFG bisimulation check | +| TI-08 | One root group has one live carrier, including during recursion, reentrancy, generator suspension, and async suspension. | Runtime assertions in tests | +| TI-09 | Runtime state is root-group-scoped and stored only in the BCM runtime closure. | Architecture test and code review | +| TI-10 | Production output uses no `eval`, `new Function`, `debugger`, dynamic script construction, or network dependency. | CSP/security tests | +| TI-11 | All generated identifiers use `NameRegistry`; all random streams use `deriveSeed()`. | Naming tests and code review | +| TI-12 | An owner trace map is never embedded in production code. | Output inspection test | +| TI-13 | Removed VM-specific options fail with an actionable migration error; none are silently accepted or ignored. | Removed-option tests | +| TI-14 | The cutover tree contains no VM interpreter, VM loader, bytecode encoder, physical opcode shuffle, backend selector, or VM fallback path. | Source inventory and package-surface tests | + +## 4. Explicit non-goals + +- Do not market the design as mathematically irreversible. +- Do not encrypt source semantics and call the ciphertext a dialect. +- Do not put two complementary opcode shares in adjacent cells. +- Do not emit a hidden opcode stream and merely move a decoder over it. +- Do not retain the old VM as a fallback, alternate preset, compatibility mode, debug engine, or hidden recovery path. +- Do not mutate generated JavaScript source text at runtime. +- Do not use self-modifying native code, WebAssembly, workers, timers, storage, or a server to make the mechanism work. +- Do not reimplement the JavaScript semantic catalog from scratch; extract the language-level handler logic from `ruamvm` before deleting the VM scaffold. +- Do not add performance hardening until the reference model and verifier are correct. +- Do not combine the first implementation with Evidential Phase Succession, Anisomorphic Ruleworlds, licensing, remote policy, or cross-installation state. + +## 5. Current architecture and replacement seams + +The legacy pipeline being removed is: + +```mermaid +flowchart LR + A["JavaScript source"] --> B["Babel parse and target selection"] + B --> C["compileFunction"] + C --> D["logical opcode units"] + D --> E["VM transforms"] + E --> F["bytecode encoder"] + F --> G["VM runtime assembler"] + G --> H["dispatch stubs and final output"] +``` + +The replacement pipeline is: + +```mermaid +flowchart LR + A["JavaScript source"] --> B["Parse, selection, grouping"] + B --> C["Canonical Semantic IR"] + C --> D["Traveling Isogloss lowering"] + D --> E["Constraint lattice"] + E --> F["Build verifier and certificate"] + F --> G["BCM artifact and runtime"] + G --> H["Protected JavaScript plus optional owner sidecar"] +``` + +There is no execution-backend branch in the target architecture. + +The replacement seams in the current code are: + +| Current location | Existing responsibility | Replacement action | +|---|---|---| +| `src/transform.ts` | Owns every pipeline phase and both shared/shielded VM branches | Rewrite as the single semantic-IR → Isogloss orchestration path | +| `src/compiler/index.ts` | Produces logical opcodes and applies VM optimizer assumptions | Return canonical semantic units; remove physical VM lowering | +| `src/compiler/emitter.ts` | Emits opcode and operand pairs | Become a semantic-IR emitter with build-only source origins and stable node IDs | +| `src/compiler/basic-blocks.ts` | Recovers basic blocks after compilation | Replace with a canonical CFG carrying typed exits | +| `src/compiler/optimizer.ts` | Produces VM superinstructions | Delete after any engine-independent optimizations are moved to semantic IR | +| `src/ruamvm/handlers/*` | Defines JavaScript semantic behavior as AST builders | Move language-level handlers into `src/runtime/handlers/` and remove opcode-table coupling | +| `src/ruamvm/builders/interpreter.ts` | Couples handler construction to VM dispatch | Extract the semantic handler catalog, then delete the VM scaffold | +| `src/ruamvm/builders/loader.ts` | Decodes and caches bytecode units | Replace with the BCM group loader and carrier-state store | +| `src/ruamvm/builders/runners.ts` | Routes unit IDs to VM execution | Replace with contract-token runners | +| `src/ruamvm/assembler.ts` | Assembles the VM runtime | Replace with the BCM runtime assembler; delete the VM assembler at cutover | +| `src/types.ts` | Mixes public options and VM bytecode internals | Replace with Ruam/Isogloss public types; delete VM bytecode types | +| `src/presets.ts` | Resolves VM hardening booleans | Redefine every preset in Isogloss terms | +| `src/option-meta.ts` | Describes VM-oriented booleans | Replace with typed Isogloss, artifact, and common-source options | +| `src/naming/*` | Central identifier system | Add Isogloss scopes and remove VM-only claims after cutover | +| `src/structural-choices.ts` | Derives VM runtime variation | Replace with lattice/runtime structural choices using isolated streams | +| `test/helpers.ts` | Native-versus-VM equivalence | Make native-versus-reference-BCM-versus-emitted-BCM the permanent oracle set | + +## 6. Public API and configuration + +### 6.1 Public type redesign + +Replace `VmObfuscationOptions` rather than retaining it as an alias: + +```ts +export type IsoglossProfile = "research" | "balanced" | "hardened"; + +export interface TravelingIsoglossOptions { + profile?: IsoglossProfile; + minCellAnonymity?: number; + expansion?: 2 | 3 | 4; + runtimeChecks?: boolean; + ownerTrace?: "off" | "sidecar" | "sidecar+runtime"; +} + +export interface RuamOptions { + isogloss?: TravelingIsoglossOptions; + preset?: PresetName; + targetMode?: "root" | "comment"; + threshold?: number; + preprocessIdentifiers?: boolean; + target?: TargetEnvironment; + // New engine-independent artifact protections only. +} +``` + +Delete `ExecutionModel`, `ResolvedVmOptions`, and `VmObfuscationOptions` from the public surface. Publish a migration guide rather than a compatibility alias. + +Defaults: + +```ts +isogloss.profile = "research" +isogloss.minCellAnonymity = 8 +isogloss.expansion = 2 +isogloss.runtimeChecks = false +isogloss.ownerTrace = "off" +``` + +Validation: + +- `minCellAnonymity` must be an integer from 4 through 64. +- `expansion` must be 2, 3, or 4. +- `ownerTrace: "sidecar+runtime"` is never enabled by a preset. +- Unknown nested Isogloss properties fail fast. +- Removed VM options fail with `RUAM_REMOVED_VM_OPTION` and a migration hint. + +### 6.2 Detailed build API + +The primary API returns verifier data and an optional owner sidecar: + +```ts +export interface ProtectionBuildResult { + code: string; + diagnostics: BuildDiagnostic[]; + stats: { + engine: "traveling-isogloss"; + rootGroupCount: number; + unitCount: number; + originalBytes: number; + outputBytes: number; + expansionRatio: number; + }; + ownerTrace?: IsoglossOwnerSidecar; +} + +export function protectCode( + source: string, + options?: RuamOptions +): ProtectionBuildResult; + +export function obfuscateCode( + source: string, + options?: RuamOptions +): string { + return protectCode(source, options).code; +} +``` + +Public-surface rules: + +- `protectCode()` is the primary API. +- `obfuscateCode()` may remain as a string-returning convenience name because it does not imply VM execution. +- Add `protectFile()` and `runProtection()`. +- Delete `runVmObfuscation()` and `VmObfuscationOptions` in the replacement major version. +- Remove the `ruamvm` CLI binary alias; ship `ruam` only. +- Rename package description, keywords, documentation, and generated messages away from VM and bytecode terminology. Package-registry renaming is a separate release decision, but no runtime compatibility depends on the old package name. + +### 6.3 CLI + +Add: + +```text +--isogloss-profile +--isogloss-min-anonymity <4..64> +--isogloss-expansion <2|3|4> +--isogloss-runtime-checks +--owner-trace +--runtime-owner-trace +``` + +Do not add `--execution-model`; there is only one engine. Refactor `src/cli.ts` to parse flags from generalized option metadata. Hand-written parsing remains only for input/output paths, help, version, and interactive mode. + +`--owner-trace` writes the sidecar returned by `protectCode()` and implies `ownerTrace: "sidecar"`. `--runtime-owner-trace` upgrades it to `"sidecar+runtime"`. + +Removed VM flags produce a concise error with the replacement concept where one exists; they never activate retained legacy code. + +### 6.4 Presets + +Replace the current VM preset contents with Isogloss-native definitions: + +```ts +interface PresetDefinition { + common: Partial; + isogloss: Required; + artifact: Partial; +} +``` + +| Preset | Isogloss expansion | Minimum anonymity | Runtime checks | Production intent | +|---|---:|---:|---|---| +| `low` | 2 | 8 | off | Smallest evaluable lattice | +| `medium` | 3 | 12 | off | Balanced lattice and artifact protection | +| `max` | 4 | 16 | on | Maximum verified lattice pressure | + +No preset contains VM options, no resolver has a VM branch, and no profile can select the removed engine. + +## 7. Internal type model + +### 7.1 Canonical semantic IR + +Move VM-internal data types out of public `src/types.ts` into `src/compiler/types.ts`. + +Create `src/compiler/ir.ts`: + +```ts +export type SemanticNodeId = number; + +export interface SourceOrigin { + file?: string; + start: number; + end: number; + line: number; + column: number; +} + +export interface SemanticInstruction { + id: SemanticNodeId; + op: SemanticOp; + operand: number; + originId: number; +} + +export type SemanticExit = + | { kind: "fallthrough"; target: SemanticNodeId } + | { kind: "branch-true"; target: SemanticNodeId } + | { kind: "branch-false"; target: SemanticNodeId } + | { kind: "exception"; target: SemanticNodeId } + | { kind: "finally"; target: SemanticNodeId } + | { kind: "return" } + | { kind: "throw" } + | { kind: "yield"; resume: SemanticNodeId } + | { kind: "await"; resume: SemanticNodeId }; + +export interface SemanticUnit { + id: string; + rootGroupId: string; + constants: ConstantPoolEntry[]; + nodes: SemanticInstruction[]; + exits: Map; + entryNode: SemanticNodeId; + origins: SourceOrigin[]; + // Existing function metadata follows. +} +``` + +The canonical IR contains semantic operations before lattice lowering. During migration, rename the language-level members of `compiler/opcodes.ts` into `SemanticOp` and delete physical opcode concerns rather than preserving a VM-flavored IR. + +The following legacy stages have no target equivalent and are removed at cutover: + +- opcode shuffle and physical opcode maps; +- VM superinstruction fusion; +- block permutation as instruction-address rewriting; +- opcode mutation; +- rolling and incremental instruction ciphers; +- VM bytecode serialization. + +Any optimization that remains useful must operate on canonical semantic IR or on the lattice topology and must preserve the verifier's source-node correspondence. + +### 7.2 Semantic signature catalog + +Create `src/compiler/semantic-signatures.ts` with one descriptor for every semantic operation: + +```ts +export interface SemanticSignature { + op: SemanticOp; + operandKind: + | "none" + | "constant" + | "register" + | "scope-name" + | "argc" + | "jump" + | "packed" + | "unit-ref"; + stackInput: StackArity; + stackOutput: StackArity; + effect: + | "pure" + | "local" + | "scope" + | "object" + | "call" + | "control" + | "exception" + | "async"; + control: + | "fallthrough" + | "conditional" + | "jump" + | "call" + | "return" + | "throw" + | "yield" + | "await"; + mayThrow: boolean; + readsThis: boolean; + readsScope: boolean; + syntheticDimensions: number; +} +``` + +Dynamic stack effects such as calls use a pure function of the operand. Every descriptor must be exhaustive through a `satisfies Record` check. A missing semantic operation must fail TypeScript compilation. + +### 7.3 Root groups + +Create `src/pipeline/groups.ts`: + +```ts +export interface RootGroup { + id: string; + rootPath: NodePath; + units: SemanticUnit[]; + entryContracts: EntryContract[]; + usedSemantics: Set; + hasAsync: boolean; + hasGenerator: boolean; +} +``` + +Group construction replaces both the normal shared-VM layout and the VM-shielding special case. Every selected root receives one lattice group; there is no shielding mode after cutover. + +### 7.4 Lattice model + +Create `src/isogloss/types.ts`: + +```ts +export type CellId = number; +export type ClauseId = number; +export type ReservoirId = number; +export type ContractId = number; + +export interface CandidateMask { + words: Uint32Array; + cardinality: number; +} + +export interface DialectClause { + id: ClauseId; + phase: number; + handlerCandidates: CandidateMask; + operandFamilies: CandidateMask; + stackShapeMask: number; + effectMask: number; + controlMask: number; + syntheticMask: Uint32Array; + neighborRefs: Uint32Array; +} + +export interface IsoglossCell { + id: CellId; + dialect: 0 | 1; + clauses: DialectClause[]; +} + +export interface OperandReservoir { + id: ReservoirId; + values: Int32Array; + projections: Uint32Array; +} + +export interface CarrierSeed { + left: CellId; + right: CellId; + leftPhase: number; + rightPhase: number; + witness: number; + epoch: number; + lineage: number; +} + +export interface EntryContract { + id: ContractId; + unitId: string; + gate: CellId; + anchorFamily: Uint32Array; +} + +export interface IsoglossGroup { + id: string; + cells: IsoglossCell[]; + reservoirs: OperandReservoir[]; + contracts: EntryContract[]; + initialCarrier: CarrierSeed; + flags: number; +} +``` + +The actual encoded runtime shape uses short randomized property names or array positions. These descriptive names exist only in TypeScript. + +### 7.5 Build certificate + +Create `src/isogloss/certificate.ts`: + +```ts +export interface IsoglossCertificate { + schemaVersion: 1; + groupId: string; + reachableStateCount: number; + verifiedTransitionCount: number; + minObservedCellAnonymity: number; + minObservedPairAmbiguity: number; + nonBoundaryUniqueResolutionCount: 0; + cfgMismatchCount: 0; + digest: string; +} +``` + +The digest detects accidental mismatch between the verified lattice and the encoded lattice during the build. It is not presented as cryptographic attestation. + +## 8. Compilation algorithm + +### 8.1 Capture source origin without changing every visitor + +Extend `Emitter` with an origin stack: + +```ts +emitter.withOrigin(node, () => { + // Existing visitor body. +}); +``` + +`emit()` copies the current origin ID onto the instruction. Wrap visitor entry points in `visitors/expressions.ts`, `visitors/statements.ts`, and `visitors/classes.ts`. Optimizations that combine nodes retain the ordered set of contributing origin IDs. + +This data is build-only and is removed unless an owner sidecar is requested. + +### 8.2 Build the canonical CFG + +Extend `compiler/basic-blocks.ts` or add `compiler/cfg.ts` to produce typed exits: + +1. Mark entry, jump targets, post-transfer positions, exception entries, finally entries, and jump-table targets as leaders. +2. Split canonical instructions into blocks. +3. Convert packed jump operands into explicit typed edges without mutating the original operand. +4. Add exceptional edges for every instruction covered by an exception range and marked `mayThrow`. +5. Add return, throw, await, and yield terminal/resume edges. +6. Validate every target exists and every nonterminal block has at least one exit. +7. Retain a map from block/node IDs back to source origins. + +### 8.3 Produce boundary families + +Create `src/isogloss/lower.ts`. For each semantic node: + +1. Read its semantic signature. +2. Allocate `expansion` boundary variants. A variant is a distinct pair of reusable cell clauses and witness class that realizes the same semantic node. +3. Select left and right candidate sets that each include the intended handler and at least `minCellAnonymity - 1` decoys. +4. Require the static left/right intersection to contain at least two handlers. +5. Choose a carrier witness predicate that reduces the intersection to the intended handler. +6. Allocate an operand family that includes the intended projection plus decoys and reused values. +7. Connect each typed CFG exit to an eligible successor boundary variant. +8. Generate local phase changes so the next visit to the same semantic node prefers a different variant. +9. Reuse each nonterminal cell across at least two semantic nodes or two exit paths. A dedicated cell per instruction is forbidden. +10. Generate contract gates and non-semantic routing corridors for root entry, escaped child entry, reentrant entry, continuation resume, return, and uncaught throw. + +All random choices use streams such as: + +```ts +deriveSeed(fileSeed, `isogloss/group/${groupId}/cells`) +deriveSeed(fileSeed, `isogloss/group/${groupId}/clauses`) +deriveSeed(fileSeed, `isogloss/group/${groupId}/operands`) +deriveSeed(fileSeed, `isogloss/group/${groupId}/topology`) +deriveSeed(fileSeed, `isogloss/group/${groupId}/phases`) +``` + +No new ad hoc PRNG or XOR-derived stream is permitted. + +### 8.4 Constraint generation strategy + +Create `src/isogloss/constraints.ts`. + +Use a deterministic bounded search: + +1. Start from the handler set compatible with the target's broad stack, effect, control, and operand categories. +2. If the compatible set is too small, add synthetic dimensions and handler aliases that preserve semantics but change clause membership. +3. Sample candidate left and right supersets. +4. Reject a pair if either side is below the anonymity minimum. +5. Reject a pair if its static intersection has fewer than two members. +6. Generate a witness mask from the carrier predecessor family. +7. Reject if the three-way intersection is not exactly the target handler. +8. Reject if the pair uniquely resolves under any witness reachable at a non-boundary neighbor. +9. Stop after a fixed attempt budget. +10. On exhaustion, emit `RUAM_ISOGLOSS_CONSTRAINT_UNSAT` with the unit, semantic node, signature, minimum anonymity, and attempts. Never weaken the requested anonymity silently. + +Rare handlers that cannot meet the requested anonymity receive generated semantic aliases from the existing handler-aliasing machinery. An alias is eligible only if differential tests prove it equivalent and its use does not expose a direct node-to-handler mapping. + +### 8.5 Operand projection + +Create `src/isogloss/operands.ts`. + +Operands must not be stored beside their semantic node. Instead: + +- Constants, register indices, scope-name indices, argument counts, unit references, and jump metadata are placed in typed reservoirs shared across the root group. +- A left clause selects a projection family. +- A right clause selects a transformation family. +- The carrier witness selects a slot within their intersection. +- The resolver reconstructs the effective operand into a local variable immediately before handler execution. +- At least half of reservoir entries must be referenced by more than one boundary family. +- Unused decoy reservoir entries are permitted only when they are structurally indistinguishable from used entries. + +Control-flow targets are never reconstructed as instruction pointers. The handler returns an exit class; topology chooses the successor boundary. + +### 8.6 Verify before encoding + +Create `src/isogloss/verify.ts`. + +The verifier performs a graph exploration from every entry contract and continuation contract: + +```text +state = carrier edge + cell phases + witness class + semantic frame class +``` + +For each reachable state: + +1. Compute left candidates. +2. Compute right candidates. +3. Assert local and pair ambiguity thresholds. +4. Apply the witness and assert one resolution. +5. Compare the handler and operand projection with the canonical semantic node. +6. Enumerate every legal exit of the canonical node. +7. Apply the corresponding refold. +8. Assert the successor boundary realizes the canonical successor node. +9. Assert the carrier changed. +10. Add the successor state to the worklist. + +Loops require state abstraction so verification terminates. Phase and witness domains are finite by construction; epoch and lineage are reduced to the finite bits actually consumed by clauses. The verifier must reject a design that reads unbounded epoch or lineage values for semantic selection. + +## 9. Runtime algorithm + +### 9.1 Runtime components + +Add `src/isogloss/runtime/`: + +| File | Responsibility | +|---|---| +| `assembler.ts` | Dependency-tiered runtime factory and final result | +| `deserializer.ts` | Decode the BCM binary envelope into typed arrays | +| `loader.ts` | Cache decoded groups and initialize one carrier per group | +| `resolver.ts` | Compute the three-way boundary intersection | +| `operands.ts` | Reconstruct one effective operand from reservoirs | +| `carrier.ts` | Apply phase changes, refolds, contract routing, epoch, and lineage | +| `interpreter.ts` | Execute the shared handler catalog under BCM control | +| `runners.ts` | Dispatch opaque contract tokens, box `this`, and route sync/async/generator calls | +| `continuations.ts` | Park and resume await/yield/reentrant frames | +| `trace.ts` | Optional opaque runtime owner events | +| `checks.ts` | Optional local invariant checks for research/max builds | + +Every emitted runtime fragment is built with the existing typed AST node system. Do not use template strings containing JavaScript. + +### 9.2 Carrier state + +The deserialized group cache owns: + +```ts +interface RuntimeCarrierState { + left: number; + right: number; + leftPhase: number; + rightPhase: number; + witness: number; + epoch: number; + lineage: number; + activeDepth: number; + frameStack: RuntimeFrame[]; + parked: Map; +} +``` + +In emitted code, use compact arrays and NameRegistry-generated identifiers. The descriptive object form is for the reference runtime only. + +### 9.3 Dispatch loop + +The emitted interpreter loop is: + +```text +route carrier to requested contract gate +create or resume frame +while frame is runnable: + read active left/right clauses + resolve one handler candidate using carrier witness + reconstruct one operand + execute shared semantic handler + classify exit + update carrier and cell phases + emit optional opaque trace event + route to successor, callee, continuation, or terminal contract +return, throw, yield, or await with native-equivalent values and scheduling +``` + +The handler catalog may be stable within a build; the prohibited mapping is a stable program-site-to-handler stream. Handlers describe the JavaScript language, while the moving boundary describes this program's behavior. + +### 9.4 Semantic handler extraction + +Do not duplicate handler bodies and do not retain a VM dispatch scaffold. + +1. Move `src/ruamvm/handlers/*` to `src/runtime/handlers/*`. +2. Rename opcode-facing registry types to semantic-operation-facing types. +3. Extract generic stack, register, scope, exception, call, `this`, and completion helpers into `src/runtime/handler-context.ts`. +4. Make `buildHandlerCatalog()` return the catalog consumed directly by the BCM resolver. +5. Remove decoded-opcode arguments and physical opcode lookup from handler construction. +6. Delete `buildVmDispatchScaffold()`, the VM handler table metadata, and VM interpreter builders after the BCM runtime passes the corresponding semantic suites. + +The source move is performed before final cutover so the reusable JavaScript language implementation survives deletion of `src/ruamvm/`. + +### 9.5 Contract dispatch stubs + +Replace `replaceFunctionBody()` with: + +```ts +replaceFunctionWithDispatchStub( + path, + contractToken, + dispatchBinding, + stubOptions +) +``` + +The stub shape remains natural: + +- rest parameters; +- direct lexical dispatcher call; +- regular functions forward `this`; +- arrows do not; +- constructors, `new.target`, home object, outer scope, and generator/async shape match the current behavior. + +The token identifies an entry contract, not a unit body. It may be different per build and is generated through the root-group stream. + +This avoids publishing runtime internals on `globalThis`. If a target shape requires global exposure, keep a target-specific **BCM** adapter and document why. Validate top-level `this`, script globals, ESM, CJS, and browser-extension behavior before cutover. + +### 9.6 Runtime factory assembly + +Assemble the generated dispatcher as a lexical binding when target semantics allow it: + +```js +var = (function () { + // Runtime and artifact state. + return ; +})(); +``` + +This avoids publishing BCM internals on `globalThis`. If a target shape requires global exposure, keep a target-specific BCM adapter and document why. Validate top-level `this`, script globals, ESM, CJS, and browser-extension behavior before cutover. + +## 10. Binary format + +### 10.1 Envelope + +Create `src/artifact/format.ts` and `src/artifact/encode.ts`. + +Use a single-engine versioned envelope: + +```text +magic 4 bytes "RUAM" +version u8 1 +flags u16 +groupCount u32 +payloads length-prefixed Isogloss groups +``` + +There is no backend discriminator and no legacy bytecode payload variant. The generated code and BCM runtime are produced together, and round-trip format tests cover only the Isogloss schema. + +### 10.2 BCM payload + +Each group payload contains: + +1. group metadata and flags; +2. unit-to-contract token table; +3. constant pools; +4. cell clause table; +5. candidate mask word table with deduplication; +6. operand reservoirs; +7. neighborhood/refold table; +8. initial carrier seed; +9. async/generator continuation metadata; +10. optional runtime-check digest. + +Use `Uint8Array`, `Uint16Array`, `Uint32Array`, and `Int32Array` views after deserialization. Avoid nested runtime objects in the hot path. + +### 10.3 No source or owner data in payload + +The following are build-only: + +- source spans; +- original function names beyond what JavaScript semantics require; +- canonical semantic node IDs; +- expected handler IDs; +- verifier witness paths; +- sidecar event descriptions. + +An output-inspection test must deserialize every production payload and assert these fields are absent. + +## 11. Owner view + +### 11.1 Sidecar + +Create `src/owner-view/types.ts` and `src/owner-view/build.ts`. + +The sidecar schema contains: + +```ts +interface IsoglossOwnerSidecar { + schemaVersion: 1; + buildId: string; + groups: Array<{ + opaqueGroupId: string; + contracts: Array<{ + opaqueContractId: number; + sourceOrigin?: SourceOrigin; + displayName?: string; + }>; + events: Array<{ + opaqueEventId: number; + sourceOrigins: SourceOrigin[]; + semanticSummary: string; + }>; + certificate: IsoglossCertificate; + }>; +} +``` + +`semanticSummary` is owner-facing text such as “read scoped binding” or “conditional exit,” not an emitted opcode number. + +### 11.2 Runtime event sink + +Only when `ownerTrace: "sidecar+runtime"`: + +- The runtime looks up a documented trace sink once during initialization. +- The sink receives opaque IDs, epoch, from/to cell IDs, and exit class. +- The production artifact contains no source descriptions. +- Missing sinks are a no-op. +- Sink exceptions are caught and ignored so tracing cannot change protected program behavior. +- Runtime trace mode is excluded from performance and security claims. + +Add `decodeIsoglossTrace(sidecar, events)` to convert opaque events into a replayable owner timeline. + +## 12. Legacy option removal and reinterpretation + +The replacement major version does not carry VM switches forward under misleading names. + +| Existing option | Cutover action | Isogloss-native replacement | +|---|---|---| +| `targetMode` | Keep | Source-selection concern | +| `threshold` | Keep after deterministic PRNG refactor | Source-selection concern | +| `preprocessIdentifiers` | Keep | Pre-compilation transformation | +| `debugLogging` | Rename | `isogloss.ownerTrace` / development trace sink | +| `encryptBytecode` | Remove | Later `encryptArtifact` over the lattice payload | +| `debugProtection` | Remove, then redesign | Later engine-independent runtime protection | +| `dynamicOpcodes` | Remove | The BCM emits only required semantic families | +| `decoyOpcodes` | Remove | Later `decoyClauses` / semantic aliases | +| `deadCodeInjection` | Remove | Later inert, never-unique lattice corridors | +| `stackEncoding` | Remove, then redesign | Later engine-independent value representation | +| `rollingCipher` | Delete permanently | Assumes a linear instruction stream | +| `integrityBinding` | Remove, then redesign | Later lattice/runtime integrity binding | +| `vmShielding` | Delete permanently | Root groups already own independent lattices | +| `mixedBooleanArithmetic` | Remove, then redesign | Later generic runtime AST transform | +| `handlerFragmentation` | Delete permanently unless a new BCM rationale is proven | No VM handler table remains | +| `stringAtomization` | Remove, then redesign | Later final-runtime string transform | +| `polymorphicDecoder` | Remove, then redesign | Later artifact decoder transform | +| `scatteredKeys` | Remove | No core BCM key material | +| `blockPermutation` | Delete permanently | Lattice topology replaces instruction ordering | +| `opcodeMutation` | Delete permanently | No opcode table or physical opcodes remain | +| `bytecodeScattering` | Remove | Later `artifactScattering` over the lattice payload | +| `incrementalCipher` | Delete permanently | Assumes linear instruction epochs | +| `semanticOpacity` | Remove | Isogloss constraints, aliases, and predicates provide the native mechanism | +| `observationResistance` | Remove, then redesign | Later BCM-specific observation model | +| `target` | Keep | Final assembly concern | + +Implement `validateRemovedVmOptions()` before parse/compile work. It reports: + +- `RUAM_REMOVED_VM_OPTION`; +- every removed property or CLI flag; +- whether a concept was deleted permanently, renamed, or deferred for redesign; +- the migration replacement where one exists. + +This validator is a tombstone list only. It imports no VM implementation and is removed after the documented migration window if desired. + +## 13. Determinism and entropy refactor + +The replacement engine requires repeatable failure reproduction. Refactor randomness before the spike: + +1. Move `generateCryptoSeed()` out of `transform.ts` into `src/random/entropy.ts`. +2. Define an internal `BuildEntropy` containing the file seed and any independently generated salts. +3. Production uses `crypto.randomBytes`. +4. Tests call an internal `protectCodeWithContext()` with fixed entropy. +5. `collectTargetFunctions()` stops using `Math.random()` for `threshold`; it receives a PRNG derived with `deriveSeed(fileSeed, "target-selection")`. +6. Every BCM module receives either its already-derived seed or a scoped PRNG. No module reads global randomness. +7. Failure diagnostics always include the file seed, group ID, stream label, and attempt count. + +Do not expose a deterministic public production seed unless a separate product decision approves reproducible builds and documents the security tradeoff. + +## 14. File-by-file change ledger + +### 14.1 New files + +```text +packages/ruam/src/artifact/encode.ts +packages/ruam/src/artifact/format.ts +packages/ruam/src/compiler/cfg.ts +packages/ruam/src/compiler/ir.ts +packages/ruam/src/compiler/semantic-ops.ts +packages/ruam/src/compiler/semantic-signatures.ts +packages/ruam/src/compiler/types.ts +packages/ruam/src/isogloss/certificate.ts +packages/ruam/src/isogloss/constraints.ts +packages/ruam/src/isogloss/lower.ts +packages/ruam/src/isogloss/operands.ts +packages/ruam/src/isogloss/reference-runtime.ts +packages/ruam/src/isogloss/types.ts +packages/ruam/src/isogloss/verify.ts +packages/ruam/src/isogloss/runtime/assembler.ts +packages/ruam/src/isogloss/runtime/carrier.ts +packages/ruam/src/isogloss/runtime/checks.ts +packages/ruam/src/isogloss/runtime/continuations.ts +packages/ruam/src/isogloss/runtime/deserializer.ts +packages/ruam/src/isogloss/runtime/interpreter.ts +packages/ruam/src/isogloss/runtime/loader.ts +packages/ruam/src/isogloss/runtime/operands.ts +packages/ruam/src/isogloss/runtime/resolver.ts +packages/ruam/src/isogloss/runtime/runners.ts +packages/ruam/src/isogloss/runtime/trace.ts +packages/ruam/src/migration/removed-vm-options.ts +packages/ruam/src/options/resolve.ts +packages/ruam/src/owner-view/build.ts +packages/ruam/src/owner-view/decode.ts +packages/ruam/src/owner-view/types.ts +packages/ruam/src/pipeline/assemble.ts +packages/ruam/src/pipeline/groups.ts +packages/ruam/src/pipeline/select.ts +packages/ruam/src/random/entropy.ts +packages/ruam/src/runtime/handler-context.ts +packages/ruam/src/runtime/handler-catalog.ts +packages/ruam/src/runtime/handlers/*.ts +packages/ruam/src/runtime/nodes.ts +packages/ruam/src/runtime/emit.ts +packages/ruam/src/testing.ts +``` + +### 14.2 Existing files with behavioral changes + +| File | Required edit | +|---|---| +| `src/transform.ts` | Rewrite as the single Isogloss build pipeline and return `ProtectionBuildResult` internally | +| `src/index.ts` | Export `protectCode`, `RuamOptions`, and owner-view APIs; remove VM-named exports | +| `src/browser-entry.ts` | Export the detailed Isogloss API and new types | +| `src/browser-worker.ts` | Use `protectCode()` and optionally transfer sidecar data | +| `src/types.ts` | Keep Ruam/Isogloss public types only; remove VM bytecode and VM option types | +| `src/compiler/index.ts` | Emit canonical `SemanticUnit` directly | +| `src/compiler/emitter.ts` | Emit `SemanticOp` nodes with IDs and source origins | +| `src/compiler/basic-blocks.ts` | Replace with or delegate to canonical CFG construction | +| `src/compiler/optimizer.ts` | Move genuinely semantic passes, then delete VM superinstruction logic | +| `src/presets.ts` | Replace preset contents with Isogloss/artifact settings | +| `src/option-meta.ts` | Replace VM options with typed Isogloss metadata and removed-option tombstones | +| `src/cli.ts` | Remove engine selection and VM flags; add Isogloss flags | +| `src/tuning.ts` | Replace VM intensity fields with lattice/runtime tuning | +| `src/structural-choices.ts` | Generate lattice and BCM-runtime choices only | +| `src/naming/claims.ts` | Replace VM claims with BCM runtime claims | +| `src/naming/compat-types.ts` | Replace `RuntimeNames` with Isogloss/runtime types; remove the compatibility framing | +| `src/naming/setup.ts` | Create root-group and BCM runtime scopes only | +| `scripts/generate-manifest.mjs` | Emit Isogloss option metadata plus migration tombstones | +| `package.json` | Remove `ruamvm` binary alias and VM/bytecode product description; prepare the replacement major version | +| `README.md` | Document the sole Isogloss engine, claim boundary, migration, examples, and limitations | +| `CLAUDE.md` | Replace VM architecture notes with BCM invariants while preserving relevant JavaScript semantic regressions | + +### 14.3 Files and systems deleted at cutover + +```text +packages/ruam/src/compiler/encode.ts +packages/ruam/src/compiler/opcodes.ts # replaced by semantic-ops.ts +packages/ruam/src/compiler/block-permutation.ts +packages/ruam/src/compiler/incremental-cipher.ts +packages/ruam/src/compiler/opcode-mutation.ts +packages/ruam/src/compiler/rolling-cipher.ts +packages/ruam/src/ruamvm/ # after generic handlers/nodes are moved +packages/ruam/test/security/*-cipher.test.ts # replaced by artifact/Isogloss tests where applicable +packages/ruam/test/security/opcode-mutation*.test.ts +packages/ruam/test/security/vm-shielding.test.ts +``` + +Before deleting any test, classify it as: + +1. JavaScript semantic correctness—port it to Isogloss; +2. generic security property—rewrite it for the lattice/artifact; +3. VM-mechanism-only—delete it with the mechanism. + +The deletion PR must include a generated source inventory proving there are no imports of `ruamvm`, `BytecodeUnit`, physical opcode maps, VM runner names, VM options, or removed encoders. + +### 14.4 Tests to add + +```text +packages/ruam/test/isogloss/certificate.test.ts +packages/ruam/test/isogloss/cfg-bisimulation.test.ts +packages/ruam/test/isogloss/constraints.test.ts +packages/ruam/test/isogloss/format-roundtrip.test.ts +packages/ruam/test/isogloss/novelty-invariants.test.ts +packages/ruam/test/isogloss/operands.test.ts +packages/ruam/test/isogloss/persistence.test.ts +packages/ruam/test/isogloss/reference-runtime.test.ts +packages/ruam/test/isogloss/runtime-async.test.ts +packages/ruam/test/isogloss/runtime-classes.test.ts +packages/ruam/test/isogloss/runtime-closures.test.ts +packages/ruam/test/isogloss/runtime-control-flow.test.ts +packages/ruam/test/isogloss/runtime-core.test.ts +packages/ruam/test/isogloss/runtime-exceptions.test.ts +packages/ruam/test/isogloss/runtime-generators.test.ts +packages/ruam/test/isogloss/runtime-reentrancy.test.ts +packages/ruam/test/isogloss/seed-stress.test.ts +packages/ruam/test/isogloss/semantic-signatures.test.ts +packages/ruam/test/isogloss/sidecar.test.ts +packages/ruam/test/isogloss/static-extractor.test.ts +packages/ruam/test/migration/removed-vm-options.test.ts +packages/ruam/test/migration/no-legacy-vm.test.ts +packages/ruam/test/options/metadata-drift.test.ts +``` + +## 15. Implementation sequence + +Each phase ends in a reviewable state. Development may call the legacy VM explicitly from migration tests until Phase 10, but product entry points never choose between engines: they either execute through the BCM or report that an unfinished semantic surface is not yet available on the replacement branch. No automatic VM fallback is permitted. + +### Phase 0 — Freeze the legacy baseline and replacement contract + +**Work** + +1. Record the current VM's correctness results, build time, output size, bootstrap time, execution time, and retained memory as versioned JSON benchmark artifacts. +2. Save representative generated outputs for core, closure, exception, class, async, generator, and max-preset fixtures. +3. Classify every existing test as JavaScript semantics, generic protection, or VM-mechanism-only. +4. Add architecture records for: + - full VM replacement and no fallback; + - canonical semantic IR; + - one carrier per root group; + - non-transactional boundary motion; + - in-memory-only persistence; + - bounded correctness/security claims. +5. Add deterministic entropy injection and remove `Math.random()` from target selection without changing legacy behavior. + +**Exit gate** + +- Existing suite, typecheck, and build pass before replacement work begins. +- Fixed entropy reproduces identical output. +- Benchmark JSON and fixture inventory are committed so later comparisons do not require retaining VM code. +- The no-backend-selector/no-fallback architecture record is approved. + +**Effort envelope:** 2–4 engineer-days. + +### Phase 1 — Extract the semantic compiler and handler catalog + +**Work** + +1. Rename language-level logical operations from `Op` to `SemanticOp`. +2. Split public API types from compiler IR types. +3. Add semantic node IDs, source origins, typed CFG exits, and exhaustive semantic signatures. +4. Move generic AST nodes, emitter, handler context, handler registry, and language handlers from `src/ruamvm/` into `src/runtime/`. +5. Remove physical opcode/shuffle parameters from the extracted catalog. +6. Add direct native-JavaScript differential tests for each semantic family. +7. Keep a thin migration-only adapter that allows the old VM tests to consume canonical semantic IR until Phase 10; do not expose it publicly. + +**Exit gate** + +- Every `SemanticOp` has a signature descriptor and native differential coverage. +- The current semantic suites pass through the extracted catalog. +- `src/runtime/` has no imports from VM encoder, shuffle, cipher, loader, or dispatch modules. +- No `ProtectionBackend`, `VmBackend`, `executionModel`, or backend selector has been introduced. + +**Effort envelope:** 7–12 engineer-days. + +### Phase 2 — Build-only lattice generator and verifier + +**Initial semantic scope** + +- constants and arguments; +- register load/store; +- stack operations; +- integer/number/string arithmetic; +- equality and ordering comparisons; +- unconditional and conditional control flow; +- return and return-void. + +**Work** + +1. Implement cells, clauses, reservoirs, topology, entry contracts, and carrier seeds. +2. Implement bounded deterministic constraint search. +3. Generate at least two boundary variants per semantic node. +4. Implement CFG-to-lattice verification and certificates. +5. Implement the hostile static extractor used by novelty tests. + +**Exit gate** + +- 10,000 generated small semantic programs produce valid certificates. +- At least 256 fixed seeds pass per curated fixture in extended CI. +- TI-01 through TI-07 are mechanically checked. +- The extractor cannot recover a site-to-handler map without carrier replay. +- Constraint failures are reproducible and include the seed, group, node, signature, and attempt count. + +**Stop condition** + +Stop and redesign if uniqueness requires a stored expected-handler token, direct next-operation pointer, hidden bytecode stream, or complementary opcode shares. + +**Effort envelope:** 8–14 engineer-days. + +### Phase 3 — TypeScript reference BCM + +**Work** + +1. Implement a readable TypeScript resolver, operand projector, carrier, and frame model. +2. Execute the Phase 2 subset directly from verified lattices. +3. Compare native JavaScript, a canonical semantic-IR reference evaluator, and the BCM reference runtime. +4. Verify repeated calls change carrier state while preserving results. +5. Verify loops revisit different boundary variants. +6. Model terminal throws and post-error carrier validity. + +**Exit gate** + +- Zero mismatches across the randomized subset corpus. +- Repeated identical inputs return identical JavaScript values while producing distinct valid lineages. +- Resolver diagnostics explain a mismatch down to group, state, clauses, witness, projected operand, and CFG edge. +- The reference runtime has no import from `src/ruamvm/`. + +**Effort envelope:** 6–10 engineer-days. + +### Phase 4 — Emitted synchronous BCM runtime + +**Work** + +1. Implement the single-engine artifact envelope and BCM encoding. +2. Implement AST-built deserializer, loader, resolver, operand projector, carrier, and sync interpreter. +3. Consume the extracted semantic handler catalog directly. +4. Emit contract-token function stubs. +5. Add memory-persistent root-group state and optional runtime invariant checks. +6. Route `protectCode()` through the BCM for the supported synchronous surface. Unsupported constructs produce explicit development errors; they never fall back to VM. + +**Exit gate** + +- Synchronous arithmetic, strings, arrays, objects, functions, and control flow pass against native JavaScript. +- Production output contains no canonical IR, direct handler token, source origin, or owner map. +- Node, browser, and browser-extension CSP fixtures pass for the supported surface. +- Every generated identifier uses NameRegistry. + +**Effort envelope:** 10–16 engineer-days. + +### Phase 5 — Scope, closures, classes, and reentrancy + +**Work** + +1. Add scope-chain and closure contracts. +2. Keep escaped child closures attached to their originating root-group carrier. +3. Add call, construct, home-object, `new.target`, and `this` transitions. +4. Add class, getter, setter, `super`, and computed method behavior. +5. Add reentrant carrier routing for getters, setters, proxies, `valueOf`, and callbacks. +6. Preserve one active carrier across nested and recursive calls. + +**Exit gate** + +- All ported closure, scope, class, and `super` semantic suites pass against native JavaScript. +- Known `NEW_CLASS`, home-object, computed-method, and this-boxing regressions have BCM tests. +- Escaped closures remain correct after unrelated calls evolve their root group. +- Reentrant calls never duplicate carrier state. + +**Effort envelope:** 10–18 engineer-days. + +### Phase 6 — Exceptions and finally + +**Work** + +1. Represent catch/finally edges directly in topology. +2. Route thrown handler values through exception exits. +3. Preserve completion type/value through nested finally paths. +4. Handle break, continue, return, and throw through one or more finally regions. +5. Land uncaught throws at terminal contract gates before rethrowing the original value. + +**Exit gate** + +- Every relevant exception/finally regression in `CLAUDE.md` is ported and passes against native JavaScript. +- Native and BCM event traces agree for nested catch/finally programs. +- A subsequent call after an uncaught error executes from a valid evolved carrier. + +**Effort envelope:** 8–14 engineer-days. + +### Phase 7 — Async and generators + +**Work** + +1. Add await/yield suspension contracts. +2. Park frames without cloning carriers. +3. Route the current carrier to resumed continuations in host scheduling order. +4. Support interleaved promises and generators within one root group. +5. Clean up terminal or abandoned continuations without append-only history. +6. Prove the optional trace sink cannot alter scheduling. + +**Exit gate** + +- Ported async and generator suites pass against native JavaScript. +- Event-order tests compare side-effect logs, not only final values. +- Two interleaved calls preserve native ordering. +- Only one carrier is active at every instrumented semantic step. +- Rejection followed by a later successful invocation works. + +**Effort envelope:** 12–20 engineer-days. + +### Phase 8 — Public API, CLI, presets, browser worker, and owner view + +**Work** + +1. Finalize `RuamOptions`, `protectCode()`, file/directory APIs, and build-result schemas. +2. Remove `VmObfuscationOptions`, `runVmObfuscation()`, and execution-model concepts. +3. Replace preset contents with Isogloss-native tuning. +4. Replace CLI VM flags with Isogloss flags and removed-option errors. +5. Update the option manifest and browser worker. +6. Implement the owner sidecar, opaque runtime events, and replay decoder. +7. Update README and migration documentation. + +**Exit gate** + +- The public type surface contains no VM-named execution types. +- CLI help, presets, browser worker, and manifest describe one engine. +- Sidecar data is absent from production code. +- Removed options fail before parse/compile work with actionable messages. + +**Effort envelope:** 6–10 engineer-days. + +### Phase 9 — Isogloss-native hardening + +Add hardening only when it has a lattice or engine-independent meaning: + +1. artifact encryption; +2. artifact scattering; +3. final-runtime string atomization; +4. generic runtime MBA transforms; +5. engine-independent debug protection; +6. value representation hardening; +7. decoy clauses and semantic aliases; +8. lattice/runtime integrity binding; +9. BCM-specific observation resistance. + +For each feature, add one option, one design note, isolated tests, seed stress, performance attribution, and only then preset coverage. + +Never restore rolling cipher, incremental cipher, opcode mutation, physical-opcode shuffling, VM shielding, or block permutation. + +**Exit gate** + +- `low`, `medium`, and `max` contain only Isogloss-native or engine-independent protections. +- Feature-pair and feature-triple matrices pass. +- No hardening creates a stable site-to-handler map or hidden instruction stream. + +**Effort envelope:** 12–24 engineer-days. + +### Phase 10 — Destructive legacy removal and product cutover + +**Work** + +1. Move the final reusable handler/node code out of `src/ruamvm/`. +2. Port all JavaScript-semantic tests and generic protection tests. +3. Delete VM-only tests and implementation modules listed in Section 14.3. +4. Delete VM encoder, interpreter, loaders, runners, assemblers, physical opcodes, shuffles, ciphers, mutation, permutation, and shielding. +5. Remove `ruamvm` imports, CLI alias, options, types, package language, and generated manifest entries. +6. Remove the migration-only VM oracle adapter. +7. Run a source/package inventory that fails on any legacy execution symbol. +8. Cut the replacement major release only after the full Isogloss suite passes; there is no release with a fallback. + +**Exit gate** + +- `rg` finds no production references to `ruamvm`, `VmBackend`, `BytecodeUnit`, physical opcode maps, VM dispatch/loader names, or removed VM options outside the migration guide and removed-option tombstones. +- Package contents include no VM runtime or bytecode encoder. +- Every supported source program executes only through the BCM. +- The full typecheck, build, Node, browser, worker, browser-extension, seed, and randomized suites pass. +- Deleting the old VM changes no passing Isogloss result because no product path referenced it before deletion. + +**Effort envelope:** 5–10 engineer-days. + +### Phase 11 — Adversarial review and release qualification + +**Work** + +1. Build an internal extractor with full format/runtime knowledge. +2. Measure body location, reusable-sequence recovery, concrete-invocation explanation, and next-lineage prediction. +3. Hook resolver, handler catalog, operand projection, and carrier independently. +4. Test whether a trace from one lineage transfers to another. +5. Benchmark against native JavaScript and the frozen Phase 0 legacy measurements. +6. Conduct external design/security review before declaring the replacement release stable. + +**Release gate** + +- Correctness is perfect across the declared JavaScript support surface. +- A static format-aware extractor cannot recover a history-independent site-to-operation body. +- A trace from one lineage does not directly decode another lineage without replay. +- Median output size is at most 3× the frozen legacy VM measurement for the same fixture and at most the separately documented absolute budget. +- Median steady-state execution is at most 3× the frozen legacy measurement; P95 is at most 5×. +- Bootstrap is at most 2× the frozen legacy measurement. +- Memory reaches a stable bound; carrier history never appends indefinitely. +- Node, browser, worker, and browser-extension CSP targets pass. +- Section 14.3's deletion inventory and TI-14 pass. + +**Kill criteria** + +End or radically redesign the replacement if any remains true: + +- the artifact requires a hidden linear instruction stream; +- unique resolution requires a stored expected-handler token; +- the lattice reduces to a direct node-to-handler table without replay; +- async equivalence requires serializing host-visible work; +- carrier state grows unboundedly; +- correctness depends on favorable seeds; +- straightforward optimization cannot bring execution below 12× or size below 10× the frozen legacy measurement; +- the practical mechanism is only control-flow flattening plus indirection. + +**Effort envelope:** 8–15 engineer-days plus external review. + +## 16. Validation strategy + +### 16.1 Per-commit checks + +From `packages/ruam`: + +```sh +bun run typecheck +bun test +bun run build +``` + +During focused development: + +```sh +bun test test/isogloss +bun test test/migration/removed-vm-options.test.ts +bun test test/migration/no-legacy-vm.test.ts +``` + +### 16.2 Seed tiers + +| Tier | Seeds | When | +|---|---:|---| +| Local fast | 8 per focused fixture | Every implementation loop | +| Pull request | 32 per curated fixture | Every PR | +| Extended CI | 256 per curated fixture | Merge gate | +| Nightly/release | 1,024 per high-risk fixture plus randomized corpus | Nightly and release candidate | + +High-risk fixtures include: + +- backward jumps; +- forward branch targets; +- nested switch/continue; +- nested return-through-finally; +- caught and uncaught exceptions; +- recursive closures; +- getters/proxies that reenter; +- escaped closures; +- multi-level `super`; +- sparse arrays and iterators; +- two interleaved async calls; +- generator throw/return; +- repeated calls after an exception. + +### 16.3 Differential oracle + +Extend `test/helpers.ts` to support: + +```ts +assertEquivalentToNative(source, options) +assertReferenceAndEmittedBcmAgree(source, options) +assertEventTraceEquivalent(source, options) +assertCarrierEvolves(source, calls, options) +``` + +For serializable results, compare values. For side effects, compare an explicit event log. For errors, compare constructor, name, message where specified by JavaScript, and event order. Do not require generated stack text to be identical unless Ruam explicitly guarantees it. + +Before Phase 10, migration-only tests may also compare against the legacy VM to diagnose refactor errors. Delete those calls with the VM oracle adapter at cutover. + +### 16.4 Property suites + +Add generators for: + +- small canonical IR graphs; +- loop and branch graphs; +- exception-region graphs; +- candidate masks at edge cardinalities; +- operand reservoir reuse; +- cell phase cycles; +- root/child contract graphs; +- async continuation interleavings. + +Every minimized failure prints a fully reproducible seed and serialized canonical IR/lattice pair. + +### 16.5 Novelty tests + +`static-extractor.test.ts` must use a deliberately hostile parser with full knowledge of the format. It should fail the build if it finds: + +- a linear array correlated one-to-one with canonical semantic nodes; +- a field whose value directly names the handler for a site; +- a left/right pair whose static intersection is unique; +- a direct next-instruction pointer; +- source origins or owner summaries in production payload; +- a carrier transition that leaves all state unchanged; +- a cell used by only one semantic node when reuse was required. + +This is an architecture regression test, not a cryptographic security proof. + +### 16.6 Performance suite + +Extend `scripts/bench.mjs` and `scripts/bench-attribution.mjs` with: + +- Isogloss profile and tuning values; +- original bytes, encoded payload bytes, runtime bytes, and sidecar bytes; +- build time split into parse, canonical compile, lattice generation, verification, encoding, and emit; +- bootstrap time; +- first call; +- repeated call; +- recursion; +- loop-heavy; +- object/property-heavy; +- exception-heavy; +- async interleave; +- peak and retained memory where supported. + +Report ratios against native JavaScript and the frozen Phase 0 legacy benchmark JSON. The benchmark harness must not import or retain the legacy VM after Phase 10. + +## 17. Risk register + +| Risk | Early signal | Mitigation | +|---|---|---| +| The representation collapses into opcode secret sharing | Adjacent pair uniquely identifies handler | Enforce pair ambiguity and third history-derived witness | +| The graph is just bytecode with renamed pointers | One node owns one cell pair and one successor | Require cell reuse, multiple boundary variants, and exit-specific local refolds | +| Constraint search explodes | Frequent attempt-budget exhaustion | Signature aliases, mask deduplication, bounded profiles, detailed UNSAT diagnostics | +| Verifier state space explodes on loops | Epoch/lineage create unbounded states | Consume only finite phase/witness projections; reject unbounded semantic dependence | +| Shared handler reuse leaks a fixed body | Sites directly reference handler indices | Resolver enumerates catalog through constraints; no site reference | +| Runtime is too large | Per-node clauses dominate payload | Cell reuse, mask interning, typed-array packing, profile-controlled expansion | +| Runtime is too slow | Catalog scan dominates | Bitset intersection, precomputed compatible families, word-level unique-bit detection | +| Reentrancy corrupts carrier state | Proxy/getter callbacks produce mismatches | One carrier frame stack and explicit resume gates; dedicated reentrancy suite | +| Async changes observable order | Event logs differ despite equal final values | Reuse host scheduling; never serialize complete calls | +| Owner view becomes an attack oracle | Production artifact exposes map | Sidecar separation; runtime emits opaque IDs only when explicitly enabled | +| Semantic extraction accidentally preserves VM coupling | `src/runtime/` imports physical opcode or VM loader modules | Import-boundary tests and extraction before deletion | +| Cutover strands unported semantics | A test is deleted because BCM does not pass it | Mandatory semantic/generic/VM-only test classification and review | +| Legacy fallback survives invisibly | A product path imports `ruamvm` or accepts `executionModel` | TI-14 source/package inventory and no-legacy test | +| Removed options silently degrade | Old flag parses but changes nothing | Tombstone validator with actionable hard failure | +| History grows forever | Memory rises per call | Fixed-width phase, epoch, lineage, bounded parked frames, no append-only log | + +## 18. Review and commit slicing + +Use small commits/PRs with the following boundaries: + +1. deterministic entropy, frozen legacy measurements, and test classification; +2. public build result type without engine selection; +3. semantic operation rename, IR, origins, and CFG; +4. generic handler/node extraction out of `ruamvm`; +5. semantic signatures and native handler differential tests; +6. lattice types, constraints, verifier, and certificate; +7. TypeScript reference BCM; +8. single-engine artifact format and encoder; +9. emitted synchronous BCM runtime and contract stubs; +10. scopes, closures, classes, and reentrancy; +11. exceptions and finally; +12. async and generators; +13. API, CLI, presets, manifest, and removed-option migration; +14. owner sidecar and replay; +15. each Isogloss-native hardening feature separately; +16. destructive VM/test/options/package removal; +17. adversarial and performance release report. + +Move the generic handlers before deleting `src/ruamvm/`, but do not combine that mechanical move with BCM semantic changes. Do not combine a new hardening feature with correctness work. The destructive deletion remains its own auditable change. + +## 19. Definition of done + +Traveling Isogloss has replaced the VM only when: + +- [ ] `protectCode()` is the primary build API and every protected function executes through the BCM. +- [ ] No public `executionModel`, `VmObfuscationOptions`, `runVmObfuscation()`, or `ruamvm` CLI alias remains. +- [ ] The production source and package contain no VM interpreter, bytecode encoder, physical opcode map, VM loader/runner, VM fallback, or backend abstraction. +- [ ] The `src/ruamvm/` directory is deleted after reusable semantic handlers and AST utilities are moved. +- [ ] The artifact contains no linear instruction stream. +- [ ] One carrier persists per root group and visibly evolves across calls. +- [ ] Every cell and pair ambiguity invariant is verified at build time. +- [ ] Canonical CFG and lattice topology pass the bisimulation verifier. +- [ ] Native JavaScript, reference BCM, and emitted BCM agree across the declared support surface. +- [ ] Closures, classes, `this`, `super`, recursion, reentrancy, exceptions, finally, async, and generators pass. +- [ ] Removed VM options fail explicitly with migration guidance. +- [ ] Node, browser, worker, and browser-extension CSP targets pass. +- [ ] Owner trace data is sidecar-only and runtime events are opt-in and opaque. +- [ ] All randomness is seed-isolated and reproducible in tests. +- [ ] All generated names use NameRegistry. +- [ ] Seed stress and randomized tests have no flakes. +- [ ] Adversarial tests show no history-independent site-to-operation body. +- [ ] Performance, output size, bootstrap, and bounded-memory release gates pass against native and frozen legacy measurements. +- [ ] README, package metadata, CLI help, and security language describe Isogloss as Ruam's sole engine. +- [ ] External design/security review approves the replacement release. + +## 20. Recommended first implementation move + +The first code change should **not** create an Isogloss cell or introduce a backend interface. It should freeze the old measurements, then extract canonical semantic IR and the reusable JavaScript handler catalog from the VM-specific directory. + +The first genuine research milestone is: + +> Compile straight-line and branching arithmetic functions into a verified in-memory lattice, execute them in the TypeScript reference BCM, and demonstrate that repeated calls traverse different boundary variants while native JavaScript, the semantic-IR evaluator, and the BCM remain identical. + +That milestone tests the new execution model before paying for binary encoding, emitted runtime generation, the full JavaScript surface, or destructive VM removal. Once the complete BCM passes the release gates, the legacy VM is deleted rather than retained as an option. diff --git a/package.json b/package.json index c30e7e4..783c72c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "test:watch": "cd packages/ruam && bun test --watch", "typecheck": "cd packages/ruam && bun run typecheck && cd ../../apps/web && bun run typecheck", "dev": "cd apps/web && bun run dev", - "stats": "cd packages/ruam && bun run stats", "clean": "rm -rf packages/ruam/dist apps/web/.next apps/web/out node_modules/.cache", "build:fresh": "bun run clean && bun run build", "test:fresh": "bun run clean && bun run test" diff --git a/packages/ruam/package.json b/packages/ruam/package.json index 68d1dc7..45e3343 100644 --- a/packages/ruam/package.json +++ b/packages/ruam/package.json @@ -1,17 +1,16 @@ { - "name": "ruamvm", + "name": "ruam", "version": "2.0.0", - "description": "JS VM obfuscator — compiles functions to custom bytecode executed by an embedded interpreter", + "description": "Isogloss JavaScript source protection with explicit local, custodied, private-function, and attested deployment profiles", "keywords": [ "obfuscator", "obfuscation", "javascript-obfuscator", - "vm", - "virtual-machine", - "bytecode", + "source-protection", + "isogloss", + "dynamic-analysis", + "analysis-cost", "code-protection", - "anti-tamper", - "anti-debug", "security" ], "homepage": "https://github.com/owengregson/Ruam#readme", @@ -34,8 +33,7 @@ } }, "bin": { - "ruam": "dist/cli.js", - "ruamvm": "dist/cli.js" + "ruam": "dist/cli.js" }, "files": [ "dist" @@ -43,14 +41,12 @@ "scripts": { "build": "tsup", "prepublishOnly": "bun run typecheck && bun test && bun run build", - "generate-manifest": "node scripts/generate-manifest.mjs", - "build:browser": "node scripts/generate-manifest.mjs && node scripts/build-browser.mjs", + "build:browser": "node scripts/build-browser.mjs", + "bench": "bun scripts/bench.mjs", "typecheck": "tsc --noEmit", "dev": "tsup --watch", "test": "bun test", - "test:watch": "bun test --watch", - "stats": "node scripts/collect-stats.mjs", - "stats:test": "node scripts/collect-stats.mjs --test" + "test:watch": "bun test --watch" }, "engines": { "node": ">=18" diff --git a/packages/ruam/scripts/bench-attribution.mjs b/packages/ruam/scripts/bench-attribution.mjs deleted file mode 100644 index c3f8097..0000000 --- a/packages/ruam/scripts/bench-attribution.mjs +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Max-preset feature-attribution harness. - * - * Starts from the full `max` option set and toggles each runtime-relevant - * feature OFF one at a time (and a few in combination), measuring the - * aggregate exec-isolated overhead. The delta vs `max` is that feature's - * runtime cost contribution *in the context of the full stack* — i.e. what - * removing it would actually save (Amdahl-correct prioritization). - * - * Key combination configs: - * - "cache-trio-off": removes {opcodeMutation, incrementalCipher, - * observationResistance} → re-enables the Phase-1 decode cache. - * - "+no-proxy": also removes stackEncoding (the Proxy stack). - * - "+no-mba": also removes mixedBooleanArithmetic. - * - * Usage: - * bun scripts/bench-attribution.mjs # default iters - * bun scripts/bench-attribution.mjs --iters 16 - * bun scripts/bench-attribution.mjs --full # heavier workloads - */ - -import { obfuscateCode } from "../src/index.ts"; -import { PRESETS } from "../src/presets.ts"; -import vmMod from "node:vm"; - -const { Script } = vmMod; - -// --- Timing --------------------------------------------------------------- - -function median(times) { - times.sort((a, b) => a - b); - const trim = Math.floor(times.length * 0.1); - const mid = times.slice(trim, times.length - trim); - return mid.reduce((s, t) => s + t, 0) / mid.length; -} - -function timeThunk(thunk, iters, warm) { - for (let i = 0; i < warm; i++) thunk(); - const times = []; - for (let i = 0; i < iters; i++) { - const t0 = performance.now(); - thunk(); - times.push(performance.now() - t0); - } - return median(times); -} - -function makeThunk(code) { - const script = new Script(code, { filename: "bench.js" }); - return () => script.runInThisContext(); -} - -// --- Workloads ------------------------------------------------------------ -// Moderate sizes so `max` (~1400x) stays fast enough for many configs. - -const args = process.argv.slice(2); -const FULL = args.includes("--full"); -const ITERS = args.includes("--iters") - ? Number(args[args.indexOf("--iters") + 1]) - : 12; -const WARM = 3; - -const WORKLOADS = FULL - ? [ - { name: "arith-loop-100k", code: `function work(){var s=0;for(var i=0;i<100000;i++){s+=i*3-(i%7);}return s;}work();` }, - { name: "fib-26", code: `function fib(n){if(n<=1)return n;return fib(n-1)+fib(n-2);}fib(26);` }, - { name: "switch-dispatch-30k", code: `function work(){var s=0;for(var i=0;i<30000;i++){switch(i%6){case 0:s+=i;break;case 1:s-=i/2;break;case 2:s+=i*3;break;case 3:s-=i;break;case 4:s+=1;break;default:s+=i%10;break;}}return Math.round(s);}work();` }, - { name: "object-prop-15k", code: `function work(){var r=0;for(var i=0;i<15000;i++){var o={x:i,y:i*2,z:i*3};r+=o.x+o.y+o.z;}return r;}work();` }, - ] - : [ - { name: "arith-loop-40k", code: `function work(){var s=0;for(var i=0;i<40000;i++){s+=i*3-(i%7);}return s;}work();` }, - { name: "fib-23", code: `function fib(n){if(n<=1)return n;return fib(n-1)+fib(n-2);}fib(23);` }, - { name: "switch-dispatch-15k", code: `function work(){var s=0;for(var i=0;i<15000;i++){switch(i%6){case 0:s+=i;break;case 1:s-=i/2;break;case 2:s+=i*3;break;case 3:s-=i;break;case 4:s+=1;break;default:s+=i%10;break;}}return Math.round(s);}work();` }, - { name: "object-prop-8k", code: `function work(){var r=0;for(var i=0;i<8000;i++){var o={x:i,y:i*2,z:i*3};r+=o.x+o.y+o.z;}return r;}work();` }, - ]; - -const BOOTSTRAP_CODE = `function work(){return 0;}work();`; - -// --- Config matrix -------------------------------------------------------- - -const MAX = { preset: "max" }; -function maxMinus(...flags) { - const o = { preset: "max" }; - for (const f of flags) o[f] = false; - return o; -} - -const CONFIGS = [ - { label: "default", opts: {} }, - { label: "medium", opts: { preset: "medium" } }, - { label: "MAX (baseline)", opts: MAX }, - // single-feature removals - { label: "−stackEncoding", opts: maxMinus("stackEncoding") }, - { label: "−mixedBooleanArithmetic", opts: maxMinus("mixedBooleanArithmetic") }, - { label: "−observationResistance", opts: maxMinus("observationResistance") }, - { label: "−opcodeMutation", opts: maxMinus("opcodeMutation") }, - { label: "−incrementalCipher", opts: maxMinus("incrementalCipher") }, - { label: "−semanticOpacity", opts: maxMinus("semanticOpacity") }, - { label: "−vmShielding", opts: maxMinus("vmShielding") }, - { label: "−deadCodeInjection", opts: maxMinus("deadCodeInjection") }, - { label: "−debugProtection", opts: maxMinus("debugProtection") }, - { label: "−blockPermutation (sanity ~0)", opts: maxMinus("blockPermutation") }, - { label: "−integrityBinding", opts: maxMinus("integrityBinding") }, - // combination removals — the decode-cache story - { - label: "−cache-trio (mut+inc+obs) [CACHE ON]", - opts: maxMinus("opcodeMutation", "incrementalCipher", "observationResistance"), - }, - { - label: "−cache-trio −stackEncoding [CACHE ON]", - opts: maxMinus("opcodeMutation", "incrementalCipher", "observationResistance", "stackEncoding"), - }, - { - label: "−cache-trio −proxy −MBA [CACHE ON]", - opts: maxMinus("opcodeMutation", "incrementalCipher", "observationResistance", "stackEncoding", "mixedBooleanArithmetic"), - }, - { - label: "−cache-trio −proxy −MBA −semOpacity", - opts: maxMinus("opcodeMutation", "incrementalCipher", "observationResistance", "stackEncoding", "mixedBooleanArithmetic", "semanticOpacity"), - }, -]; - -// --- Measure one config --------------------------------------------------- - -function measure(opts) { - const bootCode = obfuscateCode(BOOTSTRAP_CODE, opts); - const bootMs = timeThunk(makeThunk(bootCode), Math.max(ITERS, 30), WARM); - - let sumExec = 0, sumNative = 0, allOk = true, worst = 0, worstName = ""; - const perWl = []; - for (const wl of WORKLOADS) { - const obf = obfuscateCode(wl.code, opts); - const nativeThunk = makeThunk(wl.code); - const vmThunk = makeThunk(obf); - const ok = JSON.stringify(nativeThunk()) === JSON.stringify(vmThunk()); - if (!ok) allOk = false; - const nativeMs = timeThunk(nativeThunk, ITERS, WARM); - const vmMs = timeThunk(vmThunk, ITERS, WARM); - const execMs = Math.max(vmMs - bootMs, 0.0001); - sumExec += execMs; - sumNative += nativeMs; - const ov = execMs / nativeMs; - perWl.push({ name: wl.name, ov }); - if (ov > worst) { worst = ov; worstName = wl.name; } - } - return { aggExec: sumExec / sumNative, allOk, worst, worstName, perWl, bootMs }; -} - -// --- Run ------------------------------------------------------------------ - -console.log(`attribution harness iters=${ITERS} warm=${WARM} full=${FULL}`); -console.log(`workloads: ${WORKLOADS.map((w) => w.name).join(", ")}\n`); - -const results = []; -for (const cfg of CONFIGS) { - process.stdout.write(` measuring ${cfg.label.padEnd(40)} ... `); - const r = measure(cfg.opts); - results.push({ label: cfg.label, ...r }); - console.log( - `agg ${r.aggExec.toFixed(1).padStart(7)}x worst ${r.worst - .toFixed(0) - .padStart(5)}x (${r.worstName}) ${r.allOk ? "ok" : "*** MISMATCH ***"}` - ); -} - -// --- Attribution table ---------------------------------------------------- - -const max = results.find((r) => r.label.startsWith("MAX")); -console.log(`\n=== Attribution vs MAX (${max.aggExec.toFixed(1)}x aggregate) ===`); -console.log(` ${"config".padEnd(42)} ${"agg".padStart(9)} ${"Δ vs max".padStart(10)} ${"% of max".padStart(9)}`); -for (const r of results) { - const delta = r.aggExec - max.aggExec; - const pct = (r.aggExec / max.aggExec) * 100; - const sign = delta > 0 ? "+" : ""; - console.log( - ` ${r.label.padEnd(42)} ${r.aggExec.toFixed(1).padStart(8)}x ${(sign + delta.toFixed(1)).padStart(9)}x ${pct.toFixed(0).padStart(8)}%` - ); -} - -console.log(`\n=== Per-feature cost (max − feature; more negative Δ = costlier feature) ===`); -const singles = results.filter((r) => r.label.startsWith("−") && !r.label.includes("cache-trio")); -singles.sort((a, b) => a.aggExec - b.aggExec); -for (const r of singles) { - const saved = max.aggExec - r.aggExec; - const savedPct = (saved / max.aggExec) * 100; - console.log( - ` ${r.label.padEnd(42)} saves ${saved.toFixed(1).padStart(7)}x (${savedPct.toFixed(0).padStart(3)}% of max overhead)` - ); -} diff --git a/packages/ruam/scripts/bench.mjs b/packages/ruam/scripts/bench.mjs index 1d3205f..ca216fb 100644 --- a/packages/ruam/scripts/bench.mjs +++ b/packages/ruam/scripts/bench.mjs @@ -1,212 +1,133 @@ +#!/usr/bin/env bun /** - * Comprehensive VM overhead benchmark harness. + * Product-shaped Isogloss build, size, and steady-state runtime benchmark. * - * Measures obfuscated-vs-native execution overhead across presets, and - * isolates one-time runtime bootstrap cost from steady-state execution so - * per-instruction dispatch/crypto improvements are visible. - * - * Usage: - * bun scripts/bench.mjs # full table across presets - * bun scripts/bench.mjs --preset medium # single preset - * bun scripts/bench.mjs --profile # heavy single workload for CPU profiling - * bun scripts/bench.mjs --quick # fewer iterations + * This reports engineering costs only. It is not a secrecy or hardness + * measurement: holographic-local remains complete under client instrumentation. */ -import { obfuscateCode } from "../src/index.ts"; -import vmMod from "node:vm"; +import { protectCode } from "../src/index.ts"; + +const QUICK = process.argv.includes("--quick"); +const ITERATIONS = QUICK ? 5_000 : 50_000; +const ROUNDS = QUICK ? 5 : 9; +const SOURCE = ` +/* ruam:isogloss */ +function guardedKernel(x, y, z) { + return (x * y) + (z * z) + (x * z) + (y * 2) + 17; +} +`; +const OPTIONS = { + targetMode: "comment", + regionDomains: { + guardedKernel: { + x: { type: "number", min: 1, max: 31 }, + y: { type: "number", min: 1, max: 31 }, + z: { type: "number", min: 1, max: 31 }, + }, + }, +}; -const { Script } = vmMod; +function materialize(code) { + return Function(`"use strict";${code};return guardedKernel;`)(); +} -// --- Timing --------------------------------------------------------------- +function expected(x, y, z) { + return (x * y) + (z * z) + (x * z) + (y * 2) + 17; +} -function median(times) { - times.sort((a, b) => a - b); - const trim = Math.floor(times.length * 0.1); - const mid = times.slice(trim, times.length - trim); - return mid.reduce((s, t) => s + t, 0) / mid.length; +function median(values) { + const sorted = [...values].sort((a, b) => a - b); + return sorted[Math.floor(sorted.length / 2)]; } -function timeThunk(thunk, iters, warm = 20) { - for (let i = 0; i < Math.min(iters, warm); i++) thunk(); - const times = []; - for (let i = 0; i < iters; i++) { - const t0 = performance.now(); - thunk(); - times.push(performance.now() - t0); +function measure(fn) { + let checksum = 0; + for (let i = 0; i < 1_000; i++) { + const x = (i % 31) + 1; + const y = ((i * 7) % 31) + 1; + const z = ((i * 13) % 31) + 1; + checksum += fn(x, y, z); } - return median(times); + const samples = []; + for (let round = 0; round < ROUNDS; round++) { + const started = performance.now(); + for (let i = 0; i < ITERATIONS; i++) { + const x = (i % 31) + 1; + const y = ((i * 7) % 31) + 1; + const z = ((i * 13) % 31) + 1; + checksum += fn(x, y, z); + } + samples.push(performance.now() - started); + } + return { milliseconds: median(samples), checksum }; } -function makeThunk(code) { - const script = new Script(code, { filename: "bench.js" }); - return () => script.runInThisContext(); +const buildStarted = performance.now(); +const build = protectCode(SOURCE, OPTIONS); +const buildMilliseconds = performance.now() - buildStarted; +const native = materialize(SOURCE); +const protectedKernel = materialize(build.code); + +for (let x = 1; x <= 31; x += 5) { + for (let y = 1; y <= 31; y += 7) { + for (let z = 1; z <= 31; z += 11) { + const want = expected(x, y, z); + const got = protectedKernel(x, y, z); + if (got !== want) { + throw new Error( + `differential mismatch at (${x},${y},${z}): ${got} !== ${want}` + ); + } + } + } } -// --- Workloads (heavier than the test suite so steady-state dominates) ---- +const nativeTiming = measure(native); +const protectedTiming = measure(protectedKernel); +const sourceBytes = new TextEncoder().encode(SOURCE).byteLength; +const outputBytes = new TextEncoder().encode(build.code).byteLength; -const WORKLOADS = [ - { - name: "arith-loop-200k", - code: `function work(){var s=0;for(var i=0;i<200000;i++){s+=i*3-(i%7);}return s;}work();`, - }, - { - name: "fib-28", - code: `function fib(n){if(n<=1)return n;return fib(n-1)+fib(n-2);}fib(28);`, +console.log("Ruam Isogloss local benchmark"); +console.table({ + build: { + value: buildMilliseconds.toFixed(3), + unit: "ms", }, - { - name: "nested-loops-300", - code: `function work(){var c=0;for(var i=0;i<300;i++){for(var j=0;j<300;j++){if((i+j)%3===0)c++;else if((i*j)%7===0)c+=2;else c--;}}return c;}work();`, + "source size": { + value: sourceBytes, + unit: "bytes", }, - { - name: "switch-dispatch-50k", - code: `function work(){var s=0;for(var i=0;i<50000;i++){switch(i%6){case 0:s+=i;break;case 1:s-=i/2;break;case 2:s+=i*3;break;case 3:s-=i;break;case 4:s+=1;break;default:s+=i%10;break;}}return Math.round(s);}work();`, + "protected size": { + value: outputBytes, + unit: "bytes", }, - { - name: "string-build-5k", - code: `function work(){var s="";for(var i=0;i<5000;i++){s+=String.fromCharCode(65+(i%26));}var p=[];for(var j=0;j worstExec) { - worstExec = r.ovExec; - worstName = r.name; - } - } - const aggExec = sumExec / sumNative; - console.log( - ` --> aggregate exec overhead: ${aggExec.toFixed( - 1 - )}x worst: ${worstExec.toFixed(1)}x (${worstName})` - ); - return { presetName: res.presetName, aggExec, worstExec }; -} - -// --- Profile mode --------------------------------------------------------- - -function profileMode() { - // Heavy single workload, run many times in-process. Launch node with - // --cpu-prof externally; here we just spin so the profiler captures exec. - const code = WORKLOADS[0].code; // arith-loop-200k - const obf = obfuscateCode(code, { preset: "medium" }); - const thunk = makeThunk(obf); - const N = Number(process.env.PROF_N || 400); - console.error(`[profile] running arith-loop-200k x${N} at medium...`); - const t0 = performance.now(); - for (let i = 0; i < N; i++) thunk(); - console.error(`[profile] done in ${(performance.now() - t0).toFixed(0)}ms`); -} - -// --- Main ----------------------------------------------------------------- - -const args = process.argv.slice(2); -if (args.includes("--profile")) { - profileMode(); -} else { - const quick = args.includes("--quick"); - const iters = quick ? 30 : 100; - const onePreset = args.includes("--preset") - ? args[args.indexOf("--preset") + 1] - : null; - const presets = onePreset - ? { [onePreset]: PRESET_CONFIGS[onePreset] } - : PRESET_CONFIGS; - - const summary = []; - for (const [name, opts] of Object.entries(presets)) { - const res = runPreset(name, opts, iters); - summary.push(printResult(res)); - } - console.log("\n=== SUMMARY (exec-isolated overhead) ==="); - for (const s of summary) { - console.log( - ` ${s.presetName.padEnd(8)} aggregate ${s.aggExec - .toFixed(1) - .padStart(6)}x worst ${s.worstExec.toFixed(1).padStart(6)}x` - ); - } -} +}); +console.log({ + engine: build.stats.engine, + profile: build.stats.profile, + protectedRegions: build.stats.protectedRegionCount, + realizations: build.stats.realizationCount, + clientCompleteness: build.stats.clientCompleteness, + hardnessLowerBound: build.stats.hardnessLowerBound, + checksumAgreement: + nativeTiming.checksum === protectedTiming.checksum, +}); diff --git a/packages/ruam/scripts/build-browser.mjs b/packages/ruam/scripts/build-browser.mjs index 2c3693b..f024a87 100644 --- a/packages/ruam/scripts/build-browser.mjs +++ b/packages/ruam/scripts/build-browser.mjs @@ -10,7 +10,6 @@ */ import { build } from "esbuild"; -import { copyFile } from "node:fs/promises"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; @@ -56,13 +55,3 @@ await build({ }); console.log(`✓ Browser worker bundle written to ${outFile}`); - -// Copy option manifest to web app public directory -const manifestSrc = join(root, "dist", "option-manifest.json"); -const manifestDest = join(root, "..", "..", "apps", "web", "public", "option-manifest.json"); -try { - await copyFile(manifestSrc, manifestDest); - console.log(`✓ Option manifest copied to ${manifestDest}`); -} catch { - console.warn("⚠ option-manifest.json not found — run generate-manifest first"); -} diff --git a/packages/ruam/scripts/collect-stats.mjs b/packages/ruam/scripts/collect-stats.mjs deleted file mode 100644 index cd8ed08..0000000 --- a/packages/ruam/scripts/collect-stats.mjs +++ /dev/null @@ -1,548 +0,0 @@ -#!/usr/bin/env node -/** - * Collects project statistics and writes stats.json. - * - * README badges use shields.io dynamic JSON badges that read directly - * from the committed stats.json on GitHub — no README modification needed. - * - * Usage: - * node scripts/collect-stats.mjs # Code metrics + cached test/bench results - * node scripts/collect-stats.mjs --test # Also run tests - * node scripts/collect-stats.mjs --bench # Also run performance/size benchmarks (requires build) - * node scripts/collect-stats.mjs --all # Everything - */ - -import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs"; -import { execSync } from "node:child_process"; -import { dirname, join, relative } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const PKG_ROOT = join(__dirname, ".."); -const REPO_ROOT = join(PKG_ROOT, "..", ".."); -const STATS_PATH = join(PKG_ROOT, "stats.json"); -const TEST_RESULTS_PATH = join(PKG_ROOT, "test-results.json"); - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -function walkFiles(dir, ext) { - const results = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name); - if (entry.isDirectory()) results.push(...walkFiles(full, ext)); - else if (entry.name.endsWith(ext)) results.push(full); - } - return results; -} - -function countLines(files) { - let total = 0; - for (const f of files) total += readFileSync(f, "utf8").split("\n").length; - return total; -} - -function fmtNum(n) { - return n.toLocaleString("en-US"); -} - -function fmtK(n) { - return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); -} - -function loadPrevStats() { - if (!existsSync(STATS_PATH)) return null; - try { - return JSON.parse(readFileSync(STATS_PATH, "utf8")); - } catch { - return null; - } -} - -// ─── Code Metrics ──────────────────────────────────────────────────────────── - -function collectOpcodeStats() { - const src = readFileSync(join(PKG_ROOT, "src/compiler/opcodes.ts"), "utf8"); - - // Enum members - const enumMatch = src.match(/export\s+enum\s+Op\s*\{([\s\S]*?)\n\}/); - if (!enumMatch) throw new Error("Could not find Op enum"); - const members = (enumMatch[1].match(/^\s+[A-Z][A-Z_0-9]+/gm) ?? []).map( - (m) => m.trim() - ); - const count = members.length; - - // Category headings (// N. Category Name) - const catNums = new Set(); - for (const m of src.matchAll(/\/\/\s*(\d+)\.\s+[A-Z]/g)) catNums.add(m[1]); - - // Superinstructions (REG_ prefixed fused opcodes) - const superinstructions = members.filter((m) => - m.startsWith("REG_") - ).length; - - // Compound / fast-path opcodes - const compounds = members.filter((m) => { - return ( - m.startsWith("POST_INC_") || - m.startsWith("POST_DEC_") || - m.startsWith("PRE_INC_") || - m.startsWith("PRE_DEC_") || - m.includes("_ASSIGN_") || - m === "INC_SCOPED" || - m === "DEC_SCOPED" || - m === "INC_SLOT" || - m === "DEC_SLOT" - ); - }).length; - - // Slot-based opcodes (Tier 4) - const slotOpcodes = members.filter((m) => m.endsWith("_SLOT")).length; - - return { - count, - categories: catNums.size, - superinstructions, - compounds, - slotOpcodes, - }; -} - -function collectSourceStats() { - const srcFiles = walkFiles(join(PKG_ROOT, "src"), ".ts"); - const srcLines = countLines(srcFiles); - - const testDir = join(PKG_ROOT, "test"); - const testFiles = existsSync(testDir) ? walkFiles(testDir, ".ts") : []; - const testLines = countLines(testFiles); - - // Runtime template files - const templatesDir = join(PKG_ROOT, "src/runtime/templates"); - const templateCount = existsSync(templatesDir) - ? walkFiles(templatesDir, ".ts").length - : 0; - - // Compiler visitor files - const visitorsDir = join(PKG_ROOT, "src/compiler/visitors"); - const visitorCount = existsSync(visitorsDir) - ? walkFiles(visitorsDir, ".ts").length - : 0; - - return { - files: srcFiles.length, - lines: srcLines, - testFiles: testFiles.length, - testLines, - totalFiles: srcFiles.length + testFiles.length, - totalLines: srcLines + testLines, - templateFiles: templateCount, - visitorFiles: visitorCount, - }; -} - -// ─── Test Results ──────────────────────────────────────────────────────────── - -function collectTestStats(runTests) { - if (runTests) { - console.log(" Running tests..."); - try { - // bun test writes results to stderr, so merge streams - const output = execSync("bun test 2>&1", { - cwd: PKG_ROOT, - stdio: ["ignore", "pipe", "pipe"], - timeout: 600_000, - shell: true, - }).toString(); - - // Parse bun:test summary output — the FINAL lines look like: - // 2204 pass - // 0 fail - // 4486 expect() calls - // Ran 2204 tests across 41 files. [6.72s] - // - // Must match the summary lines, not per-suite "99 passed" lines. - // Summary uses "pass"/"fail" (not "passed"/"failed") with leading whitespace. - const passMatch = output.match(/^\s+(\d+)\s+pass$/m); - const failMatch = output.match(/^\s+(\d+)\s+fail$/m); - const totalMatch = output.match(/Ran\s+(\d+)\s+tests\s+across\s+(\d+)\s+files/); - const timeMatch = output.match(/\[([0-9.]+)s\]/); - - if (passMatch && totalMatch) { - const passed = parseInt(passMatch[1], 10); - const failed = failMatch ? parseInt(failMatch[1], 10) : 0; - const total = parseInt(totalMatch[1], 10); - const suites = parseInt(totalMatch[2], 10); - const durationMs = timeMatch - ? Math.round(parseFloat(timeMatch[1]) * 1000) - : null; - - const result = { total, passed, failed, suites, durationMs }; - - // Write test-results.json for caching - writeFileSync( - TEST_RESULTS_PATH, - JSON.stringify(result, null, 2) + "\n" - ); - return result; - } - } catch (e) { - // bun test exits non-zero on failures; try to parse stderr - const stderr = e.stderr?.toString() ?? ""; - const passMatch = stderr.match(/(\d+)\s+pass/); - const failMatch = stderr.match(/(\d+)\s+fail/); - const totalMatch = stderr.match(/Ran\s+(\d+)\s+tests/); - - if (passMatch && totalMatch) { - const result = { - total: parseInt(totalMatch[1], 10), - passed: parseInt(passMatch[1], 10), - failed: failMatch ? parseInt(failMatch[1], 10) : 0, - suites: 0, - durationMs: null, - }; - writeFileSync( - TEST_RESULTS_PATH, - JSON.stringify(result, null, 2) + "\n" - ); - return result; - } - - console.error(" Tests failed — could not parse output."); - return null; - } - } - - // Fall back to cached results - if (!existsSync(TEST_RESULTS_PATH)) return null; - - try { - return JSON.parse(readFileSync(TEST_RESULTS_PATH, "utf8")); - } catch { - console.error(" Could not parse test-results.json"); - return null; - } -} - -// ─── Benchmarks ────────────────────────────────────────────────────────────── - -async function collectBenchmarks() { - const distIndex = join(PKG_ROOT, "dist/index.js"); - if (!existsSync(distIndex)) { - console.error( - " Build not found — run `npm run build` first for benchmarks." - ); - return null; - } - - const { obfuscateCode } = await import(distIndex); - - // ── Size analysis ────────────────────────────────────────────────────────── - const sampleCode = `function fibonacci(n) { - if (n <= 1) return n; - return fibonacci(n - 1) + fibonacci(n - 2); -} -fibonacci(10);`; - - const obfLow = obfuscateCode(sampleCode); - const obfMed = obfuscateCode(sampleCode, { preset: "medium" }); - const obfHigh = obfuscateCode(sampleCode, { preset: "high" }); - - const inputBytes = Buffer.byteLength(sampleCode, "utf8"); - const lowBytes = Buffer.byteLength(obfLow, "utf8"); - const medBytes = Buffer.byteLength(obfMed, "utf8"); - const highBytes = Buffer.byteLength(obfHigh, "utf8"); - - // ── Performance analysis ─────────────────────────────────────────────────── - const workloads = [ - { - name: "arithmetic loop (10k)", - code: `function work(){var s=0;for(var i=0;i<10000;i++)s+=i*3-(i%7);return s}work();`, - }, - { - name: "fibonacci (n=20)", - code: `function fib(n){if(n<=1)return n;return fib(n-1)+fib(n-2)}fib(20);`, - }, - { - name: "array ops", - code: `function work(){var a=[];for(var i=0;i<500;i++)a.push((i*17)%100);a.sort(function(x,y){return x-y});return a.map(function(x){return x*2+1}).reduce(function(s,x){return s+x},0)}work();`, - }, - { - name: "string ops", - code: `function work(){var s="";for(var i=0;i<500;i++)s+=String.fromCharCode(65+(i%26));var p=[];for(var j=0;j script.runInThisContext(); - for (let i = 0; i < 15; i++) run(); // warm up - const times = []; - for (let i = 0; i < iterations; i++) { - const s = performance.now(); - run(); - times.push(performance.now() - s); - } - times.sort((a, b) => a - b); - const trim = Math.floor(times.length * 0.1); - const mid = times.slice(trim, times.length - trim); - return mid.reduce((s, t) => s + t, 0) / mid.length; - } - - const results = []; - for (const w of workloads) { - const obf = obfuscateCode(w.code); - const nativeMs = bench(w.code); - const vmMs = bench(obf); - const multiplier = vmMs / nativeMs; - results.push({ name: w.name, nativeMs, vmMs, multiplier }); - console.log( - ` ${w.name}: ${multiplier.toFixed( - 1 - )}x (native: ${nativeMs.toFixed(3)}ms, VM: ${vmMs.toFixed(3)}ms)` - ); - } - - const totalNative = results.reduce((s, r) => s + r.nativeMs, 0); - const weightedAvg = results.reduce( - (s, r) => s + r.multiplier * (r.nativeMs / totalNative), - 0 - ); - const sorted = [...results].sort((a, b) => a.multiplier - b.multiplier); - const median = sorted[Math.floor(sorted.length / 2)].multiplier; - - return { - performance: { - weightedAvg: +weightedAvg.toFixed(1), - median: +median.toFixed(1), - fastest: { - name: sorted[0].name, - multiplier: +sorted[0].multiplier.toFixed(1), - }, - slowest: { - name: sorted[sorted.length - 1].name, - multiplier: +sorted[sorted.length - 1].multiplier.toFixed(1), - }, - workloadCount: workloads.length, - workloads: results.map((r) => ({ - name: r.name, - multiplier: +r.multiplier.toFixed(1), - nativeMs: +r.nativeMs.toFixed(3), - vmMs: +r.vmMs.toFixed(3), - })), - }, - size: { - sampleInputBytes: inputBytes, - low: { - bytes: lowBytes, - ratio: +(lowBytes / inputBytes).toFixed(1), - }, - medium: { - bytes: medBytes, - ratio: +(medBytes / inputBytes).toFixed(1), - }, - high: { - bytes: highBytes, - ratio: +(highBytes / inputBytes).toFixed(1), - }, - }, - }; -} - -// ─── Hero Snippet ──────────────────────────────────────────────────────────── - -async function generateHeroSnippet() { - const distIndex = join(PKG_ROOT, "dist/index.js"); - if (!existsSync(distIndex)) return null; - - const { obfuscateCode } = await import(distIndex); - - const sampleCode = `function fibonacci(n) { - if (n <= 1) return n; - let a = 0, b = 1; - for (let i = 2; i <= n; i++) { - [a, b] = [b, a + b]; - } - return b; -}`; - - const output = obfuscateCode(sampleCode); - const allLines = output.split("\n"); - const totalLines = allLines.length; - - // Skip "use strict" and blank lines at the top - const contentStart = allLines.findIndex( - (l) => l.trim() && l.trim() !== '"use strict";' - ); - - // First 5 interesting lines, truncate long ones - const MAX_LEN = 55; - const head = allLines.slice(contentStart, contentStart + 5).map((l) => - l.length > MAX_LEN ? l.slice(0, MAX_LEN - 3) + "..." : l - ); - - // Find the function replacement at the end - const fnIdx = allLines.findIndex((l) => /function\s+fibonacci/.test(l)); - const tail = fnIdx >= 0 - ? allLines.slice(fnIdx).filter((l) => l.trim()) - : allLines.slice(-3).filter((l) => l.trim()); - - return { head, totalLines, tail }; -} - -// ─── Main ──────────────────────────────────────────────────────────────────── - -const args = process.argv.slice(2); -const runTests = args.includes("--test") || args.includes("--all"); -const runBench = args.includes("--bench") || args.includes("--all"); - -console.log("Collecting stats...\n"); - -const prev = loadPrevStats(); - -// Static code metrics -const opcodes = collectOpcodeStats(); -const source = collectSourceStats(); -console.log( - ` Opcodes: ${opcodes.count} (${opcodes.categories} categories, ${opcodes.superinstructions} superinstructions, ${opcodes.compounds} compounds)` -); -console.log( - ` Source: ${source.files} files, ${fmtNum(source.lines)} lines` -); -console.log( - ` Tests code: ${source.testFiles} files, ${fmtNum( - source.testLines - )} lines` -); -console.log( - ` Total: ${source.totalFiles} files, ${fmtNum( - source.totalLines - )} lines` -); - -// Test results -let tests = collectTestStats(runTests); -if (!tests && prev?.tests) { - tests = prev.tests; - console.log( - ` Tests: ${fmtNum(tests.passed)}/${fmtNum( - tests.total - )} passing (cached)` - ); -} else if (tests) { - console.log( - ` Tests: ${fmtNum(tests.passed)}/${fmtNum(tests.total)} passing` - ); -} else { - console.log( - " Tests: no results (run with --test or `npm test` first)" - ); -} - -// Benchmarks — always run when build is available, fall back to cached only if no build -let perfData = null; -let sizeData = null; -if (runBench || existsSync(join(PKG_ROOT, "dist/index.js"))) { - if (runBench) console.log("\n Running benchmarks..."); - const bench = await collectBenchmarks(); - if (bench) { - perfData = bench.performance; - sizeData = bench.size; - console.log(` Weighted avg overhead: ${perfData.weightedAvg}x`); - console.log(` Median overhead: ${perfData.median}x`); - console.log( - ` Size (low preset): ${sizeData.low.ratio}x (${fmtNum( - sizeData.low.bytes - )} bytes)` - ); - console.log( - ` Size (high preset): ${sizeData.high.ratio}x (${fmtNum( - sizeData.high.bytes - )} bytes)` - ); - } -} else { - perfData = prev?.performance ?? null; - sizeData = prev?.size ?? null; - if (perfData) - console.log( - ` Performance: ${perfData.weightedAvg}x weighted avg (cached — no build)` - ); - if (sizeData) - console.log(` Size (low): ${sizeData.low.ratio}x ratio (cached — no build)`); -} - -// Hero snippet -let heroSnippet = null; -try { - heroSnippet = await generateHeroSnippet(); - if (heroSnippet) { - console.log(` Hero snippet: ${fmtNum(heroSnippet.totalLines)} lines`); - } -} catch (e) { - console.error(" Hero snippet: failed -", e.message); -} -if (!heroSnippet && prev?.heroSnippet) { - heroSnippet = prev.heroSnippet; - console.log( - ` Hero snippet: ${fmtNum(heroSnippet.totalLines)} lines (cached)` - ); -} - -// ─── Build stats.json ──────────────────────────────────────────────────────── - -const stats = { - version: JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")) - .version, - collectedAt: new Date().toISOString(), - - opcodes, - source, - tests, - performance: perfData, - size: sizeData, - heroSnippet, - - // Pre-formatted display values for shields.io dynamic JSON badges. - // Query with e.g. $.badges.tests — no suffix/formatting needed client-side. - badges: { - tests: tests ? fmtNum(tests.passed) : null, - testsPassing: tests ? `${fmtNum(tests.passed)} passing` : null, - opcodes: String(opcodes.count), - categories: String(opcodes.categories), - loc: fmtK(source.lines), - totalLoc: fmtK(source.totalLines), - overhead: perfData ? `~${perfData.weightedAvg}x` : null, - overheadMedian: perfData ? `~${perfData.median}x` : null, - sizeRatioLow: sizeData ? `${sizeData.low.ratio}x` : null, - sizeRatioHigh: sizeData ? `${sizeData.high.ratio}x` : null, - }, -}; - -writeFileSync(STATS_PATH, JSON.stringify(stats, null, 2) + "\n"); -console.log(`\nWrote ${relative(REPO_ROOT, STATS_PATH)}`); diff --git a/packages/ruam/scripts/generate-manifest.mjs b/packages/ruam/scripts/generate-manifest.mjs deleted file mode 100644 index f7ce0a7..0000000 --- a/packages/ruam/scripts/generate-manifest.mjs +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Build-time manifest generator. - * - * Reads option metadata and preset definitions from the built dist/, - * produces a JSON manifest that the website consumes. Run after tsup - * builds the core library. - * - * Output: dist/option-manifest.json - * (also copied to apps/web/public/ by build:browser script) - */ - -import { readFile, writeFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const distDir = join(__dirname, "..", "dist"); - -async function main() { - // Dynamic import from the built dist - const optionMeta = await import(join(distDir, "index.js")); - const presets = await import(join(distDir, "index.js")); - - // Extract what we need - const { OPTION_META, AUTO_ENABLE_RULES } = optionMeta; - const { PRESETS } = presets; - - if (!OPTION_META || !PRESETS) { - console.error( - "ERROR: Could not find OPTION_META or PRESETS in dist/index.js" - ); - console.error( - "Make sure they are exported from the main entry point." - ); - process.exit(1); - } - - // Build the manifest - const manifest = { - options: OPTION_META.map((m) => ({ - key: m.key, - label: m.label, - category: m.category, - description: m.description, - cliFlag: m.cliFlag, - })), - presets: {}, - autoEnableRules: AUTO_ENABLE_RULES.map((r) => ({ - when: r.when, - enables: r.enables, - })), - }; - - // Extract boolean option values from presets (skip non-boolean fields) - const booleanKeys = new Set(OPTION_META.map((m) => m.key)); - for (const presetName of ["low", "medium", "max"]) { - const preset = PRESETS[presetName]; - if (!preset) continue; - const filtered = {}; - for (const [key, value] of Object.entries(preset)) { - if (booleanKeys.has(key)) { - filtered[key] = value; - } - } - manifest.presets[presetName] = filtered; - } - - const outputPath = join(distDir, "option-manifest.json"); - await writeFile(outputPath, JSON.stringify(manifest, null, 2) + "\n"); - console.log(` Generated ${outputPath}`); -} - -main().catch((err) => { - console.error("Manifest generation failed:", err); - process.exit(1); -}); diff --git a/packages/ruam/src/browser-entry.ts b/packages/ruam/src/browser-entry.ts index 51fdb97..b9a9bdd 100644 --- a/packages/ruam/src/browser-entry.ts +++ b/packages/ruam/src/browser-entry.ts @@ -1,16 +1,18 @@ /** - * Browser entry point for Ruam. - * - * Re-exports {@link obfuscateCode} with Node.js `crypto` polyfilled - * for the Web Crypto API. Used by the playground Web Worker. + * Browser entry point for the Ruam Isogloss source transform. * * @module browser-entry */ -export { obfuscateCode } from "./transform.js"; -export { PRESETS } from "./presets.js"; +export { obfuscateCode, protectCode } from "./transform.js"; +export { + resolveRuamOptions, + type IsoglossOptions, + type IsoglossRegionDomain, + type IsoglossRegionDomains, + type RuamOptions, +} from "./isogloss/options.js"; export type { - VmObfuscationOptions, - PresetName, - TargetEnvironment, -} from "./types.js"; + IsoglossSourceBuildResult, + IsoglossSourceBuildStats, +} from "./isogloss/source-transform.js"; diff --git a/packages/ruam/src/browser-worker.ts b/packages/ruam/src/browser-worker.ts index fc5be77..14a17eb 100644 --- a/packages/ruam/src/browser-worker.ts +++ b/packages/ruam/src/browser-worker.ts @@ -1,29 +1,35 @@ /** * Web Worker entry point for the Ruam playground. * - * Receives `{ code, options }` messages, runs obfuscation, and posts - * back `{ result }` or `{ error }` responses. Posts a `{ ready: true }` + * Receives `{ code, options }` messages, runs Isogloss protection, and posts + * back `{ result, stats, diagnostics }` or `{ error }` responses. Posts a `{ ready: true }` * message on load so the main thread knows the module is initialized. * * @module browser-worker */ -import { obfuscateCode } from "./transform.js"; -import type { VmObfuscationOptions } from "./types.js"; +import { protectCode } from "./transform.js"; +import type { RuamOptions } from "./isogloss/options.js"; interface WorkerRequest { id: number; code: string; - options?: VmObfuscationOptions; + options?: RuamOptions; } self.onmessage = (e: MessageEvent) => { const { id, code, options } = e.data; const start = performance.now(); try { - const result = obfuscateCode(code, options); + const build = protectCode(code, options); const elapsed = Math.round(performance.now() - start); - (self as unknown as Worker).postMessage({ id, result, elapsed }); + (self as unknown as Worker).postMessage({ + id, + result: build.code, + stats: build.stats, + diagnostics: build.diagnostics, + elapsed, + }); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); (self as unknown as Worker).postMessage({ id, error: message }); diff --git a/packages/ruam/src/cli.ts b/packages/ruam/src/cli.ts index e4665bb..ac9bdd0 100644 --- a/packages/ruam/src/cli.ts +++ b/packages/ruam/src/cli.ts @@ -1,32 +1,29 @@ #!/usr/bin/env node /** - * CLI entry point for the `ruam` command. - * - * Features: - * - Interactive wizard mode when no arguments provided - * - Animated color-cycling ASCII art header - * - Progress bar with per-file status for directory obfuscation - * - Spinner with phase updates for single-file obfuscation - * - Colored, sectioned help output + * CLI entry point for Ruam's Isogloss source-protection compiler. * * @module cli */ -import { obfuscateFile } from "./index.js"; -import type { - VmObfuscationOptions, - PresetName, - TargetEnvironment, -} from "./types.js"; import fs from "fs-extra"; import path from "path"; import chalk from "chalk"; import ora from "ora"; +import { protectCode } from "./index.js"; +import { + resolveRuamOptions, + type IsoglossDeploymentProfile, + type IsoglossRegionDomains, + type IsoglossTargetEnvironment, + type IsoglossTargetMode, + type ResolvedRuamOptions, + type RuamOptions, +} from "./isogloss/options.js"; + +type ProtectionResult = ReturnType; +type OwnerSidecar = NonNullable; -// --- Constants --- - -/** Raw ASCII art lines for the RUAM logo. */ const LOGO_LINES = [ `:::::::.. ... ::: :::. . :`, `;;;;\`\`;;;; ;; ;;; ;;\`;; ;;,. ;;;`, @@ -36,7 +33,6 @@ const LOGO_LINES = [ ` MMMM "W" "YmmMMMM"" YMM ""\` MMM M' "MMM`, ]; -/** Color palette for the cycling logo animation (HSL hue rotation). */ const PALETTE = [ "#ff6b6b", "#ff8e53", @@ -52,118 +48,148 @@ const PALETTE = [ "#fdcb6e", ]; -/** Human-readable labels for boolean obfuscation options. */ -import { OPTION_LABELS } from "./option-meta.js"; +const TAGLINE = "Isogloss JavaScript Source Protection"; -// --- CLI Argument Types --- +/** + * Former execution-engine flags remain recognizable only so the CLI can fail + * with a migration diagnostic. None of them maps to an active option. + */ +const REMOVED_EXECUTION_FLAGS = new Set([ + "--preset", + "-e", + "--encrypt", + "-d", + "--debug-protection", + "--no-debug-protection", + "--debug-logging", + "--dynamic-opcodes", + "--decoy-opcodes", + "--dead-code", + "--stack-encoding", + "--rolling-cipher", + "--integrity-binding", + "--vm-shielding", + "--mba", + "--handler-fragmentation", + "--string-atomization", + "--polymorphic-decoder", + "--scattered-keys", + "--block-permutation", + "--opcode-mutation", + "--bytecode-scattering", + "--incremental-cipher", + "--semantic-opacity", + "--observation-resistance", +]); -/** Parsed CLI arguments. */ interface CliArgs { input?: string; output?: string; - options: VmObfuscationOptions; include: string[]; exclude: string[]; help: boolean; version: boolean; interactive: boolean; + profile?: IsoglossDeploymentProfile; + minimumExactAttackQueries?: string; + ownerTracePath?: string; + custodianEndpoint?: string; + privateImplementation?: string; + attestationProvider?: string; + attestationMeasurement?: string; + targetMode?: IsoglossTargetMode; + threshold?: number; + preprocessIdentifiers?: boolean; + target?: IsoglossTargetEnvironment; + regionDomainsPath?: string; } -// --- Animated Logo --- +interface MaterializedCliOptions { + readonly input: RuamOptions; + readonly resolved: ResolvedRuamOptions; +} -/** - * Renders the logo with a color gradient offset. - * Each line gets a color from the palette, shifted by `offset`. - * - * @param offset - Palette rotation offset for animation frames. - * @returns ANSI-colored logo string. - */ -function renderLogo(offset: number): string { - const lines: string[] = []; - for (let i = 0; i < LOGO_LINES.length; i++) { - const colorIdx = (i + offset) % PALETTE.length; - lines.push(" " + chalk.hex(PALETTE[colorIdx]!)(LOGO_LINES[i]!)); +class CliUsageError extends Error { + override readonly name = "CliUsageError"; + + constructor( + readonly code: + | "RUAM_CLI_MISSING_VALUE" + | "RUAM_CLI_UNKNOWN_OPTION" + | "RUAM_REMOVED_CLI_OPTION" + | "RUAM_CLI_INVALID_REGION_DOMAINS_FILE" + | "RUAM_CLI_PROFILE_REQUIRES_PRODUCT_PLANNER", + detail: string + ) { + super(`${code}: ${detail}`); } - return lines.join("\n"); } -/** - * Animated logo controller. Runs the color-cycling animation on a - * setInterval so the main thread stays free for heavy compilation work. - * Call `stop()` to freeze the logo in place. - */ +function defaultCliArgs(): CliArgs { + return { + include: ["**/*.js"], + exclude: ["**/node_modules/**"], + help: false, + version: false, + interactive: false, + }; +} + +function renderLogo(offset: number): string { + return LOGO_LINES.map((line, index) => + chalk.hex(PALETTE[(index + offset) % PALETTE.length]!)(" " + line) + ).join("\n"); +} + class LogoAnimation { private offset = 0; private timer: ReturnType | null = null; - private lineCount = LOGO_LINES.length + 2; // logo lines + tagline + blank + private readonly lineCount = LOGO_LINES.length + 2; - /** Start the cycling animation (80ms per frame). */ start(version: string): void { if (!process.stdout.isTTY) { - // Non-TTY: print static logo once this.printStatic(version); return; } this.printFrame(version); this.timer = setInterval(() => { this.offset++; - // Move cursor up and reprint process.stdout.write(`\x1b[${this.lineCount}A`); this.printFrame(version); }, 120); } - /** Stop the animation and leave the last frame visible. */ stop(): void { - if (this.timer) { + if (this.timer !== null) { clearInterval(this.timer); this.timer = null; } } private printFrame(version: string): void { - const logo = renderLogo(this.offset); - const tagline = - " " + - chalk.dim(`v${version}`) + - chalk.dim(" \u2014 ") + - chalk.dim.italic("Virtualization-Based JavaScript Obfuscation"); - process.stdout.write(logo + "\n" + tagline + "\n\n"); + process.stdout.write( + `${renderLogo(this.offset)}\n ${chalk.dim( + `v${version} \u2014 ${TAGLINE}` + )}\n\n` + ); } private printStatic(version: string): void { - const logo = renderLogo(0); - const tagline = - " " + - chalk.dim(`v${version}`) + - chalk.dim(" \u2014 ") + - chalk.dim.italic("Virtualization-Based JavaScript Obfuscation"); - console.log(logo); - console.log(tagline); + console.log(renderLogo(0)); + console.log(` ${chalk.dim(`v${version} \u2014 ${TAGLINE}`)}`); console.log(); } } -// --- Utilities --- - -/** - * Render an inline progress bar. - * - * @param current - Items completed. - * @param total - Total items. - * @param width - Character width of the bar. - * @returns Colored progress bar string. - */ function renderBar(current: number, total: number, width = 28): string { const ratio = total > 0 ? current / total : 0; const filled = Math.round(ratio * width); - const empty = width - filled; const pct = Math.round(ratio * 100) .toString() .padStart(3); return ( chalk.cyan("\u2588".repeat(filled)) + - chalk.dim("\u2591".repeat(empty)) + + chalk.dim("\u2591".repeat(width - filled)) + " " + chalk.dim(`${pct}%`) + " " + @@ -171,31 +197,17 @@ function renderBar(current: number, total: number, width = 28): string { ); } -/** Format a byte count to a human-readable string. */ function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } -/** Format a millisecond duration to a human-readable string. */ function formatTime(ms: number): string { if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; } -/** Return the list of active protection layer labels from options. */ -function getActiveLabels(options: VmObfuscationOptions): string[] { - const active: string[] = []; - for (const [key, label] of Object.entries(OPTION_LABELS)) { - if (options[key as keyof VmObfuscationOptions]) { - active.push(label); - } - } - return active; -} - -/** Read the package version from package.json. */ async function getVersion(): Promise { try { const raw = await fs.readFile( @@ -208,40 +220,31 @@ async function getVersion(): Promise { } } -// --- Argument Parser --- - -/** - * Parse raw CLI arguments into a structured {@link CliArgs} object. - * - * @param argv - Arguments from `process.argv.slice(2)`. - * @returns Parsed CLI arguments. - */ function parseArgs(argv: string[]): CliArgs { - const result: CliArgs = { - input: undefined, - output: undefined, - options: {}, - include: ["**/*.js"], - exclude: ["**/node_modules/**"], - help: false, - version: false, - interactive: false, + const result = defaultCliArgs(); + + let index = 0; + const nextArg = (flag: string): string => { + index++; + if (index >= argv.length) { + throw new CliUsageError( + "RUAM_CLI_MISSING_VALUE", + `missing value for ${flag}` + ); + } + return argv[index]!; }; - let i = 0; - - /** Consume the next argument or exit with an error. */ - function nextArg(flag: string): string { - if (++i >= argv.length) { - console.error(chalk.red(` Missing value for ${flag}`)); - process.exit(1); + while (index < argv.length) { + const argument = argv[index]!; + if (REMOVED_EXECUTION_FLAGS.has(argument)) { + throw new CliUsageError( + "RUAM_REMOVED_CLI_OPTION", + `${argument} was removed with the former execution architecture; use --profile and the explicit Isogloss capability flags` + ); } - return argv[i]!; - } - while (i < argv.length) { - const arg = argv[i]!; - switch (arg) { + switch (argument) { case "-h": case "--help": result.help = true; @@ -256,735 +259,594 @@ function parseArgs(argv: string[]): CliArgs { break; case "-o": case "--output": - result.output = nextArg(arg); - break; - case "-m": - case "--mode": - result.options.targetMode = nextArg(arg) as "root" | "comment"; - break; - case "--preset": - result.options.preset = nextArg(arg) as PresetName; - break; - case "-e": - case "--encrypt": - result.options.encryptBytecode = true; - break; - case "-p": - case "--preprocess": - result.options.preprocessIdentifiers = true; - break; - case "-d": - case "--debug-protection": - result.options.debugProtection = true; - break; - case "--no-debug-protection": - result.options.debugProtection = false; - break; - case "--debug-logging": - result.options.debugLogging = true; - break; - case "--dynamic-opcodes": - result.options.dynamicOpcodes = true; - break; - case "--decoy-opcodes": - result.options.decoyOpcodes = true; - break; - case "--dead-code": - result.options.deadCodeInjection = true; - break; - case "--stack-encoding": - result.options.stackEncoding = true; - break; - case "--rolling-cipher": - result.options.rollingCipher = true; + result.output = nextArg(argument); break; - case "--integrity-binding": - result.options.integrityBinding = true; + case "--profile": + result.profile = nextArg( + argument + ) as IsoglossDeploymentProfile; break; - case "--vm-shielding": - result.options.vmShielding = true; + case "--minimum-exact-attack-queries": + result.minimumExactAttackQueries = nextArg(argument); break; - case "--mba": - result.options.mixedBooleanArithmetic = true; + case "--owner-trace": + result.ownerTracePath = nextArg(argument); break; - case "--handler-fragmentation": - result.options.handlerFragmentation = true; + case "--custodian-endpoint": + result.custodianEndpoint = nextArg(argument); break; - case "--string-atomization": - result.options.stringAtomization = true; + case "--private-implementation": + result.privateImplementation = nextArg(argument); break; - case "--polymorphic-decoder": - result.options.polymorphicDecoder = true; + case "--attestation-provider": + result.attestationProvider = nextArg(argument); break; - case "--scattered-keys": - result.options.scatteredKeys = true; + case "--attestation-measurement": + result.attestationMeasurement = nextArg(argument); break; - case "--block-permutation": - result.options.blockPermutation = true; + case "-m": + case "--mode": + result.targetMode = nextArg(argument) as IsoglossTargetMode; break; - case "--opcode-mutation": - result.options.opcodeMutation = true; + case "--threshold": + result.threshold = Number(nextArg(argument)); break; - case "--bytecode-scattering": - result.options.bytecodeScattering = true; + case "-p": + case "--preprocess": + result.preprocessIdentifiers = true; break; case "--target": - result.options.target = nextArg(arg) as TargetEnvironment; + result.target = nextArg( + argument + ) as IsoglossTargetEnvironment; + break; + case "--region-domains": + result.regionDomainsPath = nextArg(argument); break; case "--include": - result.include = [nextArg(arg)]; + result.include = [nextArg(argument)]; break; case "--exclude": - result.exclude = [nextArg(arg)]; + result.exclude = [nextArg(argument)]; break; default: - if (arg.startsWith("-")) { - console.error(chalk.red(` Unknown option: ${arg}`)); - console.error( - chalk.dim(" Run ruam --help for usage information") + if (argument.startsWith("-")) { + throw new CliUsageError( + "RUAM_CLI_UNKNOWN_OPTION", + `unknown option ${argument}; run ruam --help` ); - process.exit(1); } - result.input = arg; - break; + if (result.input !== undefined) { + throw new CliUsageError( + "RUAM_CLI_UNKNOWN_OPTION", + `unexpected positional argument ${argument}` + ); + } + result.input = argument; } - i++; + index++; } return result; } -// --- Help --- +async function materializeOptions( + args: CliArgs +): Promise { + const requestedProfile = args.profile ?? "holographic-local"; + if ( + args.profile !== undefined && + ![ + "holographic-local", + "holographic-custodied", + "holographic-private", + "holographic-tee", + ].includes(args.profile) + ) { + resolveRuamOptions({ isogloss: { profile: args.profile } }); + } + if ( + requestedProfile !== "holographic-local" || + args.minimumExactAttackQueries !== undefined || + args.custodianEndpoint !== undefined || + args.privateImplementation !== undefined || + args.attestationProvider !== undefined || + args.attestationMeasurement !== undefined + ) { + throw new CliUsageError( + "RUAM_CLI_PROFILE_REQUIRES_PRODUCT_PLANNER", + `${requestedProfile} custody and attestation configuration must use planIsoglossProduct() with an owner-proven execution boundary; the source CLI supports holographic-local only` + ); + } + + let regionDomains: IsoglossRegionDomains | undefined; + if (args.regionDomainsPath !== undefined) { + const filePath = path.resolve(args.regionDomainsPath); + try { + regionDomains = JSON.parse( + await fs.readFile(filePath, "utf-8") + ) as IsoglossRegionDomains; + } catch (error) { + throw new CliUsageError( + "RUAM_CLI_INVALID_REGION_DOMAINS_FILE", + `${args.regionDomainsPath}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + const isogloss = { + ...(args.profile === undefined ? {} : { profile: args.profile }), + ...(args.ownerTracePath === undefined + ? {} + : { ownerTrace: "sidecar" as const }), + }; + const input: RuamOptions = { + ...(Object.keys(isogloss).length === 0 ? {} : { isogloss }), + ...(args.targetMode === undefined + ? {} + : { targetMode: args.targetMode }), + ...(args.threshold === undefined + ? {} + : { threshold: args.threshold }), + ...(args.preprocessIdentifiers === undefined + ? {} + : { preprocessIdentifiers: args.preprocessIdentifiers }), + ...(args.target === undefined ? {} : { target: args.target }), + ...(regionDomains === undefined ? {} : { regionDomains }), + }; + + return Object.freeze({ + input, + resolved: resolveRuamOptions(input), + }); +} -/** Print the colored help text with the animated header frozen after one frame. */ function printHelp(version: string): void { console.log(); console.log(renderLogo(0)); - console.log( - " " + - chalk.dim(`v${version}`) + - chalk.dim(" \u2014 ") + - chalk.dim.italic("Virtualization-Based JavaScript Obfuscation") - ); + console.log(` ${chalk.dim(`v${version} \u2014 ${TAGLINE}`)}`); console.log(); - const h = chalk.bold.white; - const f = chalk.cyan; - const d = chalk.dim; - const a = chalk.yellow; + const heading = chalk.bold.white; + const flag = chalk.cyan; + const argument = chalk.yellow; + const detail = chalk.dim; - console.log(h(" USAGE")); + console.log(heading(" USAGE")); console.log( - ` ${f("ruam")} ${a( + ` ${flag("ruam")} ${argument( "" - )} Obfuscate a file or directory` + )} Protect a file or directory` ); console.log( - ` ${f("ruam")} ${a("")} -o ${a( + ` ${flag("ruam")} ${argument("")} -o ${argument( "" - )} Obfuscate to a specific output path` + )} Write protected source elsewhere` ); console.log( - ` ${f("ruam")} Launch interactive wizard` + ` ${flag("ruam")} Launch the interactive wizard` ); console.log(); - console.log(h(" PRESETS")); + console.log(heading(" ISOGLOSS")); console.log( - ` ${f("--preset")} ${a("")} ${d( - "low, medium, or max" + ` ${flag("--profile")} ${argument( + "" + )} Source profile ${detail( + "(holographic-local only; default)" )}` ); - console.log(` ${chalk.green("low")} VM compilation only`); console.log( - ` ${chalk.yellow( - "medium" - )} + renaming, encryption, rolling cipher, decoy/dynamic opcodes` + ` ${flag("--region-domains")} ${argument( + "" + )} Function/binding runtime-guard domains` ); - console.log(` ${chalk.red("max")} All protections enabled`); - console.log(); - - console.log(h(" OUTPUT")); console.log( - ` ${f("-o, --output")} ${a( + ` ${flag("--owner-trace")} ${argument( "" - )} Output file or directory ${d("(default: overwrite)")}` + )} Write the owner-only sidecar` ); console.log(); - console.log(h(" COMPILATION")); console.log( - ` ${f("-m, --mode")} ${a( - "" - )} Target mode: "root" or "comment"` - ); - console.log( - ` ${f("-e, --encrypt")} Enable bytecode encryption` - ); - console.log( - ` ${f("-p, --preprocess")} Preprocess/rename identifiers` + ` ${detail( + "Custodied, private-function, and TEE builds require planIsoglossProduct()" + )}` ); console.log(); - console.log(h(" SECURITY")); + console.log(heading(" SELECTION")); console.log( - ` ${f("-d, --debug-protection")} Anti-debugger timing loop` + ` ${flag("-m, --mode")} ${argument( + "" + )} Select roots or /* ruam:isogloss */ markers` ); console.log( - ` ${f( - "--no-debug-protection" - )} Disable anti-debugger (overrides preset)` + ` ${flag("--threshold")} ${argument( + "<0..1>" + )} Eligible-target selection probability` ); console.log( - ` ${f( - "--rolling-cipher" - )} Position-dependent instruction encryption` - ); - console.log( - ` ${f( - "--integrity-binding" - )} Bind decryption to interpreter integrity` + ` ${flag("-p, --preprocess")} Rename identifiers after protection` ); console.log(); - console.log(h(" HARDENING")); - console.log( - ` ${f("--dynamic-opcodes")} Filter unused opcode handlers` - ); - console.log( - ` ${f("--decoy-opcodes")} Inject fake opcode handlers` - ); - console.log( - ` ${f("--dead-code")} Inject dead bytecode sequences` - ); - console.log( - ` ${f("--stack-encoding")} Encrypt values on the VM stack` - ); - console.log( - ` ${f("--vm-shielding")} Per-function micro-interpreters` - ); - console.log( - ` ${f( - "--mba" - )} Mixed boolean arithmetic obfuscation` - ); - console.log( - ` ${f( - "--handler-fragmentation" - )} Split handlers into interleaved fragments` - ); - console.log( - ` ${f( - "--string-atomization" - )} Encode interpreter strings as table lookups` - ); - console.log( - ` ${f( - "--polymorphic-decoder" - )} Per-build randomized string decoder` - ); - console.log( - ` ${f( - "--scattered-keys" - )} Scatter key material across closure scopes` - ); - console.log( - ` ${f("--block-permutation")} Shuffle bytecode basic block order` - ); - console.log( - ` ${f("--opcode-mutation")} Runtime handler table mutations` - ); + console.log(heading(" FILES AND TARGET")); console.log( - ` ${f( - "--bytecode-scattering" - )} Scatter bytecode into mixed-type fragments` + ` ${flag("-o, --output")} ${argument("")} Output file or directory` ); - console.log(); - - console.log(h(" FILES")); console.log( - ` ${f("--include")} ${a( + ` ${flag("--include")} ${argument( "" - )} File glob for directories ${d('(default: "**/*.js")')}` + )} Directory include ${detail('(default: "**/*.js")')}` ); console.log( - ` ${f("--exclude")} ${a("")} Exclude glob ${d( + ` ${flag("--exclude")} ${argument( + "" + )} Directory exclude ${detail( '(default: "**/node_modules/**")' )}` ); - console.log(); - - console.log(h(" ENVIRONMENT")); console.log( - ` ${f("--target")} ${a("")} Target environment` - ); - console.log( - ` ${chalk.cyan("node")} Node.js (CJS / ESM)` - ); - console.log( - ` ${chalk.cyan("browser")} Plain browser scripts ${d( - "(default)" - )}` - ); - console.log( - ` ${chalk.cyan("browser-extension")} Chrome extension MAIN world` + ` ${flag("--target")} ${argument( + "" + )} node, browser, or browser-extension` ); console.log(); - console.log(h(" OTHER")); + console.log(heading(" OTHER")); console.log( - ` ${f("--debug-logging")} Inject VM trace logging` + ` ${flag("-I, --interactive")} Force interactive wizard mode` ); - console.log( - ` ${f("-I, --interactive")} Force interactive wizard mode` - ); - console.log(` ${f("-h, --help")} Show this help`); - console.log(` ${f("-v, --version")} Show version`); + console.log(` ${flag("-h, --help")} Show help`); + console.log(` ${flag("-v, --version")} Show version`); console.log(); - console.log(h(" EXAMPLES")); + console.log(heading(" EXAMPLES")); console.log( - ` ${d("$")} ${f("ruam")} app.js ${d( - "# Obfuscate in-place" - )}` + ` ${detail("$")} ${flag( + "ruam" + )} app.js --region-domains domains.json` ); console.log( - ` ${d("$")} ${f("ruam")} app.js -o app.obf.js ${d( - "# Obfuscate to new file" - )}` + ` ${detail("$")} ${flag( + "ruam" + )} app.js -m comment --owner-trace owner.json` ); + console.log(); +} + +function printConfig(options: ResolvedRuamOptions): void { console.log( - ` ${d("$")} ${f("ruam")} dist/ --preset medium ${d( - "# Directory with preset" - )}` + ` ${chalk.dim("Profile:")} ${chalk.cyan(options.isogloss.profile)}` ); console.log( - ` ${d("$")} ${f("ruam")} src/bg.js -m comment -e ${d( - "# Selective + encryption" - )}` + ` ${chalk.dim("Mode:")} ${chalk.white(options.targetMode)}${ + options.targetMode === "comment" + ? chalk.dim(" (/* ruam:isogloss */)") + : "" + }` ); console.log( - ` ${d("$")} ${f("ruam")} ${d( - "# Interactive wizard" - )}` + ` ${chalk.dim("Domains:")} ${chalk.white( + Object.keys(options.regionDomains).length + )} configured function${ + Object.keys(options.regionDomains).length === 1 ? "" : "s" + }` ); - console.log(); -} - -// --- Config Summary --- - -/** Print a summary of the resolved configuration. */ -function printConfig(options: VmObfuscationOptions): void { - const active = getActiveLabels(options); - if (options.preset) { - const presetColor = - options.preset === "max" - ? chalk.red - : options.preset === "medium" - ? chalk.yellow - : chalk.green; - process.stdout.write( - ` ${chalk.dim("Preset:")} ${presetColor(options.preset)}` - ); - if (active.length > 0) { - process.stdout.write( - chalk.dim(" + ") + - active.map((l) => chalk.cyan(l)).join(chalk.dim(", ")) - ); - } - process.stdout.write("\n"); - } else if (active.length > 0) { - console.log( - ` ${chalk.dim("Layers:")} ${active - .map((l) => chalk.cyan(l)) - .join(chalk.dim(", "))}` - ); + if (options.isogloss.ownerTrace === "sidecar") { + console.log(` ${chalk.dim("Trace:")} ${chalk.cyan("owner sidecar")}`); } - - if (options.targetMode === "comment") { + if (options.preprocessIdentifiers) { console.log( - ` ${chalk.dim("Mode:")} ${chalk.white("comment")} ${chalk.dim( - "(only /* ruam:vm */ functions)" + ` ${chalk.dim("Preprocess:")} ${chalk.cyan( + "identifier renaming" )}` ); } } -// --- Interactive Wizard --- - -/** - * Launch the interactive configuration wizard. - * Prompts the user for input path, output, preset, options, and target mode, - * then runs the obfuscation with progress. - * - * @param version - Package version string. - */ async function runInteractive(version: string): Promise { const logo = new LogoAnimation(); logo.start(version); - - // Small delay to let the user see the animation before prompts appear - await new Promise((r) => setTimeout(r, 600)); + await new Promise((resolve) => setTimeout(resolve, 600)); logo.stop(); const { input: promptInput, select, - checkbox, confirm, } = await import("@inquirer/prompts"); - // --- Input path --- const inputRaw = await promptInput({ message: chalk.bold("Input path") + chalk.dim(" (file or directory)"), - validate: async (val: string) => { - if (!val.trim()) return "Please enter a path"; - if (!(await fs.pathExists(path.resolve(val.trim())))) - return `Path does not exist: ${val}`; + validate: async (value: string) => { + if (!value.trim()) return "Please enter a path"; + if (!(await fs.pathExists(path.resolve(value.trim())))) { + return `Path does not exist: ${value}`; + } return true; }, }); - const resolvedInput = path.resolve(inputRaw.trim()); - const stat = await fs.stat(resolvedInput); - const isDir = stat.isDirectory(); - - // --- Output path --- + const isDirectory = (await fs.stat(resolvedInput)).isDirectory(); const outputRaw = await promptInput({ message: chalk.bold("Output path") + - chalk.dim(` (enter to overwrite${isDir ? " directory" : ""})`), + chalk.dim( + ` (enter to overwrite${isDirectory ? " directory" : ""})` + ), default: "", }); - - // --- Preset --- - const preset = await select({ - message: chalk.bold("Protection preset"), - choices: [ - { - name: `${chalk.green("low")} ${chalk.dim( - "\u2014 VM compilation only" - )}`, - value: "low" as const, - }, - { - name: `${chalk.yellow("medium")} ${chalk.dim( - "\u2014 + encryption, rolling cipher, decoy opcodes" - )}`, - value: "medium" as const, - }, - { - name: `${chalk.red("max")} ${chalk.dim( - "\u2014 All protections enabled" - )}`, - value: "max" as const, - }, - { - name: `${chalk.cyan("custom")} ${chalk.dim( - "\u2014 Choose individual options" - )}`, - value: "custom" as const, - }, - ], - }); - - const options: VmObfuscationOptions = {}; - - if (preset !== "custom") { - options.preset = preset; - } else { - const selected = await checkbox({ - message: chalk.bold("Select protection layers"), - choices: [ - { name: "Identifier Renaming", value: "preprocessIdentifiers" }, - { name: "Bytecode Encryption", value: "encryptBytecode" }, - { name: "Rolling Cipher", value: "rollingCipher" }, - { name: "Integrity Binding", value: "integrityBinding" }, - { name: "Debug Protection", value: "debugProtection" }, - { name: "Dynamic Opcodes", value: "dynamicOpcodes" }, - { name: "Decoy Opcodes", value: "decoyOpcodes" }, - { name: "Dead Code Injection", value: "deadCodeInjection" }, - { name: "Stack Encoding", value: "stackEncoding" }, - { name: "VM Shielding", value: "vmShielding" }, - { - name: "Mixed Boolean Arithmetic", - value: "mixedBooleanArithmetic", - }, - { - name: "Handler Fragmentation", - value: "handlerFragmentation", - }, - { name: "String Atomization", value: "stringAtomization" }, - { name: "Polymorphic Decoder", value: "polymorphicDecoder" }, - { name: "Scattered Keys", value: "scatteredKeys" }, - { name: "Block Permutation", value: "blockPermutation" }, - { name: "Opcode Mutation", value: "opcodeMutation" }, - { name: "Bytecode Scattering", value: "bytecodeScattering" }, - ], - }); - for (const opt of selected) { - (options as Record)[opt] = true; - } - } - - // --- Target mode --- - const targetMode = await select<"root" | "comment">({ + const targetMode = await select({ message: chalk.bold("Target mode"), choices: [ { - name: `${chalk.cyan("root")} ${chalk.dim( - "\u2014 All top-level functions" - )}`, - value: "root" as const, + name: "root \u2014 eligible top-level functions", + value: "root", }, { - name: `${chalk.cyan("comment")} ${chalk.dim( - "\u2014 Only /* ruam:vm */ annotated functions" - )}`, - value: "comment" as const, + name: "comment \u2014 /* ruam:isogloss */ markers", + value: "comment", }, ], }); - options.targetMode = targetMode; + const regionDomainsPath = await promptInput({ + message: + chalk.bold("Region domains JSON") + + chalk.dim(" (enter for no configured domains)"), + default: "", + validate: async (value: string) => + !value.trim() || + (await fs.pathExists(path.resolve(value.trim()))) || + `Path does not exist: ${value}`, + }); + const preprocessIdentifiers = await confirm({ + message: chalk.bold("Rename identifiers after protection?"), + default: false, + }); + const ownerTracePath = await promptInput({ + message: + chalk.bold("Owner sidecar path") + + chalk.dim(" (enter to keep owner tracing off)"), + default: "", + }); + + const args: CliArgs = { + ...defaultCliArgs(), + input: resolvedInput, + output: outputRaw.trim() || undefined, + profile: "holographic-local", + targetMode, + regionDomainsPath: regionDomainsPath.trim() || undefined, + preprocessIdentifiers, + ownerTracePath: ownerTracePath.trim() || undefined, + }; - // --- Summary --- + const materialized = await materializeOptions(args); console.log(); console.log(chalk.bold(" Configuration")); console.log(chalk.dim(" " + "\u2500".repeat(40))); console.log( - ` ${chalk.dim("Input:")} ${chalk.white( + ` ${chalk.dim("Input:")} ${chalk.white( path.relative(process.cwd(), resolvedInput) || "." )}` ); console.log( - ` ${chalk.dim("Output:")} ${ - outputRaw.trim() - ? chalk.white(outputRaw.trim()) - : chalk.dim("overwrite input") + ` ${chalk.dim("Output:")} ${ + args.output ? chalk.white(args.output) : chalk.dim("overwrite input") }` ); - - if (preset !== "custom") { - const pc = - preset === "max" - ? chalk.red - : preset === "medium" - ? chalk.yellow - : chalk.green; - console.log(` ${chalk.dim("Preset:")} ${pc(preset)}`); - } - - console.log(` ${chalk.dim("Mode:")} ${chalk.white(targetMode)}`); - - const active = getActiveLabels(options); - if (active.length > 0) { - console.log( - ` ${chalk.dim("Layers:")} ${active - .map((l) => chalk.cyan(l)) - .join(chalk.dim(", "))}` - ); - } + printConfig(materialized.resolved); console.log(); - // --- Confirm --- - const proceed = await confirm({ - message: chalk.bold("Proceed with obfuscation?"), - default: true, - }); - - if (!proceed) { + if ( + !(await confirm({ + message: chalk.bold("Proceed with protection?"), + default: true, + })) + ) { console.log(chalk.dim(" Cancelled.")); - process.exit(0); + return; } - console.log(); + await executeProtection(args, materialized); +} - // --- Run --- - const args: CliArgs = { - input: resolvedInput, - output: outputRaw.trim() || undefined, - options, - include: ["**/*.js"], - exclude: ["**/node_modules/**"], - help: false, - version: false, - interactive: false, - }; - - if (isDir) { - await obfuscateDirectoryWithProgress(resolvedInput, args); - } else { - await obfuscateSingleFileWithProgress(resolvedInput, args); +async function protectFile( + inputPath: string, + outputPath: string, + options: RuamOptions, + ownerTracePath?: string +): Promise { + const source = await fs.readFile(inputPath, "utf-8"); + const result = protectCode(source, options); + await fs.ensureDir(path.dirname(outputPath)); + await fs.writeFile(outputPath, result.code, "utf-8"); + if (ownerTracePath !== undefined) { + if (result.ownerTrace === undefined) { + throw new Error( + "RUAM_CLI_OWNER_TRACE_MISSING: protection did not return the requested owner sidecar" + ); + } + await writeOwnerTrace(ownerTracePath, result.ownerTrace); } + return result; } -// --- Single File Obfuscation --- +async function writeOwnerTrace( + outputPath: string, + trace: OwnerSidecar +): Promise { + await fs.ensureDir(path.dirname(outputPath)); + await fs.writeFile( + outputPath, + JSON.stringify( + trace, + (_key, value) => + typeof value === "bigint" ? value.toString(10) : value, + 2 + ) + "\n", + "utf-8" + ); +} -/** - * Obfuscate a single file with a spinner and summary output. - * - * @param inputPath - Absolute path to the input file. - * @param args - Parsed CLI arguments. - */ -async function obfuscateSingleFileWithProgress( +async function protectSingleFileWithProgress( inputPath: string, - args: CliArgs + args: CliArgs, + materialized: MaterializedCliOptions ): Promise { const outputPath = args.output ? path.resolve(args.output) : inputPath; - - if (args.output) { - await fs.ensureDir(path.dirname(outputPath)); - } - const inputSize = (await fs.stat(inputPath)).size; - const relInput = path.relative(process.cwd(), inputPath); - const relOutput = path.relative(process.cwd(), outputPath); - + const relativeInput = path.relative(process.cwd(), inputPath); + const relativeOutput = path.relative(process.cwd(), outputPath); const spinner = ora({ - text: `Obfuscating ${chalk.cyan(relInput)}...`, + text: `Protecting ${chalk.cyan(relativeInput)}...`, prefixText: " ", color: "cyan", }).start(); - const startTime = Date.now(); try { - await obfuscateFile(inputPath, outputPath, args.options); - } catch (err) { - spinner.fail(chalk.red("Obfuscation failed")); - console.error( - chalk.red(" " + (err instanceof Error ? err.message : String(err))) + const result = await protectFile( + inputPath, + outputPath, + materialized.input, + args.ownerTracePath === undefined + ? undefined + : path.resolve(args.ownerTracePath) ); - process.exit(1); - } - - const elapsed = Date.now() - startTime; - const outputSize = (await fs.stat(outputPath)).size; - const ratio = (outputSize / inputSize).toFixed(1); - - spinner.succeed(chalk.green("Obfuscation complete")); + const elapsed = Date.now() - startTime; + const outputSize = (await fs.stat(outputPath)).size; + const ratio = + inputSize === 0 ? "1.0" : (outputSize / inputSize).toFixed(1); - console.log(); - console.log( - ` ${chalk.dim("File:")} ${chalk.white(relInput)}${ - relInput !== relOutput - ? chalk.dim(" \u2192 ") + chalk.white(relOutput) - : "" - }` - ); - console.log( - ` ${chalk.dim("Input:")} ${chalk.white(formatBytes(inputSize))}` - ); - console.log( - ` ${chalk.dim("Output:")} ${chalk.white( - formatBytes(outputSize) - )} ${chalk.dim(`(${ratio}\u00d7)`)}` - ); - console.log( - ` ${chalk.dim("Time:")} ${chalk.white(formatTime(elapsed))}` - ); - console.log(); + spinner.succeed(chalk.green("Protection complete")); + console.log(); + console.log( + ` ${chalk.dim("File:")} ${chalk.white(relativeInput)}${ + relativeInput !== relativeOutput + ? chalk.dim(" \u2192 ") + chalk.white(relativeOutput) + : "" + }` + ); + console.log( + ` ${chalk.dim("Regions:")} ${chalk.white( + result.stats.protectedRegionCount + )}` + ); + console.log( + ` ${chalk.dim("Input:")} ${chalk.white( + formatBytes(inputSize) + )}` + ); + console.log( + ` ${chalk.dim("Output:")} ${chalk.white( + formatBytes(outputSize) + )} ${chalk.dim(`(${ratio}\u00d7)`)}` + ); + console.log( + ` ${chalk.dim("Time:")} ${chalk.white(formatTime(elapsed))}` + ); + console.log(); + } catch (error) { + spinner.fail(chalk.red("Protection failed")); + throw error; + } } -// --- Directory Obfuscation --- - -/** - * Obfuscate all matching files in a directory with a progress bar. - * - * @param inputPath - Absolute path to the input directory. - * @param args - Parsed CLI arguments. - */ -async function obfuscateDirectoryWithProgress( +async function protectDirectoryWithProgress( inputPath: string, - args: CliArgs + args: CliArgs, + materialized: MaterializedCliOptions ): Promise { - const outputDir = args.output ? path.resolve(args.output) : inputPath; - - if (outputDir !== inputPath) { - await fs.copy(inputPath, outputDir); + const outputDirectory = args.output ? path.resolve(args.output) : inputPath; + if (outputDirectory !== inputPath) { + await fs.copy(inputPath, outputDirectory); } const { globby } = await import("globby"); const files = await globby(args.include, { - cwd: outputDir, + cwd: outputDirectory, ignore: args.exclude, absolute: false, }); - if (files.length === 0) { console.log(chalk.yellow(" No matching files found.")); return; } - const relDir = path.relative(process.cwd(), outputDir) || "."; console.log( - ` ${chalk.dim("Directory:")} ${chalk.white(relDir)} ${chalk.dim( - `(${files.length} file${files.length === 1 ? "" : "s"})` - )}` + ` ${chalk.dim("Directory:")} ${chalk.white( + path.relative(process.cwd(), outputDirectory) || "." + )} ${chalk.dim(`(${files.length} file${files.length === 1 ? "" : "s"})`)}` ); console.log(); const startTime = Date.now(); let totalInputSize = 0; let totalOutputSize = 0; - let errorCount = 0; + let protectedRegionCount = 0; const errors: { file: string; message: string }[] = []; + const spinner = ora({ text: "", prefixText: " ", color: "cyan" }).start(); - const spinner = ora({ - text: "", - prefixText: " ", - color: "cyan", - }).start(); - - for (let i = 0; i < files.length; i++) { - const file = files[i]!; - const filePath = path.join(outputDir, file); - - let inputSize: number; - try { - inputSize = (await fs.stat(filePath)).size; - } catch { - inputSize = 0; - } + for (let index = 0; index < files.length; index++) { + const file = files[index]!; + const filePath = path.join(outputDirectory, file); + const inputSize = (await fs.stat(filePath)).size; totalInputSize += inputSize; - - spinner.text = `${renderBar(i, files.length)} ${chalk.dim(file)}`; + spinner.text = `${renderBar(index, files.length)} ${chalk.dim(file)}`; try { - await obfuscateFile(filePath, filePath, args.options); - const outputSize = (await fs.stat(filePath)).size; - totalOutputSize += outputSize; - } catch (err) { - errorCount++; + const ownerTracePath = + args.ownerTracePath === undefined + ? undefined + : path.join( + path.resolve(args.ownerTracePath), + `${file}.owner-trace.json` + ); + const result = await protectFile( + filePath, + filePath, + materialized.input, + ownerTracePath + ); + protectedRegionCount += result.stats.protectedRegionCount; + totalOutputSize += (await fs.stat(filePath)).size; + } catch (error) { errors.push({ file, - message: err instanceof Error ? err.message : String(err), + message: + error instanceof Error ? error.message : String(error), }); } } - const elapsed = Date.now() - startTime; - const successCount = files.length - errorCount; - const ratio = - totalInputSize > 0 - ? (totalOutputSize / totalInputSize).toFixed(1) - : "0"; - - if (errorCount === 0) { + const successCount = files.length - errors.length; + if (errors.length === 0) { spinner.succeed( chalk.green( `${successCount} file${ successCount === 1 ? "" : "s" - } obfuscated` + } protected` ) ); } else { spinner.warn( - chalk.yellow(`${successCount} obfuscated, ${errorCount} failed`) + chalk.yellow(`${successCount} protected, ${errors.length} failed`) ); } + const ratio = + totalInputSize === 0 + ? "1.0" + : (totalOutputSize / totalInputSize).toFixed(1); console.log(); + console.log( + ` ${chalk.dim("Regions:")} ${chalk.white(protectedRegionCount)}` + ); console.log( ` ${chalk.dim("Input:")} ${chalk.white( formatBytes(totalInputSize) @@ -996,86 +858,98 @@ async function obfuscateDirectoryWithProgress( )} ${chalk.dim(`(${ratio}\u00d7)`)}` ); console.log( - ` ${chalk.dim("Time:")} ${chalk.white(formatTime(elapsed))}` + ` ${chalk.dim("Time:")} ${chalk.white( + formatTime(Date.now() - startTime) + )}` ); if (errors.length > 0) { console.log(); console.log(chalk.red(" Errors:")); - for (const e of errors) { + for (const error of errors) { console.log( - ` ${chalk.red("\u2717")} ${chalk.dim(e.file)}: ${e.message}` + ` ${chalk.red("\u2717")} ${chalk.dim(error.file)}: ${ + error.message + }` ); } + process.exitCode = 1; } - console.log(); } -// --- Main --- +async function executeProtection( + args: CliArgs, + materialized: MaterializedCliOptions +): Promise { + if (args.input === undefined) { + throw new CliUsageError( + "RUAM_CLI_MISSING_VALUE", + "an input path is required" + ); + } + const inputPath = path.resolve(args.input); + if (!(await fs.pathExists(inputPath))) { + throw new CliUsageError( + "RUAM_CLI_MISSING_VALUE", + `${args.input} does not exist` + ); + } + + const stat = await fs.stat(inputPath); + if (stat.isDirectory()) { + await protectDirectoryWithProgress(inputPath, args, materialized); + } else { + await protectSingleFileWithProgress(inputPath, args, materialized); + } +} -/** CLI entry point. Routes to interactive wizard, help, or direct obfuscation. */ async function main(): Promise { const args = parseArgs(process.argv.slice(2)); const version = await getVersion(); if (args.help) { printHelp(version); - process.exit(0); + return; } - if (args.version) { console.log(version); - process.exit(0); + return; } - - // Interactive mode: no input provided or explicit --interactive - if (!args.input || args.interactive) { + if (args.input === undefined || args.interactive) { if (!process.stdin.isTTY) { printHelp(version); - process.exit(1); + process.exitCode = 1; + return; } await runInteractive(version); return; } - // --- Direct mode --- - const inputPath = path.resolve(args.input); - - if (!(await fs.pathExists(inputPath))) { - console.error(chalk.red(` Error: ${args.input} does not exist`)); - process.exit(1); - } - - // Show animated logo briefly, then proceed + const materialized = await materializeOptions(args); const logo = new LogoAnimation(); logo.start(version); - await new Promise((r) => setTimeout(r, 800)); + if (process.stdout.isTTY) { + await new Promise((resolve) => setTimeout(resolve, 800)); + } logo.stop(); - - printConfig(args.options); + printConfig(materialized.resolved); console.log(); - - const stat = await fs.stat(inputPath); - - if (stat.isDirectory()) { - await obfuscateDirectoryWithProgress(inputPath, args); - } else { - await obfuscateSingleFileWithProgress(inputPath, args); - } + await executeProtection(args, materialized); } -main().catch((err) => { - // Handle Ctrl+C from @inquirer/prompts +main().catch((error) => { if ( - err && - typeof err === "object" && - "name" in err && - err.name === "ExitPromptError" + error && + typeof error === "object" && + "name" in error && + error.name === "ExitPromptError" ) { console.log(chalk.dim("\n Cancelled.")); - process.exit(0); + return; } - console.error(chalk.red(err instanceof Error ? err.message : String(err))); - process.exit(1); + console.error( + chalk.red(error instanceof Error ? error.message : String(error)) + ); + process.exitCode = 1; }); diff --git a/packages/ruam/src/compiler/basic-blocks.ts b/packages/ruam/src/compiler/basic-blocks.ts deleted file mode 100644 index 6713c0c..0000000 --- a/packages/ruam/src/compiler/basic-blocks.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Basic block identification for bytecode units. - * - * Extracts the shared logic for identifying basic block boundaries from - * the instruction stream. Used by block permutation, incremental cipher, - * and any future pass that operates on control-flow structure. - * - * @module compiler/basic-blocks - */ - -import type { BytecodeUnit } from "../types.js"; -import { JUMP_OPS, PACKED_JUMP_OPS } from "./opcodes.js"; - -// --- Basic block types --- - -/** A basic block: a contiguous range of instructions [startIp, endIp). */ -export interface BasicBlock { - /** First instruction IP (inclusive). */ - startIp: number; - /** One past the last instruction IP (exclusive). */ - endIp: number; -} - -// --- Basic block identification --- - -/** - * Identify basic block boundaries in a bytecode unit. - * - * Block boundaries occur at: - * - IP 0 (always a block start) - * - Jump targets (start of block) - * - Instructions after jumps (start of block) - * - Packed jump targets extracted from upper bits (start of block) - * - Exception handler entry/exit points (start of block) - * - Jump table target IPs (start of block) - * - * @param unit - The bytecode unit to analyze - * @returns Array of basic blocks covering the entire instruction stream - */ -export function identifyBasicBlocks(unit: BytecodeUnit): BasicBlock[] { - const instrs = unit.instructions; - if (instrs.length === 0) return []; - - // Collect all block-start IPs - const blockStarts = new Set(); - blockStarts.add(0); // First instruction is always a block start - - for (let ip = 0; ip < instrs.length; ip++) { - const instr = instrs[ip]!; - const opcode = instr.opcode; - - if (JUMP_OPS.has(opcode)) { - // The jump target is a block start - const target = instr.operand; - if (target >= 0 && target < instrs.length) { - blockStarts.add(target); - } - // The instruction after the jump is a block start - if (ip + 1 < instrs.length) { - blockStarts.add(ip + 1); - } - } - - if (PACKED_JUMP_OPS.has(opcode)) { - // Packed jumps encode target in upper bits - const target = instr.operand >>> 16; - if (target >= 0 && target < instrs.length) { - blockStarts.add(target); - } - if (ip + 1 < instrs.length) { - blockStarts.add(ip + 1); - } - } - } - - // Exception handler entry points - for (const entry of unit.exceptionTable) { - if (entry.catchIp >= 0) blockStarts.add(entry.catchIp); - if (entry.finallyIp >= 0) blockStarts.add(entry.finallyIp); - blockStarts.add(entry.startIp); - if (entry.endIp < instrs.length) blockStarts.add(entry.endIp); - } - - // Jump table target IPs - for (const ip of Object.values(unit.jumpTable)) { - if (ip >= 0 && ip < instrs.length) { - blockStarts.add(ip); - } - } - - // Sort block starts and create blocks - const sorted = [...blockStarts] - .filter((ip) => ip < instrs.length) - .sort((a, b) => a - b); - const blocks: BasicBlock[] = []; - for (let i = 0; i < sorted.length; i++) { - const start = sorted[i]!; - const end = i + 1 < sorted.length ? sorted[i + 1]! : instrs.length; - if (start < end) { - blocks.push({ startIp: start, endIp: end }); - } - } - - return blocks; -} diff --git a/packages/ruam/src/compiler/block-permutation.ts b/packages/ruam/src/compiler/block-permutation.ts deleted file mode 100644 index 8f6bd42..0000000 --- a/packages/ruam/src/compiler/block-permutation.ts +++ /dev/null @@ -1,302 +0,0 @@ -/** - * Bytecode block permutation. - * - * Compiler pass that identifies basic blocks in a bytecode unit, - * shuffles their physical order via seeded Fisher-Yates, and rewrites - * all jump targets to new positions. - * - * Combined with dead code injection, attackers see fake blocks - * interleaved with real out-of-order blocks. Zero runtime overhead — - * blocks are fixed at compile time, rolling cipher encrypts in the - * permuted positions naturally. - * - * Advancement over js-confuser-vm's PATCH opcode: - * - Permutes ALL blocks randomly (no predictable "end of bytecode" pattern) - * - Interleaves with dead code injection for maximal confusion - * - Zero runtime overhead (blocks reordered at compile time) - * - CSP-safe (no eval or Function constructor needed) - * - * @module compiler/block-permutation - */ - -import type { BytecodeUnit, Instruction } from "../types.js"; -import { lcgNext } from "../naming/scope.js"; - -// --- Jump analysis --- - -// Import opcode sets for identifying jumps. We import the names and check -// against the canonical opcode enum (before shuffle map is applied). -import { Op, ALL_JUMP_OPS, PACKED_JUMP_OPS } from "./opcodes.js"; - -// --- Basic block identification (shared module) --- - -import { identifyBasicBlocks } from "./basic-blocks.js"; -import type { BasicBlock } from "./basic-blocks.js"; - -// --- Terminal opcodes --- -// Opcodes that never fall through to the next instruction. -const TERMINAL_OPS = new Set([ - Op.JMP, - Op.RETURN, - Op.RETURN_VOID, - Op.THROW, - Op.RETHROW, - Op.GENERATOR_RETURN, - Op.GENERATOR_THROW, - Op.ASYNC_GENERATOR_RETURN, - Op.ASYNC_GENERATOR_THROW, -]); - -// --- Block permutation --- - -/** - * Permute the basic blocks of a bytecode unit. - * - * Shuffles block order via seeded Fisher-Yates, then rewrites all - * jump targets, exception table entries, and the jump table to - * reference new instruction positions. - * - * @param unit - The bytecode unit to permute (modified in-place) - * @param seed - Per-build seed for deterministic shuffling - */ -export function permuteBlocks(unit: BytecodeUnit, seed: number): void { - const blocks = identifyBasicBlocks(unit); - - // Need at least 3 blocks to make permutation meaningful - if (blocks.length < 3) return; - - // Fisher-Yates shuffle of block order (skip first block — keep entry point) - let state = (seed ^ (unit.id.charCodeAt(0) || 0x42)) >>> 0; - const permuted = [...blocks]; - // Keep block 0 in place (entry point must be first) - for (let i = permuted.length - 1; i > 1; i--) { - state = lcgNext(state); - const j = 1 + ((state >>> 16) % i); // j in [1, i] - const tmp = permuted[i]!; - permuted[i] = permuted[j]!; - permuted[j] = tmp; - } - - // Check if permutation actually changed anything - let changed = false; - for (let i = 0; i < blocks.length; i++) { - if (blocks[i]!.startIp !== permuted[i]!.startIp) { - changed = true; - break; - } - } - if (!changed) return; - - // --- Phase 1: Insert explicit JMPs for fall-through blocks --- - // Before reordering, identify blocks that don't end with a terminal - // instruction. These rely on sequential fall-through to the next block, - // which will break after reordering. Insert an explicit JMP at the end - // of such blocks pointing to their original successor. - // Deep-copy instructions to avoid sharing objects with unit.instructions. - // Phase 1 modifies operands in-place; shared objects would corrupt the - // original unit state visible to recursive child-unit processing. - const oldInstrs = unit.instructions.map((i) => ({ ...i })); - - // Build a map from original block start → original block index - const blockIndexByStart = new Map(); - for (let bi = 0; bi < blocks.length; bi++) { - blockIndexByStart.set(blocks[bi]!.startIp, bi); - } - - // Determine which blocks need a fall-through JMP and insert them. - // Track IP expansion so we can adjust block boundaries. - const expandedInstrs: Instruction[] = [...oldInstrs]; - let totalExpansion = 0; - const expansionByBlock = new Map(); // block startIp -> expansion count - - for (let bi = 0; bi < blocks.length; bi++) { - const block = blocks[bi]!; - const lastIp = block.endIp - 1; - const lastInstr = oldInstrs[lastIp]; - - if (!lastInstr) continue; - - // If the block already ends with a terminal or unconditional jump, no fix needed - if (TERMINAL_OPS.has(lastInstr.opcode)) continue; - - // If this is the last block, it has no successor to fall through to - if (bi + 1 >= blocks.length) continue; - - // Insert a JMP to the original successor block's start IP - const successorStartIp = blocks[bi + 1]!.startIp; - const insertPos = block.endIp + totalExpansion; - expandedInstrs.splice(insertPos, 0, { - opcode: Op.JMP, - operand: successorStartIp, // Will be patched by IP mapping below - }); - totalExpansion++; - expansionByBlock.set( - block.startIp, - (expansionByBlock.get(block.startIp) ?? 0) + 1 - ); - } - - // Rebuild block boundaries accounting for inserted JMPs - const expandedBlocks: BasicBlock[] = []; - let ipOffset = 0; - for (let bi = 0; bi < blocks.length; bi++) { - const origBlock = blocks[bi]!; - const blockLen = origBlock.endIp - origBlock.startIp; - const expansion = expansionByBlock.get(origBlock.startIp) ?? 0; - expandedBlocks.push({ - startIp: origBlock.startIp + ipOffset, - endIp: origBlock.startIp + ipOffset + blockLen + expansion, - }); - ipOffset += expansion; - } - - // Rebuild the permuted order using expanded blocks - // The permuted array references original blocks by startIp — map to expanded - const expandedPermuted: BasicBlock[] = []; - const origToExpanded = new Map(); - for (let bi = 0; bi < blocks.length; bi++) { - origToExpanded.set(blocks[bi]!.startIp, expandedBlocks[bi]!); - } - for (const origBlock of permuted) { - expandedPermuted.push(origToExpanded.get(origBlock.startIp)!); - } - - // Patch the JMP operands: they point to original successor IPs which - // need to be adjusted for the expansion. - const origIpToExpanded = new Map(); - ipOffset = 0; - for (let bi = 0; bi < blocks.length; bi++) { - const origBlock = blocks[bi]!; - for (let ip = origBlock.startIp; ip < origBlock.endIp; ip++) { - origIpToExpanded.set(ip, ip + ipOffset); - } - ipOffset += expansionByBlock.get(origBlock.startIp) ?? 0; - } - - // --- Build combined original→permuted IP mapping --- - // Compose origIpToExpanded with expandedIpToPermuted in one step - // to avoid mutating shared instruction objects. - const ipMap = new Map(); - let newIp = 0; - for (const block of expandedPermuted) { - const blockLen = block.endIp - block.startIp; - for (let offset = 0; offset < blockLen; offset++) { - ipMap.set(block.startIp + offset, newIp + offset); - } - newIp += blockLen; - } - - // Combined mapping: original IP → permuted IP - const origToPermuted = new Map(); - for (const [origIp, expandedIp] of origIpToExpanded) { - const permuted = ipMap.get(expandedIp); - if (permuted != null) origToPermuted.set(origIp, permuted); - } - // Inserted fall-through JMPs use original successor IPs as operands. - // Their expanded IPs are in ipMap but not in origIpToExpanded. - // We'll patch them via origToPermuted (their operands are original IPs). - - // Rewrite instructions in permuted order (fresh copies, no mutation) - const newInstrs: Instruction[] = []; - for (const block of expandedPermuted) { - for (let ip = block.startIp; ip < block.endIp; ip++) { - newInstrs.push({ ...expandedInstrs[ip]! }); - } - } - - // Map a single packed 16-bit IP half through the permutation. - // Preserves the 0xFFFF sentinel (TRY_PUSH's "no catch" / "no finally" - // marker) and folds fall-off-end targets to one past the last - // instruction, matching the simple-jump handling above. - const mapPackedIp = (v: number): number => { - if (v === 0xffff) return 0xffff; - const mapped = origToPermuted.get(v); - if (mapped != null) return mapped; - if (v >= oldInstrs.length) return newInstrs.length; - return v; - }; - - // Patch jump targets in new instruction array. - // Operands are still ORIGINAL IPs (no in-place Phase 1 mutation). - // Use origToPermuted for direct original→permuted mapping. - for (let ip = 0; ip < newInstrs.length; ip++) { - const instr = newInstrs[ip]!; - - if (ALL_JUMP_OPS.has(instr.opcode)) { - const newTarget = origToPermuted.get(instr.operand); - if (newTarget != null) { - instr.operand = newTarget; - } else { - // Fall-off-end targets (>= origInstrCount) map to newInstrs.length - if (instr.operand >= oldInstrs.length) { - instr.operand = newInstrs.length; - } - } - } - - if (PACKED_JUMP_OPS.has(instr.opcode)) { - if (instr.opcode === Op.TRY_PUSH) { - // TRY_PUSH packs TWO IP targets: catchIp in the upper 16 - // bits and finallyIp in the lower 16 bits (see - // encodeTryTarget in visitors/statements.ts). BOTH must be - // remapped through the permutation — patching only the - // catch target leaves finallyIp pointing at a stale - // pre-permutation IP, corrupting return-through-finally and - // nested try/catch/finally control flow. - const newCatch = mapPackedIp(instr.operand >>> 16); - const newFinally = mapPackedIp(instr.operand & 0xffff); - instr.operand = - ((newCatch & 0xffff) << 16) | (newFinally & 0xffff); - } else { - // REG_LT_CONST_JF / REG_LT_REG_JF: only the upper 16 bits - // are an IP target; the lower 16 bits hold register and - // constant indices, which must be preserved untouched. - const target = instr.operand >>> 16; - const lower = instr.operand & 0xffff; - const newTarget = origToPermuted.get(target); - if (newTarget != null) { - instr.operand = (newTarget << 16) | lower; - } else if (target >= oldInstrs.length && target !== 0xffff) { - instr.operand = (newInstrs.length << 16) | lower; - } - } - } - } - - // Patch jump table - const newJumpTable: Record = {}; - for (const [label, ip] of Object.entries(unit.jumpTable)) { - const newTarget = origToPermuted.get(ip); - newJumpTable[Number(label)] = newTarget ?? ip; - } - - // Patch exception table: map original IPs through expansion + permutation. - // endIp can equal expandedInstrs.length (one past last instruction) — - // handle by mapping to newInstrs.length when not found. - const mapIp = (origIp: number): number => { - const expandedIp = origIpToExpanded.get(origIp) ?? origIp; - const mapped = ipMap.get(expandedIp); - if (mapped != null) return mapped; - // If expandedIp === expandedInstrs.length, map to newInstrs.length - if (expandedIp >= expandedInstrs.length) return newInstrs.length; - return expandedIp; - }; - const newExceptionTable = unit.exceptionTable.map((entry) => ({ - startIp: mapIp(entry.startIp), - endIp: mapIp(entry.endIp), - catchIp: entry.catchIp >= 0 ? mapIp(entry.catchIp) : entry.catchIp, - finallyIp: - entry.finallyIp >= 0 ? mapIp(entry.finallyIp) : entry.finallyIp, - })); - - // Apply changes - unit.instructions = newInstrs; - unit.jumpTable = newJumpTable; - unit.exceptionTable = newExceptionTable; - - // Recursively permute child units - for (const child of unit.childUnits) { - state = lcgNext(state); - permuteBlocks(child, state); - } -} diff --git a/packages/ruam/src/compiler/call-graph.ts b/packages/ruam/src/compiler/call-graph.ts new file mode 100644 index 0000000..0ea9575 --- /dev/null +++ b/packages/ruam/src/compiler/call-graph.ts @@ -0,0 +1,734 @@ +/** + * Deterministic root-group call-boundary and SCC inventory. + * + * Canonical IR proves nested closure allocations through `unit-ref` operands. + * Invocation operands still encode argument shape rather than targets, so this + * module consumes the proof-only abstract dataflow inventory to connect a call + * only when that closure identity reaches the exact runtime callee position. + * + * Treating a call operand as a unit reference would be unsound (an argument + * count can accidentally equal a constant-pool index). Any call, construction, + * dynamic-code, or reflective boundary without an exact dataflow fact remains + * indirect-or-external. + * + * @module compiler/call-graph + */ + +import type { + SemanticInstruction, + SemanticNodeId, + SemanticRootGroup, + SemanticUnit, + SourceOrigin, + SourceOriginId, +} from "./ir.js"; +import { + analyzeCanonicalDirectCallTargets, + type CanonicalDirectCallTargetFact, +} from "./direct-call-targets.js"; +import { + assertCanonicalSemanticOp, + semanticOpName, + type SemanticOp, +} from "./semantic-ops.js"; +import { + getSemanticSignature, + type OperandKind, + type SemanticCallKind, + type SemanticCoercion, + type SemanticCompletion, + type SemanticEffect, + type SemanticSuspensionKind, +} from "./semantic-signatures.js"; +import type { RootGroupId, SemanticUnitId } from "./types.js"; + +type UserCodeCallKind = Exclude; + +/** Stable source identity for one observable boundary or closure allocation. */ +export interface CanonicalCallSource { + unitId: SemanticUnitId; + nodeId: SemanticNodeId; + originId: SourceOriginId; + origin: Readonly; +} + +/** + * A nested unit identity explicitly proven by a canonical `unit-ref`. + * + * This is lexical allocation evidence, not evidence that the child is invoked. + */ +export interface CanonicalClosureSite extends CanonicalCallSource { + targetUnitId: SemanticUnitId; + op: SemanticOp; + opName: string; + evidence: "unit-ref-constant-and-child-membership"; +} + +export type CanonicalBoundaryKind = + | "invoke" + | "construct" + | "dynamic-code" + | "dynamic-module" + | "reflection" + | "unknown"; + +export type CanonicalUnresolvedCallReason = + | "callee-identity-not-represented" + | "constructor-identity-not-represented" + | "runtime-generated-code" + | "runtime-module-resolution" + | "runtime-hook-dispatch" + | "unknown-call-semantics"; + +/** + * Observable facts later fission/lowering stages must preserve around a + * boundary. `mayExecuteArbitraryUserCode` deliberately dominates the narrower + * signature effects: getters, proxies, coercion hooks, and callees can touch + * state not named by the local instruction. + */ +export interface CanonicalBoundaryObservability { + callKind: UserCodeCallKind; + effect: SemanticEffect; + completion: SemanticCompletion; + coercion: SemanticCoercion; + suspension: SemanticSuspensionKind; + mayThrow: boolean; + maySuspend: boolean; + mayExecuteArbitraryUserCode: true; + mayReadOrWriteProgramState: true; + mayReenterRootGroup: true; +} + +export type CanonicalCallResolution = + | { + kind: "direct-intra-group"; + targetUnitId: SemanticUnitId; + evidence: "exact-canonical-dataflow"; + } + | { + kind: "indirect-or-external"; + reason: CanonicalUnresolvedCallReason; + }; + +/** + * One operation which can invoke user or host-controlled code. + * + * The encoded operand and its kind are retained for auditing. They must not be + * interpreted as a target unless {@link CanonicalCallResolution} carries + * explicit target evidence. + */ +export interface CanonicalCallBoundary extends CanonicalCallSource { + op: SemanticOp; + opName: string; + boundaryKind: CanonicalBoundaryKind; + operand: number; + operandKind: OperandKind; + resolution: CanonicalCallResolution; + observability: CanonicalBoundaryObservability; +} + +/** A call-graph edge backed by explicit canonical target evidence. */ +export interface CanonicalDirectCallEdge extends CanonicalCallSource { + targetUnitId: SemanticUnitId; + evidence: "exact-canonical-dataflow"; +} + +export interface CanonicalCallScc { + id: string; + unitIds: readonly SemanticUnitId[]; + incomingSccIds: readonly string[]; + outgoingSccIds: readonly string[]; + /** Proven by a self-edge or a component containing multiple units. */ + isRecursive: boolean; + /** Proven only when the component contains multiple units. */ + isMutuallyRecursive: boolean; + /** An unresolved boundary could dispatch back into this same unit. */ + hasUnresolvedRecursionRisk: boolean; + /** An unresolved boundary could dispatch into another unit in the group. */ + hasUnresolvedMutualRecursionRisk: boolean; + /** Arbitrary user code at a member boundary can reenter the protected root. */ + reentrancyRelevant: boolean; + requiresInterproceduralFission: boolean; +} + +export interface CanonicalCallGraphSummary { + hasProvenRecursion: boolean; + hasProvenMutualRecursion: boolean; + hasUnresolvedRecursionRisk: boolean; + hasUnresolvedMutualRecursionRisk: boolean; + reentrancyRelevantSccIds: readonly string[]; + interproceduralFissionSccIds: readonly string[]; +} + +/** + * Explicitly records the proof policy used to construct direct edges. + */ +export interface CanonicalCallTargetPolicy { + mode: "canonical-evidence-only"; + directTargetRepresentation: "unit-ref-plus-exact-dataflow"; + unprovenBoundaryClassification: "indirect-or-external"; + supportedValueFlows: readonly [ + "stack", + "register", + "argument", + "slot", + ]; + precisionLossBoundaries: readonly [ + "scope-chain", + "dynamic-stack", + "abrupt-control", + "unsupported-aliasing", + ]; +} + +export interface CanonicalCallGraphInventory { + rootGroupId: RootGroupId; + entryUnitId: SemanticUnitId; + unitIds: readonly SemanticUnitId[]; + targetPolicy: CanonicalCallTargetPolicy; + closureSites: readonly CanonicalClosureSite[]; + boundaries: readonly CanonicalCallBoundary[]; + directEdges: readonly CanonicalDirectCallEdge[]; + sccs: readonly CanonicalCallScc[]; + summary: CanonicalCallGraphSummary; +} + +/** + * Inventory all canonical user-code boundaries and compute SCCs from only + * direct edges whose target is explicitly represented in canonical IR. + */ +export function buildCanonicalCallGraphInventory( + group: SemanticRootGroup +): CanonicalCallGraphInventory { + const units = validateAndIndexGroup(group); + const unitIds = Object.freeze([...units.keys()].sort(compareStrings)); + const closureSites: CanonicalClosureSite[] = []; + const boundaries: CanonicalCallBoundary[] = []; + + for (const unitId of unitIds) { + const unit = units.get(unitId)!; + for (const node of unit.nodes) { + const source = sourceFor(unit, node); + const signature = getSemanticSignature(node.op); + + if (signature.operandKind === "unit-ref") { + closureSites.push( + createClosureSite(unit, node, source, units) + ); + } + } + } + + const targetFacts = analyzeCanonicalDirectCallTargets(group); + const targetFactsByUnit = new Map< + SemanticUnitId, + Map + >(); + for (const fact of targetFacts.facts) { + let factsByNode = targetFactsByUnit.get(fact.sourceUnitId); + if (!factsByNode) { + factsByNode = new Map(); + targetFactsByUnit.set(fact.sourceUnitId, factsByNode); + } + if (factsByNode.has(fact.sourceNodeId)) { + throw new Error( + `RUAM_DUPLICATE_DIRECT_CALL_TARGET_FACT: ${fact.sourceUnitId}:${fact.sourceNodeId}` + ); + } + factsByNode.set(fact.sourceNodeId, fact); + } + for (const unitId of unitIds) { + const unit = units.get(unitId)!; + for (const node of unit.nodes) { + const source = sourceFor(unit, node); + const signature = getSemanticSignature(node.op); + if (signature.callKind !== "none") { + boundaries.push( + createBoundary( + node, + source, + signature.callKind, + targetFactsByUnit.get(unit.id)?.get(node.id) + ) + ); + } + } + } + + closureSites.sort(compareSources); + boundaries.sort(compareSources); + + const directEdges = Object.freeze( + boundaries + .flatMap((boundary): CanonicalDirectCallEdge[] => { + if (boundary.resolution.kind !== "direct-intra-group") return []; + return [ + Object.freeze({ + unitId: boundary.unitId, + nodeId: boundary.nodeId, + originId: boundary.originId, + origin: boundary.origin, + targetUnitId: boundary.resolution.targetUnitId, + evidence: boundary.resolution.evidence, + }), + ]; + }) + .sort(compareEdges) + ); + const sccs = buildDeterministicSccs(unitIds, directEdges, boundaries); + const summary = summarizeSccs(sccs); + + return Object.freeze({ + rootGroupId: group.id, + entryUnitId: group.entryUnitId, + unitIds, + targetPolicy: Object.freeze({ + mode: "canonical-evidence-only", + directTargetRepresentation: "unit-ref-plus-exact-dataflow", + unprovenBoundaryClassification: "indirect-or-external", + supportedValueFlows: Object.freeze([ + "stack", + "register", + "argument", + "slot", + ] as const), + precisionLossBoundaries: Object.freeze([ + "scope-chain", + "dynamic-stack", + "abrupt-control", + "unsupported-aliasing", + ] as const), + }), + closureSites: Object.freeze(closureSites), + boundaries: Object.freeze(boundaries), + directEdges, + sccs, + summary, + }); +} + +function validateAndIndexGroup( + group: SemanticRootGroup +): Map { + if (group.units.length === 0) { + throw new Error(`RUAM_EMPTY_SEMANTIC_ROOT_GROUP: ${group.id}`); + } + + const units = new Map(); + for (const unit of group.units) { + if (units.has(unit.id)) { + throw new Error(`RUAM_DUPLICATE_SEMANTIC_UNIT: ${unit.id}`); + } + if (unit.rootGroupId !== group.id) { + throw new Error( + `RUAM_CALL_GRAPH_ROOT_GROUP_MISMATCH: ${unit.id} belongs to ${unit.rootGroupId}, expected ${group.id}` + ); + } + units.set(unit.id, unit); + } + + if (!units.has(group.entryUnitId)) { + throw new Error( + `RUAM_MISSING_CALL_GRAPH_ENTRY_UNIT: ${group.entryUnitId}` + ); + } + + for (const unit of units.values()) { + validateUnit(unit, units); + } + return units; +} + +function validateUnit( + unit: SemanticUnit, + units: ReadonlyMap +): void { + if (unit.nodes.length === 0) { + throw new Error(`RUAM_EMPTY_CALL_GRAPH_UNIT: ${unit.id}`); + } + if ( + !Number.isSafeInteger(unit.entryNode) || + unit.entryNode < 0 || + unit.entryNode >= unit.nodes.length + ) { + throw new Error( + `RUAM_INVALID_CALL_GRAPH_ENTRY_NODE: ${unit.id}:${unit.entryNode}` + ); + } + + const childIds = new Set(); + for (const childId of unit.childUnitIds) { + if (childIds.has(childId)) { + throw new Error( + `RUAM_DUPLICATE_CALL_GRAPH_CHILD: ${unit.id} -> ${childId}` + ); + } + childIds.add(childId); + if (!units.has(childId)) { + throw new Error( + `RUAM_UNKNOWN_CALL_GRAPH_CHILD: ${unit.id} -> ${childId}` + ); + } + } + + for (let index = 0; index < unit.nodes.length; index++) { + const node = unit.nodes[index]!; + assertCanonicalSemanticOp(node.op); + if (node.id !== index) { + throw new Error( + `RUAM_NONDETERMINISTIC_CALL_GRAPH_NODE_ORDER: ${unit.id}:${node.id} at ${index}` + ); + } + if (!Number.isSafeInteger(node.operand)) { + throw new Error( + `RUAM_INVALID_CALL_GRAPH_OPERAND: ${unit.id}:${node.id}` + ); + } + if ( + !Number.isSafeInteger(node.originId) || + node.originId < 0 || + node.originId >= unit.origins.length + ) { + throw new Error( + `RUAM_MISSING_CALL_GRAPH_ORIGIN: ${unit.id}:${node.id}` + ); + } + if (!unit.exits.has(node.id)) { + throw new Error( + `RUAM_MISSING_CALL_GRAPH_EXITS: ${unit.id}:${node.id}` + ); + } + } +} + +function sourceFor( + unit: SemanticUnit, + node: SemanticInstruction +): CanonicalCallSource { + const origin = unit.origins[node.originId]!; + return Object.freeze({ + unitId: unit.id, + nodeId: node.id, + originId: node.originId, + origin: Object.freeze({ ...origin }), + }); +} + +function createClosureSite( + unit: SemanticUnit, + node: SemanticInstruction, + source: CanonicalCallSource, + units: ReadonlyMap +): CanonicalClosureSite { + const constant = unit.constants[node.operand]; + if (constant?.type !== "string") { + throw new Error( + `RUAM_INVALID_CANONICAL_UNIT_REF: ${unit.id}:${node.id} operand ${node.operand}` + ); + } + const targetUnitId = constant.value; + if (!units.has(targetUnitId)) { + throw new Error( + `RUAM_UNKNOWN_CANONICAL_UNIT_REF: ${unit.id}:${node.id} -> ${targetUnitId}` + ); + } + if (!unit.childUnitIds.includes(targetUnitId)) { + throw new Error( + `RUAM_NON_CHILD_CANONICAL_UNIT_REF: ${unit.id}:${node.id} -> ${targetUnitId}` + ); + } + + return Object.freeze({ + ...source, + targetUnitId, + op: node.op, + opName: semanticOpName(node.op), + evidence: "unit-ref-constant-and-child-membership", + }); +} + +function createBoundary( + node: SemanticInstruction, + source: CanonicalCallSource, + callKind: UserCodeCallKind, + targetFact: CanonicalDirectCallTargetFact | undefined +): CanonicalCallBoundary { + const signature = getSemanticSignature(node.op); + return Object.freeze({ + ...source, + op: node.op, + opName: semanticOpName(node.op), + boundaryKind: boundaryKindFor(callKind), + operand: node.operand, + operandKind: signature.operandKind, + resolution: targetFact + ? Object.freeze({ + kind: "direct-intra-group", + targetUnitId: targetFact.targetUnitId, + evidence: targetFact.evidence, + }) + : Object.freeze({ + kind: "indirect-or-external", + reason: unresolvedReasonFor(callKind), + }), + observability: Object.freeze({ + callKind, + effect: signature.effect, + completion: signature.completion, + coercion: signature.coercion, + suspension: signature.suspension, + mayThrow: signature.mayThrow, + maySuspend: + signature.suspension !== "none" || + callKind === "dynamic-import", + mayExecuteArbitraryUserCode: true, + mayReadOrWriteProgramState: true, + mayReenterRootGroup: true, + }), + }); +} + +function boundaryKindFor( + callKind: UserCodeCallKind +): CanonicalBoundaryKind { + switch (callKind) { + case "invoke": + return "invoke"; + case "construct": + return "construct"; + case "direct-eval": + return "dynamic-code"; + case "dynamic-import": + return "dynamic-module"; + case "coercion-hook": + case "host-protocol": + return "reflection"; + case "unknown": + return "unknown"; + } +} + +function unresolvedReasonFor( + callKind: UserCodeCallKind +): CanonicalUnresolvedCallReason { + switch (callKind) { + case "invoke": + return "callee-identity-not-represented"; + case "construct": + return "constructor-identity-not-represented"; + case "direct-eval": + return "runtime-generated-code"; + case "dynamic-import": + return "runtime-module-resolution"; + case "coercion-hook": + case "host-protocol": + return "runtime-hook-dispatch"; + case "unknown": + return "unknown-call-semantics"; + } +} + +function buildDeterministicSccs( + unitIds: readonly SemanticUnitId[], + edges: readonly CanonicalDirectCallEdge[], + boundaries: readonly CanonicalCallBoundary[] +): readonly CanonicalCallScc[] { + const adjacency = new Map( + unitIds.map((unitId) => [unitId, []]) + ); + for (const edge of edges) { + const targets = adjacency.get(edge.unitId); + if (!targets || !adjacency.has(edge.targetUnitId)) { + throw new Error( + `RUAM_CALL_GRAPH_EDGE_OUTSIDE_GROUP: ${edge.unitId} -> ${edge.targetUnitId}` + ); + } + if (!targets.includes(edge.targetUnitId)) { + targets.push(edge.targetUnitId); + targets.sort(compareStrings); + } + } + + let nextIndex = 0; + const indexByUnit = new Map(); + const lowLinkByUnit = new Map(); + const stack: SemanticUnitId[] = []; + const onStack = new Set(); + const components: SemanticUnitId[][] = []; + + const visit = (unitId: SemanticUnitId): void => { + const index = nextIndex++; + indexByUnit.set(unitId, index); + lowLinkByUnit.set(unitId, index); + stack.push(unitId); + onStack.add(unitId); + + for (const targetId of adjacency.get(unitId)!) { + if (!indexByUnit.has(targetId)) { + visit(targetId); + lowLinkByUnit.set( + unitId, + Math.min( + lowLinkByUnit.get(unitId)!, + lowLinkByUnit.get(targetId)! + ) + ); + } else if (onStack.has(targetId)) { + lowLinkByUnit.set( + unitId, + Math.min( + lowLinkByUnit.get(unitId)!, + indexByUnit.get(targetId)! + ) + ); + } + } + + if (lowLinkByUnit.get(unitId) !== indexByUnit.get(unitId)) return; + const component: SemanticUnitId[] = []; + while (stack.length > 0) { + const member = stack.pop()!; + onStack.delete(member); + component.push(member); + if (member === unitId) break; + } + component.sort(compareStrings); + components.push(component); + }; + + for (const unitId of unitIds) { + if (!indexByUnit.has(unitId)) visit(unitId); + } + components.sort(compareComponents); + + const sccIdByUnit = new Map(); + const sccIds = components.map((_, index) => `scc_${index}`); + for (let index = 0; index < components.length; index++) { + for (const unitId of components[index]!) { + sccIdByUnit.set(unitId, sccIds[index]!); + } + } + + return Object.freeze( + components.map((component, index) => { + const id = sccIds[index]!; + const memberIds = new Set(component); + const incoming = new Set(); + const outgoing = new Set(); + let hasSelfEdge = false; + + for (const edge of edges) { + const sourceSccId = sccIdByUnit.get(edge.unitId)!; + const targetSccId = sccIdByUnit.get(edge.targetUnitId)!; + if ( + edge.unitId === edge.targetUnitId && + memberIds.has(edge.unitId) + ) { + hasSelfEdge = true; + } + if (sourceSccId === id && targetSccId !== id) { + outgoing.add(targetSccId); + } + if (targetSccId === id && sourceSccId !== id) { + incoming.add(sourceSccId); + } + } + + const unresolved = boundaries.some( + (boundary) => + memberIds.has(boundary.unitId) && + boundary.resolution.kind === "indirect-or-external" + ); + const reentrancyRelevant = boundaries.some( + (boundary) => + memberIds.has(boundary.unitId) && + boundary.observability.mayReenterRootGroup + ); + const isMutuallyRecursive = component.length > 1; + const isRecursive = isMutuallyRecursive || hasSelfEdge; + const hasUnresolvedRecursionRisk = unresolved; + const hasUnresolvedMutualRecursionRisk = + unresolved && unitIds.length > 1; + + return Object.freeze({ + id, + unitIds: Object.freeze(component.slice()), + incomingSccIds: Object.freeze( + [...incoming].sort(compareStrings) + ), + outgoingSccIds: Object.freeze( + [...outgoing].sort(compareStrings) + ), + isRecursive, + isMutuallyRecursive, + hasUnresolvedRecursionRisk, + hasUnresolvedMutualRecursionRisk, + reentrancyRelevant, + requiresInterproceduralFission: + isRecursive || reentrancyRelevant, + }); + }) + ); +} + +function summarizeSccs( + sccs: readonly CanonicalCallScc[] +): CanonicalCallGraphSummary { + return Object.freeze({ + hasProvenRecursion: sccs.some((scc) => scc.isRecursive), + hasProvenMutualRecursion: sccs.some( + (scc) => scc.isMutuallyRecursive + ), + hasUnresolvedRecursionRisk: sccs.some( + (scc) => scc.hasUnresolvedRecursionRisk + ), + hasUnresolvedMutualRecursionRisk: sccs.some( + (scc) => scc.hasUnresolvedMutualRecursionRisk + ), + reentrancyRelevantSccIds: Object.freeze( + sccs + .filter((scc) => scc.reentrancyRelevant) + .map((scc) => scc.id) + ), + interproceduralFissionSccIds: Object.freeze( + sccs + .filter((scc) => scc.requiresInterproceduralFission) + .map((scc) => scc.id) + ), + }); +} + +function compareSources( + left: CanonicalCallSource, + right: CanonicalCallSource +): number { + return ( + compareStrings(left.unitId, right.unitId) || + left.nodeId - right.nodeId + ); +} + +function compareEdges( + left: CanonicalDirectCallEdge, + right: CanonicalDirectCallEdge +): number { + return ( + compareSources(left, right) || + compareStrings(left.targetUnitId, right.targetUnitId) + ); +} + +function compareComponents( + left: readonly SemanticUnitId[], + right: readonly SemanticUnitId[] +): number { + for (let index = 0; index < Math.min(left.length, right.length); index++) { + const comparison = compareStrings(left[index]!, right[index]!); + if (comparison !== 0) return comparison; + } + return left.length - right.length; +} + +function compareStrings(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} diff --git a/packages/ruam/src/compiler/cfg.ts b/packages/ruam/src/compiler/cfg.ts new file mode 100644 index 0000000..51298d9 --- /dev/null +++ b/packages/ruam/src/compiler/cfg.ts @@ -0,0 +1,319 @@ +/** + * Canonical control-flow graph construction. + * + * This module translates the compiler's unencoded, pre-superinstruction + * instruction stream into node-identity control flow. It has no dependency on + * either execution backend. + * + * @module compiler/cfg + */ + +import type { EmittedSemanticInstruction } from "./types.js"; +import { + createSemanticInstruction, + type SemanticExit, + type SemanticInstruction, + type SemanticNodeId, + type SourceOriginId, +} from "./ir.js"; +import { + assertCanonicalSemanticOp, + SEMANTIC_OP_COUNT, + SemanticOp, + type SemanticOp as SemanticOpValue, +} from "./semantic-ops.js"; +import { getSemanticSignature } from "./semantic-signatures.js"; + +/** Input needed to create canonical nodes and typed exits for one unit. */ +export interface CanonicalCfgInput { + instructions: readonly EmittedSemanticInstruction[]; + originIds: readonly SourceOriginId[]; +} + +/** Execution-independent nodes plus their validated outgoing edges. */ +export interface CanonicalCfg { + nodes: SemanticInstruction[]; + exits: Map; + entryNode: SemanticNodeId; +} + +interface HandlerTarget { + catchTarget: SemanticNodeId | null; + finallyTarget: SemanticNodeId | null; +} + +const TAKEN_WHEN_FALSE = new Set([ + SemanticOp.JMP_FALSE, + SemanticOp.JMP_FALSE_KEEP, + SemanticOp.LOGICAL_AND, +]); + +const CONDITIONAL_DIRECT_JUMPS = new Set([ + SemanticOp.JMP_TRUE, + SemanticOp.JMP_FALSE, + SemanticOp.JMP_NULLISH, + SemanticOp.JMP_UNDEFINED, + SemanticOp.JMP_TRUE_KEEP, + SemanticOp.JMP_FALSE_KEEP, + SemanticOp.JMP_NULLISH_KEEP, + SemanticOp.LOGICAL_AND, + SemanticOp.LOGICAL_OR, + SemanticOp.NULLISH_COALESCE, +]); + +const STRUCTURED_EXCEPTION_OPS = new Set([ + SemanticOp.TRY_PUSH, + SemanticOp.TRY_POP, + SemanticOp.CATCH_BIND, + SemanticOp.CATCH_BIND_PATTERN, + SemanticOp.FINALLY_MARK, + SemanticOp.END_FINALLY, +]); + +/** + * Build a canonical CFG and fail closed on representation-specific opcodes or + * malformed direct targets. + */ +export function buildCanonicalCfg(input: CanonicalCfgInput): CanonicalCfg { + const { instructions, originIds } = input; + if (instructions.length === 0) { + throw new Error("RUAM_EMPTY_SEMANTIC_UNIT"); + } + if (originIds.length !== instructions.length) { + throw new Error( + `RUAM_ORIGIN_ARITY_MISMATCH: ${originIds.length} origins for ${instructions.length} nodes` + ); + } + + const nodes = instructions.map((instruction, id) => { + if ( + !Number.isSafeInteger(instruction.opcode) || + instruction.opcode < 0 || + instruction.opcode >= SEMANTIC_OP_COUNT + ) { + throw new Error( + `RUAM_INVALID_SEMANTIC_OP: node ${id} -> ${instruction.opcode}` + ); + } + const op = instruction.opcode as SemanticOpValue; + assertCanonicalSemanticOp(op); + return createSemanticInstruction({ + id, + op, + operand: instruction.operand, + originId: originIds[id]!, + }); + }); + + const exits = new Map(); + for (const node of nodes) exits.set(node.id, []); + + const pending: Array<{ + nodeId: SemanticNodeId; + handlers: HandlerTarget[]; + }> = [{ nodeId: 0, handlers: [] }]; + const visitedStates = new Set(); + + while (pending.length > 0) { + const state = pending.pop()!; + const stateKey = handlerStateKey(state.nodeId, state.handlers); + if (visitedStates.has(stateKey)) continue; + visitedStates.add(stateKey); + + const node = nodes[state.nodeId]!; + const nodeExits = exitsForNode(node, nodes.length, state.handlers); + const accumulated = exits.get(node.id)!; + for (const exit of nodeExits) { + if (!containsSameExit(accumulated, exit)) accumulated.push(exit); + } + + const normalHandlers = handlersAfterNormalExit( + node, + state.handlers, + nodes.length + ); + for (const exit of nodeExits) { + const target = exitTarget(exit); + if (target == null) continue; + const targetHandlers = + exit.kind === "exception" || exit.kind === "finally" + ? state.handlers.slice(0, -1) + : normalHandlers; + pending.push({ nodeId: target, handlers: targetHandlers }); + } + } + + for (const [id, nodeExits] of exits) { + exits.set(id, Object.freeze(nodeExits.slice()) as SemanticExit[]); + } + + return { + nodes: Object.freeze(nodes.slice()) as SemanticInstruction[], + exits, + entryNode: 0, + }; +} + +function exitsForNode( + node: SemanticInstruction, + nodeCount: number, + handlers: readonly HandlerTarget[] +): SemanticExit[] { + const { op, operand, id } = node; + const next = id + 1; + const innermostHandler = handlers[handlers.length - 1]; + const signature = getSemanticSignature(op); + let normal: SemanticExit[]; + + if (op === SemanticOp.JMP) { + normal = [{ kind: "fallthrough", target: directTarget(operand, nodeCount, id) }]; + } else if (CONDITIONAL_DIRECT_JUMPS.has(op)) { + const taken = directTarget(operand, nodeCount, id); + const notTaken = fallthroughTarget(next, nodeCount, id); + normal = TAKEN_WHEN_FALSE.has(op) + ? [ + { kind: "branch-false", target: taken }, + { kind: "branch-true", target: notTaken }, + ] + : [ + { kind: "branch-true", target: taken }, + { kind: "branch-false", target: notTaken }, + ]; + } else if ( + op === SemanticOp.TABLE_SWITCH || + op === SemanticOp.LOOKUP_SWITCH + ) { + throw new Error( + `RUAM_CANONICAL_SWITCH_TABLE_REQUIRES_TARGETS: node ${id}` + ); + } else if (signature.control === "call") { + normal = [{ kind: "call", resume: fallthroughTarget(next, nodeCount, id) }]; + } else if (signature.control === "yield") { + normal = [{ kind: "yield", resume: fallthroughTarget(next, nodeCount, id) }]; + } else if (signature.control === "await") { + normal = [{ kind: "await", resume: fallthroughTarget(next, nodeCount, id) }]; + } else if (signature.control === "return") { + normal = innermostHandler?.finallyTarget != null + ? [{ kind: "finally", target: innermostHandler.finallyTarget }] + : [{ kind: "return" }]; + } else if (op === SemanticOp.THROW_IF_NOT_OBJECT) { + normal = [ + { + kind: "fallthrough", + target: fallthroughTarget(next, nodeCount, id), + }, + ]; + } else if (signature.control === "throw") { + normal = abruptExceptionExit(innermostHandler); + } else { + normal = [{ kind: "fallthrough", target: fallthroughTarget(next, nodeCount, id) }]; + } + + if ( + signature.mayThrow && + (signature.control !== "throw" || op === SemanticOp.THROW_IF_NOT_OBJECT) && + !STRUCTURED_EXCEPTION_OPS.has(op) + ) { + const exceptional = abruptExceptionExit(innermostHandler); + if (!containsSameExit(normal, exceptional[0]!)) normal.push(...exceptional); + } + + return normal; +} + +function handlersAfterNormalExit( + node: SemanticInstruction, + handlers: readonly HandlerTarget[], + nodeCount: number +): HandlerTarget[] { + if (node.op === SemanticOp.TRY_PUSH) { + return [ + ...handlers, + decodeTryTargets(node.operand, nodeCount, node.id), + ]; + } + if (node.op === SemanticOp.TRY_POP) { + if (handlers.length === 0) { + throw new Error(`RUAM_UNBALANCED_TRY_POP: node ${node.id}`); + } + return handlers.slice(0, -1); + } + return handlers.slice(); +} + +function decodeTryTargets( + operand: number, + nodeCount: number, + source: SemanticNodeId +): HandlerTarget { + const catchRaw = (operand >>> 16) & 0xffff; + const finallyRaw = operand & 0xffff; + const catchTarget = + catchRaw === 0xffff ? null : directTarget(catchRaw, nodeCount, source); + const finallyTarget = + finallyRaw === 0xffff + ? null + : directTarget(finallyRaw, nodeCount, source); + if (catchTarget == null && finallyTarget == null) { + throw new Error(`RUAM_EMPTY_TRY_HANDLER: node ${source}`); + } + return { catchTarget, finallyTarget }; +} + +function abruptExceptionExit( + handler: HandlerTarget | undefined +): SemanticExit[] { + if (handler?.catchTarget != null) { + return [{ kind: "exception", target: handler.catchTarget }]; + } + if (handler?.finallyTarget != null) { + return [{ kind: "finally", target: handler.finallyTarget }]; + } + return [{ kind: "throw" }]; +} + +function directTarget( + target: number, + nodeCount: number, + source: SemanticNodeId +): SemanticNodeId { + if (!Number.isSafeInteger(target) || target < 0 || target >= nodeCount) { + throw new Error( + `RUAM_INVALID_SEMANTIC_TARGET: node ${source} -> ${target} (size ${nodeCount})` + ); + } + return target; +} + +function fallthroughTarget( + target: number, + nodeCount: number, + source: SemanticNodeId +): SemanticNodeId { + return directTarget(target, nodeCount, source); +} + +function containsSameExit( + exits: readonly SemanticExit[], + candidate: SemanticExit +): boolean { + return exits.some((exit) => JSON.stringify(exit) === JSON.stringify(candidate)); +} + +function exitTarget(exit: SemanticExit): SemanticNodeId | null { + if ("target" in exit) return exit.target; + if ("resume" in exit) return exit.resume; + return null; +} + +function handlerStateKey( + nodeId: SemanticNodeId, + handlers: readonly HandlerTarget[] +): string { + return `${nodeId}|${handlers + .map( + (handler) => + `${handler.catchTarget ?? "-"}:${handler.finallyTarget ?? "-"}` + ) + .join(",")}`; +} diff --git a/packages/ruam/src/compiler/crypto.ts b/packages/ruam/src/compiler/crypto.ts deleted file mode 100644 index f5562c9..0000000 --- a/packages/ruam/src/compiler/crypto.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Stream cipher and custom binary encoding — used for bytecode encryption. - * - * Provides build-time implementations for: - * - FNV-1a+LCG stream cipher (symmetric encrypt/decrypt) - * - Custom alphabet binary encoding (replaces base64) - * - * @module compiler/crypto - */ - -import { LCG_MULTIPLIER, LCG_INCREMENT } from "../constants.js"; - -// --------------------------------------------------------------------------- -// Build-time implementations -// --------------------------------------------------------------------------- - -/** - * Custom symmetric cipher — FNV-1a key derivation + LCG keystream. - * - * Replaces RC4 with a structure that doesn't exhibit recognizable - * cipher patterns (no S-box, no KSA/PRGA, no swap operations). - * Symmetric via XOR — same function encrypts and decrypts. - */ -export function rc4(data: Uint8Array, key: string): Uint8Array { - // Derive 32-bit state from key via FNV-1a - let h = 0x811c9dc5; // FNV offset basis - for (let i = 0; i < key.length; i++) { - h = Math.imul(h ^ key.charCodeAt(i), 0x01000193); // FNV prime - } - h >>>= 0; - - // Transform each byte via LCG-driven keystream - const out = new Uint8Array(data.length); - for (let i = 0; i < data.length; i++) { - h = (Math.imul(h, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - out[i] = data[i]! ^ ((h >>> 16) & 0xff); - } - return out; -} - -// --------------------------------------------------------------------------- -// Custom alphabet encoding (replaces base64 in output) -// --------------------------------------------------------------------------- - -/** - * Encode a Uint8Array using a custom 64-character alphabet. - * - * Same bit-packing as base64 (3 bytes → 4 chars) but with a per-build - * shuffled alphabet and no padding characters. The output contains only - * identifier-safe characters, eliminating the telltale `+/=` of base64. - * - * @param data - Binary data to encode. - * @param alphabet - A 64-character encoding alphabet. - * @returns The encoded string. - */ -export function customEncode(data: Uint8Array, alphabet: string): string { - let result = ""; - const n = data.length; - let i = 0; - - // Process full groups of 3 bytes → 4 chars - for (; i + 2 < n; i += 3) { - const a = data[i]!; - const b = data[i + 1]!; - const c = data[i + 2]!; - result += alphabet[(a >> 2) & 63]; - result += alphabet[((a & 3) << 4) | (b >> 4)]; - result += alphabet[((b & 15) << 2) | (c >> 6)]; - result += alphabet[c & 63]; - } - - // Handle remaining bytes (no padding) - if (i < n) { - const a = data[i]!; - result += alphabet[(a >> 2) & 63]; - if (i + 1 < n) { - const b = data[i + 1]!; - result += alphabet[((a & 3) << 4) | (b >> 4)]; - result += alphabet[(b & 15) << 2]; - } else { - result += alphabet[(a & 3) << 4]; - } - } - - return result; -} diff --git a/packages/ruam/src/compiler/direct-call-targets.ts b/packages/ruam/src/compiler/direct-call-targets.ts new file mode 100644 index 0000000..4dbbbb1 --- /dev/null +++ b/packages/ruam/src/compiler/direct-call-targets.ts @@ -0,0 +1,894 @@ +/** + * Proof-only direct-call target analysis over canonical semantic IR. + * + * A value is known only when it originates at a validated `unit-ref` closure + * allocation and survives exact stack/frame transfer. Joins retain a target + * only when every incoming state agrees. Dynamic stack behavior, exceptional + * transfer, unsupported aliasing, and unknown mutation erase precision. + * + * Call conventions mirror the reference VM: + * + * - plain/tagged/constructor: `[callee, ...args]` + * - method: `[callee, receiver, ...args]` + * - fast calls: fixed versions of the plain convention + * - super/eval/import: no stack-carried callee identity + * + * @module compiler/direct-call-targets + */ + +import type { + SemanticExit, + SemanticInstruction, + SemanticNodeId, + SemanticRootGroup, + SemanticUnit, +} from "./ir.js"; +import { + assertCanonicalSemanticOp, + SemanticOp, + type SemanticOp as SemanticOpValue, +} from "./semantic-ops.js"; +import { + getSemanticSignature, + resolveStackArity, + type ResolvedStackArity, +} from "./semantic-signatures.js"; +import type { RootGroupId, SemanticUnitId } from "./types.js"; + +export type CanonicalDirectCallConvention = + | "plain" + | "method" + | "construct" + | "tagged-template" + | "fast-0" + | "fast-1" + | "fast-2" + | "fast-3"; + +/** One target identity proven at the input of a canonical call node. */ +export interface CanonicalDirectCallTargetFact { + rootGroupId: RootGroupId; + sourceUnitId: SemanticUnitId; + sourceNodeId: SemanticNodeId; + targetUnitId: SemanticUnitId; + convention: CanonicalDirectCallConvention; + evidence: "exact-canonical-dataflow"; +} + +export interface CanonicalDirectCallTargetInventory { + rootGroupId: RootGroupId; + facts: readonly CanonicalDirectCallTargetFact[]; +} + +type AbstractValue = SemanticUnitId | null; + +interface AbstractState { + /** `null` means stack height or ordering is no longer known. */ + stack: AbstractValue[] | null; + registers: AbstractValue[]; + arguments: AbstractValue[]; + slots: AbstractValue[]; +} + +interface CallLayout { + convention: CanonicalDirectCallConvention; + argumentCount: number; + calleeDepth: number; + inputCount: number; +} + +interface IndexedGroup { + units: ReadonlyMap; + unitRefs: ReadonlyMap>; +} + +const DIRECT_CALL_OPS = new Set([ + SemanticOp.CALL, + SemanticOp.CALL_METHOD, + SemanticOp.CALL_NEW, + SemanticOp.CALL_OPTIONAL, + SemanticOp.CALL_METHOD_OPTIONAL, + SemanticOp.CALL_TAGGED_TEMPLATE, + SemanticOp.CALL_0, + SemanticOp.CALL_1, + SemanticOp.CALL_2, + SemanticOp.CALL_3, + SemanticOp.TAGGED_TEMPLATE, +]); + +/** + * Analyze every unit independently from its canonical invocation entry. + * + * Registers, arguments, and slots begin unknown. This deliberately does not + * invent captured-scope facts across units; `LOAD_SCOPED` therefore produces + * unknown unless canonical IR later grows an explicit lexical-frame contract. + */ +export function analyzeCanonicalDirectCallTargets( + group: SemanticRootGroup +): CanonicalDirectCallTargetInventory { + const indexed = validateAndIndexGroup(group); + const facts: CanonicalDirectCallTargetFact[] = []; + const unitIds = [...indexed.units.keys()].sort(compareStrings); + + for (const unitId of unitIds) { + const unit = indexed.units.get(unitId)!; + const unitFacts = analyzeUnit( + group.id, + unit, + indexed.unitRefs.get(unitId)! + ); + facts.push(...unitFacts); + } + facts.sort(compareFacts); + + return Object.freeze({ + rootGroupId: group.id, + facts: Object.freeze(facts), + }); +} + +function analyzeUnit( + rootGroupId: RootGroupId, + unit: SemanticUnit, + unitRefs: ReadonlyMap +): CanonicalDirectCallTargetFact[] { + const nodeById = new Map(unit.nodes.map((node) => [node.id, node])); + const states = new Map(); + const queued = new Set(); + const queue: SemanticNodeId[] = []; + const enqueue = (nodeId: SemanticNodeId): void => { + if (queued.has(nodeId)) return; + queued.add(nodeId); + queue.push(nodeId); + queue.sort((left, right) => left - right); + }; + + states.set(unit.entryNode, initialState(unit)); + enqueue(unit.entryNode); + + while (queue.length > 0) { + const nodeId = queue.shift()!; + queued.delete(nodeId); + const node = nodeById.get(nodeId); + if (!node) { + throw new Error( + `RUAM_DIRECT_TARGET_UNKNOWN_NODE: ${unit.id}:${nodeId}` + ); + } + const input = states.get(nodeId)!; + const output = transfer(unit, node, input, unitRefs); + const exits = unit.exits.get(nodeId); + if (!exits) { + throw new Error( + `RUAM_DIRECT_TARGET_MISSING_EXITS: ${unit.id}:${nodeId}` + ); + } + + for (const exit of exits) { + const target = exitTarget(exit); + if (target == null) continue; + if (!nodeById.has(target)) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_EXIT: ${unit.id}:${nodeId} -> ${target}` + ); + } + const candidate = isAbruptExit(exit) + ? unknownState(unit) + : output; + const previous = states.get(target); + const joined = previous + ? joinStates(previous, candidate) + : cloneState(candidate); + if (!previous || !sameState(previous, joined)) { + states.set(target, joined); + enqueue(target); + } + } + } + + const facts: CanonicalDirectCallTargetFact[] = []; + for (const node of unit.nodes) { + if (!DIRECT_CALL_OPS.has(node.op)) continue; + const state = states.get(node.id); + if (!state) continue; + const layout = callLayout(node); + if (!layout || state.stack == null) continue; + assertStackDepth(unit, node, state.stack, layout.inputCount); + const target = + state.stack[state.stack.length - 1 - layout.calleeDepth] ?? null; + if (target == null) continue; + facts.push( + Object.freeze({ + rootGroupId, + sourceUnitId: unit.id, + sourceNodeId: node.id, + targetUnitId: target, + convention: layout.convention, + evidence: "exact-canonical-dataflow", + }) + ); + } + return facts; +} + +function transfer( + unit: SemanticUnit, + node: SemanticInstruction, + input: AbstractState, + unitRefs: ReadonlyMap +): AbstractState { + const state = cloneState(input); + + switch (node.op) { + case SemanticOp.NEW_CLOSURE: + case SemanticOp.NEW_FUNCTION: + case SemanticOp.NEW_ARROW: + case SemanticOp.NEW_ASYNC: + case SemanticOp.NEW_GENERATOR: + case SemanticOp.NEW_ASYNC_GENERATOR: + push(state, unitRefs.get(node.id) ?? null); + return state; + case SemanticOp.POP: + pop(unit, node, state); + return state; + case SemanticOp.POP_N: + assertNonNegativeOperand(unit, node); + for (let index = 0; index < node.operand; index++) { + pop(unit, node, state); + } + return state; + case SemanticOp.DUP: { + const value = pop(unit, node, state); + push(state, value); + push(state, value); + return state; + } + case SemanticOp.DUP2: { + const right = pop(unit, node, state); + const left = pop(unit, node, state); + push(state, left); + push(state, right); + push(state, left); + push(state, right); + return state; + } + case SemanticOp.SWAP: { + const right = pop(unit, node, state); + const left = pop(unit, node, state); + push(state, right); + push(state, left); + return state; + } + case SemanticOp.ROT3: + rotate(unit, node, state, 3); + return state; + case SemanticOp.ROT4: + rotate(unit, node, state, 4); + return state; + case SemanticOp.PICK: + assertNonNegativeOperand(unit, node); + if (state.stack == null) return state; + if (node.operand >= state.stack.length) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_PICK: ${unit.id}:${node.id} depth ${node.operand}` + ); + } + push( + state, + state.stack[state.stack.length - node.operand - 1] ?? null + ); + return state; + case SemanticOp.LOAD_REG: + push(state, readFrame(unit, node, state.registers, "register")); + return state; + case SemanticOp.STORE_REG: + writeFrame( + unit, + node, + state.registers, + "register", + pop(unit, node, state) + ); + return state; + case SemanticOp.LOAD_ARG: + case SemanticOp.LOAD_ARG_OR_DEFAULT: + push(state, readFrame(unit, node, state.arguments, "argument")); + return state; + case SemanticOp.STORE_ARG: + writeFrame( + unit, + node, + state.arguments, + "argument", + pop(unit, node, state) + ); + return state; + case SemanticOp.LOAD_SLOT: + push(state, readFrame(unit, node, state.slots, "slot")); + return state; + case SemanticOp.STORE_SLOT: + writeFrame( + unit, + node, + state.slots, + "slot", + pop(unit, node, state) + ); + return state; + case SemanticOp.DECLARE_SLOT: { + const slot = node.operand & 0xffff; + writeFrameAt(unit, node, state.slots, "slot", slot, null); + return state; + } + default: + break; + } + + const layout = callLayout(node); + if (layout) { + applyKnownCallLayout(unit, node, state, layout); + clearAliasedStateAfterUserCode(state, node.op); + return state; + } + + switch (node.op) { + case SemanticOp.DIRECT_EVAL: + applyStackShape(unit, node, state, 1, 1); + state.registers.fill(null); + state.arguments.fill(null); + state.slots.fill(null); + return state; + case SemanticOp.DYNAMIC_IMPORT: + applyStackShape(unit, node, state, 1, 1); + state.slots.fill(null); + return state; + case SemanticOp.SUPER_CALL: { + const argumentCount = nonNegativeCallArgumentCount(unit, node); + applyStackShape(unit, node, state, argumentCount, 1); + state.slots.fill(null); + state.arguments.fill(null); + return state; + } + case SemanticOp.CALL_SUPER_METHOD: { + const argumentCount = node.operand & 0xffff; + applyStackShape(unit, node, state, argumentCount, 1); + state.slots.fill(null); + state.arguments.fill(null); + return state; + } + default: + break; + } + + const signature = getSemanticSignature(node.op); + const stackInput = resolveStackArity(signature.stackInput, node.operand); + const stackOutput = resolveStackArity(signature.stackOutput, node.operand); + applyResolvedStackShape(unit, node, state, stackInput, stackOutput); + + if ( + signature.operandKind === "register" && + (signature.frameAccess === "write" || + signature.frameAccess === "read-write" || + signature.frameAccess === "unknown") + ) { + writeFrame(unit, node, state.registers, "register", null); + } else if ( + signature.operandKind === "argument" && + (signature.frameAccess === "write" || + signature.frameAccess === "read-write" || + signature.frameAccess === "unknown") + ) { + writeFrame(unit, node, state.arguments, "argument", null); + } else if ( + signature.operandKind === "slot" && + (signature.frameAccess === "write" || + signature.frameAccess === "read-write" || + signature.frameAccess === "unknown") + ) { + writeFrame(unit, node, state.slots, "slot", null); + } + + if (signature.callKind !== "none") { + clearAliasedStateAfterUserCode(state, node.op); + } + if ( + signature.objectAccess === "write" || + signature.objectAccess === "read-write" || + signature.objectAccess === "unknown" + ) { + // The arguments object can alias argument slots. Without an explicit + // object-identity fact, any object write may target that alias. + state.arguments.fill(null); + } + if (signature.suspension !== "none") { + state.slots.fill(null); + state.arguments.fill(null); + } + return state; +} + +function callLayout(node: SemanticInstruction): CallLayout | null { + switch (node.op) { + case SemanticOp.CALL: + case SemanticOp.CALL_OPTIONAL: { + const argumentCount = absoluteArgumentCount(node); + return { + convention: "plain", + argumentCount, + calleeDepth: argumentCount, + inputCount: argumentCount + 1, + }; + } + case SemanticOp.CALL_METHOD: + case SemanticOp.CALL_METHOD_OPTIONAL: { + const argumentCount = absoluteArgumentCount(node); + return { + convention: "method", + argumentCount, + calleeDepth: argumentCount + 1, + inputCount: argumentCount + 2, + }; + } + case SemanticOp.CALL_NEW: { + const argumentCount = nonNegativeArgumentCount(node); + return { + convention: "construct", + argumentCount, + calleeDepth: argumentCount, + inputCount: argumentCount + 1, + }; + } + case SemanticOp.CALL_TAGGED_TEMPLATE: + case SemanticOp.TAGGED_TEMPLATE: { + const argumentCount = nonNegativeArgumentCount(node); + return { + convention: "tagged-template", + argumentCount, + calleeDepth: argumentCount, + inputCount: argumentCount + 1, + }; + } + case SemanticOp.CALL_0: + return { + convention: "fast-0", + argumentCount: 0, + calleeDepth: 0, + inputCount: 1, + }; + case SemanticOp.CALL_1: + return { + convention: "fast-1", + argumentCount: 1, + calleeDepth: 1, + inputCount: 2, + }; + case SemanticOp.CALL_2: + return { + convention: "fast-2", + argumentCount: 2, + calleeDepth: 2, + inputCount: 3, + }; + case SemanticOp.CALL_3: + return { + convention: "fast-3", + argumentCount: 3, + calleeDepth: 3, + inputCount: 4, + }; + default: + return null; + } +} + +function applyKnownCallLayout( + unit: SemanticUnit, + node: SemanticInstruction, + state: AbstractState, + layout: CallLayout +): void { + applyStackShape(unit, node, state, layout.inputCount, 1); +} + +function applyResolvedStackShape( + unit: SemanticUnit, + node: SemanticInstruction, + state: AbstractState, + input: ResolvedStackArity, + output: ResolvedStackArity +): void { + if (input === "dynamic" || output === "dynamic") { + state.stack = null; + return; + } + applyStackShape(unit, node, state, input, output); +} + +function applyStackShape( + unit: SemanticUnit, + node: SemanticInstruction, + state: AbstractState, + inputCount: number, + outputCount: number +): void { + if (state.stack == null) return; + assertStackDepth(unit, node, state.stack, inputCount); + state.stack.length -= inputCount; + for (let index = 0; index < outputCount; index++) state.stack.push(null); +} + +function clearAliasedStateAfterUserCode( + state: AbstractState, + op: SemanticOpValue +): void { + state.slots.fill(null); + state.arguments.fill(null); + if (op === SemanticOp.DIRECT_EVAL) { + state.registers.fill(null); + } +} + +function push(state: AbstractState, value: AbstractValue): void { + if (state.stack != null) state.stack.push(value); +} + +function pop( + unit: SemanticUnit, + node: SemanticInstruction, + state: AbstractState +): AbstractValue { + if (state.stack == null) return null; + assertStackDepth(unit, node, state.stack, 1); + return state.stack.pop() ?? null; +} + +function rotate( + unit: SemanticUnit, + node: SemanticInstruction, + state: AbstractState, + count: number +): void { + if (state.stack == null) return; + assertStackDepth(unit, node, state.stack, count); + const values = state.stack.splice(state.stack.length - count, count); + state.stack.push(values[count - 1]!, ...values.slice(0, -1)); +} + +function readFrame( + unit: SemanticUnit, + node: SemanticInstruction, + frame: readonly AbstractValue[], + kind: "register" | "argument" | "slot" +): AbstractValue { + validateFrameIndex(unit, node, frame, kind, node.operand); + return frame[node.operand] ?? null; +} + +function writeFrame( + unit: SemanticUnit, + node: SemanticInstruction, + frame: AbstractValue[], + kind: "register" | "argument" | "slot", + value: AbstractValue +): void { + writeFrameAt(unit, node, frame, kind, node.operand, value); +} + +function writeFrameAt( + unit: SemanticUnit, + node: SemanticInstruction, + frame: AbstractValue[], + kind: "register" | "argument" | "slot", + index: number, + value: AbstractValue +): void { + validateFrameIndex(unit, node, frame, kind, index); + frame[index] = value; +} + +function validateFrameIndex( + unit: SemanticUnit, + node: SemanticInstruction, + frame: readonly AbstractValue[], + kind: "register" | "argument" | "slot", + index: number +): void { + if (!Number.isSafeInteger(index) || index < 0 || index >= frame.length) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_${kind.toUpperCase()}_INDEX: ${unit.id}:${node.id} -> ${index} (size ${frame.length})` + ); + } +} + +function assertStackDepth( + unit: SemanticUnit, + node: SemanticInstruction, + stack: readonly AbstractValue[], + required: number +): void { + if ( + !Number.isSafeInteger(required) || + required < 0 || + stack.length < required + ) { + throw new Error( + `RUAM_DIRECT_TARGET_STACK_UNDERFLOW: ${unit.id}:${node.id} needs ${required}, has ${stack.length}` + ); + } +} + +function absoluteArgumentCount(node: SemanticInstruction): number { + if (!Number.isSafeInteger(node.operand)) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_CALL_OPERAND: ${node.id} -> ${node.operand}` + ); + } + return Math.abs(node.operand); +} + +function nonNegativeArgumentCount(node: SemanticInstruction): number { + if (!Number.isSafeInteger(node.operand) || node.operand < 0) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_CALL_OPERAND: ${node.id} -> ${node.operand}` + ); + } + return node.operand; +} + +function nonNegativeCallArgumentCount( + unit: SemanticUnit, + node: SemanticInstruction +): number { + try { + return nonNegativeArgumentCount(node); + } catch { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_CALL_OPERAND: ${unit.id}:${node.id} -> ${node.operand}` + ); + } +} + +function assertNonNegativeOperand( + unit: SemanticUnit, + node: SemanticInstruction +): void { + if (!Number.isSafeInteger(node.operand) || node.operand < 0) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_STACK_OPERAND: ${unit.id}:${node.id} -> ${node.operand}` + ); + } +} + +function initialState(unit: SemanticUnit): AbstractState { + return { + stack: [], + registers: Array.from({ length: unit.registerCount }, () => null), + arguments: Array.from( + { length: Math.max(unit.paramCount, 0) }, + () => null + ), + slots: Array.from({ length: unit.slotCount }, () => null), + }; +} + +function unknownState(unit: SemanticUnit): AbstractState { + return { + stack: null, + registers: Array.from({ length: unit.registerCount }, () => null), + arguments: Array.from( + { length: Math.max(unit.paramCount, 0) }, + () => null + ), + slots: Array.from({ length: unit.slotCount }, () => null), + }; +} + +function cloneState(state: AbstractState): AbstractState { + return { + stack: state.stack?.slice() ?? null, + registers: state.registers.slice(), + arguments: state.arguments.slice(), + slots: state.slots.slice(), + }; +} + +function joinStates( + left: AbstractState, + right: AbstractState +): AbstractState { + return { + stack: joinStack(left.stack, right.stack), + registers: joinFrame(left.registers, right.registers), + arguments: joinFrame(left.arguments, right.arguments), + slots: joinFrame(left.slots, right.slots), + }; +} + +function joinStack( + left: readonly AbstractValue[] | null, + right: readonly AbstractValue[] | null +): AbstractValue[] | null { + if (left == null || right == null || left.length !== right.length) { + return null; + } + return left.map((value, index) => + value != null && value === right[index] ? value : null + ); +} + +function joinFrame( + left: readonly AbstractValue[], + right: readonly AbstractValue[] +): AbstractValue[] { + if (left.length !== right.length) { + throw new Error( + `RUAM_DIRECT_TARGET_FRAME_SHAPE_MISMATCH: ${left.length} vs ${right.length}` + ); + } + return left.map((value, index) => + value != null && value === right[index] ? value : null + ); +} + +function sameState(left: AbstractState, right: AbstractState): boolean { + return ( + sameValues(left.stack, right.stack) && + sameValues(left.registers, right.registers) && + sameValues(left.arguments, right.arguments) && + sameValues(left.slots, right.slots) + ); +} + +function sameValues( + left: readonly AbstractValue[] | null, + right: readonly AbstractValue[] | null +): boolean { + if (left == null || right == null) return left === right; + if (left.length !== right.length) return false; + return left.every((value, index) => value === right[index]); +} + +function validateAndIndexGroup(group: SemanticRootGroup): IndexedGroup { + const units = new Map(); + for (const unit of group.units) { + if (units.has(unit.id)) { + throw new Error(`RUAM_DUPLICATE_DIRECT_TARGET_UNIT: ${unit.id}`); + } + if (unit.rootGroupId !== group.id) { + throw new Error( + `RUAM_DIRECT_TARGET_ROOT_GROUP_MISMATCH: ${unit.id}` + ); + } + if ( + !Number.isSafeInteger(unit.registerCount) || + unit.registerCount < 0 || + !Number.isSafeInteger(unit.paramCount) || + unit.paramCount < 0 || + !Number.isSafeInteger(unit.slotCount) || + unit.slotCount < 0 + ) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_FRAME_SHAPE: ${unit.id}` + ); + } + units.set(unit.id, unit); + } + if (!units.has(group.entryUnitId)) { + throw new Error( + `RUAM_DIRECT_TARGET_MISSING_ENTRY_UNIT: ${group.entryUnitId}` + ); + } + + const unitRefs = new Map< + SemanticUnitId, + ReadonlyMap + >(); + for (const unit of units.values()) { + if (unit.nodes.length === 0) { + throw new Error(`RUAM_EMPTY_DIRECT_TARGET_UNIT: ${unit.id}`); + } + if ( + !Number.isSafeInteger(unit.entryNode) || + unit.entryNode < 0 || + unit.entryNode >= unit.nodes.length + ) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_ENTRY_NODE: ${unit.id}:${unit.entryNode}` + ); + } + const children = new Set(); + for (const childId of unit.childUnitIds) { + if (children.has(childId)) { + throw new Error( + `RUAM_DUPLICATE_DIRECT_TARGET_CHILD: ${unit.id} -> ${childId}` + ); + } + children.add(childId); + if (!units.has(childId)) { + throw new Error( + `RUAM_UNKNOWN_DIRECT_TARGET_CHILD: ${unit.id} -> ${childId}` + ); + } + } + + const refs = new Map(); + for (let index = 0; index < unit.nodes.length; index++) { + const node = unit.nodes[index]!; + assertCanonicalSemanticOp(node.op); + if (node.id !== index) { + throw new Error( + `RUAM_NONDETERMINISTIC_DIRECT_TARGET_NODE_ORDER: ${unit.id}:${node.id}` + ); + } + if (!Number.isSafeInteger(node.operand)) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_OPERAND: ${unit.id}:${node.id}` + ); + } + const exits = unit.exits.get(node.id); + if (!exits) { + throw new Error( + `RUAM_DIRECT_TARGET_MISSING_EXITS: ${unit.id}:${node.id}` + ); + } + for (const exit of exits) { + const target = exitTarget(exit); + if ( + target != null && + (!Number.isSafeInteger(target) || + target < 0 || + target >= unit.nodes.length) + ) { + throw new Error( + `RUAM_DIRECT_TARGET_INVALID_EXIT: ${unit.id}:${node.id} -> ${target}` + ); + } + } + if (getSemanticSignature(node.op).operandKind !== "unit-ref") { + continue; + } + const constant = unit.constants[node.operand]; + if (constant?.type !== "string") { + throw new Error( + `RUAM_INVALID_DIRECT_TARGET_UNIT_REF: ${unit.id}:${node.id}` + ); + } + if ( + !units.has(constant.value) || + !unit.childUnitIds.includes(constant.value) + ) { + throw new Error( + `RUAM_UNKNOWN_DIRECT_TARGET_UNIT_REF: ${unit.id}:${node.id} -> ${constant.value}` + ); + } + refs.set(node.id, constant.value); + } + unitRefs.set(unit.id, refs); + } + return { units, unitRefs }; +} + +function exitTarget(exit: SemanticExit): SemanticNodeId | null { + if ("target" in exit) return exit.target; + if ("resume" in exit) return exit.resume; + return null; +} + +function isAbruptExit(exit: SemanticExit): boolean { + return exit.kind === "exception" || exit.kind === "finally"; +} + +function compareFacts( + left: CanonicalDirectCallTargetFact, + right: CanonicalDirectCallTargetFact +): number { + return ( + compareStrings(left.sourceUnitId, right.sourceUnitId) || + left.sourceNodeId - right.sourceNodeId || + compareStrings(left.targetUnitId, right.targetUnitId) + ); +} + +function compareStrings(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} diff --git a/packages/ruam/src/compiler/emitter.ts b/packages/ruam/src/compiler/emitter.ts index 6322991..eb99461 100644 --- a/packages/ruam/src/compiler/emitter.ts +++ b/packages/ruam/src/compiler/emitter.ts @@ -1,14 +1,25 @@ /** - * Bytecode emitter and constant pool manager. + * Temporary semantic emitter and literal-pool manager. * - * The {@link Emitter} accumulates instructions and constants during - * compilation. It also provides helpers for jump-patching and - * de-duplication of constant pool entries. + * The {@link Emitter} accumulates source operations and literals during + * canonical compilation. It also provides control-target patching and literal + * de-duplication. * * @module compiler/emitter */ -import type { Instruction, ConstantPoolEntry } from "../types.js"; +import type { + ConstantPoolEntry, + EmittedSemanticInstruction, +} from "./types.js"; +import type { SourceOrigin, SourceOriginId } from "./ir.js"; + +const UNKNOWN_SOURCE_ORIGIN: SourceOrigin = Object.freeze({ + start: 0, + end: 0, + line: 1, + column: 0, +}); /** * Bytecode emitter — the write side of compilation. @@ -22,14 +33,34 @@ import type { Instruction, ConstantPoolEntry } from "../types.js"; */ export class Emitter { /** Accumulated instruction stream. */ - readonly instructions: Instruction[] = []; + readonly instructions: EmittedSemanticInstruction[] = []; /** Accumulated constant pool. */ readonly constants: ConstantPoolEntry[] = []; + /** + * Deduplicated source locations referenced by + * {@link instructionOriginIds}. This metadata is deliberately parallel to + * the temporary operation array so origin tracking cannot change semantics. + */ + readonly origins: SourceOrigin[] = []; + + /** Source-origin index recorded at the moment each instruction is emitted. */ + readonly instructionOriginIds: SourceOriginId[] = []; + /** Map from serialised constant key → pool index (for de-duplication). */ private readonly constantMap = new Map(); + /** Map from a stable source-location key → origin table index. */ + private readonly originMap = new Map(); + + /** Origin inherited by emit calls in the current lexical compilation span. */ + private currentOriginId: SourceOriginId; + + constructor(defaultOrigin: SourceOrigin = UNKNOWN_SOURCE_ORIGIN) { + this.currentOriginId = this.addOrigin(defaultOrigin); + } + /** Current instruction pointer (= number of emitted instructions). */ get ip(): number { return this.instructions.length; @@ -43,9 +74,28 @@ export class Emitter { emit(opcode: number, operand: number = 0): number { const idx = this.instructions.length; this.instructions.push({ opcode, operand }); + this.instructionOriginIds.push(this.currentOriginId); return idx; } + /** + * Associate every instruction emitted by `emitWithin` with `origin`. + * + * The previous origin is restored even when a visitor throws, and nested + * spans behave like a lexical stack. Visitors can adopt this incrementally: + * instructions emitted outside a span retain the emitter's default origin. + */ + withOrigin(origin: SourceOrigin | null | undefined, emitWithin: () => T): T { + if (!origin) return emitWithin(); + const previousOriginId = this.currentOriginId; + this.currentOriginId = this.addOrigin(origin); + try { + return emitWithin(); + } finally { + this.currentOriginId = previousOriginId; + } + } + /** Patch a previously-emitted jump instruction's target. */ patchJump(instrIndex: number, target: number): void { this.instructions[instrIndex]!.operand = target; @@ -98,6 +148,17 @@ export class Emitter { addBigIntConstant(value: string): number { return this.addConstant({ type: "bigint", value }); } + + private addOrigin(origin: SourceOrigin): SourceOriginId { + const normalized = normalizeOrigin(origin); + const key = originKey(normalized); + const existing = this.originMap.get(key); + if (existing !== undefined) return existing; + const id = this.origins.length; + this.origins.push(Object.freeze(normalized)); + this.originMap.set(key, id); + return id; + } } // --------------------------------------------------------------------------- @@ -111,3 +172,31 @@ function constantKey(entry: ConstantPoolEntry): string { } return `${entry.type}:${String(entry.value)}`; } + +function normalizeOrigin(origin: SourceOrigin): SourceOrigin { + const start = Number.isSafeInteger(origin.start) && origin.start >= 0 + ? origin.start + : 0; + const end = Number.isSafeInteger(origin.end) && origin.end >= start + ? origin.end + : start; + const line = Number.isSafeInteger(origin.line) && origin.line >= 1 + ? origin.line + : 1; + const column = Number.isSafeInteger(origin.column) && origin.column >= 0 + ? origin.column + : 0; + return origin.file + ? { file: origin.file, start, end, line, column } + : { start, end, line, column }; +} + +function originKey(origin: SourceOrigin): string { + return [ + origin.file ?? "", + origin.start, + origin.end, + origin.line, + origin.column, + ].join(":"); +} diff --git a/packages/ruam/src/compiler/encode.ts b/packages/ruam/src/compiler/encode.ts deleted file mode 100644 index 2d384bd..0000000 --- a/packages/ruam/src/compiler/encode.ts +++ /dev/null @@ -1,518 +0,0 @@ -/** - * Bytecode serialization — JSON and compact binary formats. - * - * Two serialization strategies are provided: - * - * - **JSON** ({@link serializeUnitToJson}) — human-debuggable, larger output. - * Used by default. - * - **Binary** ({@link encodeBytecodeUnit}) — compact `Uint8Array` format - * that can optionally be RC4-encrypted and base64-encoded. - * - * @module compiler/encode - */ - -import type { BytecodeUnit, ConstantPoolEntry } from "../types.js"; -import { computeFingerprint } from "./fingerprint.js"; -import { rc4, customEncode } from "./crypto.js"; -import { - LCG_MULTIPLIER, - LCG_INCREMENT, - GOLDEN_RATIO_PRIME, - BINARY_TAG_NULL, - BINARY_TAG_UNDEFINED, - BINARY_TAG_FALSE, - BINARY_TAG_TRUE, - BINARY_TAG_INT8, - BINARY_TAG_INT16, - BINARY_TAG_INT32, - BINARY_TAG_FLOAT64, - BINARY_TAG_BIGINT, - BINARY_TAG_REGEX, - BINARY_TAG_STRING, - BINARY_TAG_ENCODED_STRING, -} from "../constants.js"; -import { deriveImplicitKey, rollingEncrypt } from "./rolling-cipher.js"; -import { buildCipherBlocks, incrementalEncrypt } from "./incremental-cipher.js"; - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** Options for {@link encodeBytecodeUnit}. */ -export interface EncodeOptions { - /** Logical → physical opcode shuffle map. */ - shuffleMap: number[]; - /** When `true`, RC4-encrypt the binary output. */ - encrypt: boolean; - /** Apply rolling cipher encryption to the instruction stream. */ - rollingCipher?: boolean; - /** Integrity hash to fold into the rolling cipher key. */ - integrityHash?: number; - /** Per-build cipher salt mixed into the rolling cipher key derivation. */ - cipherSalt?: number; - /** Key anchor — replaces FNV offset basis in key derivation, entangled with handler table. */ - keyAnchor?: number; - /** XOR string encoding key for constant pool strings. */ - stringKey?: number; - /** Custom encoding alphabet (shuffled 64-char string). */ - alphabet: string; - /** Apply incremental cipher encryption on top of rolling cipher. */ - incrementalCipher?: boolean; - /** Pre-computed cipher blocks (needed when opcode mutation modifies the unit). */ - precomputedCipherBlocks?: ReturnType; -} - -/** - * Serialize a bytecode unit to a compact binary format, optionally encrypted. - * - * @returns A base64-encoded string (suitable for embedding in JS source). - */ -export function encodeBytecodeUnit( - unit: BytecodeUnit, - options: EncodeOptions -): string { - const bytes = serializeUnit( - unit, - options.shuffleMap, - options.rollingCipher, - options.integrityHash, - options.cipherSalt, - options.keyAnchor, - options.stringKey, - options.incrementalCipher, - options.precomputedCipherBlocks - ); - if (options.encrypt) { - const key = computeFingerprint().toString(16); - const encrypted = rc4(bytes, key); - return customEncode(encrypted, options.alphabet); - } - return customEncode(bytes, options.alphabet); -} - -/** - * XOR-encode a string's char codes using an LCG key stream. - * - * Each string gets a unique key stream derived from the master key and - * the constant pool index, so identical strings at different positions - * produce different encodings. - */ -export function encodeStringChars( - str: string, - key: number, - index: number -): number[] { - const encoded: number[] = []; - let k = (key ^ (index * GOLDEN_RATIO_PRIME)) >>> 0; - for (let i = 0; i < str.length; i++) { - k = (k * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - encoded.push(str.charCodeAt(i) ^ (k & 0xffff)); - } - return encoded; -} - -/** Options controlling JSON serialization behavior. */ -export interface JsonSerializeOptions { - /** Logical → physical opcode shuffle map. */ - shuffleMap: number[]; - /** XOR string encoding key (omit to leave strings as plaintext). */ - stringKey?: number; - /** Apply rolling cipher encryption to the instruction stream. */ - rollingCipher?: boolean; - /** Integrity hash to fold into the rolling cipher key. */ - integrityHash?: number; - /** Per-build cipher salt mixed into the rolling cipher key derivation. */ - cipherSalt?: number; - /** Key anchor — replaces FNV offset basis in key derivation, entangled with handler table. */ - keyAnchor?: number; -} - -/** - * Serialize a bytecode unit to a JSON string. - * - * The output uses short property names (`c`, `i`, `r`, `p`, …) to reduce - * size. Special constant types (regex, bigint) are encoded as tagged objects - * that the runtime decoder can recognise. - * - * When `stringKey` is provided, string constants are XOR-encoded and - * stored as number arrays instead of plaintext strings. - */ -export function serializeUnitToJson( - unit: BytecodeUnit, - opts: JsonSerializeOptions -): string; -export function serializeUnitToJson( - unit: BytecodeUnit, - shuffleMap: number[], - stringKey?: number -): string; -export function serializeUnitToJson( - unit: BytecodeUnit, - optsOrMap: JsonSerializeOptions | number[], - stringKeyLegacy?: number -): string { - // Normalise overloaded arguments - let shuffleMap: number[]; - let stringKey: number | undefined; - let rollingCipher = false; - let integrityHash: number | undefined; - let cipherSalt: number | undefined; - let keyAnchor: number | undefined; - - if (Array.isArray(optsOrMap)) { - shuffleMap = optsOrMap; - stringKey = stringKeyLegacy; - } else { - shuffleMap = optsOrMap.shuffleMap; - stringKey = optsOrMap.stringKey; - rollingCipher = optsOrMap.rollingCipher ?? false; - integrityHash = optsOrMap.integrityHash; - cipherSalt = optsOrMap.cipherSalt; - keyAnchor = optsOrMap.keyAnchor; - } - - // Combine key anchor + integrity hash into a single effective anchor - // that replaces the FNV offset basis in deriveImplicitKey. - // Must match what rcDeriveKey() produces at runtime. - let effectiveAnchor = keyAnchor; - if (effectiveAnchor !== undefined && integrityHash !== undefined) { - effectiveAnchor = (effectiveAnchor ^ integrityHash) >>> 0; - } - - // When rolling cipher is on, use the implicit key for string encoding - // so no plaintext seed appears in the output. - // Must match what rcDeriveKey() produces at runtime. - let effectiveStringKey = stringKey; - if (rollingCipher && stringKey !== undefined) { - const k = deriveImplicitKey( - unit.instructions.length, - unit.registerCount, - unit.paramCount, - unit.constants.length, - cipherSalt, - effectiveAnchor - ); - effectiveStringKey = k; - } - - const constants: unknown[] = unit.constants.map((c, idx) => { - if (c.type === "regex") { - return { __regex__: true, p: c.value.pattern, f: c.value.flags }; - } - if (c.type === "bigint") return { __bigint__: true, v: c.value }; - if (c.type === "string" && effectiveStringKey !== undefined) { - return encodeStringChars(c.value, effectiveStringKey, idx); - } - return c.value; - }); - - const instrs: number[] = []; - for (const instr of unit.instructions) { - instrs.push(shuffleMap[instr.opcode]!); - instrs.push(instr.operand); - } - - // Apply rolling cipher if enabled (must happen after shuffle but before serialization) - if (rollingCipher) { - const masterKey = deriveImplicitKey( - unit.instructions.length, - unit.registerCount, - unit.paramCount, - unit.constants.length, - cipherSalt, - effectiveAnchor - ); - rollingEncrypt(instrs, masterKey); - } - - return JSON.stringify({ - c: constants, - i: instrs, - r: unit.registerCount, - sl: unit.slotCount || 0, - p: unit.paramCount, - g: unit.isGenerator, - s: unit.isAsync, - st: unit.isStrict, - a: unit.isArrow || false, - el: unit.scopeless || false, - xh: unit.usesExceptions || false, - tc: unit.usesThisContext || false, - }); -} - -// --------------------------------------------------------------------------- -// Binary serialization internals -// --------------------------------------------------------------------------- - -/** Serialize a bytecode unit into a compact `Uint8Array`. */ -function serializeUnit( - unit: BytecodeUnit, - shuffleMap: number[], - applyRollingCipher: boolean = false, - integrityHash?: number, - cipherSalt?: number, - keyAnchor?: number, - stringKey?: number, - applyIncrementalCipher: boolean = false, - precomputedCipherBlocks?: ReturnType -): Uint8Array { - const buf = new ArrayBuffer(estimateSize(unit)); - const view = new DataView(buf); - let offset = 0; - - function writeU8(v: number) { - view.setUint8(offset, v); - offset += 1; - } - function writeU16(v: number) { - view.setUint16(offset, v, true); - offset += 2; - } - function writeU32(v: number) { - view.setUint32(offset, v, true); - offset += 4; - } - function writeI32(v: number) { - view.setInt32(offset, v, true); - offset += 4; - } - function writeF64(v: number) { - view.setFloat64(offset, v, true); - offset += 8; - } - function writeStr(s: string) { - const bytes = new TextEncoder().encode(s); - writeU32(bytes.length); - for (let i = 0; i < bytes.length; i++) writeU8(bytes[i]!); - } - - // Compute effective string key (same derivation as JSON path) - let effectiveStringKey = stringKey; - if (applyRollingCipher && stringKey !== undefined) { - let effectiveAnchor = keyAnchor; - if (effectiveAnchor !== undefined && integrityHash !== undefined) { - effectiveAnchor = (effectiveAnchor ^ integrityHash) >>> 0; - } - effectiveStringKey = deriveImplicitKey( - unit.instructions.length, - unit.registerCount, - unit.paramCount, - unit.constants.length, - cipherSalt, - effectiveAnchor - ); - } - - // Header - const flags = - (unit.isGenerator ? 1 : 0) | - (unit.isAsync ? 2 : 0) | - (unit.isStrict ? 4 : 0) | - (unit.isArrow ? 8 : 0) | - (unit.scopeless ? 16 : 0) | - // bit 32: unit uses exception-completion machinery (PE/HPE/CT/CV). - // Precomputed at compile time on LOGICAL opcodes — `unit.instructions` - // here may already be physical (adjustEncodingForMutations). - (unit.usesExceptions ? 32 : 0) | - // bit 64: unit reads this-context slots (TV/NT/HO). - (unit.usesThisContext ? 64 : 0); - - writeU8(1); // format version - writeU16(flags); - writeU16(unit.paramCount); - writeU16(unit.registerCount); - - // Constants - writeU32(unit.constants.length); - for (let ci = 0; ci < unit.constants.length; ci++) { - const c = unit.constants[ci]!; - writeConstant( - c, - ci, - writeU8, - writeU16, - writeI32, - writeF64, - writeStr, - effectiveStringKey - ); - } - - // Build flat instruction array with shuffled opcodes - const flatInstrs: number[] = []; - for (const instr of unit.instructions) { - flatInstrs.push(shuffleMap[instr.opcode]!); - flatInstrs.push(instr.operand); - } - - // Apply rolling cipher encryption if enabled (must happen after shuffle) - // Compute master key for both rolling cipher and incremental cipher - let masterKey: number | undefined; - if (applyRollingCipher) { - // Combine key anchor + integrity hash into effective anchor - let effectiveAnchor = keyAnchor; - if (effectiveAnchor !== undefined && integrityHash !== undefined) { - effectiveAnchor = (effectiveAnchor ^ integrityHash) >>> 0; - } - masterKey = deriveImplicitKey( - unit.instructions.length, - unit.registerCount, - unit.paramCount, - unit.constants.length, - cipherSalt, - effectiveAnchor - ); - rollingEncrypt(flatInstrs, masterKey); - } - - // Apply incremental cipher on top of rolling cipher (outer layer). - // Uses pre-computed cipher blocks when available (necessary for opcode - // mutation which modifies the unit's opcodes before serialization). - let cipherBlocks: ReturnType | undefined; - if (applyIncrementalCipher && masterKey !== undefined) { - cipherBlocks = precomputedCipherBlocks ?? buildCipherBlocks(unit); - incrementalEncrypt(flatInstrs, masterKey, cipherBlocks); - } - - // Write instructions to binary buffer - writeU32(unit.instructions.length); - for (let i = 0; i < flatInstrs.length; i += 2) { - writeU16(flatInstrs[i]!); - writeI32(flatInstrs[i + 1]!); - } - - // Jump table - writeU32(Object.keys(unit.jumpTable).length); - for (const [ip, target] of Object.entries(unit.jumpTable)) { - writeU32(Number(ip)); - writeU32(target); - } - - // Exception table - writeU32(unit.exceptionTable.length); - for (const entry of unit.exceptionTable) { - writeU32(entry.startIp); - writeU32(entry.endIp); - writeI32(entry.catchIp); - writeI32(entry.finallyIp); - } - - // Block leader map (incremental cipher) — serialize block boundaries - // as (startIp, blockId) pairs so runtime can reconstruct block leader set. - if (cipherBlocks !== undefined) { - writeU32(cipherBlocks.length); - for (const block of cipherBlocks) { - writeU32(block.startIp); - writeU32(block.blockId); - } - } else { - writeU32(0); // no blocks — incremental cipher disabled - } - - // Function name constant index - writeI32(unit.nameConstIndex); - - return new Uint8Array(buf, 0, offset); -} - -/** Write a single constant pool entry to the binary buffer. */ -function writeConstant( - c: ConstantPoolEntry, - constIndex: number, - writeU8: (v: number) => void, - writeU16: (v: number) => void, - writeI32: (v: number) => void, - writeF64: (v: number) => void, - writeStr: (s: string) => void, - stringKey?: number -): void { - switch (c.type) { - case "null": - writeU8(BINARY_TAG_NULL); - break; - case "undefined": - writeU8(BINARY_TAG_UNDEFINED); - break; - case "boolean": - writeU8(c.value ? BINARY_TAG_TRUE : BINARY_TAG_FALSE); - break; - case "number": { - const n = c.value; - if (Number.isInteger(n)) { - if (n >= -128 && n <= 127) { - writeU8(BINARY_TAG_INT8); - writeU8(n & 0xff); - } else if (n >= -32768 && n <= 32767) { - writeU8(BINARY_TAG_INT16); - writeU8(n & 0xff); - writeU8((n >> 8) & 0xff); - } else if (n >= -2147483648 && n <= 2147483647) { - writeU8(BINARY_TAG_INT32); - writeI32(n); - } else { - writeU8(BINARY_TAG_FLOAT64); - writeF64(n); - } - } else { - writeU8(BINARY_TAG_FLOAT64); - writeF64(n); - } - break; - } - case "string": - if (stringKey !== undefined) { - // XOR-encode string char codes for defense in depth - const encoded = encodeStringChars( - c.value, - stringKey, - constIndex - ); - writeU8(BINARY_TAG_ENCODED_STRING); - writeU16(encoded.length); // char count (not byte count) - for (const v of encoded) writeU16(v); - } else { - writeU8(BINARY_TAG_STRING); - writeStr(c.value); - } - break; - case "bigint": - writeU8(BINARY_TAG_BIGINT); - writeStr(c.value); - break; - case "regex": { - const v = c.value; - writeU8(BINARY_TAG_REGEX); - writeStr(v.pattern); - writeStr(v.flags); - break; - } - } -} - -/** - * Conservatively estimate the byte size of a serialized unit. - * Over-allocates to avoid reallocation. - */ -function estimateSize(unit: BytecodeUnit): number { - let size = 1 + 2 + 2 + 2; // header - size += 4; // constant count - for (const c of unit.constants) { - size += 1; // type tag - if (c.type === "string") size += 4 + c.value.length * 3; - // covers both UTF-8 and u16 encoding - else if (c.type === "number") size += 8; - else if (c.type === "bigint") size += 4 + c.value.length * 3; - else if (c.type === "regex") { - size += 8 + c.value.pattern.length * 3 + c.value.flags.length * 3; - } else { - size += 8; - } - } - size += 4 + unit.instructions.length * 6; // instructions - size += 4 + Object.keys(unit.jumpTable).length * 8; // jump table - size += 4 + unit.exceptionTable.length * 16; // exception table - size += 4 + unit.instructions.length * 8; // block leader map (worst case: 1 block per instruction) - size += 4; // name const index - return size + 1024; // safety margin -} diff --git a/packages/ruam/src/compiler/fingerprint.ts b/packages/ruam/src/compiler/fingerprint.ts deleted file mode 100644 index a399ff5..0000000 --- a/packages/ruam/src/compiler/fingerprint.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Environment fingerprinting for bytecode encryption. - * - * Generates a deterministic hash from the host engine's built-in function - * `.length` properties. This produces a value that is the same for a - * given JS engine version but differs across engines, providing a weak - * form of environment binding. - * - * @module compiler/fingerprint - */ - -// The fingerprint uses an inverted-square-root magic constant as a seed, -// then XORs in the `.length` of several built-in functions at different -// bit positions. The result is mixed with a Murmur3-style finalizer. - -const SEED = 0x5f3759df; - -/** - * Compute the fingerprint at build time (for encrypting bytecode before - * the runtime exists). - * - * Must produce the exact same value as the runtime version. - */ -export function computeFingerprint(): number { - let h = SEED; - h ^= Array.prototype.reduce.length << 0x18; - h ^= String.prototype.charCodeAt.length << 0x14; - h ^= Math.floor.length << 0x10; - h ^= Object.keys.length << 0x0c; - h ^= JSON.stringify.length << 0x08; - h ^= parseInt.length << 0x04; - h = (h ^ (h >>> 16)) * 0x45d9f3b; - h = (h ^ (h >>> 13)) * 0x45d9f3b; - h = h ^ (h >>> 16); - return h >>> 0; -} diff --git a/packages/ruam/src/compiler/incremental-cipher.ts b/packages/ruam/src/compiler/incremental-cipher.ts deleted file mode 100644 index 5d67df9..0000000 --- a/packages/ruam/src/compiler/incremental-cipher.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Build-time incremental cipher — block-epoch keyed instruction encryption. - * - * Each basic block gets a base key derived from (masterKey, blockId). - * Within a block, each instruction's decryption key chains from the - * previous instruction's decrypted values, creating sequential - * dependency. At block boundaries the chain resets to the target block's - * base key. - * - * @module compiler/incremental-cipher - */ - -import { - FNV_PRIME, - MIX_PRIME1, - MIX_PRIME2, - GOLDEN_RATIO_PRIME, -} from "../constants.js"; -import type { BytecodeUnit } from "../types.js"; -import { identifyBasicBlocks, type BasicBlock } from "./basic-blocks.js"; - -// --- Types --- - -/** A cipher block: basic block with an assigned sequential ID. */ -export interface CipherBlock extends BasicBlock { - /** Sequential block ID used for key derivation. */ - blockId: number; -} - -// --- Key derivation --- - -/** - * Derive a per-block base key from the master key and block ID. - * - * Uses FNV-1a-style mixing with Murmur3 finalization to produce - * a well-distributed 32-bit unsigned key. - * - * @param masterKey - The master encryption key for the unit. - * @param blockId - Sequential block identifier. - * @returns A 32-bit unsigned per-block base key. - */ -export function deriveBlockKey(masterKey: number, blockId: number): number { - let h = masterKey; - h = Math.imul(h ^ blockId, FNV_PRIME) >>> 0; - h = - Math.imul( - h ^ (Math.imul(blockId, GOLDEN_RATIO_PRIME) >>> 0), - MIX_PRIME1 - ) >>> 0; - h ^= h >>> 16; - h = Math.imul(h, MIX_PRIME2) >>> 0; - h ^= h >>> 13; - return h >>> 0; -} - -// --- Chain feedback --- - -/** - * Chain feedback — mix current state with decrypted instruction values. - * - * Advances the chain state based on the plaintext opcode and operand, - * creating sequential dependency within a basic block. - * - * @param state - Current chain state. - * @param opcode - Decrypted (plaintext) opcode value. - * @param operand - Decrypted (plaintext) operand value. - * @returns Updated 32-bit unsigned chain state. - */ -export function chainMix( - state: number, - opcode: number, - operand: number -): number { - let h = state; - h = Math.imul(h ^ opcode, MIX_PRIME1) >>> 0; - h = Math.imul(h ^ operand, MIX_PRIME2) >>> 0; - h ^= h >>> 16; - return h >>> 0; -} - -// --- Cipher block construction --- - -/** - * Build cipher blocks from a bytecode unit's basic blocks. - * - * Uses {@link identifyBasicBlocks} to find block boundaries and assigns - * sequential block IDs starting from 0. - * - * @param unit - The bytecode unit to analyze. - * @returns Array of cipher blocks with assigned IDs. - */ -export function buildCipherBlocks(unit: BytecodeUnit): CipherBlock[] { - const blocks = identifyBasicBlocks(unit); - return blocks.map((block, index) => ({ - startIp: block.startIp, - endIp: block.endIp, - blockId: index, - })); -} - -// --- Incremental encryption --- - -/** - * Encrypt a flat instruction array in-place using block-epoch keyed - * incremental encryption. - * - * The instruction array is a flat `[opcode, operand, opcode, operand, ...]` - * sequence where each instruction occupies two consecutive slots. - * - * For each block: - * 1. Initialize chainState = deriveBlockKey(masterKey, block.blockId) - * 2. For each instruction in the block: - * a. Save plaintext opcode and operand before encryption - * b. XOR opcode with lower 16 bits of chain state - * c. XOR operand with full 32 bits of chain state (signed) - * d. Advance chain using the PLAINTEXT values - * - * At runtime, the decryptor performs the inverse: - * 1. XOR to recover plaintext - * 2. Advance chain with the recovered plaintext - * - * Both sides produce identical chain state progression because the chain - * is always advanced with plaintext values. - * - * @param instrs - Flat instruction array `[op, operand, op, operand, ...]`. - * Modified in-place. - * @param masterKey - Master encryption key for the unit. - * @param blocks - Cipher blocks from {@link buildCipherBlocks}. - */ -export function incrementalEncrypt( - instrs: number[], - masterKey: number, - blocks: CipherBlock[] -): void { - for (const block of blocks) { - let chainState = deriveBlockKey(masterKey, block.blockId); - - for (let ip = block.startIp; ip < block.endIp; ip++) { - const opcodeIdx = ip * 2; - const operandIdx = ip * 2 + 1; - - // Save plaintext values before encryption - const plainOp = instrs[opcodeIdx]!; - const plainOperand = instrs[operandIdx]!; - - // Encrypt: XOR with chain state - instrs[opcodeIdx] = (plainOp ^ (chainState & 0xffff)) & 0xffff; - instrs[operandIdx] = (plainOperand ^ chainState) | 0; - - // Advance chain using PLAINTEXT values (same as runtime will use - // after decryption — ensures identical chain progression) - chainState = chainMix(chainState, plainOp, plainOperand); - } - } -} diff --git a/packages/ruam/src/compiler/index.ts b/packages/ruam/src/compiler/index.ts index 4b472d5..e61f4da 100644 --- a/packages/ruam/src/compiler/index.ts +++ b/packages/ruam/src/compiler/index.ts @@ -1,9 +1,9 @@ /** - * Main bytecode compiler entry point. + * Canonical semantic compiler entry point. * - * {@link compileFunction} is the public API — it takes a Babel - * `NodePath` and produces a {@link BytecodeUnit} (plus any - * child units for nested functions). + * Babel visitors emit a temporary source-operation stream which is frozen + * into execution-independent semantic IR. No serialized instruction artifact + * or executable backend leaves this module. * * @module compiler */ @@ -12,7 +12,7 @@ import type { NodePath } from "@babel/traverse"; import type * as t from "@babel/types"; import { Emitter } from "./emitter.js"; import { ScopeAnalyzer } from "./scope.js"; -import { Op } from "./opcodes.js"; +import { Op } from "./operations.js"; import { compileExpression } from "./visitors/expressions.js"; import { compileBody, @@ -20,17 +20,106 @@ import { type LoopContext, } from "./visitors/statements.js"; import { compileClassExpr } from "./visitors/classes.js"; -import type { BytecodeUnit } from "../types.js"; +import type { + RootGroupId, + SemanticCompileUnit, +} from "./types.js"; import { computeUsesExceptions, computeUsesThisContext, -} from "./slot-analysis.js"; +} from "./metadata-analysis.js"; import { LCG_MULTIPLIER, LCG_INCREMENT } from "../constants.js"; import { analyzeCapturedVars, type CaptureAnalysisResult, } from "./capture-analysis.js"; -import { optimizeInstructions } from "./optimizer.js"; +import { buildCanonicalCfg } from "./cfg.js"; +import type { + SemanticRootGroup, + SemanticUnit, + SourceOrigin, +} from "./ir.js"; + +export { buildCanonicalCallGraphInventory } from "./call-graph.js"; +export type { + CanonicalBoundaryKind, + CanonicalBoundaryObservability, + CanonicalCallBoundary, + CanonicalCallGraphInventory, + CanonicalCallGraphSummary, + CanonicalCallResolution, + CanonicalCallScc, + CanonicalCallSource, + CanonicalCallTargetPolicy, + CanonicalClosureSite, + CanonicalDirectCallEdge, + CanonicalUnresolvedCallReason, +} from "./call-graph.js"; +export { analyzeCanonicalDirectCallTargets } from "./direct-call-targets.js"; +export type { + CanonicalDirectCallConvention, + CanonicalDirectCallTargetFact, + CanonicalDirectCallTargetInventory, +} from "./direct-call-targets.js"; +export { + buildEffectRegionGraph, + validateEffectRegionGraph, +} from "./regions.js"; +export type { + EffectRegion, + EffectRegionExit, + EffectRegionGraph, + EffectRegionId, + RegionBoundaryContract, + RegionEffectSummary, + RegionStateDomain, + RegionStatePort, +} from "./regions.js"; +export { + MAX_PURE_REGION_INTEGER_MAGNITUDE, + isPureRegionValueInDomain, + lowerEffectRegionsToPureContract, +} from "./pure-region-lowering.js"; +export type { + LoweredPureRegionContract, + PureRegionBinding, + PureRegionInputAssumption, + PureRegionInputBinding, + PureRegionLoweringRequest, + PureRegionOutputBinding, + PureRegionValueDomain, +} from "./pure-region-lowering.js"; +export { planPureRegionCandidates } from "./pure-region-planning.js"; +export type { + PlannedPureRegionCandidate, + PureRegionCandidatePlan, + PureRegionCandidateRejection, + PureRegionCandidateRejectionCode, + PureRegionEntryAssumptionMap, + PureRegionEntryAssumptionProvider, + PureRegionEntryAssumptionSource, + PureRegionLoweringRejectionCode, +} from "./pure-region-planning.js"; +export { + analyzePureRegionLearnability, + assessMaximumCustodyLearnability, + PURE_REGION_LEARNABILITY_NON_CLAIM, +} from "./pure-region-learnability.js"; +export type { + DenseInterpolationAnalysis, + DenseInterpolationInapplicability, + MaximumCustodyLearnabilityDecision, + MaximumCustodyLearnabilityPolicy, + MaximumCustodyLearnabilityReason, + PureRegionExactAttackMethod, + PureRegionExactAttackUpperBound, + PureRegionInputDomainAnalysis, + PureRegionLearnabilityAnalysis, + PureRegionLearnabilityIssue, + PureRegionLearnabilityIssueCode, + PureRegionOutputLearnabilityAnalysis, + PureRegionValueDegreeAnalysis, +} from "./pure-region-learnability.js"; // --------------------------------------------------------------------------- // Scope-object elision @@ -136,7 +225,7 @@ export function resetUnitCounter(seed?: number): void { } /** - * Generate the next unique bytecode unit ID. + * Generate the next unique semantic unit ID. * Uses a seeded LCG to produce random-looking alphanumeric IDs * (e.g. `"k7m2"`, `"x9fp"`) instead of sequential `u_NNNN`. */ @@ -185,21 +274,62 @@ export interface CompileContext { blockDepth: number; } +type SemanticUnitDraft = Omit; + // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /** - * Compile a single top-level function into a bytecode unit. + * Compile one protected root into execution-independent semantic IR. * - * Nested functions / classes are recursively compiled into child units - * that are attached to the returned unit's {@link BytecodeUnit.childUnits}. + * Temporary visitor emissions are discarded after the root group is frozen. */ -export function compileFunction(fnPath: NodePath): BytecodeUnit { - const allUnits: BytecodeUnit[] = []; - const unit = compileFunctionInner(fnPath, allUnits); - unit.childUnits = allUnits; - return unit; +export function compileSemanticFunction( + fnPath: NodePath, + rootGroupId?: RootGroupId +): SemanticRootGroup { + const compileChildren: SemanticCompileUnit[] = []; + const drafts: SemanticUnitDraft[] = []; + const compileRoot = compileFunctionInner(fnPath, compileChildren, drafts); + const resolvedRootGroupId = rootGroupId ?? `rg_${compileRoot.id}`; + const draftById = new Map(drafts.map((draft) => [draft.id, draft])); + const orderedIds: string[] = []; + const visitedIds = new Set(); + const visitUnit = (id: string): void => { + if (visitedIds.has(id)) return; + visitedIds.add(id); + orderedIds.push(id); + const draft = draftById.get(id); + if (!draft) throw new Error(`RUAM_MISSING_SEMANTIC_UNIT: ${id}`); + for (const childId of draft.childUnitIds) visitUnit(childId); + }; + visitUnit(compileRoot.id); + if (orderedIds.length !== drafts.length) { + throw new Error( + `RUAM_ORPHANED_SEMANTIC_UNIT: reached ${orderedIds.length} of ${drafts.length}` + ); + } + const units = orderedIds.map((id) => { + const draft = draftById.get(id); + if (!draft) { + throw new Error(`RUAM_MISSING_SEMANTIC_UNIT: ${id}`); + } + return Object.freeze({ + ...draft, + rootGroupId: resolvedRootGroupId, + }) as SemanticUnit; + }); + const usedSemantics = new Set(units.flatMap((unit) => unit.nodes.map((node) => node.op))); + + return { + id: resolvedRootGroupId, + entryUnitId: compileRoot.id, + units, + usedSemantics, + hasAsync: units.some((unit) => unit.isAsync), + hasGenerator: units.some((unit) => unit.isGenerator), + }; } // --------------------------------------------------------------------------- @@ -207,21 +337,21 @@ export function compileFunction(fnPath: NodePath): BytecodeUnit { // --------------------------------------------------------------------------- /** - * Inner function compiler — produces a single BytecodeUnit. - * - * Called both for top-level functions and recursively for nested - * functions/closures. + * Inner source compiler. Its temporary unit exists only to support recursive + * visitor composition and is never serialized or executed. */ function compileFunctionInner( fnPath: NodePath, - allUnits: BytecodeUnit[] -): BytecodeUnit { + allUnits: SemanticCompileUnit[], + semanticDrafts?: SemanticUnitDraft[] +): SemanticCompileUnit { const node = fnPath.node; const params = fnPath.get("params") as NodePath[]; const paramCount = params.length; - const emitter = new Emitter(); + const emitter = new Emitter(sourceOriginFromPath(fnPath)); const scope = new ScopeAnalyzer(0); + const directChildUnitIds: string[] = []; const isStrict = detectStrict(fnPath); const isGenerator = !!node.generator; @@ -257,56 +387,58 @@ function compileFunctionInner( // -- Declare simple parameters ------------------------------------------- for (let i = 0; i < params.length; i++) { const param = params[i]!; - if (param.isIdentifier()) { - declareAndStoreParam( - param.node.name, - i, - emitter, - scope, - registerMap, - slotMap, - captureResult - ); - } else if (param.isAssignmentPattern()) { - const left = param.get("left"); - if (left.isIdentifier()) { - scope.declare(left.node.name, "param"); - const pName = left.node.name; - if (registerMap.has(pName)) { - // Will be stored via register in compileComplexParams - } else if (slotMap.has(pName)) { - const slotIdx = slotMap.get(pName)!; - const nameIdx = emitter.addStringConstant(pName); - emitter.emit( - Op.DECLARE_SLOT, - (slotIdx & 0xffff) | ((nameIdx & 0xffff) << 16) - ); - } else { - const nameIdx = emitter.addStringConstant(pName); - emitter.emit(Op.DECLARE_VAR, nameIdx); + emitter.withOrigin(sourceOriginFromPath(param), () => { + if (param.isIdentifier()) { + declareAndStoreParam( + param.node.name, + i, + emitter, + scope, + registerMap, + slotMap, + captureResult + ); + } else if (param.isAssignmentPattern()) { + const left = param.get("left"); + if (left.isIdentifier()) { + scope.declare(left.node.name, "param"); + const pName = left.node.name; + if (registerMap.has(pName)) { + // Will be stored via register in compileComplexParams + } else if (slotMap.has(pName)) { + const slotIdx = slotMap.get(pName)!; + const nameIdx = emitter.addStringConstant(pName); + emitter.emit( + Op.DECLARE_SLOT, + (slotIdx & 0xffff) | ((nameIdx & 0xffff) << 16) + ); + } else { + const nameIdx = emitter.addStringConstant(pName); + emitter.emit(Op.DECLARE_VAR, nameIdx); + } } - } - } else if (param.isRestElement()) { - const arg = param.get("argument"); - if (arg.isIdentifier()) { - scope.declare(arg.node.name, "param"); - const pName = arg.node.name; - if (registerMap.has(pName)) { - // Will be stored via register in compileComplexParams - } else if (slotMap.has(pName)) { - const slotIdx = slotMap.get(pName)!; - const nameIdx = emitter.addStringConstant(pName); - emitter.emit( - Op.DECLARE_SLOT, - (slotIdx & 0xffff) | ((nameIdx & 0xffff) << 16) - ); - } else { - const nameIdx = emitter.addStringConstant(pName); - emitter.emit(Op.DECLARE_VAR, nameIdx); + } else if (param.isRestElement()) { + const arg = param.get("argument"); + if (arg.isIdentifier()) { + scope.declare(arg.node.name, "param"); + const pName = arg.node.name; + if (registerMap.has(pName)) { + // Will be stored via register in compileComplexParams + } else if (slotMap.has(pName)) { + const slotIdx = slotMap.get(pName)!; + const nameIdx = emitter.addStringConstant(pName); + emitter.emit( + Op.DECLARE_SLOT, + (slotIdx & 0xffff) | ((nameIdx & 0xffff) << 16) + ); + } else { + const nameIdx = emitter.addStringConstant(pName); + emitter.emit(Op.DECLARE_VAR, nameIdx); + } } } - } - // Destructuring params are handled in the second pass below. + // Destructuring params are handled in the second pass below. + }); } // -- Build CompileContext ------------------------------------------------ @@ -316,21 +448,38 @@ function compileFunctionInner( blockDepth: 0, compileNestedFunction(innerFnPath, parentEmitter, _parentScope) { - const childUnit = compileFunctionInner(innerFnPath, allUnits); + const childUnit = compileFunctionInner( + innerFnPath, + allUnits, + semanticDrafts + ); allUnits.push(childUnit); + directChildUnitIds.push(childUnit.id); const idIdx = parentEmitter.addStringConstant(childUnit.id); - parentEmitter.emit(Op.NEW_CLOSURE, idIdx); + parentEmitter.withOrigin(sourceOriginFromPath(innerFnPath), () => { + parentEmitter.emit(Op.NEW_CLOSURE, idIdx); + }); }, compileClassExpression(classPath, parentEmitter, parentScope) { - compileClassExpr( - classPath, - parentEmitter, - parentScope, - this, - allUnits, - compileFunctionInner - ); + parentEmitter.withOrigin(sourceOriginFromPath(classPath), () => { + compileClassExpr( + classPath, + parentEmitter, + parentScope, + this, + allUnits, + (innerFnPath, nestedAllUnits) => { + const childUnit = compileFunctionInner( + innerFnPath, + nestedAllUnits, + semanticDrafts + ); + directChildUnitIds.push(childUnit.id); + return childUnit; + } + ); + }); }, compileDestructuring(pattern, em, sc) { @@ -343,27 +492,42 @@ function compileFunctionInner( // -- Compile the function body ------------------------------------------- const bodyPath = fnPath.get("body"); - if (bodyPath.isBlockStatement()) { - const loopStack: LoopContext[] = []; - compileBody(bodyPath.get("body"), emitter, scope, ctx, loopStack); - } else if (bodyPath.isExpression()) { - compileExpression( - bodyPath as NodePath, - emitter, - scope, - ctx - ); - emitter.emit(Op.RETURN, 0); - } + emitter.withOrigin(sourceOriginFromPath(bodyPath), () => { + if (bodyPath.isBlockStatement()) { + const loopStack: LoopContext[] = []; + compileBody(bodyPath.get("body"), emitter, scope, ctx, loopStack); + } else if (bodyPath.isExpression()) { + compileExpression( + bodyPath as NodePath, + emitter, + scope, + ctx + ); + emitter.emit(Op.RETURN, 0); + } + }); // Ensure every code path ends with a return ensureTrailingReturn(emitter); - // -- Optimization passes (Tiers 2 & 3) ---------------------------------- - optimizeInstructions(emitter); + // Snapshot the canonical language stream before constructing metadata. + const canonicalCfg = semanticDrafts + ? buildCanonicalCfg({ + instructions: emitter.instructions.map((instruction) => ({ + ...instruction, + })), + originIds: emitter.instructionOriginIds.slice(), + }) + : null; + const canonicalConstants = semanticDrafts + ? emitter.constants.map((constant) => ({ ...constant })) + : null; + const canonicalOrigins = semanticDrafts + ? emitter.origins.map((origin) => ({ ...origin })) + : null; // -- Scope-object elision ----------------------------------------------- - // Scan the FINAL opcodes (still logical here, before the per-file shuffle) + // Scan the final temporary semantic operations // for any scope-dependent opcode. When none are present and the function // has no dynamic scope, the per-call `Object.create(OS)` layer is provably // redundant and the runtime can use `SC = OS` directly. @@ -372,17 +536,13 @@ function compileFunctionInner( captureResult.hasDynamicScope ); - // Per-unit interpreter-slot usage flags. Computed here on the FINAL logical - // opcodes (post-optimization, pre-shuffle/mutation) — NOT in encode.ts, - // because `adjustEncodingForMutations` rewrites `instructions[].opcode` to - // physical values before serialization, which would make a logical-opcode - // scan there return seed-dependent garbage. These drive the hoisted-slot - // save/restore minimization (see compiler/slot-analysis.ts). + // Preserve conservative semantic metadata needed by downstream planning. const usesExceptions = computeUsesExceptions(emitter.instructions); const usesThisContext = computeUsesThisContext(emitter.instructions); - return { - id: genUnitId(), + const id = genUnitId(); + const unit: SemanticCompileUnit = { + id, constants: emitter.constants, instructions: emitter.instructions, jumpTable: {}, @@ -401,6 +561,37 @@ function compileFunctionInner( outerNames: scope.outerNames, childUnits: [], }; + + if ( + semanticDrafts && + canonicalCfg && + canonicalConstants && + canonicalOrigins + ) { + semanticDrafts.push({ + id, + constants: canonicalConstants, + nodes: canonicalCfg.nodes, + exits: canonicalCfg.exits, + entryNode: canonicalCfg.entryNode, + origins: canonicalOrigins, + childUnitIds: directChildUnitIds, + paramCount, + registerCount: scope.totalRegisters, + slotCount: slotMap.size, + isStrict, + isGenerator, + isAsync, + isArrow, + scopeless, + usesExceptions, + usesThisContext, + nameConstIndex, + outerNames: scope.outerNames.slice(), + }); + } + + return unit; } // --------------------------------------------------------------------------- @@ -454,21 +645,22 @@ function compileComplexParams( ): void { for (let i = 0; i < params.length; i++) { const param = params[i]!; - - if (param.isAssignmentPattern()) { - compileDefaultParam(param, i, emitter, scope, ctx); - } else if (param.isRestElement()) { - compileRestParam(param, i, emitter, scope, ctx); - } else if (!param.isIdentifier()) { - // Destructuring param - emitter.emit(Op.LOAD_ARG, i); - compileDestructuringPattern( - param as NodePath, - emitter, - scope, - ctx - ); - } + emitter.withOrigin(sourceOriginFromPath(param), () => { + if (param.isAssignmentPattern()) { + compileDefaultParam(param, i, emitter, scope, ctx); + } else if (param.isRestElement()) { + compileRestParam(param, i, emitter, scope, ctx); + } else if (!param.isIdentifier()) { + // Destructuring param + emitter.emit(Op.LOAD_ARG, i); + compileDestructuringPattern( + param as NodePath, + emitter, + scope, + ctx + ); + } + }); } } @@ -592,3 +784,30 @@ function ensureTrailingReturn(emitter: Emitter): void { emitter.emit(Op.RETURN_VOID, 0); } } + +/** Convert Babel's nullable location fields into canonical owner metadata. */ +function sourceOriginFromPath(path: NodePath): SourceOrigin { + const node = path.node; + const location = node.loc as + | (t.SourceLocation & { filename?: string | null }) + | null + | undefined; + const start = typeof node.start === "number" && node.start >= 0 ? node.start : 0; + const end = + typeof node.end === "number" && node.end >= start ? node.end : start; + const file = location?.filename ?? undefined; + return file + ? { + file, + start, + end, + line: location?.start.line ?? 1, + column: location?.start.column ?? 0, + } + : { + start, + end, + line: location?.start.line ?? 1, + column: location?.start.column ?? 0, + }; +} diff --git a/packages/ruam/src/compiler/ir.ts b/packages/ruam/src/compiler/ir.ts new file mode 100644 index 0000000..3050cb7 --- /dev/null +++ b/packages/ruam/src/compiler/ir.ts @@ -0,0 +1,126 @@ +/** + * Canonical, execution-representation-independent semantic IR. + * + * Node identities and typed exits replace instruction-pointer control flow. + * This IR is the contract between JavaScript compilation and Isogloss lattice + * lowering; it must never contain physical encoding or backend offsets. + * + * @module compiler/ir + */ + +import { + assertCanonicalSemanticOp, + type SemanticOp, +} from "./semantic-ops.js"; +import type { + ConstantPoolEntry, + RootGroupCompatible, + RootGroupId, + SemanticFunctionMetadata, + SemanticUnitId, +} from "./types.js"; + +/** Dense unit-local identity of a canonical semantic node. */ +export type SemanticNodeId = number; + +/** Dense unit-local index into {@link SemanticUnit.origins}. */ +export type SourceOriginId = number; + +/** Source location associated with one or more canonical semantic nodes. */ +export interface SourceOrigin { + /** Original filename when compilation was given one. */ + file?: string; + /** Inclusive UTF-16 source offset. */ + start: number; + /** Exclusive UTF-16 source offset. */ + end: number; + /** One-based source line. */ + line: number; + /** Zero-based source column. */ + column: number; +} + +/** One canonical language-level action before lattice lowering. */ +export interface SemanticInstruction { + /** Stable identity within the containing unit. */ + id: SemanticNodeId; + /** Language-level behavior, never a shuffled or physical opcode. */ + op: SemanticOp; + /** Operation-specific payload interpreted by its semantic signature. */ + operand: number; + /** Index into the unit's source-origin table. */ + originId: SourceOriginId; +} + +/** Construct one instruction while enforcing the canonical-operation fence. */ +export function createSemanticInstruction( + instruction: SemanticInstruction +): SemanticInstruction { + assertCanonicalSemanticOp(instruction.op); + if (!Number.isSafeInteger(instruction.id) || instruction.id < 0) { + throw new Error(`RUAM_INVALID_SEMANTIC_NODE_ID: ${instruction.id}`); + } + if (!Number.isSafeInteger(instruction.originId) || instruction.originId < 0) { + throw new Error(`RUAM_INVALID_SOURCE_ORIGIN_ID: ${instruction.originId}`); + } + return Object.freeze({ ...instruction }); +} + +/** + * Typed control transfer from a canonical semantic node. + * + * Calls resume in the same unit after the invoked value completes. Exception + * and finally edges name compiler-known handlers; uncaught throws use the + * terminal `throw` form. + */ +export type SemanticExit = + | { kind: "fallthrough"; target: SemanticNodeId } + | { kind: "branch-true"; target: SemanticNodeId } + | { kind: "branch-false"; target: SemanticNodeId } + | { kind: "call"; resume: SemanticNodeId } + | { kind: "exception"; target: SemanticNodeId } + | { kind: "finally"; target: SemanticNodeId } + | { kind: "return" } + | { kind: "throw" } + | { kind: "yield"; resume: SemanticNodeId } + | { kind: "await"; resume: SemanticNodeId }; + +/** Canonical representation of one source function. */ +export interface SemanticUnit + extends RootGroupCompatible, + SemanticFunctionMetadata { + /** Literal pool referenced by semantic operands. */ + constants: ConstantPoolEntry[]; + /** Canonical semantic nodes in deterministic ID order. */ + nodes: SemanticInstruction[]; + /** All legal outgoing edges for each reachable node. */ + exits: Map; + /** First node selected by the unit's entry contract. */ + entryNode: SemanticNodeId; + /** Deduplicated source locations used by owner diagnostics. */ + origins: SourceOrigin[]; + /** Nested semantic units referenced by closure-creation operations. */ + childUnitIds: SemanticUnitId[]; +} + +/** + * Compiler-facing root-group description. + * + * The future pipeline layer can extend this with Babel paths and entry + * contracts without making the canonical IR depend on Babel. + */ +export interface SemanticRootGroup { + id: RootGroupId; + entryUnitId: SemanticUnitId; + units: SemanticUnit[]; + usedSemantics: ReadonlySet; + hasAsync: boolean; + hasGenerator: boolean; +} + +/** Whether an exit completes the current invocation instead of naming a node. */ +export function isTerminalSemanticExit( + exit: SemanticExit +): exit is Extract { + return exit.kind === "return" || exit.kind === "throw"; +} diff --git a/packages/ruam/src/compiler/metadata-analysis.ts b/packages/ruam/src/compiler/metadata-analysis.ts new file mode 100644 index 0000000..35035a9 --- /dev/null +++ b/packages/ruam/src/compiler/metadata-analysis.ts @@ -0,0 +1,61 @@ +/** + * Conservative per-function semantic metadata analysis. + * + * These classifiers describe whether canonical planning must account for + * exception completion or receiver/new-target/super context. + * + * @module compiler/metadata-analysis + */ + +import { Op } from "./operations.js"; + +/** + * Structural operations that require exception-completion state. + */ +export const EXC_OPCODES: ReadonlySet = new Set([ + Op.TRY_PUSH, + Op.TRY_POP, + Op.CATCH_BIND, + Op.CATCH_BIND_PATTERN, + Op.FINALLY_MARK, + Op.END_FINALLY, + Op.RETHROW, +]); + +/** + * Operations that require receiver, new-target, closure, or super context. + */ +export const THIS_CTX_OPCODES: ReadonlySet = new Set([ + Op.PUSH_THIS, + Op.PUSH_NEW_TARGET, + Op.NEW_ARROW, + Op.NEW_CLOSURE, + Op.GET_SUPER_PROP, + Op.SET_SUPER_PROP, + Op.CALL_SUPER_METHOD, + Op.SUPER_CALL, +]); + +/** + * Whether a temporary source-operation sequence needs exception state. + */ +export function computeUsesExceptions( + instructions: { opcode: number }[] +): boolean { + for (const ins of instructions) { + if (EXC_OPCODES.has(ins.opcode as Op)) return true; + } + return false; +} + +/** + * Whether a temporary source-operation sequence needs receiver context. + */ +export function computeUsesThisContext( + instructions: { opcode: number }[] +): boolean { + for (const ins of instructions) { + if (THIS_CTX_OPCODES.has(ins.opcode as Op)) return true; + } + return false; +} diff --git a/packages/ruam/src/compiler/opcode-mutation.ts b/packages/ruam/src/compiler/opcode-mutation.ts deleted file mode 100644 index 7237a8d..0000000 --- a/packages/ruam/src/compiler/opcode-mutation.ts +++ /dev/null @@ -1,397 +0,0 @@ -/** - * Runtime opcode mutation. - * - * Inserts MUTATE instructions into compiled bytecode that permute the - * handler table at runtime. The same physical opcode byte executes - * different handlers at different points in execution, making static - * disassembly impossible. - * - * Advancement over Aether-VM: - * - Mutations are crypto-entangled with the rolling cipher - * - Cumulative state (each mutation builds on the previous) - * - The MUTATE opcode itself is encrypted by the rolling cipher - * - Mutation parameters derived from build seed (deterministic but opaque) - * - * @module compiler/opcode-mutation - */ - -import type { BytecodeUnit, Instruction } from "../types.js"; -import { Op, OPCODE_COUNT, ALL_JUMP_OPS, PACKED_JUMP_OPS } from "./opcodes.js"; -import { GOLDEN_RATIO_PRIME } from "../constants.js"; -import { lcgNext } from "../naming/scope.js"; - -// --- Constants --- - -/** Minimum instructions between mutation points. */ -const MIN_MUTATION_INTERVAL = 20; -/** Maximum instructions between mutation points. */ -const MAX_MUTATION_INTERVAL = 50; -/** Number of swaps per mutation. */ -const SWAPS_PER_MUTATION = 4; - -/** - * Opcodes that unconditionally transfer control, so the instruction lexically - * following them is NOT reached by fall-through. A MUTATE inserted before such - * a successor would never execute via the straight-line path — it is only - * reachable (if at all) by a jump that lands *after* the inserted MUTATE, or it - * is dead code. Either way the runtime mutation state desyncs from the linear - * build-time encoding. (TABLE_SWITCH/LOOKUP_SWITCH are reserved/unused today but - * are unconditional transfers at runtime, so they are listed for safety.) - */ -const UNCONDITIONAL_TRANSFER_OPS = new Set([ - Op.JMP, - Op.RETURN, - Op.RETURN_VOID, - Op.THROW, - Op.RETHROW, - Op.TABLE_SWITCH, - Op.LOOKUP_SWITCH, -]); - -// --- Mutation state tracking --- - -/** - * Tracks the cumulative handler table mutation state. - * - * At compile time, we maintain the current state of the handler table - * so that subsequent opcodes can be encoded against the post-mutation mapping. - */ -export class MutationState { - /** Current forward mapping: logical opcode → physical opcode (after mutations). */ - private forwardMap: number[]; - /** Current reverse mapping: physical opcode → logical opcode. */ - private reverseMap: number[]; - - constructor(shuffleMap: number[]) { - // shuffleMap[logical] = physical - this.forwardMap = [...shuffleMap]; - this.reverseMap = new Array(shuffleMap.length).fill(0); - for (let i = 0; i < shuffleMap.length; i++) { - this.reverseMap[shuffleMap[i]!] = i; - } - } - - /** Get the current physical opcode for a logical opcode. */ - getPhysical(logical: number): number { - return this.forwardMap[logical] ?? logical; - } - - /** Apply a mutation (same algorithm as runtime). */ - applyMutation(mutSeed: number, tableSize: number): void { - let seed = mutSeed >>> 0; - for (let k = 0; k < SWAPS_PER_MUTATION; k++) { - seed = lcgNext(seed); - const i = (seed >>> 16) % tableSize; - seed = lcgNext(seed); - const j = (seed >>> 16) % tableSize; - if (i === j) continue; - - // Swap entries in reverse map (which the runtime _ht represents) - const vi = this.reverseMap[i]; - const vj = this.reverseMap[j]; - if (vi == null || vj == null) continue; - this.reverseMap[i] = vj; - this.reverseMap[j] = vi; - - // Update forward map to match - this.forwardMap[vj] = i; - this.forwardMap[vi] = j; - } - } -} - -// --- Loop detection --- - -/** - * Build the set of IPs that are UNSAFE for a MUTATE. - * - * `adjustEncodingForMutations` encodes instructions in a single LINEAR pass, - * assuming every MUTATE executes exactly once, in lexical order, before any - * instruction lexically after it. A MUTATE only honours that assumption when it - * sits on a point reached exactly once on every execution path. Two control-flow - * shapes violate it and so must be excluded: - * - * - **Backward-jump (loop) spans** `[target, jumpIp]`: a MUTATE inside a loop - * body executes on every iteration, diverging from the single-execution model. - * - **Forward-jump spans** `[jumpIp+1, target]`: instructions skipped when a - * forward branch is taken (if/else arms, switch cases) — and, crucially, the - * body of a `try` (an exception edge jumps from anywhere in the try body to - * the catch/finally target packed in `TRY_PUSH`). A MUTATE there is reached - * only on some paths, so the runtime mutation state desyncs from the encoding. - * The span is **target-inclusive**: a forward branch *lands on* its target, - * and an inserted MUTATE precedes the original instruction at that IP, so the - * landing edge is remapped to *after* the MUTATE (the loader inserts the - * MUTATE before the target's instruction). The branch-taken path therefore - * bypasses the MUTATE while the fall-through path runs it — exactly the - * desync the linear encoding cannot represent. So the target itself is unsafe. - * - **Jump-only / dead successors**: an IP whose lexical predecessor is an - * unconditional transfer (`JMP`/`RETURN`/`THROW`/…) has no fall-through edge. - * A MUTATE placed before it never runs on the straight-line path — control - * only arrives via a jump (which lands after the MUTATE) or never (dead - * code) — so build-time counts a mutation the runtime never applies. - * - * Excluding all of these leaves only straight-line, fall-through-reached - * positions whose mutation is crossed exactly once — correctness over mutation - * density (heavily-branched units may receive no MUTATEs, which is fine). - */ -function findUnsafeMutationIPs( - instrs: readonly Instruction[], - jumpTable: Record -): Set { - const unsafe = new Set(); - const mark = (a: number, b: number): void => { - const lo = Math.max(0, a); - const hi = Math.min(instrs.length - 1, b); - for (let j = lo; j <= hi; j++) unsafe.add(j); - }; - - // An IP with no fall-through predecessor is reachable only via a jump (which - // lands *after* an inserted MUTATE) or not at all. Either way a MUTATE there - // would not be crossed exactly-once on the linear path. Mark all such IPs. - for (let ip = 1; ip < instrs.length; ip++) { - if (UNCONDITIONAL_TRANSFER_OPS.has(instrs[ip - 1]!.opcode)) { - unsafe.add(ip); - } - } - - // Exception handling is reached via RUNTIME control edges (the exec loop's - // catch routes `IP = _h._ci*2 / _h._fi*2`, and RETURN/RETHROW defer through - // the finally) that are invisible to instruction-stream jump analysis. A - // catch/finally body — and anything after it, since finally-resumption can - // re-route — is reachable in non-linear order, so once a unit enters any - // try region the linear single-pass mutation encoding no longer holds. - // Conservatively exclude everything from the first TRY_PUSH to the end; only - // the straight-line prologue before any try can safely carry a MUTATE. - let firstTry = -1; - for (let ip = 0; ip < instrs.length; ip++) { - if (instrs[ip]!.opcode === Op.TRY_PUSH) { - firstTry = ip; - break; - } - } - if (firstTry >= 0) mark(firstTry, instrs.length - 1); - - for (let ip = 0; ip < instrs.length; ip++) { - const instr = instrs[ip]!; - const targets: number[] = []; - - if (ALL_JUMP_OPS.has(instr.opcode)) { - targets.push(jumpTable[instr.operand] ?? instr.operand); - } else if (PACKED_JUMP_OPS.has(instr.opcode)) { - // Upper 16 bits: catch IP (TRY_PUSH) or jump target (REG_*_JF). - const hi = (instr.operand >>> 16) & 0xffff; - if (hi !== 0xffff) targets.push(jumpTable[hi] ?? hi); - // TRY_PUSH packs a SECOND forward target (finally IP) in the low bits. - if (instr.opcode === Op.TRY_PUSH) { - const lo = instr.operand & 0xffff; - if (lo !== 0xffff) targets.push(jumpTable[lo] ?? lo); - } - } - - for (const t of targets) { - if (t === 0xffff) continue; // "no target" sentinel - if (t <= ip) { - mark(t, ip); // backward jump → loop body (target inclusive) - } else { - // Forward jump → conditionally-skipped region, TARGET INCLUSIVE. - // The target is the branch's landing point; a MUTATE inserted - // before it is bypassed by the branch-taken path but run by the - // fall-through path, so it cannot sit at the target either. - mark(ip + 1, t); - } - } - } - - return unsafe; -} - -/** - * Insert MUTATE instructions into a compiled bytecode unit. - * - * Inserts at pseudo-random intervals (20-50 instructions) to break up - * the instruction stream. Each MUTATE carries a seed operand that - * determines the specific permutation. - * - * MUTATE instructions are only inserted at IPs that execute exactly once - * (outside loop bodies). This is critical because adjustEncodingForMutations - * assumes each MUTATE executes once; a MUTATE inside a loop would permute - * the handler table on every iteration, diverging from compile-time state. - * - * @param unit - The bytecode unit to modify (mutated in-place) - * @param seed - Per-build seed for deterministic placement - * @returns Array of mutation seeds in order (for runtime verification) - */ -export function insertMutationOpcodes( - unit: BytecodeUnit, - seed: number -): number[] { - const instrs = unit.instructions; - if (instrs.length < MIN_MUTATION_INTERVAL * 2) return []; - - // Skip units that have child units (closures, inner functions). - // Children share the parent's handler table `_ht` at runtime. - // MUTATEs in the parent permute `_ht`, but children are encoded - // against the initial shuffleMap — their dispatch would be wrong. - if (unit.childUnits.length > 0) return []; - - // Identify IPs that are unsafe for a MUTATE — inside loop bodies (backward - // jumps), conditionally-skipped forward-branch spans, or try bodies. The - // build-time linear encoding only holds for MUTATEs reached exactly once on - // every path, so MUTATEs go only at unconditionally-reached positions. - const unsafeIPs = findUnsafeMutationIPs(instrs, unit.jumpTable); - - let state = (seed ^ GOLDEN_RATIO_PRIME) >>> 0; - const mutationSeeds: number[] = []; - const newInstrs: Instruction[] = []; - let nextMutation = 0; - - // Determine first mutation point - state = lcgNext(state); - nextMutation = - MIN_MUTATION_INTERVAL + - ((state >>> 16) % (MAX_MUTATION_INTERVAL - MIN_MUTATION_INTERVAL + 1)); - - let instrCount = 0; - for (let ip = 0; ip < instrs.length; ip++) { - // Insert mutation before this instruction if interval reached - // AND we're not inside a loop body - if (instrCount >= nextMutation && ip > 0 && !unsafeIPs.has(ip)) { - // Generate mutation seed - state = lcgNext(state); - const mutSeed = state >>> 0; - mutationSeeds.push(mutSeed); - - newInstrs.push({ opcode: Op.MUTATE, operand: mutSeed }); - - // Reset counter and pick next interval - instrCount = 0; - state = lcgNext(state); - nextMutation = - MIN_MUTATION_INTERVAL + - ((state >>> 16) % - (MAX_MUTATION_INTERVAL - MIN_MUTATION_INTERVAL + 1)); - } - - newInstrs.push(instrs[ip]!); - instrCount++; - } - - // Update unit instructions (jump targets need patching) - if (mutationSeeds.length > 0) { - // Build IP remapping: old IP → new IP (accounting for inserted MUTATEs) - const ipMap = new Map(); - let mutIdx = 0; - let newIp = 0; - let count = 0; - let nextMut2 = 0; - - // Recompute insertion points to build the map - let st2 = (seed ^ GOLDEN_RATIO_PRIME) >>> 0; - st2 = lcgNext(st2); - nextMut2 = - MIN_MUTATION_INTERVAL + - ((st2 >>> 16) % - (MAX_MUTATION_INTERVAL - MIN_MUTATION_INTERVAL + 1)); - - for (let oldIp = 0; oldIp < instrs.length; oldIp++) { - // Must mirror the exact insertion condition from above - if (count >= nextMut2 && oldIp > 0 && !unsafeIPs.has(oldIp)) { - newIp++; // Skip the MUTATE instruction - count = 0; - st2 = lcgNext(st2); // mutSeed - st2 = lcgNext(st2); // next interval - nextMut2 = - MIN_MUTATION_INTERVAL + - ((st2 >>> 16) % - (MAX_MUTATION_INTERVAL - MIN_MUTATION_INTERVAL + 1)); - } - ipMap.set(oldIp, newIp); - newIp++; - count++; - } - - // Patch jump instruction operands (direct IP targets) in new instructions - for (const instr of newInstrs) { - if (instr.opcode === Op.MUTATE) continue; - if (ALL_JUMP_OPS.has(instr.opcode)) { - const newTarget = ipMap.get(instr.operand); - if (newTarget !== undefined) { - instr.operand = newTarget; - } - } else if (PACKED_JUMP_OPS.has(instr.opcode)) { - const lo = instr.operand & 0xffff; - const hi = (instr.operand >>> 16) & 0xffff; - const newHi = ipMap.get(hi); - if (newHi !== undefined) { - instr.operand = (newHi << 16) | lo; - } - } - } - unit.instructions = newInstrs; - - // Patch jump table - const newJumpTable: Record = {}; - for (const [label, ip] of Object.entries(unit.jumpTable)) { - const newTarget = ipMap.get(ip); - newJumpTable[Number(label)] = newTarget ?? ip; - } - unit.jumpTable = newJumpTable; - - // Patch exception table - unit.exceptionTable = unit.exceptionTable.map((entry) => ({ - startIp: ipMap.get(entry.startIp) ?? entry.startIp, - endIp: ipMap.get(entry.endIp) ?? entry.endIp, - catchIp: - entry.catchIp >= 0 - ? ipMap.get(entry.catchIp) ?? entry.catchIp - : entry.catchIp, - finallyIp: - entry.finallyIp >= 0 - ? ipMap.get(entry.finallyIp) ?? entry.finallyIp - : entry.finallyIp, - })); - } - - // NOTE: Do NOT recursively process child units. Child units (closures, - // inner functions) share the parent's handler table `_ht` at runtime. - // Mutations in the parent change the table state before the child runs, - // but the child's opcodes were encoded against the initial state. - // Only root-level units get mutations; child units run with whatever - // mutation state the parent has established. - - return mutationSeeds; -} - -/** - * Apply mutation state tracking to the encode pass. - * - * After inserting MUTATE opcodes but before encoding with the shuffle map, - * this function walks the instruction stream and adjusts the physical - * opcode encoding to account for cumulative mutations. - * - * @param unit - The bytecode unit with MUTATE instructions inserted - * @param shuffleMap - The per-build opcode shuffle map - * @param tableSize - Handler table size (for modular swap indices) - */ -export function adjustEncodingForMutations( - unit: BytecodeUnit, - shuffleMap: number[], - tableSize: number -): void { - const state = new MutationState(shuffleMap); - - for (const instr of unit.instructions) { - if (instr.opcode === Op.MUTATE) { - // The MUTATE opcode itself is encoded with the pre-mutation map - instr.opcode = state.getPhysical(Op.MUTATE); - // Apply the mutation for subsequent opcodes - state.applyMutation(instr.operand, tableSize); - } else { - // Encode with the current (post-mutation) map - instr.opcode = state.getPhysical(instr.opcode); - } - } - - // Child units use the plain shuffle map — no mutation adjustment needed - // (they don't contain MUTATE instructions and share the parent's _ht) -} diff --git a/packages/ruam/src/compiler/opcodes.ts b/packages/ruam/src/compiler/operations.ts similarity index 82% rename from packages/ruam/src/compiler/opcodes.ts rename to packages/ruam/src/compiler/operations.ts index 0230a61..5c5ed69 100644 --- a/packages/ruam/src/compiler/opcodes.ts +++ b/packages/ruam/src/compiler/operations.ts @@ -1,25 +1,18 @@ /** - * Virtual machine opcode definitions and shuffle-map utilities. + * Source-operation catalog used by canonical semantic lowering. * - * The {@link Op} enum assigns a stable *logical* opcode number to every VM - * instruction. At build time a per-file shuffle map permutes these into - * *physical* opcodes embedded in the bytecode, making static analysis harder. - * - * Opcodes are organised into 24 categories covering every JavaScript language - * feature. Some opcodes are "fast-path" variants that fuse common multi-step - * patterns into a single instruction for future optimisation passes. + * Values are temporary compiler identities only. They are never serialized, + * shuffled, or dispatched by the shipped Isogloss execution architecture. * * @module compiler/opcodes */ -import { LCG_MULTIPLIER, LCG_INCREMENT } from "../constants.js"; - // ═══════════════════════════════════════════════════════════════════════════════ // Opcode enum // ═══════════════════════════════════════════════════════════════════════════════ /** - * Every instruction the Ruam VM can execute. + * Source operations recognized by the canonical compiler. * * **Categories (24):** * @@ -860,234 +853,3 @@ export enum Op { /** Sentinel — not a real opcode; its numeric value equals the total count. */ __COUNT, } - -/** Total number of real opcodes (excludes `__COUNT`). */ -export const OPCODE_COUNT = Op.__COUNT; - -// ═══════════════════════════════════════════════════════════════════════════════ -// Centralized opcode sets -// ═══════════════════════════════════════════════════════════════════════════════ - -/** - * Simple jump opcodes whose operand is a direct IP target. - * Used by the peephole optimizer for jump threading. - */ -export const JUMP_OPS = new Set([ - Op.JMP, - Op.JMP_TRUE, - Op.JMP_FALSE, - Op.JMP_NULLISH, - Op.JMP_UNDEFINED, - Op.JMP_TRUE_KEEP, - Op.JMP_FALSE_KEEP, - Op.JMP_NULLISH_KEEP, -]); - -/** - * All opcodes whose operand encodes an IP target — includes simple jumps, - * switch dispatch, and logical short-circuit operators. - * Used by dead-code injection and jump target patching. - */ -export const ALL_JUMP_OPS = new Set([ - ...JUMP_OPS, - Op.TABLE_SWITCH, - Op.LOOKUP_SWITCH, - Op.LOGICAL_AND, - Op.LOGICAL_OR, - Op.NULLISH_COALESCE, -]); - -/** - * Opcodes that pack a jump target into upper bits of the operand. - * Used by jump target patching to extract/repack IP targets. - */ -export const PACKED_JUMP_OPS = new Set([ - Op.TRY_PUSH, - // Compare-and-branch superinstructions: jump target in bits 16-31. - Op.REG_LT_CONST_JF, - Op.REG_LTE_CONST_JF, - Op.REG_GT_CONST_JF, - Op.REG_GTE_CONST_JF, - Op.REG_SEQ_CONST_JF, - Op.REG_SNEQ_CONST_JF, - Op.REG_LT_REG_JF, - Op.REG_LTE_REG_JF, - Op.REG_GT_REG_JF, - Op.REG_GTE_REG_JF, - Op.REG_SEQ_REG_JF, - Op.REG_SNEQ_REG_JF, - // Const compare-and-branch (TOS vs constant): jump target in bits 16-31, - // constant index in the low 16 bits. - Op.CONST_LT_JF, - Op.CONST_LTE_JF, - Op.CONST_GT_JF, - Op.CONST_GTE_JF, - Op.CONST_SEQ_JF, - Op.CONST_SNEQ_JF, -]); - -/** - * Numeric binary opcodes eligible for constant folding. - * Maps each foldable opcode to its JS evaluation function. - */ -export const FOLDABLE_BINOPS = new Map< - Op, - (a: number, b: number) => number | null ->([ - [Op.ADD, (a, b) => a + b], - [Op.SUB, (a, b) => a - b], - [Op.MUL, (a, b) => a * b], - [Op.DIV, (a, b) => (b !== 0 ? a / b : null)], - [Op.MOD, (a, b) => (b !== 0 ? a % b : null)], - [Op.BIT_AND, (a, b) => a & b], - [Op.BIT_OR, (a, b) => a | b], - [Op.BIT_XOR, (a, b) => a ^ b], - [Op.SHL, (a, b) => a << b], - [Op.SHR, (a, b) => a >> b], - [Op.USHR, (a, b) => a >>> b], -]); - -/** - * Pure push opcodes — side-effect-free instructions that only push a value. - * Used by dead pair elimination (DUP+POP, PUSH+POP). - */ -export const PURE_PUSH_OPS = new Set([ - Op.PUSH_CONST, - Op.PUSH_UNDEFINED, - Op.PUSH_NULL, - Op.PUSH_TRUE, - Op.PUSH_FALSE, - Op.PUSH_ZERO, - Op.PUSH_ONE, - Op.PUSH_NEG_ONE, - Op.PUSH_EMPTY_STRING, - Op.PUSH_NAN, - Op.PUSH_INFINITY, - Op.PUSH_NEG_INFINITY, - Op.LOAD_REG, -]); - -/** - * Mapping from standard binary opcode to its register-register superinstruction. - * Used by the superinstruction fusion pass. - */ -export const REG_BINOP_MAP = new Map([ - [Op.ADD, Op.REG_ADD], - [Op.SUB, Op.REG_SUB], - [Op.MUL, Op.REG_MUL], - [Op.DIV, Op.REG_DIV], - [Op.MOD, Op.REG_MOD], - [Op.LT, Op.REG_LT], - [Op.LTE, Op.REG_LTE], - [Op.GT, Op.REG_GT], - [Op.GTE, Op.REG_GTE], - [Op.SEQ, Op.REG_SEQ], - [Op.SNEQ, Op.REG_SNEQ], -]); - -/** - * Mapping from standard binary opcode to its register-constant superinstruction. - * Used by the superinstruction fusion pass. - */ -export const REG_CONST_BINOP_MAP = new Map([ - [Op.SUB, Op.REG_CONST_SUB], - [Op.MUL, Op.REG_CONST_MUL], - [Op.MOD, Op.REG_CONST_MOD], -]); - -/** - * Mapping from comparison opcode to its fused register-vs-constant - * compare-and-branch superinstruction (`LOAD_REG + PUSH_CONST + + - * JMP_FALSE`). Used by the superinstruction fusion pass. - */ -export const REG_CONST_CMP_JF_MAP = new Map([ - [Op.LT, Op.REG_LT_CONST_JF], - [Op.LTE, Op.REG_LTE_CONST_JF], - [Op.GT, Op.REG_GT_CONST_JF], - [Op.GTE, Op.REG_GTE_CONST_JF], - [Op.SEQ, Op.REG_SEQ_CONST_JF], - [Op.SNEQ, Op.REG_SNEQ_CONST_JF], -]); - -/** - * Mapping from comparison opcode to its fused register-vs-register - * compare-and-branch superinstruction (`LOAD_REG + LOAD_REG + + - * JMP_FALSE`). Used by the superinstruction fusion pass. - */ -export const REG_REG_CMP_JF_MAP = new Map([ - [Op.LT, Op.REG_LT_REG_JF], - [Op.LTE, Op.REG_LTE_REG_JF], - [Op.GT, Op.REG_GT_REG_JF], - [Op.GTE, Op.REG_GTE_REG_JF], - [Op.SEQ, Op.REG_SEQ_REG_JF], - [Op.SNEQ, Op.REG_SNEQ_REG_JF], -]); - -/** - * Mapping from comparison opcode to its fused TOS-vs-constant compare-and-branch - * superinstruction (`PUSH_CONST + + JMP_FALSE`). The LHS is popped off the - * stack and compared against the constant. Used by the superinstruction pass. - */ -export const CONST_CMP_JF_MAP = new Map([ - [Op.LT, Op.CONST_LT_JF], - [Op.LTE, Op.CONST_LTE_JF], - [Op.GT, Op.CONST_GT_JF], - [Op.GTE, Op.CONST_GTE_JF], - [Op.SEQ, Op.CONST_SEQ_JF], - [Op.SNEQ, Op.CONST_SNEQ_JF], -]); - -/** - * Push-then-pop strength-reduction map: a stack-pushing register op directly - * followed by POP (statement-form `i++`, `s += x`, etc.) reduces to a variant - * that omits the dead push. POST_INC/DEC_REG reuse the existing no-push - * INC/DEC_REG; the compound-assign forms map to dedicated `*_VOID` opcodes. - * Used by the peephole pass. The operand (register index) is preserved. - */ -export const PUSH_POP_VOID_MAP = new Map([ - [Op.POST_INC_REG, Op.INC_REG], - [Op.POST_DEC_REG, Op.DEC_REG], - [Op.ADD_ASSIGN_REG, Op.REG_ADD_ASSIGN_VOID], - [Op.SUB_ASSIGN_REG, Op.REG_SUB_ASSIGN_VOID], - [Op.MUL_ASSIGN_REG, Op.REG_MUL_ASSIGN_VOID], - [Op.DIV_ASSIGN_REG, Op.REG_DIV_ASSIGN_VOID], - [Op.MOD_ASSIGN_REG, Op.REG_MOD_ASSIGN_VOID], -]); - -// ═══════════════════════════════════════════════════════════════════════════════ -// Shuffle map utilities -// ═══════════════════════════════════════════════════════════════════════════════ - -/** - * Generate a deterministic permutation of opcode indices via Fisher-Yates - * shuffle driven by an LCG PRNG. - * - * @param seed - 32-bit unsigned integer seed. - * @returns Array where `map[logicalOp]` gives the physical opcode. - */ -export function generateShuffleMap(seed: number): number[] { - const map: number[] = []; - for (let i = 0; i < OPCODE_COUNT; i++) map[i] = i; - - let s = seed >>> 0; - for (let i = OPCODE_COUNT - 1; i > 0; i--) { - s = (s * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - const j = s % (i + 1); - const tmp = map[i]!; - map[i] = map[j]!; - map[j] = tmp; - } - - return map; -} - -/** - * Invert a shuffle map so `inv[physicalOp]` yields the logical opcode. - */ -export function invertShuffleMap(map: number[]): number[] { - const inv: number[] = new Array(map.length); - for (let i = 0; i < map.length; i++) { - inv[map[i]!] = i; - } - return inv; -} diff --git a/packages/ruam/src/compiler/optimizer.ts b/packages/ruam/src/compiler/optimizer.ts deleted file mode 100644 index ae9df79..0000000 --- a/packages/ruam/src/compiler/optimizer.ts +++ /dev/null @@ -1,553 +0,0 @@ -/** - * Bytecode optimization passes. - * - * Runs after initial compilation to improve instruction density: - * - * - **Peephole optimizer** (Tier 2): Constant folding, dead pair elimination, - * jump threading, strength reduction. - * - **Superinstruction fusion** (Tier 3): Fuses common register-based - * instruction sequences into single dispatches. - * - * @module compiler/optimizer - */ - -import type { Instruction, ConstantPoolEntry } from "../types.js"; -import { - Op, - JUMP_OPS, - ALL_JUMP_OPS, - PACKED_JUMP_OPS, - FOLDABLE_BINOPS, - PURE_PUSH_OPS, - REG_BINOP_MAP, - REG_CONST_BINOP_MAP, - REG_CONST_CMP_JF_MAP, - REG_REG_CMP_JF_MAP, - CONST_CMP_JF_MAP, - PUSH_POP_VOID_MAP, -} from "./opcodes.js"; -import type { Emitter } from "./emitter.js"; - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** - * Run all optimization passes on a compiled instruction stream. - * Modifies the emitter's instructions in place. - */ -export function optimizeInstructions(emitter: Emitter): void { - let changed = true; - let passes = 0; - const maxPasses = 5; - - while (changed && passes < maxPasses) { - changed = false; - if (peepholePass(emitter)) changed = true; - if (superinstructionPass(emitter)) changed = true; - passes++; - } - - // Final cleanup: remove NOPs - removeNops(emitter); -} - -// --------------------------------------------------------------------------- -// Peephole Optimizer (Tier 2) -// --------------------------------------------------------------------------- - -function peepholePass(emitter: Emitter): boolean { - const instrs = emitter.instructions; - const consts = emitter.constants; - let changed = false; - const jumpTargets = computeJumpTargets(instrs); - - for (let i = 0; i < instrs.length; i++) { - const cur = instrs[i]!; - - // --- Dead pair elimination --- - - // DUP + POP → NOP + NOP - if ( - cur.opcode === Op.DUP && - i + 1 < instrs.length && - instrs[i + 1]!.opcode === Op.POP - ) { - if (!jumpTargets.has(i + 1)) { - cur.opcode = Op.NOP; - cur.operand = 0; - instrs[i + 1]!.opcode = Op.NOP; - instrs[i + 1]!.operand = 0; - changed = true; - continue; - } - } - - // PUSH_X + POP → NOP + NOP (for side-effect-free pushes) - if ( - PURE_PUSH_OPS.has(cur.opcode) && - i + 1 < instrs.length && - instrs[i + 1]!.opcode === Op.POP - ) { - if (!jumpTargets.has(i + 1)) { - cur.opcode = Op.NOP; - cur.operand = 0; - instrs[i + 1]!.opcode = Op.NOP; - instrs[i + 1]!.operand = 0; - changed = true; - continue; - } - } - - // --- Push-then-pop strength reduction --- - // A register op whose pushed result is immediately discarded by POP - // (statement-form `i++`, `s += x`, etc.) reduces to a no-push variant. - // POST_INC/DEC_REG → INC/DEC_REG (reuse); compound assigns → *_VOID. - // The operand (register index) is preserved unchanged. - if ( - i + 1 < instrs.length && - instrs[i + 1]!.opcode === Op.POP && - !jumpTargets.has(i + 1) - ) { - const voidOp = PUSH_POP_VOID_MAP.get(cur.opcode); - if (voidOp != null) { - cur.opcode = voidOp; - instrs[i + 1]!.opcode = Op.NOP; - instrs[i + 1]!.operand = 0; - changed = true; - continue; - } - } - - // --- Constant folding --- - // PUSH_CONST(a) + PUSH_CONST(b) + → PUSH_CONST(result) - if (cur.opcode === Op.PUSH_CONST && i + 2 < instrs.length) { - const next = instrs[i + 1]!; - const binop = instrs[i + 2]!; - if ( - next.opcode === Op.PUSH_CONST && - FOLDABLE_BINOPS.has(binop.opcode) - ) { - const a = getNumericConst(consts, cur.operand); - const b = getNumericConst(consts, next.operand); - if ( - a !== null && - b !== null && - !jumpTargets.has(i + 1) && - !jumpTargets.has(i + 2) - ) { - const result = foldBinop(binop.opcode, a, b); - if (result !== null && isFinite(result)) { - const idx = emitter.addNumberConstant(result); - cur.opcode = Op.PUSH_CONST; - cur.operand = idx; - next.opcode = Op.NOP; - next.operand = 0; - binop.opcode = Op.NOP; - binop.operand = 0; - changed = true; - continue; - } - } - } - } - - // --- Strength reduction --- - // PUSH_CONST(1) + SUB → DEC (SUB is always numeric, so this is safe) - // NOTE: We do NOT reduce PUSH_CONST(1) + ADD → INC because ADD does - // string concatenation ("prop_" + 1 → "prop_1"), while INC is always - // numeric (+x + 1). - if (cur.opcode === Op.PUSH_CONST && i + 1 < instrs.length) { - const next = instrs[i + 1]!; - const val = getNumericConst(consts, cur.operand); - if ( - val === 1 && - next.opcode === Op.SUB && - !jumpTargets.has(i + 1) - ) { - cur.opcode = Op.NOP; - cur.operand = 0; - next.opcode = Op.DEC; - next.operand = 0; - changed = true; - continue; - } - } - - // --- Jump threading --- - // JMP(L) where L points to JMP(L2) → JMP(L2) - if (JUMP_OPS.has(cur.opcode) && cur.operand >= 0) { - const targetIdx = cur.operand; // target instruction index - if (targetIdx < instrs.length) { - const targetInstr = instrs[targetIdx]!; - if ( - targetInstr.opcode === Op.JMP && - targetInstr.operand !== targetIdx - ) { - cur.operand = targetInstr.operand; - changed = true; - continue; - } - } - } - - // --- Redundant store+load --- - // STORE_REG(r) + LOAD_REG(r) → DUP + STORE_REG(r) - if (cur.opcode === Op.STORE_REG && i + 1 < instrs.length) { - const next = instrs[i + 1]!; - if ( - next.opcode === Op.LOAD_REG && - next.operand === cur.operand && - !jumpTargets.has(i + 1) - ) { - // Reorder: DUP then STORE_REG (saves one instruction's worth of dispatch) - next.opcode = Op.STORE_REG; - next.operand = cur.operand; - cur.opcode = Op.DUP; - cur.operand = 0; - changed = true; - continue; - } - } - - // --- JMP to next instruction → NOP --- - if (cur.opcode === Op.JMP && cur.operand === i + 1) { - cur.opcode = Op.NOP; - cur.operand = 0; - changed = true; - continue; - } - } - - return changed; -} - -// --------------------------------------------------------------------------- -// Superinstruction Fusion (Tier 3) -// --------------------------------------------------------------------------- - -function superinstructionPass(emitter: Emitter): boolean { - const instrs = emitter.instructions; - const consts = emitter.constants; - let changed = false; - const jumpTargets = computeJumpTargets(instrs); - - for (let i = 0; i < instrs.length - 1; i++) { - const a = instrs[i]!; - const b = instrs[i + 1]!; - - // Guard: don't fuse across jump targets - if (jumpTargets.has(i + 1)) continue; - - // --- Two-instruction fusions --- - - // LOAD_REG(r) + GET_PROP_STATIC(name) → REG_GET_PROP(r | name<<16) - if (a.opcode === Op.LOAD_REG && b.opcode === Op.GET_PROP_STATIC) { - const r = a.operand; - const name = b.operand; - if (r <= 0xffff && name <= 0xffff) { - a.opcode = Op.REG_GET_PROP; - a.operand = (r & 0xffff) | ((name & 0xffff) << 16); - b.opcode = Op.NOP; - b.operand = 0; - changed = true; - continue; - } - } - - // LOAD_REG(r) + GET_PROP_DYNAMIC → IDX_REG(r) — index TOS by register. - // (Distinct 2nd opcode from every 3-/4-instruction fusion, so eager - // fusion here is safe; the LOAD_REG+LOAD_REG+GET_PROP_DYNAMIC form is - // caught by REG_GET_PROP_DYN below since its 2nd op is LOAD_REG.) - if (a.opcode === Op.LOAD_REG && b.opcode === Op.GET_PROP_DYNAMIC) { - a.opcode = Op.IDX_REG; - // a.operand already holds the register index — leave unchanged. - b.opcode = Op.NOP; - b.operand = 0; - changed = true; - continue; - } - - if (i + 2 >= instrs.length) continue; - const c = instrs[i + 2]!; - if (jumpTargets.has(i + 2)) continue; - - // --- Four-instruction fusions --- - // - // Attempted BEFORE the three-instruction fusions below: a - // compare-and-branch sequence (`LOAD_REG + operand + + - // JMP_FALSE`) must collapse into a single fused opcode. If the - // three-instruction reg-reg / reg-const binop fusions ran first they - // would consume the `` (e.g. `LOAD_REG + LOAD_REG + LT` → - // REG_LT), leaving the JMP_FALSE unfused and the `*_REG_JF` - // superinstructions dead. On guard failure these intentionally fall - // through to the three-instruction fusions. - if (i + 3 < instrs.length && !jumpTargets.has(i + 3)) { - const d = instrs[i + 3]!; - - // LOAD_REG(r) + PUSH_CONST(c) + + JMP_FALSE(target) - // → REG__CONST_JF(r | c<<8 | target<<16) - if ( - a.opcode === Op.LOAD_REG && - b.opcode === Op.PUSH_CONST && - d.opcode === Op.JMP_FALSE - ) { - const superOp = REG_CONST_CMP_JF_MAP.get(c.opcode); - if (superOp != null) { - const r = a.operand; - const constIdx = b.operand; - const target = d.operand; - if (r <= 0xff && constIdx <= 0xff && target <= 0xffff) { - a.opcode = superOp; - a.operand = - (r & 0xff) | - ((constIdx & 0xff) << 8) | - ((target & 0xffff) << 16); - b.opcode = Op.NOP; - b.operand = 0; - c.opcode = Op.NOP; - c.operand = 0; - d.opcode = Op.NOP; - d.operand = 0; - changed = true; - continue; - } - } - } - - // LOAD_REG(a) + LOAD_REG(b) + + JMP_FALSE(target) - // → REG__REG_JF(a | b<<8 | target<<16) - if ( - a.opcode === Op.LOAD_REG && - b.opcode === Op.LOAD_REG && - d.opcode === Op.JMP_FALSE - ) { - const superOp = REG_REG_CMP_JF_MAP.get(c.opcode); - if (superOp != null) { - const ra = a.operand; - const rb = b.operand; - const target = d.operand; - if (ra <= 0xff && rb <= 0xff && target <= 0xffff) { - a.opcode = superOp; - a.operand = - (ra & 0xff) | - ((rb & 0xff) << 8) | - ((target & 0xffff) << 16); - b.opcode = Op.NOP; - b.operand = 0; - c.opcode = Op.NOP; - c.operand = 0; - d.opcode = Op.NOP; - d.operand = 0; - changed = true; - continue; - } - } - } - - // LOAD_REG(r) + PUSH_CONST(c) + ADD + STORE_REG(r) → REG_ADD_CONST - if ( - a.opcode === Op.LOAD_REG && - b.opcode === Op.PUSH_CONST && - c.opcode === Op.ADD && - d.opcode === Op.STORE_REG && - d.operand === a.operand - ) { - const r = a.operand; - const constIdx = b.operand; - if (r <= 0xffff && constIdx <= 0xffff) { - a.opcode = Op.REG_ADD_CONST; - a.operand = (r & 0xffff) | ((constIdx & 0xffff) << 16); - b.opcode = Op.NOP; - b.operand = 0; - c.opcode = Op.NOP; - c.operand = 0; - d.opcode = Op.NOP; - d.operand = 0; - changed = true; - continue; - } - } - } - - // --- Three-instruction fusions --- - - // LOAD_REG(a) + LOAD_REG(b) + GET_PROP_DYNAMIC → REG_GET_PROP_DYN - // → push R[a][R[b]] (a | b<<16). Pure read; no receiver/`this`/write. - // Attempted before the reg-reg binop fusion (different 3rd op, so no - // conflict, but kept adjacent to its LOAD_REG+LOAD_REG sibling). - if ( - a.opcode === Op.LOAD_REG && - b.opcode === Op.LOAD_REG && - c.opcode === Op.GET_PROP_DYNAMIC - ) { - const ra = a.operand; - const rb = b.operand; - if (ra <= 0xffff && rb <= 0xffff) { - a.opcode = Op.REG_GET_PROP_DYN; - a.operand = (ra & 0xffff) | ((rb & 0xffff) << 16); - b.opcode = Op.NOP; - b.operand = 0; - c.opcode = Op.NOP; - c.operand = 0; - changed = true; - continue; - } - } - - // LOAD_REG(a) + LOAD_REG(b) + → REG_(a | b<<16) - if (a.opcode === Op.LOAD_REG && b.opcode === Op.LOAD_REG) { - const ra = a.operand; - const rb = b.operand; - if (ra <= 0xffff && rb <= 0xffff) { - const superOp = REG_BINOP_MAP.get(c.opcode); - if (superOp != null) { - a.opcode = superOp; - a.operand = (ra & 0xffff) | ((rb & 0xffff) << 16); - b.opcode = Op.NOP; - b.operand = 0; - c.opcode = Op.NOP; - c.operand = 0; - changed = true; - continue; - } - } - } - - // LOAD_REG(r) + PUSH_CONST(c) + → REG_CONST_(r | c<<16) - if (a.opcode === Op.LOAD_REG && b.opcode === Op.PUSH_CONST) { - const r = a.operand; - const ci = b.operand; - if (r <= 0xffff && ci <= 0xffff) { - const superOp = REG_CONST_BINOP_MAP.get(c.opcode); - if (superOp != null) { - a.opcode = superOp; - a.operand = (r & 0xffff) | ((ci & 0xffff) << 16); - b.opcode = Op.NOP; - b.operand = 0; - c.opcode = Op.NOP; - c.operand = 0; - changed = true; - continue; - } - } - } - - // PUSH_CONST(c) + + JMP_FALSE(t) → CONST__JF(c | t<<16) - // → `if (!(pop() C[c])) IP = t*2`. TOS is the comparison LHS - // (popped), the constant is the RHS — operand order preserved. The - // LOAD_REG-prefixed form is already handled by the 4-instruction - // REG__CONST_JF fusions above; this is the bare-TOS variant. - if (a.opcode === Op.PUSH_CONST && c.opcode === Op.JMP_FALSE) { - const superOp = CONST_CMP_JF_MAP.get(b.opcode); - if (superOp != null) { - const constIdx = a.operand; - const target = c.operand; - if (constIdx <= 0xffff && target <= 0xffff) { - a.opcode = superOp; - a.operand = (constIdx & 0xffff) | ((target & 0xffff) << 16); - b.opcode = Op.NOP; - b.operand = 0; - c.opcode = Op.NOP; - c.operand = 0; - changed = true; - continue; - } - } - } - } - - return changed; -} - -// --------------------------------------------------------------------------- -// NOP removal + jump retargeting -// --------------------------------------------------------------------------- - -function removeNops(emitter: Emitter): void { - const instrs = emitter.instructions; - const nopCount = instrs.filter((i) => i.opcode === Op.NOP).length; - if (nopCount === 0) return; - - // Build a mapping from old index to new index - const indexMap = new Array(instrs.length); - let newIdx = 0; - for (let i = 0; i < instrs.length; i++) { - indexMap[i] = newIdx; - if (instrs[i]!.opcode !== Op.NOP) newIdx++; - } - - // Retarget all jumps - for (const instr of instrs) { - // Simple jumps: operand is a direct IP target - if (ALL_JUMP_OPS.has(instr.opcode)) { - if (instr.operand >= 0 && instr.operand < instrs.length) { - instr.operand = indexMap[instr.operand]!; - } - } - // Packed jumps: target(s) encoded in upper/lower bits - if (instr.opcode === Op.TRY_PUSH) { - let catchIp = (instr.operand >> 16) & 0xffff; - let finallyIp = instr.operand & 0xffff; - if (catchIp !== 0xffff && catchIp < instrs.length) - catchIp = indexMap[catchIp]!; - if (finallyIp !== 0xffff && finallyIp < instrs.length) - finallyIp = indexMap[finallyIp]!; - instr.operand = ((catchIp & 0xffff) << 16) | (finallyIp & 0xffff); - } else if (PACKED_JUMP_OPS.has(instr.opcode)) { - // All other packed jumps (the REG_*_CONST_JF / REG_*_REG_JF - // compare-and-branch superinstructions) carry a single IP target - // in bits 16-31; the low 16 bits hold register/constant indices. - const low = instr.operand & 0xffff; - let target = (instr.operand >>> 16) & 0xffff; - if (target < instrs.length) target = indexMap[target]!; - instr.operand = low | ((target & 0xffff) << 16); - } - } - - // Remove NOPs - const filtered = instrs.filter((i) => i.opcode !== Op.NOP); - instrs.length = 0; - for (const instr of filtered) instrs.push(instr); -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Evaluate a foldable binary opcode on two numeric constants. */ -function foldBinop(op: Op, a: number, b: number): number | null { - const fn = FOLDABLE_BINOPS.get(op); - return fn ? fn(a, b) : null; -} - -function getNumericConst( - consts: ConstantPoolEntry[], - idx: number -): number | null { - const c = consts[idx]; - if (!c) return null; - if (c.type === "number") return c.value; - return null; -} - -/** - * Pre-compute the set of all instruction indices that are jump targets. - * This replaces the O(n) per-query isJumpTarget scan with O(1) lookups. - */ -function computeJumpTargets(instrs: Instruction[]): Set { - const targets = new Set(); - for (const instr of instrs) { - if (ALL_JUMP_OPS.has(instr.opcode)) { - if (instr.operand >= 0) targets.add(instr.operand); - } - if (PACKED_JUMP_OPS.has(instr.opcode)) { - const upper = (instr.operand >>> 16) & 0xffff; - const lower = instr.operand & 0xffff; - if (upper !== 0xffff) targets.add(upper); - if (instr.opcode === Op.TRY_PUSH && lower !== 0xffff) - targets.add(lower); - } - } - return targets; -} diff --git a/packages/ruam/src/compiler/pure-region-learnability.ts b/packages/ruam/src/compiler/pure-region-learnability.ts new file mode 100644 index 0000000..062c9e3 --- /dev/null +++ b/packages/ruam/src/compiler/pure-region-learnability.ts @@ -0,0 +1,890 @@ +/** + * Owner-side black-box learnability analysis for bounded pure-region contracts. + * + * Every query count in this module is a constructive exact-attack upper bound. + * It is intentionally not a hardness estimate and never becomes a client + * artifact. + * + * @module compiler/pure-region-learnability + */ + +import type { + PureRegionContract, + PureRegionFormula, + PureValueRef, + PureValueType, +} from "../isogloss/bprf/index.js"; +import type { + LoweredPureRegionContract, + PureRegionValueDomain, +} from "./pure-region-lowering.js"; + +export const PURE_REGION_LEARNABILITY_NON_CLAIM = + "Constructive exact black-box attack query upper bounds only; this analysis does not establish a hardness lower bound or resistance guarantee."; + +export type PureRegionLearnabilityIssueCode = + | "RUAM_PURE_REGION_LEARNABILITY_NO_INPUTS" + | "RUAM_PURE_REGION_LEARNABILITY_TOO_FEW_STEPS" + | "RUAM_PURE_REGION_LEARNABILITY_NO_OUTPUTS" + | "RUAM_PURE_REGION_LEARNABILITY_INPUT_DOMAIN_COUNT_MISMATCH" + | "RUAM_PURE_REGION_LEARNABILITY_MISSING_INPUT_DOMAIN" + | "RUAM_PURE_REGION_LEARNABILITY_INVALID_INPUT_DOMAIN" + | "RUAM_PURE_REGION_LEARNABILITY_INPUT_TYPE_MISMATCH" + | "RUAM_PURE_REGION_LEARNABILITY_INVALID_VALUE_TYPE" + | "RUAM_PURE_REGION_LEARNABILITY_INVALID_VALUE_REF" + | "RUAM_PURE_REGION_LEARNABILITY_FORMULA_TYPE_MISMATCH" + | "RUAM_PURE_REGION_LEARNABILITY_INVALID_LITERAL" + | "RUAM_PURE_REGION_LEARNABILITY_UNSUPPORTED_FORMULA" + | "RUAM_PURE_REGION_LEARNABILITY_INVALID_LOWERED_INPUT_BINDING"; + +export interface PureRegionLearnabilityIssue { + code: PureRegionLearnabilityIssueCode; + detail: string; + inputIndex: number | null; + stepIndex: number | null; + outputIndex: number | null; + valueRef: PureValueRef | null; +} + +export interface PureRegionInputDomainAnalysis { + inputIndex: number; + type: PureValueType | null; + domain: PureRegionValueDomain | null; + /** Exact finite cardinality of the guarded input domain. */ + cardinality: bigint | null; +} + +export interface PureRegionValueDegreeAnalysis { + valueRef: PureValueRef; + source: "input" | "step"; + type: PureValueType | null; + /** Structural total-degree upper bound, or null when analysis is incomplete. */ + algebraicDegreeUpperBound: bigint | null; +} + +export type DenseInterpolationInapplicability = + | "unknown-algebraic-degree" + | "invalid-input-domain" + | "boolean-input-domain" + | "insufficient-distinct-numeric-points" + | null; + +export interface DenseInterpolationAnalysis { + totalDegreeUpperBound: bigint | null; + /** + * Number of dense total-degree monomials, C(inputCount + degree, degree). + * This is a query attack bound only when attackQueryUpperBound is non-null. + */ + basisQueryCount: bigint | null; + attackQueryUpperBound: bigint | null; + inapplicableReason: DenseInterpolationInapplicability; +} + +export type PureRegionExactAttackMethod = + | "input-enumeration" + | "dense-interpolation" + | "tied"; + +export interface PureRegionExactAttackUpperBound { + method: PureRegionExactAttackMethod; + queries: bigint; +} + +export interface PureRegionOutputLearnabilityAnalysis { + outputIndex: number; + valueRef: PureValueRef; + type: PureValueType | null; + algebraicDegreeUpperBound: bigint | null; + denseInterpolation: DenseInterpolationAnalysis; + cheapestKnownExactAttack: PureRegionExactAttackUpperBound | null; +} + +export interface PureRegionLearnabilityAnalysis { + source: "contract" | "lowered-contract"; + inputDomains: readonly PureRegionInputDomainAnalysis[]; + values: readonly PureRegionValueDegreeAnalysis[]; + outputs: readonly PureRegionOutputLearnabilityAnalysis[]; + /** Exact queries needed to enumerate the complete guarded input domain. */ + inputEnumerationQueryUpperBound: bigint | null; + /** One common sample set sufficient to interpolate every output. */ + denseInterpolation: DenseInterpolationAnalysis; + /** Cheapest known exact attack for learning all outputs simultaneously. */ + cheapestKnownExactAttack: PureRegionExactAttackUpperBound | null; + issues: readonly PureRegionLearnabilityIssue[]; + boundInterpretation: "constructive-exact-attack-upper-bound"; + hardnessLowerBound: null; + nonClaim: typeof PURE_REGION_LEARNABILITY_NON_CLAIM; +} + +export interface MaximumCustodyLearnabilityPolicy { + /** + * Reject when the cheapest known exact attack uses strictly fewer queries. + */ + minimumExactAttackQueries: bigint; +} + +export type MaximumCustodyLearnabilityReason = + | { + code: "RUAM_PURE_REGION_MAXIMUM_CUSTODY_ANALYSIS_ISSUE"; + issue: PureRegionLearnabilityIssue; + } + | { + code: "RUAM_PURE_REGION_MAXIMUM_CUSTODY_NO_EXACT_ATTACK_BOUND"; + } + | { + code: "RUAM_PURE_REGION_MAXIMUM_CUSTODY_CHEAP_EXACT_ATTACK"; + method: PureRegionExactAttackMethod; + queries: bigint; + minimumExactAttackQueries: bigint; + }; + +export interface MaximumCustodyLearnabilityDecision { + decision: "eligible" | "rejected"; + eligibleForMaximumCustody: boolean; + minimumExactAttackQueries: bigint; + cheapestKnownExactAttack: PureRegionExactAttackUpperBound | null; + reasons: readonly MaximumCustodyLearnabilityReason[]; + boundInterpretation: "constructive-exact-attack-upper-bound"; + hardnessLowerBound: null; + nonClaim: typeof PURE_REGION_LEARNABILITY_NON_CLAIM; +} + +interface ValueFact { + type: PureValueType | null; + degree: bigint | null; +} + +interface DerivedDomains { + domains: Array; + issues: PureRegionLearnabilityIssue[]; +} + +export function analyzePureRegionLearnability( + lowered: LoweredPureRegionContract +): PureRegionLearnabilityAnalysis; +export function analyzePureRegionLearnability( + contract: PureRegionContract, + inputDomains: readonly PureRegionValueDomain[] +): PureRegionLearnabilityAnalysis; +export function analyzePureRegionLearnability( + source: LoweredPureRegionContract | PureRegionContract, + inputDomains?: readonly PureRegionValueDomain[] +): PureRegionLearnabilityAnalysis { + if (isLoweredContract(source)) { + const derived = deriveLoweredInputDomains(source); + return analyzeContract( + source.contract, + derived.domains, + "lowered-contract", + derived.issues + ); + } + return analyzeContract( + source, + inputDomains ? Array.from(inputDomains) : [], + "contract", + [] + ); +} + +/** + * Apply only the black-box learnability policy gate for maximum custody. + * + * An eligible result means merely that this particular known-attack upper + * bound did not fall below the configured threshold. It is not evidence that + * the real attack cost reaches that threshold. + */ +export function assessMaximumCustodyLearnability( + analysis: PureRegionLearnabilityAnalysis, + policy: MaximumCustodyLearnabilityPolicy +): MaximumCustodyLearnabilityDecision { + if ( + typeof policy.minimumExactAttackQueries !== "bigint" || + policy.minimumExactAttackQueries < 0n + ) { + throw new Error( + "RUAM_PURE_REGION_LEARNABILITY_INVALID_POLICY_THRESHOLD" + ); + } + + const reasons: MaximumCustodyLearnabilityReason[] = analysis.issues.map( + (issue) => + Object.freeze({ + code: "RUAM_PURE_REGION_MAXIMUM_CUSTODY_ANALYSIS_ISSUE", + issue, + }) + ); + const attack = analysis.cheapestKnownExactAttack; + if (!attack) { + reasons.push( + Object.freeze({ + code: "RUAM_PURE_REGION_MAXIMUM_CUSTODY_NO_EXACT_ATTACK_BOUND", + }) + ); + } else if (attack.queries < policy.minimumExactAttackQueries) { + reasons.push( + Object.freeze({ + code: "RUAM_PURE_REGION_MAXIMUM_CUSTODY_CHEAP_EXACT_ATTACK", + method: attack.method, + queries: attack.queries, + minimumExactAttackQueries: policy.minimumExactAttackQueries, + }) + ); + } + + const eligibleForMaximumCustody = reasons.length === 0; + return Object.freeze({ + decision: eligibleForMaximumCustody ? "eligible" : "rejected", + eligibleForMaximumCustody, + minimumExactAttackQueries: policy.minimumExactAttackQueries, + cheapestKnownExactAttack: attack, + reasons: Object.freeze(reasons), + boundInterpretation: "constructive-exact-attack-upper-bound", + hardnessLowerBound: null, + nonClaim: PURE_REGION_LEARNABILITY_NON_CLAIM, + }); +} + +function analyzeContract( + contract: PureRegionContract, + inputDomains: Array, + source: "contract" | "lowered-contract", + initialIssues: readonly PureRegionLearnabilityIssue[] +): PureRegionLearnabilityAnalysis { + const issues = Array.from(initialIssues); + if (contract.inputs.length === 0) { + addIssue(issues, "RUAM_PURE_REGION_LEARNABILITY_NO_INPUTS", "inputs"); + } + if (contract.steps.length < 2) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_TOO_FEW_STEPS", + String(contract.steps.length) + ); + } + if (contract.outputs.length === 0) { + addIssue(issues, "RUAM_PURE_REGION_LEARNABILITY_NO_OUTPUTS", "outputs"); + } + if (inputDomains.length !== contract.inputs.length) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_INPUT_DOMAIN_COUNT_MISMATCH", + `${inputDomains.length}:${contract.inputs.length}` + ); + } + + const domainAnalyses: PureRegionInputDomainAnalysis[] = []; + const valueFacts: ValueFact[] = []; + const values: PureRegionValueDegreeAnalysis[] = []; + + for (let inputIndex = 0; inputIndex < contract.inputs.length; inputIndex++) { + const declaredType = validValueType(contract.inputs[inputIndex]!.type) + ? contract.inputs[inputIndex]!.type + : null; + if (!declaredType) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_INVALID_VALUE_TYPE", + String(contract.inputs[inputIndex]!.type), + { inputIndex, valueRef: inputIndex } + ); + } + const domain = inputDomains[inputIndex]; + const analyzedDomain = analyzeDomain( + domain, + declaredType, + inputIndex, + issues + ); + domainAnalyses.push(analyzedDomain); + const fact = Object.freeze({ + type: declaredType, + degree: declaredType ? 1n : null, + }); + valueFacts.push(fact); + values.push( + Object.freeze({ + valueRef: inputIndex, + source: "input", + type: fact.type, + algebraicDegreeUpperBound: fact.degree, + }) + ); + } + + for (let stepIndex = 0; stepIndex < contract.steps.length; stepIndex++) { + const step = contract.steps[stepIndex]!; + const valueRef = contract.inputs.length + stepIndex; + const declaredType = validValueType(step.type) ? step.type : null; + if (!declaredType) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_INVALID_VALUE_TYPE", + String(step.type), + { stepIndex, valueRef } + ); + } + const degree = analyzeFormulaDegree( + step.formula, + declaredType, + valueFacts, + stepIndex, + valueRef, + issues + ); + const fact = Object.freeze({ type: declaredType, degree }); + valueFacts.push(fact); + values.push( + Object.freeze({ + valueRef, + source: "step", + type: fact.type, + algebraicDegreeUpperBound: fact.degree, + }) + ); + } + + const enumerationQueries = completeEnumerationQueries(domainAnalyses); + const outputs: PureRegionOutputLearnabilityAnalysis[] = []; + for (let outputIndex = 0; outputIndex < contract.outputs.length; outputIndex++) { + const valueRef = contract.outputs[outputIndex]!; + const fact = readOutputFact(valueRef, valueFacts, outputIndex, issues); + const dense = analyzeDenseInterpolation( + contract.inputs.length, + fact?.degree ?? null, + domainAnalyses + ); + outputs.push( + Object.freeze({ + outputIndex, + valueRef, + type: fact?.type ?? null, + algebraicDegreeUpperBound: fact?.degree ?? null, + denseInterpolation: dense, + cheapestKnownExactAttack: fact + ? chooseCheapestAttack( + enumerationQueries, + dense.attackQueryUpperBound + ) + : null, + }) + ); + } + + const allOutputDegrees = + outputs.length > 0 && + outputs.every( + (output) => output.algebraicDegreeUpperBound !== null + ) + ? outputs.map((output) => output.algebraicDegreeUpperBound!) + : null; + const contractDegree = allOutputDegrees + ? maxBigInt(allOutputDegrees) + : null; + const denseInterpolation = analyzeDenseInterpolation( + contract.inputs.length, + contractDegree, + domainAnalyses + ); + const hasValidOutputs = + outputs.length === contract.outputs.length && + outputs.length > 0 && + outputs.every((output) => output.type !== null); + const cheapestKnownExactAttack = hasValidOutputs + ? chooseCheapestAttack( + enumerationQueries, + denseInterpolation.attackQueryUpperBound + ) + : null; + + return Object.freeze({ + source, + inputDomains: Object.freeze(domainAnalyses), + values: Object.freeze(values), + outputs: Object.freeze(outputs), + inputEnumerationQueryUpperBound: enumerationQueries, + denseInterpolation, + cheapestKnownExactAttack, + issues: Object.freeze(issues), + boundInterpretation: "constructive-exact-attack-upper-bound", + hardnessLowerBound: null, + nonClaim: PURE_REGION_LEARNABILITY_NON_CLAIM, + }); +} + +function analyzeDomain( + domain: PureRegionValueDomain | undefined, + declaredType: PureValueType | null, + inputIndex: number, + issues: PureRegionLearnabilityIssue[] +): PureRegionInputDomainAnalysis { + if (!domain) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_MISSING_INPUT_DOMAIN", + String(inputIndex), + { inputIndex, valueRef: inputIndex } + ); + return Object.freeze({ + inputIndex, + type: declaredType, + domain: null, + cardinality: null, + }); + } + const frozenDomain = freezeDomain(domain); + if ( + domain.type !== "boolean" && + (domain.type !== "number" || + !Number.isSafeInteger(domain.min) || + !Number.isSafeInteger(domain.max) || + domain.min > domain.max) + ) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_INVALID_INPUT_DOMAIN", + String(inputIndex), + { inputIndex, valueRef: inputIndex } + ); + return Object.freeze({ + inputIndex, + type: declaredType, + domain: frozenDomain, + cardinality: null, + }); + } + if (declaredType !== domain.type) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_INPUT_TYPE_MISMATCH", + `${String(declaredType)}:${domain.type}`, + { inputIndex, valueRef: inputIndex } + ); + return Object.freeze({ + inputIndex, + type: declaredType, + domain: frozenDomain, + cardinality: null, + }); + } + const cardinality = + domain.type === "boolean" + ? 2n + : BigInt(domain.max) - BigInt(domain.min) + 1n; + return Object.freeze({ + inputIndex, + type: declaredType, + domain: frozenDomain, + cardinality, + }); +} + +function analyzeFormulaDegree( + formula: PureRegionFormula, + resultType: PureValueType | null, + facts: readonly ValueFact[], + stepIndex: number, + valueRef: PureValueRef, + issues: PureRegionLearnabilityIssue[] +): bigint | null { + const context = { stepIndex, valueRef }; + const unary = ( + ref: PureValueRef, + expectedType: PureValueType + ): ValueFact | null => { + const fact = readFormulaFact(ref, facts, issues, context); + if (fact && (resultType !== expectedType || fact.type !== expectedType)) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_FORMULA_TYPE_MISMATCH", + `${String(resultType)}:${String(fact.type)}:${expectedType}`, + context + ); + return null; + } + return fact; + }; + const binary = ( + leftRef: PureValueRef, + rightRef: PureValueRef, + expectedType: PureValueType + ): readonly [ValueFact, ValueFact] | null => { + const left = readFormulaFact(leftRef, facts, issues, context); + const right = readFormulaFact(rightRef, facts, issues, context); + if (!left || !right) return null; + if ( + resultType !== expectedType || + left.type !== expectedType || + right.type !== expectedType + ) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_FORMULA_TYPE_MISMATCH", + `${String(resultType)}:${String(left.type)}:${String(right.type)}`, + context + ); + return null; + } + return [left, right]; + }; + + switch (formula.tag) { + case "literal": { + const validLiteral = + formula.type === resultType && + ((formula.type === "number" && + typeof formula.value === "number" && + Number.isFinite(formula.value)) || + (formula.type === "boolean" && + typeof formula.value === "boolean")); + if (!validLiteral) { + addIssue( + issues, + formula.type === resultType + ? "RUAM_PURE_REGION_LEARNABILITY_INVALID_LITERAL" + : "RUAM_PURE_REGION_LEARNABILITY_FORMULA_TYPE_MISMATCH", + String(formula.type), + context + ); + return null; + } + return 0n; + } + case "sum": + case "difference": { + const pair = binary(formula.left, formula.right, "number"); + return pair && + pair[0].degree !== null && + pair[1].degree !== null + ? maxBigInt([pair[0].degree, pair[1].degree]) + : null; + } + case "product": { + const pair = binary(formula.left, formula.right, "number"); + return pair && + pair[0].degree !== null && + pair[1].degree !== null + ? pair[0].degree + pair[1].degree + : null; + } + case "negate": { + const value = unary(formula.value, "number"); + return value?.degree ?? null; + } + case "not": { + const value = unary(formula.value, "boolean"); + return value?.degree ?? null; + } + case "and": + case "or": + case "xor": { + const pair = binary(formula.left, formula.right, "boolean"); + return pair && + pair[0].degree !== null && + pair[1].degree !== null + ? pair[0].degree + pair[1].degree + : null; + } + case "select": { + const gate = readFormulaFact( + formula.gate, + facts, + issues, + context + ); + const whenTrue = readFormulaFact( + formula.whenTrue, + facts, + issues, + context + ); + const whenFalse = readFormulaFact( + formula.whenFalse, + facts, + issues, + context + ); + if (!gate || !whenTrue || !whenFalse) return null; + if ( + gate.type !== "boolean" || + resultType === null || + whenTrue.type !== resultType || + whenFalse.type !== resultType + ) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_FORMULA_TYPE_MISMATCH", + `${String(gate.type)}:${String(whenTrue.type)}:${String(whenFalse.type)}`, + context + ); + return null; + } + if ( + gate.degree === null || + whenTrue.degree === null || + whenFalse.degree === null + ) { + return null; + } + return maxBigInt([ + whenFalse.degree, + gate.degree + whenTrue.degree, + gate.degree + whenFalse.degree, + ]); + } + default: + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_UNSUPPORTED_FORMULA", + String((formula as { tag?: unknown }).tag), + context + ); + return null; + } +} + +function analyzeDenseInterpolation( + inputCount: number, + degree: bigint | null, + domains: readonly PureRegionInputDomainAnalysis[] +): DenseInterpolationAnalysis { + if (degree === null) { + return freezeDense(null, null, null, "unknown-algebraic-degree"); + } + const basisQueries = + inputCount > 0 ? denseMonomialCount(inputCount, degree) : null; + if ( + basisQueries === null || + domains.length !== inputCount || + domains.some((domain) => domain.cardinality === null) + ) { + return freezeDense( + degree, + basisQueries, + null, + "invalid-input-domain" + ); + } + if (degree === 0n) { + return freezeDense(degree, basisQueries, 1n, null); + } + if (domains.some((domain) => domain.domain?.type === "boolean")) { + return freezeDense( + degree, + basisQueries, + null, + "boolean-input-domain" + ); + } + const requiredDistinctPoints = degree + 1n; + if ( + domains.some( + (domain) => domain.cardinality! < requiredDistinctPoints + ) + ) { + return freezeDense( + degree, + basisQueries, + null, + "insufficient-distinct-numeric-points" + ); + } + return freezeDense(degree, basisQueries, basisQueries, null); +} + +function denseMonomialCount( + inputCount: number, + degree: bigint +): bigint { + let result = 1n; + for (let input = 1; input <= inputCount; input++) { + const factor = BigInt(input); + result = (result * (degree + factor)) / factor; + } + return result; +} + +function completeEnumerationQueries( + domains: readonly PureRegionInputDomainAnalysis[] +): bigint | null { + if ( + domains.length === 0 || + domains.some((domain) => domain.cardinality === null) + ) { + return null; + } + return domains.reduce( + (product, domain) => product * domain.cardinality!, + 1n + ); +} + +function chooseCheapestAttack( + enumerationQueries: bigint | null, + denseQueries: bigint | null +): PureRegionExactAttackUpperBound | null { + if (enumerationQueries === null && denseQueries === null) return null; + if (enumerationQueries === null) { + return Object.freeze({ + method: "dense-interpolation", + queries: denseQueries!, + }); + } + if (denseQueries === null) { + return Object.freeze({ + method: "input-enumeration", + queries: enumerationQueries, + }); + } + if (enumerationQueries === denseQueries) { + return Object.freeze({ + method: "tied", + queries: enumerationQueries, + }); + } + return enumerationQueries < denseQueries + ? Object.freeze({ + method: "input-enumeration", + queries: enumerationQueries, + }) + : Object.freeze({ + method: "dense-interpolation", + queries: denseQueries, + }); +} + +function readFormulaFact( + ref: PureValueRef, + facts: readonly ValueFact[], + issues: PureRegionLearnabilityIssue[], + context: { stepIndex: number; valueRef: PureValueRef } +): ValueFact | null { + if (!validPriorRef(ref, facts.length)) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_INVALID_VALUE_REF", + String(ref), + { ...context, valueRef: ref } + ); + return null; + } + return facts[ref]!; +} + +function readOutputFact( + ref: PureValueRef, + facts: readonly ValueFact[], + outputIndex: number, + issues: PureRegionLearnabilityIssue[] +): ValueFact | null { + if (!validPriorRef(ref, facts.length)) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_INVALID_VALUE_REF", + String(ref), + { outputIndex, valueRef: ref } + ); + return null; + } + return facts[ref]!; +} + +function deriveLoweredInputDomains( + lowered: LoweredPureRegionContract +): DerivedDomains { + const domains = Array( + lowered.contract.inputs.length + ).fill(undefined); + const issues: PureRegionLearnabilityIssue[] = []; + for (const binding of lowered.inputBindings) { + if ( + !Number.isSafeInteger(binding.contractInput) || + binding.contractInput < 0 || + binding.contractInput >= domains.length || + domains[binding.contractInput] !== undefined + ) { + addIssue( + issues, + "RUAM_PURE_REGION_LEARNABILITY_INVALID_LOWERED_INPUT_BINDING", + String(binding.contractInput), + { + inputIndex: Number.isSafeInteger(binding.contractInput) + ? binding.contractInput + : null, + } + ); + continue; + } + domains[binding.contractInput] = binding.domain; + } + return { domains, issues }; +} + +function freezeDense( + degree: bigint | null, + basisQueries: bigint | null, + attackQueries: bigint | null, + inapplicableReason: DenseInterpolationInapplicability +): DenseInterpolationAnalysis { + return Object.freeze({ + totalDegreeUpperBound: degree, + basisQueryCount: basisQueries, + attackQueryUpperBound: attackQueries, + inapplicableReason, + }); +} + +function freezeDomain( + domain: PureRegionValueDomain +): PureRegionValueDomain { + return Object.freeze({ ...domain }) as PureRegionValueDomain; +} + +function validValueType(value: unknown): value is PureValueType { + return value === "number" || value === "boolean"; +} + +function validPriorRef(ref: PureValueRef, upperBound: number): boolean { + return ( + Number.isSafeInteger(ref) && + ref >= 0 && + ref < upperBound + ); +} + +function maxBigInt(values: readonly bigint[]): bigint { + let result = values[0]!; + for (let index = 1; index < values.length; index++) { + if (values[index]! > result) result = values[index]!; + } + return result; +} + +function isLoweredContract( + source: LoweredPureRegionContract | PureRegionContract +): source is LoweredPureRegionContract { + return "contract" in source; +} + +function addIssue( + issues: PureRegionLearnabilityIssue[], + code: PureRegionLearnabilityIssueCode, + detail: string, + location: Partial< + Pick< + PureRegionLearnabilityIssue, + "inputIndex" | "stepIndex" | "outputIndex" | "valueRef" + > + > = {} +): void { + issues.push( + Object.freeze({ + code, + detail, + inputIndex: location.inputIndex ?? null, + stepIndex: location.stepIndex ?? null, + outputIndex: location.outputIndex ?? null, + valueRef: location.valueRef ?? null, + }) + ); +} diff --git a/packages/ruam/src/compiler/pure-region-lowering.ts b/packages/ruam/src/compiler/pure-region-lowering.ts new file mode 100644 index 0000000..66378a0 --- /dev/null +++ b/packages/ruam/src/compiler/pure-region-lowering.ts @@ -0,0 +1,935 @@ +/** + * Sound lowering gate from canonical effect-region spans to bounded BPRF + * pure-region contracts. + * + * This module is compiler/owner-side only. It consumes SemanticOp identities + * while proving a typed regional relation, then emits a PureRegionContract + * whose generated BPRF artifact contains no operation or source-node identity. + * + * @module compiler/pure-region-lowering + */ + +import type { + PureRegionContract, + PureRegionFormula, + PureRegionStep, + PureScalar, + PureValueRef, +} from "../isogloss/bprf/index.js"; +import type { SemanticInstruction, SemanticUnit } from "./ir.js"; +import type { + EffectRegion, + EffectRegionExit, + EffectRegionGraph, + EffectRegionId, +} from "./regions.js"; +import { + SemanticOp, + semanticOpName, +} from "./semantic-ops.js"; + +/** + * Keeps logical and BPRF's expanded polynomial coordinates comfortably below + * Number's exact-integer ceiling. This is a spike gate, not a general JS-number + * lowering rule. + */ +export const MAX_PURE_REGION_INTEGER_MAGNITUDE = 1_000_000; + +/** + * Stack indices are bottom-to-top within the selected span's bounded entry or + * exit stack window. Frame bindings use canonical argument/register/slot + * indices and are later rewritten by contextual frame assignment. + */ +export type PureRegionBinding = + | { kind: "stack"; index: number } + | { kind: "argument"; index: number } + | { kind: "register"; index: number } + | { kind: "slot"; index: number }; + +export type PureRegionValueDomain = + | { type: "boolean" } + | { + type: "number"; + /** Inclusive, exact safe-integer lower bound. */ + min: number; + /** Inclusive, exact safe-integer upper bound. */ + max: number; + }; + +/** A guarded external value assumed at the selected span's entry. */ +export interface PureRegionInputAssumption { + binding: PureRegionBinding; + domain: PureRegionValueDomain; +} + +export interface PureRegionLoweringRequest { + /** Consecutive effect regions in canonical graph order. */ + regionIds: readonly EffectRegionId[]; + /** + * Exact entry guards. Number domains accept only non-negative-zero safe + * integers within the declared inclusive range. + */ + assumptions: readonly PureRegionInputAssumption[]; +} + +export interface PureRegionInputBinding { + contractInput: number; + binding: PureRegionBinding; + domain: PureRegionValueDomain; +} + +export interface PureRegionOutputBinding { + contractOutput: number; + valueRef: PureValueRef; + binding: PureRegionBinding; + domain: PureRegionValueDomain; +} + +export interface LoweredPureRegionContract { + unitId: string; + regionIds: readonly EffectRegionId[]; + contract: PureRegionContract; + inputBindings: readonly PureRegionInputBinding[]; + outputBindings: readonly PureRegionOutputBinding[]; +} + +interface AbstractValueBase { + symbol: number; +} + +interface AbstractBoolean extends AbstractValueBase { + type: "boolean"; +} + +interface AbstractNumber extends AbstractValueBase { + type: "number"; + min: number; + max: number; +} + +type AbstractValue = AbstractBoolean | AbstractNumber; +type FrameValue = AbstractValue | typeof UNINITIALIZED; + +interface SymbolicInput { + value: AbstractValue; + binding: PureRegionBinding; + domain: PureRegionValueDomain; +} + +interface SymbolicStep { + value: AbstractValue; + formula: PureRegionFormula; +} + +interface PendingOutput { + binding: PureRegionBinding; + value: AbstractValue; +} + +const UNINITIALIZED = Symbol("ruam-pure-region-uninitialized"); +const STATE_BINDING_ORDER: Readonly> = Object.freeze({ + argument: 0, + register: 1, + slot: 2, +}); + +/** + * Lower a consecutive, single-entry/single-fallthrough region span. + * + * The function throws on every unproved case. A successful result is valid + * only under its explicit input binding guards; callers must route guard + * failures to an ordinary JavaScript fallback rather than coerce them. + */ +export function lowerEffectRegionsToPureContract( + unit: SemanticUnit, + graph: EffectRegionGraph, + request: PureRegionLoweringRequest +): LoweredPureRegionContract { + if (graph.unitId !== unit.id) { + fail("RUAM_PURE_REGION_UNIT_MISMATCH"); + } + const selected = selectAndValidateTopology(graph, request.regionIds); + const nodes = new Map(unit.nodes.map((node) => [node.id, node])); + const assumptions = buildAssumptionMap(request.assumptions); + const usedAssumptions = new Set(); + const symbolicInputs: SymbolicInput[] = []; + const symbolicSteps: SymbolicStep[] = []; + const stack: AbstractValue[] = []; + const frame = new Map(); + const modifiedBindings = new Map(); + let nextSymbol = 0; + + const createInput = (binding: PureRegionBinding): AbstractValue => { + const key = bindingKey(binding); + const assumption = assumptions.get(key); + if (!assumption) { + fail( + "RUAM_PURE_REGION_INPUT_TYPE_REQUIRED", + `${binding.kind}:${binding.index}` + ); + } + usedAssumptions.add(key); + const domain = validateAndFreezeDomain(assumption.domain); + const value = valueFromDomain(nextSymbol++, domain); + symbolicInputs.push({ + value, + binding: freezeBinding(binding), + domain, + }); + return value; + }; + + const addStep = ( + domain: PureRegionValueDomain, + formula: PureRegionFormula + ): AbstractValue => { + const frozenDomain = validateAndFreezeDomain(domain); + const value = valueFromDomain(nextSymbol++, frozenDomain); + symbolicSteps.push({ + value, + formula: Object.freeze({ ...formula }) as PureRegionFormula, + }); + return value; + }; + + const firstStack = selected[0]!.inputs.stack; + if (firstStack === "dynamic") { + fail("RUAM_PURE_REGION_DYNAMIC_STACK"); + } + for (let index = 0; index < firstStack; index++) { + stack.push(createInput({ kind: "stack", index })); + } + + const readFrame = ( + kind: "argument" | "register" | "slot", + index: number + ): AbstractValue => { + const binding = { kind, index } as const; + const key = bindingKey(binding); + let value = frame.get(key); + if (value === undefined) { + value = createInput(binding); + frame.set(key, value); + } + if (value === UNINITIALIZED) { + fail( + "RUAM_PURE_REGION_UNINITIALIZED_READ", + `${kind}:${index}` + ); + } + return value; + }; + + const writeFrame = ( + kind: "argument" | "register" | "slot", + index: number, + value: FrameValue + ): void => { + const binding = freezeBinding({ kind, index }); + const key = bindingKey(binding); + frame.set(key, value); + modifiedBindings.set(key, binding); + }; + + const pop = (node: SemanticInstruction): AbstractValue => { + const value = stack.pop(); + if (!value) { + fail( + "RUAM_PURE_REGION_STACK_UNDERFLOW", + `${node.id}:${semanticOpName(node.op)}` + ); + } + return value; + }; + + const pushLiteral = ( + domain: PureRegionValueDomain, + value: number | boolean + ): void => { + stack.push( + addStep( + domain, + typeof value === "boolean" + ? { tag: "literal", type: "boolean", value } + : { tag: "literal", type: "number", value } + ) + ); + }; + + for (const region of selected) { + if (region.inputs.stack === "dynamic") { + fail("RUAM_PURE_REGION_DYNAMIC_STACK", region.id); + } + if (stack.length !== region.inputs.stack) { + fail( + "RUAM_PURE_REGION_STACK_CONTRACT_MISMATCH", + `${region.id}: expected ${region.inputs.stack}, received ${stack.length}` + ); + } + + for (const nodeId of region.nodeIds) { + const node = nodes.get(nodeId); + if (!node) fail("RUAM_PURE_REGION_UNKNOWN_NODE", String(nodeId)); + + switch (node.op) { + case SemanticOp.PUSH_CONST: { + const constant = unit.constants[node.operand]; + if (!constant) { + fail( + "RUAM_PURE_REGION_INVALID_CONSTANT", + String(node.operand) + ); + } + if (constant.type === "boolean") { + pushLiteral({ type: "boolean" }, constant.value); + } else if (constant.type === "number") { + const domain = exactNumberDomain(constant.value); + pushLiteral(domain, constant.value); + } else { + unsupported(node); + } + break; + } + case SemanticOp.PUSH_ZERO: + pushLiteral(exactNumberDomain(0), 0); + break; + case SemanticOp.PUSH_ONE: + pushLiteral(exactNumberDomain(1), 1); + break; + case SemanticOp.PUSH_NEG_ONE: + pushLiteral(exactNumberDomain(-1), -1); + break; + case SemanticOp.PUSH_TRUE: + pushLiteral({ type: "boolean" }, true); + break; + case SemanticOp.PUSH_FALSE: + pushLiteral({ type: "boolean" }, false); + break; + case SemanticOp.POP: + pop(node); + break; + case SemanticOp.POP_N: { + if (!Number.isSafeInteger(node.operand) || node.operand < 0) { + fail( + "RUAM_PURE_REGION_INVALID_STACK_OPERAND", + String(node.operand) + ); + } + for (let index = 0; index < node.operand; index++) pop(node); + break; + } + case SemanticOp.DUP: { + const value = pop(node); + stack.push(value, value); + break; + } + case SemanticOp.DUP2: { + const right = pop(node); + const left = pop(node); + stack.push(left, right, left, right); + break; + } + case SemanticOp.SWAP: { + const right = pop(node); + const left = pop(node); + stack.push(right, left); + break; + } + case SemanticOp.ROT3: + rotateStack(stack, 3, node); + break; + case SemanticOp.ROT4: + rotateStack(stack, 4, node); + break; + case SemanticOp.PICK: { + if ( + !Number.isSafeInteger(node.operand) || + node.operand < 0 || + node.operand >= stack.length + ) { + fail( + "RUAM_PURE_REGION_INVALID_STACK_OPERAND", + String(node.operand) + ); + } + stack.push(stack[stack.length - node.operand - 1]!); + break; + } + case SemanticOp.LOAD_ARG: + stack.push(readFrame("argument", node.operand)); + break; + case SemanticOp.LOAD_ARG_OR_DEFAULT: + stack.push(readFrame("argument", node.operand)); + break; + case SemanticOp.STORE_ARG: + writeFrame("argument", node.operand, pop(node)); + break; + case SemanticOp.LOAD_REG: + stack.push(readFrame("register", node.operand)); + break; + case SemanticOp.STORE_REG: + writeFrame("register", node.operand, pop(node)); + break; + case SemanticOp.LOAD_SLOT: + stack.push(readFrame("slot", node.operand)); + break; + case SemanticOp.STORE_SLOT: + writeFrame("slot", node.operand, pop(node)); + break; + case SemanticOp.DECLARE_SLOT: + writeFrame("slot", node.operand, UNINITIALIZED); + break; + case SemanticOp.ADD: + lowerBinaryNumber(node, stack, "sum", addStep); + break; + case SemanticOp.SUB: + lowerBinaryNumber(node, stack, "difference", addStep); + break; + case SemanticOp.MUL: + lowerBinaryNumber(node, stack, "product", addStep); + break; + case SemanticOp.NEG: { + const input = requireNumber(pop(node), node); + if (containsZero(input)) { + fail( + "RUAM_PURE_REGION_NEGATIVE_ZERO_RISK", + String(node.id) + ); + } + const domain = boundedNumberDomain(-input.max, -input.min); + stack.push( + addStep(domain, { tag: "negate", value: input.symbol }) + ); + break; + } + case SemanticOp.NOT: { + const input = requireBoolean(pop(node), node); + stack.push( + addStep( + { type: "boolean" }, + { tag: "not", value: input.symbol } + ) + ); + break; + } + case SemanticOp.TO_BOOLEAN: { + const input = requireBoolean(pop(node), node); + stack.push(input); + break; + } + case SemanticOp.NOP: + break; + case SemanticOp.SOURCE_MAP: + break; + default: + unsupported(node); + } + } + + if ( + region.outputs.stack === "dynamic" || + stack.length !== region.outputs.stack + ) { + fail( + "RUAM_PURE_REGION_STACK_CONTRACT_MISMATCH", + `${region.id}: produced ${stack.length}` + ); + } + } + + for (const key of assumptions.keys()) { + if (!usedAssumptions.has(key)) { + fail("RUAM_PURE_REGION_UNUSED_ASSUMPTION", key); + } + } + + const pendingOutputs: PendingOutput[] = stack.map((value, index) => ({ + binding: freezeBinding({ kind: "stack", index }), + value, + })); + const stateOutputs = [...modifiedBindings.values()].sort(compareBindings); + for (const binding of stateOutputs) { + const value = frame.get(bindingKey(binding)); + if (value === undefined || value === UNINITIALIZED) { + fail( + "RUAM_PURE_REGION_UNREPRESENTABLE_STATE_OUTPUT", + bindingKey(binding) + ); + } + pendingOutputs.push({ binding, value }); + } + + if (symbolicInputs.length === 0) { + fail("RUAM_PURE_REGION_REQUIRES_INPUT"); + } + if (symbolicSteps.length < 2) { + fail("RUAM_PURE_REGION_REQUIRES_BRAIDABLE_STEPS"); + } + if (pendingOutputs.length === 0) { + fail("RUAM_PURE_REGION_REQUIRES_OUTPUT"); + } + + const finalized = finalizeContract( + symbolicInputs, + symbolicSteps, + pendingOutputs + ); + return Object.freeze({ + unitId: unit.id, + regionIds: Object.freeze(selected.map((region) => region.id)), + contract: finalized.contract, + inputBindings: finalized.inputBindings, + outputBindings: finalized.outputBindings, + }); +} + +/** Check the exact guard represented by a lowering input binding. */ +export function isPureRegionValueInDomain( + value: PureScalar, + domain: PureRegionValueDomain +): boolean { + if (domain.type === "boolean") return typeof value === "boolean"; + return ( + typeof value === "number" && + Number.isSafeInteger(value) && + !Object.is(value, -0) && + value >= domain.min && + value <= domain.max + ); +} + +function selectAndValidateTopology( + graph: EffectRegionGraph, + regionIds: readonly EffectRegionId[] +): EffectRegion[] { + if (regionIds.length === 0) { + fail("RUAM_PURE_REGION_EMPTY_SELECTION"); + } + const indexes = regionIds.map((regionId) => + graph.regions.findIndex((region) => region.id === regionId) + ); + if (indexes.some((index) => index < 0)) { + fail("RUAM_PURE_REGION_UNKNOWN_REGION"); + } + if (new Set(regionIds).size !== regionIds.length) { + fail("RUAM_PURE_REGION_DUPLICATE_REGION"); + } + for (let index = 1; index < indexes.length; index++) { + if (indexes[index] !== indexes[index - 1]! + 1) { + fail("RUAM_PURE_REGION_NONCONTIGUOUS_SELECTION"); + } + } + const selected = indexes.map((index) => graph.regions[index]!); + const selectedIds = new Set(regionIds); + const incoming = new Map< + EffectRegionId, + Array<{ source: EffectRegionId; exit: EffectRegionExit }> + >(); + for (const region of graph.regions) { + for (const exit of region.exits) { + const target = exitRegionId(exit); + if (!target) continue; + const entries = incoming.get(target) ?? []; + entries.push({ source: region.id, exit }); + incoming.set(target, entries); + } + } + + for (let index = 0; index < selected.length; index++) { + const region = selected[index]!; + const next = selected[index + 1]; + for (const exit of region.exits) { + if ( + exit.kind === "branch-true" || + exit.kind === "branch-false" || + exit.kind === "call" || + exit.kind === "yield" || + exit.kind === "await" || + exit.kind === "finally" || + exit.kind === "return" + ) { + fail( + "RUAM_PURE_REGION_UNREPRESENTABLE_CONTROL", + `${region.id}:${exit.kind}` + ); + } + if ( + (exit.kind === "exception" || exit.kind === "throw") && + "targetRegionId" in exit && + selectedIds.has(exit.targetRegionId) + ) { + fail( + "RUAM_PURE_REGION_EXCEPTION_PATH_SELECTED", + region.id + ); + } + } + const fallthroughs = region.exits.filter( + ( + exit + ): exit is Extract< + EffectRegionExit, + { targetRegionId: EffectRegionId } + > => + exit.kind === "fallthrough" + ); + if (fallthroughs.length !== 1) { + fail("RUAM_PURE_REGION_REQUIRES_FALLTHROUGH", region.id); + } + if (next) { + if (fallthroughs[0]!.targetRegionId !== next.id) { + fail("RUAM_PURE_REGION_NONLINEAR_SELECTION", region.id); + } + } else if (selectedIds.has(fallthroughs[0]!.targetRegionId)) { + fail("RUAM_PURE_REGION_CYCLIC_SELECTION", region.id); + } + + const entries = incoming.get(region.id) ?? []; + const normalEntries = entries.filter( + (entry) => + entry.exit.kind !== "exception" && + entry.exit.kind !== "finally" + ); + const exceptionalEntries = entries.filter( + (entry) => + entry.exit.kind === "exception" || + entry.exit.kind === "finally" + ); + if (exceptionalEntries.length > 0) { + fail("RUAM_PURE_REGION_EXCEPTION_ENTRY", region.id); + } + if (index === 0) { + if ( + normalEntries.length > 1 || + normalEntries.some( + (entry) => entry.exit.kind !== "fallthrough" + ) + ) { + fail("RUAM_PURE_REGION_CONTROL_JOIN", region.id); + } + } else if ( + normalEntries.length !== 1 || + normalEntries[0]!.source !== selected[index - 1]!.id || + normalEntries[0]!.exit.kind !== "fallthrough" + ) { + fail("RUAM_PURE_REGION_CONTROL_JOIN", region.id); + } + } + return selected; +} + +function buildAssumptionMap( + assumptions: readonly PureRegionInputAssumption[] +): Map { + const result = new Map(); + for (const assumption of assumptions) { + validateBinding(assumption.binding); + const key = bindingKey(assumption.binding); + if (result.has(key)) { + fail("RUAM_PURE_REGION_DUPLICATE_ASSUMPTION", key); + } + result.set(key, { + binding: freezeBinding(assumption.binding), + domain: validateAndFreezeDomain(assumption.domain), + }); + } + return result; +} + +function lowerBinaryNumber( + node: SemanticInstruction, + stack: AbstractValue[], + tag: "sum" | "difference" | "product", + addStep: ( + domain: PureRegionValueDomain, + formula: PureRegionFormula + ) => AbstractValue +): void { + const rightValue = stack.pop(); + const leftValue = stack.pop(); + if (!rightValue || !leftValue) { + fail( + "RUAM_PURE_REGION_STACK_UNDERFLOW", + `${node.id}:${semanticOpName(node.op)}` + ); + } + const left = requireNumber(leftValue, node); + const right = requireNumber(rightValue, node); + let domain: PureRegionValueDomain; + if (tag === "sum") { + domain = boundedNumberDomain(left.min + right.min, left.max + right.max); + } else if (tag === "difference") { + domain = boundedNumberDomain(left.min - right.max, left.max - right.min); + } else { + if ( + (containsZero(left) && right.min < 0) || + (containsZero(right) && left.min < 0) + ) { + fail( + "RUAM_PURE_REGION_NEGATIVE_ZERO_RISK", + String(node.id) + ); + } + const products = [ + left.min * right.min, + left.min * right.max, + left.max * right.min, + left.max * right.max, + ]; + domain = boundedNumberDomain( + Math.min(...products), + Math.max(...products) + ); + } + stack.push( + addStep(domain, { + tag, + left: left.symbol, + right: right.symbol, + }) + ); +} + +function finalizeContract( + inputs: readonly SymbolicInput[], + steps: readonly SymbolicStep[], + outputs: readonly PendingOutput[] +): { + contract: PureRegionContract; + inputBindings: readonly PureRegionInputBinding[]; + outputBindings: readonly PureRegionOutputBinding[]; +} { + const refs = new Map(); + const contractInputs = inputs.map((input, index) => { + refs.set(input.value.symbol, index); + return Object.freeze({ type: input.domain.type }); + }); + const inputBindings = inputs.map((input, index) => + Object.freeze({ + contractInput: index, + binding: input.binding, + domain: input.domain, + }) + ); + const contractSteps: PureRegionStep[] = []; + for (const step of steps) { + const formula = remapFormula(step.formula, refs); + const ref = contractInputs.length + contractSteps.length; + refs.set(step.value.symbol, ref); + contractSteps.push( + Object.freeze({ + type: step.value.type, + formula, + }) + ); + } + const contractOutputs = outputs.map((output) => { + const ref = refs.get(output.value.symbol); + if (ref == null) fail("RUAM_PURE_REGION_INTERNAL_MISSING_REF"); + return ref; + }); + const outputBindings = outputs.map((output, index) => + Object.freeze({ + contractOutput: index, + valueRef: contractOutputs[index]!, + binding: output.binding, + domain: domainFromValue(output.value), + }) + ); + return { + contract: Object.freeze({ + inputs: Object.freeze(contractInputs), + steps: Object.freeze(contractSteps), + outputs: Object.freeze(contractOutputs), + }), + inputBindings: Object.freeze(inputBindings), + outputBindings: Object.freeze(outputBindings), + }; +} + +function remapFormula( + formula: PureRegionFormula, + refs: ReadonlyMap +): PureRegionFormula { + const ref = (symbol: number): PureValueRef => { + const value = refs.get(symbol); + if (value == null) fail("RUAM_PURE_REGION_INTERNAL_FORWARD_REF"); + return value; + }; + switch (formula.tag) { + case "literal": + return Object.freeze({ ...formula }); + case "negate": + case "not": + return Object.freeze({ ...formula, value: ref(formula.value) }); + case "sum": + case "difference": + case "product": + case "and": + case "or": + case "xor": + return Object.freeze({ + ...formula, + left: ref(formula.left), + right: ref(formula.right), + }); + case "select": + return Object.freeze({ + ...formula, + gate: ref(formula.gate), + whenTrue: ref(formula.whenTrue), + whenFalse: ref(formula.whenFalse), + }); + } +} + +function rotateStack( + stack: AbstractValue[], + width: 3 | 4, + node: SemanticInstruction +): void { + if (stack.length < width) { + fail( + "RUAM_PURE_REGION_STACK_UNDERFLOW", + `${node.id}:${semanticOpName(node.op)}` + ); + } + const values = stack.splice(stack.length - width, width); + stack.push(values[width - 1]!, ...values.slice(0, width - 1)); +} + +function requireNumber( + value: AbstractValue, + node: SemanticInstruction +): AbstractNumber { + if (value.type !== "number") { + fail( + "RUAM_PURE_REGION_TYPE_MISMATCH", + `${node.id}:${semanticOpName(node.op)} expected number` + ); + } + return value; +} + +function requireBoolean( + value: AbstractValue, + node: SemanticInstruction +): AbstractBoolean { + if (value.type !== "boolean") { + fail( + "RUAM_PURE_REGION_TYPE_MISMATCH", + `${node.id}:${semanticOpName(node.op)} expected boolean` + ); + } + return value; +} + +function valueFromDomain( + symbol: number, + domain: PureRegionValueDomain +): AbstractValue { + return domain.type === "boolean" + ? Object.freeze({ symbol, type: "boolean" }) + : Object.freeze({ + symbol, + type: "number", + min: domain.min, + max: domain.max, + }); +} + +function domainFromValue(value: AbstractValue): PureRegionValueDomain { + return value.type === "boolean" + ? Object.freeze({ type: "boolean" }) + : Object.freeze({ type: "number", min: value.min, max: value.max }); +} + +function exactNumberDomain(value: number): PureRegionValueDomain { + if ( + !Number.isSafeInteger(value) || + Object.is(value, -0) || + Math.abs(value) > MAX_PURE_REGION_INTEGER_MAGNITUDE + ) { + fail("RUAM_PURE_REGION_UNSAFE_NUMBER_LITERAL", String(value)); + } + return Object.freeze({ type: "number", min: value, max: value }); +} + +function boundedNumberDomain( + min: number, + max: number +): PureRegionValueDomain { + if ( + !Number.isSafeInteger(min) || + !Number.isSafeInteger(max) || + min > max || + Math.abs(min) > MAX_PURE_REGION_INTEGER_MAGNITUDE || + Math.abs(max) > MAX_PURE_REGION_INTEGER_MAGNITUDE + ) { + fail( + "RUAM_PURE_REGION_NUMERIC_DOMAIN_OVERFLOW", + `${min}:${max}` + ); + } + return Object.freeze({ type: "number", min, max }); +} + +function validateAndFreezeDomain( + domain: PureRegionValueDomain +): PureRegionValueDomain { + if (domain.type === "boolean") { + return Object.freeze({ type: "boolean" }); + } + return boundedNumberDomain(domain.min, domain.max); +} + +function containsZero(value: AbstractNumber): boolean { + return value.min <= 0 && value.max >= 0; +} + +function validateBinding(binding: PureRegionBinding): void { + if ( + !Number.isSafeInteger(binding.index) || + binding.index < 0 + ) { + fail( + "RUAM_PURE_REGION_INVALID_BINDING", + `${binding.kind}:${binding.index}` + ); + } +} + +function freezeBinding(binding: PureRegionBinding): PureRegionBinding { + validateBinding(binding); + return Object.freeze({ ...binding }) as PureRegionBinding; +} + +function bindingKey(binding: PureRegionBinding): string { + return `${binding.kind}:${binding.index}`; +} + +function compareBindings( + left: PureRegionBinding, + right: PureRegionBinding +): number { + const leftOrder = STATE_BINDING_ORDER[left.kind] ?? -1; + const rightOrder = STATE_BINDING_ORDER[right.kind] ?? -1; + return leftOrder - rightOrder || left.index - right.index; +} + +function exitRegionId(exit: EffectRegionExit): EffectRegionId | null { + if ("targetRegionId" in exit) return exit.targetRegionId; + if ("resumeRegionId" in exit) return exit.resumeRegionId; + return null; +} + +function unsupported(node: SemanticInstruction): never { + fail( + "RUAM_PURE_REGION_UNSUPPORTED_OP", + `${node.id}:${semanticOpName(node.op)}` + ); +} + +function fail(code: string, detail?: string): never { + throw new Error(detail ? `${code}: ${detail}` : code); +} diff --git a/packages/ruam/src/compiler/pure-region-planning.ts b/packages/ruam/src/compiler/pure-region-planning.ts new file mode 100644 index 0000000..c6bc55b --- /dev/null +++ b/packages/ruam/src/compiler/pure-region-planning.ts @@ -0,0 +1,253 @@ +/** + * Deterministic compiler-side candidate planning for bounded pure regions. + * + * The planner does not infer JavaScript value types or duplicate the lowering + * proof. Every eligible entry receives explicit assumptions from its caller, + * and every candidate is accepted only by lowerEffectRegionsToPureContract. + * + * @module compiler/pure-region-planning + */ + +import type { SemanticUnit } from "./ir.js"; +import { + lowerEffectRegionsToPureContract, + type LoweredPureRegionContract, + type PureRegionInputAssumption, +} from "./pure-region-lowering.js"; +import type { + EffectRegion, + EffectRegionGraph, + EffectRegionId, +} from "./regions.js"; + +const LOWERING_REJECTION_CODES = Object.freeze([ + "RUAM_PURE_REGION_CONTROL_JOIN", + "RUAM_PURE_REGION_CYCLIC_SELECTION", + "RUAM_PURE_REGION_DUPLICATE_ASSUMPTION", + "RUAM_PURE_REGION_DYNAMIC_STACK", + "RUAM_PURE_REGION_EXCEPTION_ENTRY", + "RUAM_PURE_REGION_EXCEPTION_PATH_SELECTED", + "RUAM_PURE_REGION_INPUT_TYPE_REQUIRED", + "RUAM_PURE_REGION_INVALID_BINDING", + "RUAM_PURE_REGION_NEGATIVE_ZERO_RISK", + "RUAM_PURE_REGION_NONLINEAR_SELECTION", + "RUAM_PURE_REGION_NUMERIC_DOMAIN_OVERFLOW", + "RUAM_PURE_REGION_REQUIRES_BRAIDABLE_STEPS", + "RUAM_PURE_REGION_REQUIRES_FALLTHROUGH", + "RUAM_PURE_REGION_REQUIRES_INPUT", + "RUAM_PURE_REGION_REQUIRES_OUTPUT", + "RUAM_PURE_REGION_TYPE_MISMATCH", + "RUAM_PURE_REGION_UNINITIALIZED_READ", + "RUAM_PURE_REGION_UNREPRESENTABLE_CONTROL", + "RUAM_PURE_REGION_UNREPRESENTABLE_STATE_OUTPUT", + "RUAM_PURE_REGION_UNSAFE_NUMBER_LITERAL", + "RUAM_PURE_REGION_UNSUPPORTED_OP", + "RUAM_PURE_REGION_UNUSED_ASSUMPTION", +] as const); + +const LOWERING_REJECTION_CODE_SET: ReadonlySet = new Set( + LOWERING_REJECTION_CODES +); +const NO_ASSUMPTIONS_CODE = + "RUAM_PURE_REGION_PLAN_ASSUMPTIONS_UNAVAILABLE" as const; + +export type PureRegionLoweringRejectionCode = + (typeof LOWERING_REJECTION_CODES)[number]; +export type PureRegionCandidateRejectionCode = + | PureRegionLoweringRejectionCode + | typeof NO_ASSUMPTIONS_CODE; + +/** + * Called at most once for each unconsumed graph-order entry considered by the + * planner. Returning undefined makes the entry ineligible; an empty array is + * an explicit assumption set and is still submitted to the sound lowerer. + */ +export type PureRegionEntryAssumptionProvider = ( + entryRegion: EffectRegion, + entryRegionIndex: number +) => readonly PureRegionInputAssumption[] | undefined; + +/** Assumptions keyed by the region at which their guards would be installed. */ +export type PureRegionEntryAssumptionMap = ReadonlyMap< + EffectRegionId, + readonly PureRegionInputAssumption[] +>; + +export type PureRegionEntryAssumptionSource = + | PureRegionEntryAssumptionMap + | PureRegionEntryAssumptionProvider; + +export interface PlannedPureRegionCandidate { + entryRegionId: EffectRegionId; + /** Inclusive index in EffectRegionGraph.regions. */ + startRegionIndex: number; + /** Inclusive index in EffectRegionGraph.regions. */ + endRegionIndex: number; + lowered: LoweredPureRegionContract; +} + +/** + * One deterministic failed lowering attempt. Diagnostics remain compiler-only + * and may retain region identities; generated BPRF artifacts do not. + */ +export interface PureRegionCandidateRejection { + entryRegionId: EffectRegionId; + startRegionIndex: number; + /** Inclusive attempted end, or the start for an untyped entry. */ + endRegionIndex: number; + regionIds: readonly EffectRegionId[]; + code: PureRegionCandidateRejectionCode; + detail: string | null; +} + +export interface PureRegionCandidatePlan { + unitId: string; + candidates: readonly PlannedPureRegionCandidate[]; + rejections: readonly PureRegionCandidateRejection[]; +} + +/** + * Discover disjoint maximal lowerable spans in canonical graph order. + * + * At each unconsumed entry with explicit assumptions, ends are attempted from + * longest to shortest. The first success is therefore maximal for that entry + * under precisely those guards. Its complete span is consumed before planning + * resumes, which makes candidates disjoint and gives deterministic left-to- + * right precedence. + * + * Only recognized proof failures become diagnostics. Malformed canonical + * inputs, lowerer invariant failures, provider exceptions, and all unexpected + * errors propagate instead of being mislabeled as ordinary ineligibility. + */ +export function planPureRegionCandidates( + unit: SemanticUnit, + graph: EffectRegionGraph, + assumptionSource: PureRegionEntryAssumptionSource +): PureRegionCandidatePlan { + if (graph.unitId !== unit.id) { + throw new Error( + `RUAM_PURE_REGION_PLAN_UNIT_MISMATCH: ${String(graph.unitId)}:${unit.id}` + ); + } + + const candidates: PlannedPureRegionCandidate[] = []; + const rejections: PureRegionCandidateRejection[] = []; + let startRegionIndex = 0; + + while (startRegionIndex < graph.regions.length) { + const entryRegion = graph.regions[startRegionIndex]!; + const assumptions = resolveAssumptions( + assumptionSource, + entryRegion, + startRegionIndex + ); + + if (assumptions === undefined) { + rejections.push( + freezeRejection({ + entryRegionId: entryRegion.id, + startRegionIndex, + endRegionIndex: startRegionIndex, + regionIds: Object.freeze([entryRegion.id]), + code: NO_ASSUMPTIONS_CODE, + detail: null, + }) + ); + startRegionIndex++; + continue; + } + + let accepted: PlannedPureRegionCandidate | undefined; + for ( + let endRegionIndex = graph.regions.length - 1; + endRegionIndex >= startRegionIndex; + endRegionIndex-- + ) { + const regionIds = Object.freeze( + graph.regions + .slice(startRegionIndex, endRegionIndex + 1) + .map((region) => region.id) + ); + try { + const lowered = lowerEffectRegionsToPureContract(unit, graph, { + regionIds, + assumptions, + }); + accepted = Object.freeze({ + entryRegionId: entryRegion.id, + startRegionIndex, + endRegionIndex, + lowered, + }); + break; + } catch (error) { + const rejection = classifyOrdinaryRejection(error); + if (!rejection) throw error; + rejections.push( + freezeRejection({ + entryRegionId: entryRegion.id, + startRegionIndex, + endRegionIndex, + regionIds, + code: rejection.code, + detail: rejection.detail, + }) + ); + } + } + + if (accepted) { + candidates.push(accepted); + startRegionIndex = accepted.endRegionIndex + 1; + } else { + startRegionIndex++; + } + } + + return Object.freeze({ + unitId: unit.id, + candidates: Object.freeze(candidates), + rejections: Object.freeze(rejections), + }); +} + +function resolveAssumptions( + source: PureRegionEntryAssumptionSource, + entryRegion: EffectRegion, + entryRegionIndex: number +): readonly PureRegionInputAssumption[] | undefined { + const assumptions = + typeof source === "function" + ? source(entryRegion, entryRegionIndex) + : source.get(entryRegion.id); + if (assumptions === undefined) return undefined; + + return Object.freeze( + assumptions.map((assumption) => + Object.freeze({ + binding: Object.freeze({ ...assumption.binding }), + domain: Object.freeze({ ...assumption.domain }), + }) + ) + ); +} + +function classifyOrdinaryRejection( + error: unknown +): { code: PureRegionLoweringRejectionCode; detail: string | null } | null { + if (!(error instanceof Error)) return null; + const match = /^(RUAM_PURE_REGION_[A-Z_]+)(?:: (.*))?$/.exec( + error.message + ); + if (!match || !LOWERING_REJECTION_CODE_SET.has(match[1]!)) return null; + return { + code: match[1] as PureRegionLoweringRejectionCode, + detail: match[2] ?? null, + }; +} + +function freezeRejection( + rejection: PureRegionCandidateRejection +): PureRegionCandidateRejection { + return Object.freeze(rejection); +} diff --git a/packages/ruam/src/compiler/regions.ts b/packages/ruam/src/compiler/regions.ts new file mode 100644 index 0000000..b849106 --- /dev/null +++ b/packages/ruam/src/compiler/regions.ts @@ -0,0 +1,772 @@ +/** + * Conservative effect-delimited regions over canonical semantic control flow. + * + * Regions are a compiler artifact. They retain canonical node ownership for + * verification, but production BPRF lowering must not publish these identities. + * Fusion fails closed at every observable, exceptional, dynamic-stack, call, + * suspension, branch, join, or structured-completion boundary. + * + * @module compiler/regions + */ + +import type { CanonicalCfg } from "./cfg.js"; +import type { + SemanticExit, + SemanticInstruction, + SemanticNodeId, + SemanticUnit, +} from "./ir.js"; +import { + getSemanticSignature, + resolveStackArity, + type ResolvedStackArity, + type SemanticAccess, + type SemanticAllocation, + type SemanticCallKind, + type SemanticCoercion, + type SemanticCompletion, + type SemanticEffect, + type SemanticPurity, + type SemanticSignature, + type SemanticSuspensionKind, + type SemanticThrowBehavior, +} from "./semantic-signatures.js"; + +export type EffectRegionId = string; + +export type RegionStateDomain = + | "register" + | "argument" + | "slot" + | "frame" + | "scope" + | "object" + | "global" + | "this"; + +/** One state dependency crossing a region boundary. */ +export interface RegionStatePort { + domain: RegionStateDomain; + /** Known register, argument, slot, or constant-pool name index. */ + key?: number; +} + +/** Stack and non-stack dependencies crossing one side of a region. */ +export interface RegionBoundaryContract { + stack: ResolvedStackArity; + state: readonly RegionStatePort[]; +} + +/** Aggregate effect facts for a complete region. */ +export interface RegionEffectSummary { + effects: readonly SemanticEffect[]; + purity: SemanticPurity; + throwBehavior: SemanticThrowBehavior; + coercions: readonly SemanticCoercion[]; + callKinds: readonly SemanticCallKind[]; + suspensions: readonly SemanticSuspensionKind[]; + frameAccess: SemanticAccess; + scopeAccess: SemanticAccess; + objectAccess: SemanticAccess; + globalAccess: SemanticAccess; + allocations: readonly SemanticAllocation[]; + completions: readonly SemanticCompletion[]; + readsThis: boolean; + hasDynamicStack: boolean; +} + +interface RegionExitBase { + sourceNodeId: SemanticNodeId; +} + +/** Typed transfer between effect regions or out of the current invocation. */ +export type EffectRegionExit = + | (RegionExitBase & { + kind: + | "fallthrough" + | "branch-true" + | "branch-false" + | "exception" + | "finally"; + targetNodeId: SemanticNodeId; + targetRegionId: EffectRegionId; + }) + | (RegionExitBase & { + kind: "call" | "yield" | "await"; + resumeNodeId: SemanticNodeId; + resumeRegionId: EffectRegionId; + }) + | (RegionExitBase & { kind: "return" | "throw" }); + +export interface EffectRegion { + id: EffectRegionId; + entryNodeId: SemanticNodeId; + nodeIds: readonly SemanticNodeId[]; + inputs: RegionBoundaryContract; + outputs: RegionBoundaryContract; + effects: RegionEffectSummary; + exits: readonly EffectRegionExit[]; +} + +export interface EffectRegionGraph { + /** Present when constructed from a SemanticUnit rather than a bare CFG. */ + unitId: string | null; + entryRegionId: EffectRegionId; + regions: readonly EffectRegion[]; + nodeOwnership: ReadonlyMap; +} + +type RegionGraphInput = Pick< + CanonicalCfg, + "nodes" | "exits" | "entryNode" +> & { id?: string }; + +interface Predecessor { + source: SemanticNodeId; + exit: SemanticExit; +} + +const PURITY_ORDER: readonly SemanticPurity[] = [ + "pure", + "frame-local", + "observable", +]; +const THROW_ORDER: readonly SemanticThrowBehavior[] = [ + "never", + "may-throw", + "always-throws", +]; +const ACCESS_ORDER: readonly SemanticAccess[] = [ + "none", + "read", + "write", + "read-write", + "unknown", +]; + +/** Build and validate deterministic effect-delimited regions. */ +export function buildEffectRegionGraph( + input: CanonicalCfg | SemanticUnit +): EffectRegionGraph { + const graphInput = input as RegionGraphInput; + const nodes = validateAndIndexInput(graphInput); + const predecessors = buildPredecessors(graphInput, nodes); + const orderedNodeIds = [...nodes.keys()].sort((left, right) => left - right); + const ownership = new Map(); + const nodeGroups: SemanticNodeId[][] = []; + + for (const nodeId of orderedNodeIds) { + if (ownership.has(nodeId)) continue; + + const group = [nodeId]; + let current = nodeId; + while (true) { + const next = fusionSuccessor( + current, + graphInput, + nodes, + predecessors, + ownership + ); + if (next == null) break; + group.push(next); + current = next; + } + + const regionId = regionIdFor(group[0]!); + for (const ownedNodeId of group) { + if (ownership.has(ownedNodeId)) { + throw new Error( + `RUAM_DUPLICATE_REGION_OWNER: node ${ownedNodeId}` + ); + } + ownership.set(ownedNodeId, regionId); + } + nodeGroups.push(group); + } + + const regions = nodeGroups.map((nodeIds) => { + const id = ownership.get(nodeIds[0]!)!; + const regionNodes = nodeIds.map((nodeId) => nodes.get(nodeId)!); + const boundary = computeBoundaryContracts(regionNodes); + const lastNodeId = nodeIds[nodeIds.length - 1]!; + const semanticExits = graphInput.exits.get(lastNodeId); + if (!semanticExits) { + throw new Error(`RUAM_MISSING_SEMANTIC_EXITS: node ${lastNodeId}`); + } + const exits = semanticExits.map((exit) => + mapRegionExit(lastNodeId, exit, ownership) + ); + + return Object.freeze({ + id, + entryNodeId: nodeIds[0]!, + nodeIds: Object.freeze(nodeIds.slice()), + inputs: boundary.inputs, + outputs: boundary.outputs, + effects: summarizeEffects(regionNodes), + exits: Object.freeze(exits), + }); + }); + + const entryRegionId = ownership.get(graphInput.entryNode); + if (!entryRegionId) { + throw new Error( + `RUAM_MISSING_ENTRY_REGION: node ${graphInput.entryNode}` + ); + } + + const graph: EffectRegionGraph = Object.freeze({ + unitId: typeof graphInput.id === "string" ? graphInput.id : null, + entryRegionId, + regions: Object.freeze(regions), + nodeOwnership: ownership, + }); + validateEffectRegionGraph(graphInput, graph); + return graph; +} + +/** + * Recheck ownership, boundary, and transfer invariants. + * + * Exposed so certificate/verifier work can validate deserialized owner-side + * region graphs without trusting their builder. + */ +export function validateEffectRegionGraph( + input: Pick, + graph: EffectRegionGraph +): void { + const graphInput = input as RegionGraphInput; + const nodes = validateAndIndexInput(graphInput); + const predecessors = buildPredecessors(graphInput, nodes); + const seen = new Set(); + const regionIds = new Set(); + let previousEntryNodeId = -1; + + for (const region of graph.regions) { + if (regionIds.has(region.id)) { + throw new Error(`RUAM_DUPLICATE_REGION_ID: ${region.id}`); + } + regionIds.add(region.id); + if (region.nodeIds.length === 0) { + throw new Error(`RUAM_EMPTY_EFFECT_REGION: ${region.id}`); + } + if (region.id !== regionIdFor(region.nodeIds[0]!)) { + throw new Error(`RUAM_NONDETERMINISTIC_REGION_ID: ${region.id}`); + } + if (region.entryNodeId !== region.nodeIds[0]) { + throw new Error(`RUAM_INVALID_REGION_ENTRY: ${region.id}`); + } + if (region.entryNodeId <= previousEntryNodeId) { + throw new Error("RUAM_NONDETERMINISTIC_REGION_ORDER"); + } + previousEntryNodeId = region.entryNodeId; + + for (let index = 0; index < region.nodeIds.length; index++) { + const nodeId = region.nodeIds[index]!; + if (!nodes.has(nodeId)) { + throw new Error(`RUAM_UNKNOWN_REGION_NODE: ${nodeId}`); + } + if (seen.has(nodeId)) { + throw new Error(`RUAM_DUPLICATE_REGION_OWNER: node ${nodeId}`); + } + seen.add(nodeId); + if (graph.nodeOwnership.get(nodeId) !== region.id) { + throw new Error(`RUAM_REGION_OWNERSHIP_MISMATCH: node ${nodeId}`); + } + + const next = region.nodeIds[index + 1]; + if (next != null) { + const exits = graphInput.exits.get(nodeId)!; + if ( + exits.length !== 1 || + exits[0]!.kind !== "fallthrough" || + exits[0]!.target !== next + ) { + throw new Error( + `RUAM_REGION_CROSSES_CONTROL_BOUNDARY: ${region.id}` + ); + } + if ( + isFusionBarrier( + nodes.get(nodeId)!, + exits, + predecessors.get(nodeId)!, + graphInput.entryNode + ) || + isFusionBarrier( + nodes.get(next)!, + graphInput.exits.get(next)!, + predecessors.get(next)!, + graphInput.entryNode + ) + ) { + throw new Error( + `RUAM_REGION_CROSSES_EFFECT_BOUNDARY: ${region.id}` + ); + } + } + } + + const regionNodes = region.nodeIds.map((nodeId) => nodes.get(nodeId)!); + const expectedBoundary = computeBoundaryContracts(regionNodes); + if ( + !structurallyEqual(region.inputs, expectedBoundary.inputs) || + !structurallyEqual(region.outputs, expectedBoundary.outputs) || + !structurallyEqual(region.effects, summarizeEffects(regionNodes)) + ) { + throw new Error(`RUAM_INVALID_REGION_CONTRACT: ${region.id}`); + } + const lastNodeId = region.nodeIds[region.nodeIds.length - 1]!; + const expectedExits = graphInput.exits + .get(lastNodeId)! + .map((exit) => + mapRegionExit(lastNodeId, exit, graph.nodeOwnership) + ); + if (!structurallyEqual(region.exits, expectedExits)) { + throw new Error(`RUAM_REGION_EXIT_MISMATCH: ${region.id}`); + } + } + + if (seen.size !== nodes.size) { + throw new Error( + `RUAM_INCOMPLETE_REGION_OWNERSHIP: ${seen.size} of ${nodes.size}` + ); + } + if (graph.nodeOwnership.size !== nodes.size) { + throw new Error("RUAM_INVALID_REGION_OWNERSHIP_SIZE"); + } + for (const nodeId of graph.nodeOwnership.keys()) { + if (!nodes.has(nodeId)) { + throw new Error(`RUAM_OWNER_FOR_UNKNOWN_NODE: ${nodeId}`); + } + } + if (graph.entryRegionId !== graph.nodeOwnership.get(graphInput.entryNode)) { + throw new Error("RUAM_ENTRY_REGION_MISMATCH"); + } + + for (const region of graph.regions) { + for (const exit of region.exits) { + if ( + "targetRegionId" in exit && + !regionIds.has(exit.targetRegionId) + ) { + throw new Error( + `RUAM_UNKNOWN_TARGET_REGION: ${exit.targetRegionId}` + ); + } + if ( + "resumeRegionId" in exit && + !regionIds.has(exit.resumeRegionId) + ) { + throw new Error( + `RUAM_UNKNOWN_RESUME_REGION: ${exit.resumeRegionId}` + ); + } + } + } +} + +function validateAndIndexInput( + input: RegionGraphInput +): Map { + if (input.nodes.length === 0) { + throw new Error("RUAM_EMPTY_SEMANTIC_UNIT"); + } + const nodes = new Map(); + for (const node of input.nodes) { + if (!Number.isSafeInteger(node.id) || node.id < 0) { + throw new Error(`RUAM_INVALID_SEMANTIC_NODE_ID: ${node.id}`); + } + if (nodes.has(node.id)) { + throw new Error(`RUAM_DUPLICATE_SEMANTIC_NODE_ID: ${node.id}`); + } + nodes.set(node.id, node); + if (!input.exits.has(node.id)) { + throw new Error(`RUAM_MISSING_SEMANTIC_EXITS: node ${node.id}`); + } + } + if (!nodes.has(input.entryNode)) { + throw new Error(`RUAM_INVALID_SEMANTIC_ENTRY: ${input.entryNode}`); + } + for (const [nodeId, exits] of input.exits) { + if (!nodes.has(nodeId)) { + throw new Error(`RUAM_EXITS_FOR_UNKNOWN_NODE: ${nodeId}`); + } + for (const exit of exits) { + const target = semanticExitTarget(exit); + if (target != null && !nodes.has(target)) { + throw new Error( + `RUAM_INVALID_SEMANTIC_TARGET: node ${nodeId} -> ${target}` + ); + } + } + } + return nodes; +} + +function buildPredecessors( + input: RegionGraphInput, + nodes: ReadonlyMap +): Map { + const predecessors = new Map(); + for (const nodeId of nodes.keys()) predecessors.set(nodeId, []); + for (const [source, exits] of input.exits) { + for (const exit of exits) { + const target = semanticExitTarget(exit); + if (target == null) continue; + predecessors.get(target)!.push({ source, exit }); + } + } + for (const entries of predecessors.values()) { + entries.sort( + (left, right) => + left.source - right.source || + left.exit.kind.localeCompare(right.exit.kind) + ); + } + return predecessors; +} + +function fusionSuccessor( + nodeId: SemanticNodeId, + input: RegionGraphInput, + nodes: ReadonlyMap, + predecessors: ReadonlyMap, + ownership: ReadonlyMap +): SemanticNodeId | null { + const node = nodes.get(nodeId)!; + const exits = input.exits.get(nodeId)!; + if ( + isFusionBarrier( + node, + exits, + predecessors.get(nodeId)!, + input.entryNode + ) + ) { + return null; + } + if (exits.length !== 1 || exits[0]!.kind !== "fallthrough") return null; + + const target = exits[0]!.target; + if (ownership.has(target)) return null; + const targetNode = nodes.get(target)!; + const targetPredecessors = predecessors.get(target)!; + if ( + targetPredecessors.length !== 1 || + targetPredecessors[0]!.source !== nodeId + ) { + return null; + } + if ( + isFusionBarrier( + targetNode, + input.exits.get(target)!, + targetPredecessors, + input.entryNode + ) + ) { + return null; + } + return target; +} + +function isFusionBarrier( + node: SemanticInstruction, + exits: readonly SemanticExit[], + predecessors: readonly Predecessor[], + entryNode: SemanticNodeId +): boolean { + const signature = getSemanticSignature(node.op); + const stackInput = resolveStackArity(signature.stackInput, node.operand); + const stackOutput = resolveStackArity(signature.stackOutput, node.operand); + if (stackInput === "dynamic" || stackOutput === "dynamic") return true; + if (signature.precision !== "classified") return true; + if (signature.purity === "observable") return true; + if (signature.throwBehavior !== "never") return true; + if ( + signature.coercion === "observable" || + signature.coercion === "unknown" + ) { + return true; + } + if (signature.callKind !== "none") return true; + if (signature.suspension !== "none") return true; + if (signature.scopeAccess !== "none") return true; + if (signature.objectAccess !== "none") return true; + if (signature.globalAccess !== "none") return true; + if (signature.allocation !== "none") return true; + if (signature.frameAccess === "unknown") return true; + if (signature.completion !== "normal") return true; + if ( + exits.length !== 1 || + exits[0]!.kind !== "fallthrough" + ) { + return true; + } + if (node.id === entryNode) { + return predecessors.length !== 0; + } + if (predecessors.length !== 1) return true; + return predecessors.some( + (predecessor) => + predecessor.exit.kind === "exception" || + predecessor.exit.kind === "finally" + ); +} + +function computeBoundaryContracts( + nodes: readonly SemanticInstruction[] +): { + inputs: RegionBoundaryContract; + outputs: RegionBoundaryContract; +} { + let stackDepth = 0; + let requiredStack = 0; + let dynamicStack = false; + const stateInputs = new Map(); + const stateOutputs = new Map(); + const writtenState = new Set(); + + for (const node of nodes) { + const signature = getSemanticSignature(node.op); + const stackInput = resolveStackArity(signature.stackInput, node.operand); + const stackOutput = resolveStackArity(signature.stackOutput, node.operand); + if (stackInput === "dynamic" || stackOutput === "dynamic") { + dynamicStack = true; + } else if (!dynamicStack) { + requiredStack = Math.max(requiredStack, stackInput - stackDepth); + stackDepth += stackOutput - stackInput; + } + + for (const access of stateAccesses(node, signature)) { + const key = statePortKey(access.port); + if ( + (access.access === "read" || + access.access === "read-write" || + access.access === "unknown") && + !writtenState.has(key) + ) { + stateInputs.set(key, access.port); + } + if ( + access.access === "write" || + access.access === "read-write" || + access.access === "unknown" + ) { + writtenState.add(key); + stateOutputs.set(key, access.port); + } + } + } + + const outputStack = dynamicStack + ? "dynamic" + : requiredStack + stackDepth; + if (outputStack !== "dynamic" && outputStack < 0) { + throw new Error("RUAM_NEGATIVE_REGION_STACK_OUTPUT"); + } + + return { + inputs: Object.freeze({ + stack: dynamicStack ? "dynamic" : requiredStack, + state: Object.freeze(sortStatePorts(stateInputs.values())), + }), + outputs: Object.freeze({ + stack: outputStack, + state: Object.freeze(sortStatePorts(stateOutputs.values())), + }), + }; +} + +function stateAccesses( + node: SemanticInstruction, + signature: SemanticSignature +): Array<{ access: SemanticAccess; port: RegionStatePort }> { + const result: Array<{ access: SemanticAccess; port: RegionStatePort }> = []; + if (signature.frameAccess !== "none") { + const domain: RegionStateDomain = + signature.operandKind === "register" + ? "register" + : signature.operandKind === "argument" + ? "argument" + : signature.operandKind === "slot" + ? "slot" + : "frame"; + result.push({ + access: signature.frameAccess, + port: Object.freeze({ + domain, + ...(domain !== "frame" ? { key: node.operand } : {}), + }), + }); + } + for (const [domain, access] of [ + ["scope", signature.scopeAccess], + ["object", signature.objectAccess], + ["global", signature.globalAccess], + ] as const) { + if (access === "none") continue; + result.push({ + access, + port: Object.freeze({ + domain, + ...((domain === "scope" || domain === "global") && + signature.operandKind === "scope-name" + ? { key: node.operand } + : {}), + }), + }); + } + if (signature.readsThis) { + result.push({ + access: "read", + port: Object.freeze({ domain: "this" }), + }); + } + return result; +} + +function summarizeEffects( + nodes: readonly SemanticInstruction[] +): RegionEffectSummary { + const signatures = nodes.map((node) => getSemanticSignature(node.op)); + return Object.freeze({ + effects: uniqueSorted(signatures.map((signature) => signature.effect)), + purity: maximum( + signatures.map((signature) => signature.purity), + PURITY_ORDER + ), + throwBehavior: maximum( + signatures.map((signature) => signature.throwBehavior), + THROW_ORDER + ), + coercions: uniqueSorted( + signatures.map((signature) => signature.coercion) + ), + callKinds: uniqueSorted( + signatures.map((signature) => signature.callKind) + ), + suspensions: uniqueSorted( + signatures.map((signature) => signature.suspension) + ), + frameAccess: combineAccess( + signatures.map((signature) => signature.frameAccess) + ), + scopeAccess: combineAccess( + signatures.map((signature) => signature.scopeAccess) + ), + objectAccess: combineAccess( + signatures.map((signature) => signature.objectAccess) + ), + globalAccess: combineAccess( + signatures.map((signature) => signature.globalAccess) + ), + allocations: uniqueSorted( + signatures.map((signature) => signature.allocation) + ), + completions: uniqueSorted( + signatures.map((signature) => signature.completion) + ), + readsThis: signatures.some((signature) => signature.readsThis), + hasDynamicStack: nodes.some((node, index) => { + const signature = signatures[index]!; + return ( + resolveStackArity(signature.stackInput, node.operand) === "dynamic" || + resolveStackArity(signature.stackOutput, node.operand) === "dynamic" + ); + }), + }); +} + +function mapRegionExit( + sourceNodeId: SemanticNodeId, + exit: SemanticExit, + ownership: ReadonlyMap +): EffectRegionExit { + if ("target" in exit) { + const targetRegionId = ownership.get(exit.target); + if (!targetRegionId) { + throw new Error( + `RUAM_MISSING_TARGET_REGION: node ${sourceNodeId} -> ${exit.target}` + ); + } + return Object.freeze({ + kind: exit.kind, + sourceNodeId, + targetNodeId: exit.target, + targetRegionId, + }); + } + if ("resume" in exit) { + const resumeRegionId = ownership.get(exit.resume); + if (!resumeRegionId) { + throw new Error( + `RUAM_MISSING_RESUME_REGION: node ${sourceNodeId} -> ${exit.resume}` + ); + } + return Object.freeze({ + kind: exit.kind, + sourceNodeId, + resumeNodeId: exit.resume, + resumeRegionId, + }); + } + return Object.freeze({ kind: exit.kind, sourceNodeId }); +} + +function semanticExitTarget(exit: SemanticExit): SemanticNodeId | null { + if ("target" in exit) return exit.target; + if ("resume" in exit) return exit.resume; + return null; +} + +function regionIdFor(firstNodeId: SemanticNodeId): EffectRegionId { + return `r_${firstNodeId.toString(36)}`; +} + +function statePortKey(port: RegionStatePort): string { + return `${port.domain}:${port.key ?? "*"}`; +} + +function sortStatePorts( + ports: Iterable +): RegionStatePort[] { + return [...ports].sort((left, right) => + statePortKey(left).localeCompare(statePortKey(right)) + ); +} + +function uniqueSorted(values: readonly T[]): readonly T[] { + return Object.freeze([...new Set(values)].sort()); +} + +function structurallyEqual(left: unknown, right: unknown): boolean { + return JSON.stringify(left) === JSON.stringify(right); +} + +function maximum( + values: readonly T[], + order: readonly T[] +): T { + let result = order[0]!; + for (const value of values) { + if (order.indexOf(value) > order.indexOf(result)) result = value; + } + return result; +} + +function combineAccess(values: readonly SemanticAccess[]): SemanticAccess { + if (values.includes("unknown")) return "unknown"; + const reads = values.some( + (value) => value === "read" || value === "read-write" + ); + const writes = values.some( + (value) => value === "write" || value === "read-write" + ); + if (reads && writes) return "read-write"; + if (reads) return "read"; + if (writes) return "write"; + return "none"; +} diff --git a/packages/ruam/src/compiler/rolling-cipher.ts b/packages/ruam/src/compiler/rolling-cipher.ts deleted file mode 100644 index 446652f..0000000 --- a/packages/ruam/src/compiler/rolling-cipher.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Rolling cipher for bytecode instruction encryption. - * - * Encrypts each instruction's opcode and operand with a rolling state - * that evolves instruction-by-instruction. The master key is derived - * from bytecode metadata — no plaintext seed is stored in the output. - * - * @module compiler/rolling-cipher - */ - -import { - FNV_OFFSET_BASIS, - FNV_PRIME, - GOLDEN_RATIO_PRIME, - MIX_PRIME1, - MIX_PRIME2, - AVALANCHE_CONSTANT, -} from "../constants.js"; - -// --------------------------------------------------------------------------- -// Build-time: derive master key from bytecode metadata -// --------------------------------------------------------------------------- - -/** - * Derive the implicit master key from a bytecode unit's structural - * properties. This produces the same value at build time and runtime - * because the metadata is available in both contexts. - * - * When {@link cipherSalt} is provided it is mixed into the hash after - * the metadata rounds but before the avalanche finalization. This - * makes the key non-derivable from bytecode metadata alone — the salt - * is a per-build random value embedded as a numeric literal in the - * runtime output. - * - * @param instrCount Number of instructions in the unit. - * @param registerCount Number of registers used by the unit. - * @param paramCount Number of parameters the unit accepts. - * @param constantCount Number of constants in the constant pool. - * @param cipherSalt Optional per-build random salt. - * @param keyAnchor Optional key anchor (XOR-folded into the derived key - * after avalanche finalization — matches the runtime - * `rcDeriveKey` which XORs the closure variable `_ka` - * as the final step). - */ -export function deriveImplicitKey( - instrCount: number, - registerCount: number, - paramCount: number, - constantCount: number, - cipherSalt?: number, - keyAnchor?: number -): number { - let h = FNV_OFFSET_BASIS; - h = Math.imul(h ^ instrCount, FNV_PRIME); - h = Math.imul(h ^ registerCount, FNV_PRIME); - h = Math.imul(h ^ paramCount, FNV_PRIME); - h = Math.imul(h ^ constantCount, FNV_PRIME); - if (cipherSalt !== undefined) { - h = Math.imul(h ^ cipherSalt, FNV_PRIME); - } - h ^= h >>> 16; - h = Math.imul(h, AVALANCHE_CONSTANT); - h ^= h >>> 13; - let k = h >>> 0; - if (keyAnchor !== undefined) { - k = (k ^ keyAnchor) >>> 0; - } - return k; -} - -/** - * Mix function: advance the rolling state using the decrypted values. - */ -function mixState(state: number, opcode: number, operand: number): number { - let h = state; - h = Math.imul(h ^ opcode, MIX_PRIME1) >>> 0; - h = Math.imul(h ^ operand, MIX_PRIME2) >>> 0; - h ^= h >>> 16; - return h >>> 0; -} - -// --------------------------------------------------------------------------- -// Build-time: encrypt instruction stream in-place -// --------------------------------------------------------------------------- - -/** - * Encrypt an instruction array (flat `[opcode, operand, ...]`) using - * position-dependent encryption. Modifies the array in place. - * - * Each instruction is encrypted with a key derived from the master key - * and the instruction's position index. This is robust across jumps - * and non-linear control flow — no sequential state dependency. - * - * @param instrs Flat instruction array `[op0, operand0, op1, operand1, ...]` - * @param masterKey Key derived from {@link deriveImplicitKey}. - * If a key anchor + integrity hash are used, they should already - * be folded into the key via {@link deriveImplicitKey}'s `keyAnchor` param. - */ -export function rollingEncrypt(instrs: number[], masterKey: number): void { - const baseKey = masterKey; - - for (let i = 0; i < instrs.length; i += 2) { - const idx = i >>> 1; - // Position-dependent key stream: mix base key with instruction index - const keyStream = mixState(baseKey, idx, idx ^ GOLDEN_RATIO_PRIME); - instrs[i] = (instrs[i]! ^ (keyStream & 0xffff)) & 0xffff; - instrs[i + 1] = (instrs[i + 1]! ^ keyStream) | 0; - } -} diff --git a/packages/ruam/src/compiler/semantic-ops.ts b/packages/ruam/src/compiler/semantic-ops.ts new file mode 100644 index 0000000..23a0dcd --- /dev/null +++ b/packages/ruam/src/compiler/semantic-ops.ts @@ -0,0 +1,127 @@ +/** + * Canonical names for Ruam's language-level source operations. + * + * The visitor catalog is private compiler vocabulary. Isogloss consumers use + * these names only after canonical validation has excluded temporary markers + * and obsolete fused forms. + * + * @module compiler/semantic-ops + */ + +import { Op } from "./operations.js"; + +/** + * Semantic-operation catalog shared by the visitor and analysis stages. + */ +export const SemanticOp = Op; + +/** + * A real language-level operation. + * + * The enum includes `__COUNT` as a numeric sentinel. Excluding it from the + * semantic type prevents the sentinel from entering canonical IR. + */ +export type SemanticOp = Exclude; + +/** Number of real semantic operations (excludes `__COUNT`). */ +export const SEMANTIC_OP_COUNT = Op.__COUNT; + +/** + * Stable ordered list of every real semantic operation. + * + * It is derived from the enum sentinel so newly appended operations are + * included automatically. The semantic-signature tests additionally prove + * that the corresponding descriptor table remains total. + */ +export const ALL_SEMANTIC_OPS: readonly SemanticOp[] = Object.freeze( + Array.from( + { length: SEMANTIC_OP_COUNT }, + (_, value) => value as SemanticOp + ) +); + +/** + * Temporary or obsolete operations that are not legal in canonical IR. + */ +export const NON_CANONICAL_SEMANTIC_OPS: ReadonlySet = new Set([ + // Patched or consumed before canonical IR is finalized. + Op.BREAK, + Op.CONTINUE, + Op.LABEL, + + // Tier-3 optimizer fusions. Canonical optimization may rewrite nodes, but + // it may not introduce a second hidden instruction vocabulary. + Op.REG_ADD, + Op.REG_SUB, + Op.REG_MUL, + Op.REG_LT, + Op.REG_LTE, + Op.REG_GT, + Op.REG_SEQ, + Op.REG_SNEQ, + Op.REG_LT_CONST_JF, + Op.REG_GET_PROP, + Op.REG_ADD_CONST, + Op.REG_GTE, + Op.REG_DIV, + Op.REG_MOD, + Op.REG_CONST_SUB, + Op.REG_CONST_MUL, + Op.REG_CONST_MOD, + Op.REG_LT_REG_JF, + Op.REG_LTE_CONST_JF, + Op.REG_GT_CONST_JF, + Op.REG_GTE_CONST_JF, + Op.REG_SEQ_CONST_JF, + Op.REG_SNEQ_CONST_JF, + Op.REG_LTE_REG_JF, + Op.REG_GT_REG_JF, + Op.REG_GTE_REG_JF, + Op.REG_SEQ_REG_JF, + Op.REG_SNEQ_REG_JF, + Op.REG_ADD_ASSIGN_VOID, + Op.REG_SUB_ASSIGN_VOID, + Op.REG_MUL_ASSIGN_VOID, + Op.REG_DIV_ASSIGN_VOID, + Op.REG_MOD_ASSIGN_VOID, + Op.IDX_REG, + Op.REG_GET_PROP_DYN, + Op.CONST_LT_JF, + Op.CONST_LTE_JF, + Op.CONST_GT_JF, + Op.CONST_GTE_JF, + Op.CONST_SEQ_JF, + Op.CONST_SNEQ_JF, + + // Retired physical-dispatch marker; never emitted or executed. + Op.MUTATE, +]); + +/** Stable ordered list of operations accepted by canonical semantic IR. */ +export const ALL_CANONICAL_SEMANTIC_OPS: readonly SemanticOp[] = Object.freeze( + ALL_SEMANTIC_OPS.filter((op) => !NON_CANONICAL_SEMANTIC_OPS.has(op)) +); + +/** Whether an operation may cross the canonical-IR/Isogloss boundary. */ +export function isCanonicalSemanticOp(op: SemanticOp): boolean { + return !NON_CANONICAL_SEMANTIC_OPS.has(op); +} + +/** + * Fail closed when a migration adapter tries to introduce representation- + * specific behavior into canonical semantic IR. + */ +export function assertCanonicalSemanticOp( + op: SemanticOp +): asserts op is SemanticOp { + if (!isCanonicalSemanticOp(op)) { + throw new Error( + `RUAM_NON_CANONICAL_SEMANTIC_OP: ${semanticOpName(op)} (${op})` + ); + } +} + +/** Return the symbolic name of a semantic operation for diagnostics. */ +export function semanticOpName(op: SemanticOp): string { + return Op[op]; +} diff --git a/packages/ruam/src/compiler/semantic-signatures.ts b/packages/ruam/src/compiler/semantic-signatures.ts new file mode 100644 index 0000000..0d415ad --- /dev/null +++ b/packages/ruam/src/compiler/semantic-signatures.ts @@ -0,0 +1,1715 @@ +/** + * Exhaustive semantic-operation descriptor catalog. + * + * Signatures describe the observable shape of an operation without exposing + * a physical dispatch identity. The Isogloss lattice generator uses these + * dimensions to construct overlapping candidate clauses; the CFG builder uses + * the control classification to produce typed exits. + * + * Every current operation receives a descriptor. Well-understood operation + * families have explicit precise overrides. Operations not yet fully + * classified receive a deliberately conservative descriptor (`mayThrow`, + * scope/this reads, dynamic stack arity) rather than an unsafe optimistic one. + * + * @module compiler/semantic-signatures + */ + +import { + ALL_SEMANTIC_OPS, + SemanticOp, + type SemanticOp as SemanticOpValue, + semanticOpName, +} from "./semantic-ops.js"; + +export type OperandKind = + | "none" + | "constant" + | "register" + | "argument" + | "slot" + | "scope-name" + | "argc" + | "jump" + | "table" + | "count" + | "packed" + | "unit-ref"; + +/** Resolved number of values consumed/produced, or a runtime-dependent shape. */ +export type ResolvedStackArity = number | "dynamic"; + +/** Fixed arity or a pure projection from the encoded semantic operand. */ +export type StackArity = + | ResolvedStackArity + | ((operand: number) => ResolvedStackArity); + +export type SemanticEffect = + | "pure" + | "local" + | "scope" + | "object" + | "call" + | "control" + | "exception" + | "async"; + +/** Whether an operation is safe to move/fuse without crossing observability. */ +export type SemanticPurity = "pure" | "frame-local" | "observable"; + +/** Throw behavior after the operation's inputs have already been evaluated. */ +export type SemanticThrowBehavior = + | "never" + | "may-throw" + | "always-throws"; + +/** + * Conversion performed by the operation. + * + * `intrinsic` conversions (for example ToBoolean) cannot invoke user code. + * `observable` conversions can invoke proxies or user-defined conversion + * hooks. `unknown` deliberately fails closed. + */ +export type SemanticCoercion = + | "none" + | "intrinsic" + | "observable" + | "unknown"; + +/** User-code invocation performed directly by the operation. */ +export type SemanticCallKind = + | "none" + | "invoke" + | "construct" + | "direct-eval" + | "dynamic-import" + | "coercion-hook" + | "host-protocol" + | "unknown"; + +/** Suspension protocol entered by the operation. */ +export type SemanticSuspensionKind = + | "none" + | "yield" + | "await" + | "runtime" + | "unknown"; + +/** Conservative read/write classification for one state domain. */ +export type SemanticAccess = + | "none" + | "read" + | "write" + | "read-write" + | "unknown"; + +/** Identity-bearing allocation performed by the operation. */ +export type SemanticAllocation = + | "none" + | "closure" + | "object" + | "array" + | "arguments" + | "iterator" + | "promise" + | "unknown"; + +/** Completion shape emitted after the operation executes. */ +export type SemanticCompletion = + | "normal" + | "conditional" + | "jump" + | "call" + | "return" + | "throw" + | "yield" + | "await"; + +export type SemanticControl = + | "fallthrough" + | "conditional" + | "jump" + | "call" + | "return" + | "throw" + | "yield" + | "await"; + +export type SemanticFamily = + | "stack" + | "register" + | "argument" + | "arithmetic" + | "bitwise" + | "logical" + | "comparison" + | "control" + | "property" + | "scope" + | "call" + | "aggregate" + | "class" + | "closure" + | "suspension" + | "exception" + | "iterator" + | "conversion" + | "template" + | "destructuring" + | "environment" + | "mutation" + | "unclassified"; + +export interface SemanticSignature { + op: SemanticOpValue; + operandKind: OperandKind; + stackInput: StackArity; + stackOutput: StackArity; + effect: SemanticEffect; + purity: SemanticPurity; + control: SemanticControl; + completion: SemanticCompletion; + mayThrow: boolean; + throwBehavior: SemanticThrowBehavior; + coercion: SemanticCoercion; + callKind: SemanticCallKind; + suspension: SemanticSuspensionKind; + /** Access to unit-local registers, arguments, or indexed slots. */ + frameAccess: SemanticAccess; + scopeAccess: SemanticAccess; + objectAccess: SemanticAccess; + globalAccess: SemanticAccess; + allocation: SemanticAllocation; + readsThis: boolean; + readsScope: boolean; + /** + * Minimum number of non-semantic constraint dimensions available to the + * lattice generator when hiding this operation among candidates. + */ + syntheticDimensions: number; + /** Broad family used by diagnostics and candidate balancing. */ + family: SemanticFamily; + /** Whether the descriptor is explicit or a safe migration fallback. */ + precision: "classified" | "conservative"; +} + +type SignatureOverride = Partial>; + +const dynamicArity: ResolvedStackArity = "dynamic"; +const popOperandCount = (operand: number): ResolvedStackArity => + operand >= 0 ? operand : dynamicArity; +const callInputs = (operand: number): ResolvedStackArity => + operand >= 0 ? operand + 1 : dynamicArity; +const methodCallInputs = (operand: number): ResolvedStackArity => + operand >= 0 ? operand + 2 : dynamicArity; + +const overrides = new Map(); + +function classify( + ops: readonly SemanticOpValue[], + override: SignatureOverride +): void { + for (const op of ops) { + overrides.set(op, { + ...overrides.get(op), + ...override, + precision: override.precision ?? "classified", + }); + } +} + +// Stack primitives and literal sources. +classify( + [ + SemanticOp.PUSH_UNDEFINED, + SemanticOp.PUSH_NULL, + SemanticOp.PUSH_TRUE, + SemanticOp.PUSH_FALSE, + SemanticOp.PUSH_ZERO, + SemanticOp.PUSH_ONE, + SemanticOp.PUSH_NEG_ONE, + SemanticOp.PUSH_EMPTY_STRING, + SemanticOp.PUSH_NAN, + SemanticOp.PUSH_INFINITY, + SemanticOp.PUSH_NEG_INFINITY, + ], + { + operandKind: "none", + stackInput: 0, + stackOutput: 1, + effect: "pure", + mayThrow: false, + readsThis: false, + readsScope: false, + syntheticDimensions: 2, + family: "stack", + } +); +classify([SemanticOp.PUSH_CONST], { + operandKind: "constant", + stackInput: 0, + stackOutput: 1, + effect: "pure", + mayThrow: false, + readsThis: false, + readsScope: false, + syntheticDimensions: 2, + family: "stack", +}); +classify([SemanticOp.POP], { + operandKind: "none", + stackInput: 1, + stackOutput: 0, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "stack", +}); +classify([SemanticOp.POP_N], { + operandKind: "count", + stackInput: popOperandCount, + stackOutput: 0, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "stack", +}); +classify([SemanticOp.DUP], { + operandKind: "none", + stackInput: 1, + stackOutput: 2, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "stack", +}); +classify([SemanticOp.DUP2], { + operandKind: "none", + stackInput: 2, + stackOutput: 4, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "stack", +}); +classify([SemanticOp.SWAP], { + operandKind: "none", + stackInput: 2, + stackOutput: 2, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "stack", +}); +classify([SemanticOp.ROT3], { + operandKind: "none", + stackInput: 3, + stackOutput: 3, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "stack", +}); +classify([SemanticOp.ROT4], { + operandKind: "none", + stackInput: 4, + stackOutput: 4, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "stack", +}); +classify([SemanticOp.PICK], { + operandKind: "count", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "stack", +}); + +// Registers and arguments. +classify( + [ + SemanticOp.LOAD_REG, + SemanticOp.INC_REG, + SemanticOp.DEC_REG, + SemanticOp.POST_INC_REG, + SemanticOp.POST_DEC_REG, + ], + { + operandKind: "register", + stackInput: 0, + stackOutput: 1, + effect: "local", + readsThis: false, + readsScope: false, + family: "register", + } +); +// A plain frame read cannot execute JavaScript. Increment/decrement variants +// perform ToNumeric and retain the conservative throwing classification. +classify([SemanticOp.LOAD_REG], { + mayThrow: false, +}); +classify([SemanticOp.STORE_REG], { + operandKind: "register", + stackInput: 1, + stackOutput: 0, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "register", +}); +classify( + [ + SemanticOp.ADD_ASSIGN_REG, + SemanticOp.SUB_ASSIGN_REG, + SemanticOp.MUL_ASSIGN_REG, + SemanticOp.DIV_ASSIGN_REG, + SemanticOp.MOD_ASSIGN_REG, + ], + { + operandKind: "register", + stackInput: 1, + stackOutput: 1, + effect: "local", + readsThis: false, + readsScope: false, + family: "register", + } +); +classify( + [ + SemanticOp.LOAD_ARG, + SemanticOp.LOAD_ARG_OR_DEFAULT, + SemanticOp.GET_ARG_COUNT, + ], + { + operandKind: "argument", + stackInput: 0, + stackOutput: 1, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "argument", + } +); +classify([SemanticOp.STORE_ARG], { + operandKind: "argument", + stackInput: 1, + stackOutput: 0, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "argument", +}); +classify([SemanticOp.LOAD_SLOT], { + operandKind: "slot", + stackInput: 0, + stackOutput: 1, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: true, + family: "scope", +}); +classify([SemanticOp.STORE_SLOT], { + operandKind: "slot", + stackInput: 1, + stackOutput: 0, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: true, + family: "scope", +}); +classify([SemanticOp.DECLARE_SLOT], { + operandKind: "slot", + stackInput: 0, + stackOutput: 0, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: true, + family: "scope", +}); +classify( + [ + SemanticOp.INC_SLOT, + SemanticOp.DEC_SLOT, + SemanticOp.POST_INC_SLOT, + SemanticOp.POST_DEC_SLOT, + ], + { + operandKind: "slot", + stackInput: 0, + stackOutput: 1, + effect: "local", + mayThrow: true, + readsThis: false, + readsScope: true, + family: "scope", + } +); +classify( + [ + SemanticOp.ADD_ASSIGN_SLOT, + SemanticOp.SUB_ASSIGN_SLOT, + SemanticOp.MUL_ASSIGN_SLOT, + ], + { + operandKind: "slot", + stackInput: 1, + stackOutput: 1, + effect: "local", + mayThrow: true, + readsThis: false, + readsScope: true, + family: "scope", + } +); + +// Ordinary value operations. +classify( + [ + SemanticOp.ADD, + SemanticOp.SUB, + SemanticOp.MUL, + SemanticOp.DIV, + SemanticOp.MOD, + SemanticOp.POW, + ], + { + operandKind: "none", + stackInput: 2, + stackOutput: 1, + effect: "pure", + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "arithmetic", + } +); +classify( + [ + SemanticOp.NEG, + SemanticOp.UNARY_PLUS, + SemanticOp.INC, + SemanticOp.DEC, + ], + { + operandKind: "none", + stackInput: 1, + stackOutput: 1, + effect: "pure", + readsThis: false, + readsScope: false, + family: "arithmetic", + } +); +classify( + [ + SemanticOp.BIT_AND, + SemanticOp.BIT_OR, + SemanticOp.BIT_XOR, + SemanticOp.SHL, + SemanticOp.SHR, + SemanticOp.USHR, + ], + { + operandKind: "none", + stackInput: 2, + stackOutput: 1, + effect: "pure", + readsThis: false, + readsScope: false, + family: "bitwise", + } +); +classify([SemanticOp.BIT_NOT, SemanticOp.NOT], { + operandKind: "none", + stackInput: 1, + stackOutput: 1, + effect: "pure", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "logical", +}); +// ToBoolean is non-observable, while bitwise conversion can invoke +// Symbol.toPrimitive and can reject Symbols or mixed numeric domains. +classify([SemanticOp.BIT_NOT], { + mayThrow: true, +}); +classify( + [ + SemanticOp.EQ, + SemanticOp.NEQ, + SemanticOp.SEQ, + SemanticOp.SNEQ, + SemanticOp.LT, + SemanticOp.LTE, + SemanticOp.GT, + SemanticOp.GTE, + SemanticOp.IN_OP, + SemanticOp.INSTANCEOF, + ], + { + operandKind: "none", + stackInput: 2, + stackOutput: 1, + effect: "pure", + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "comparison", + } +); +// Strict equality performs no conversion and cannot invoke user code. +classify([SemanticOp.SEQ, SemanticOp.SNEQ], { + mayThrow: false, +}); + +// Typed control exits. +classify([SemanticOp.JMP, SemanticOp.BREAK, SemanticOp.CONTINUE], { + operandKind: "jump", + stackInput: 0, + stackOutput: 0, + effect: "control", + control: "jump", + mayThrow: false, + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "control", +}); +classify( + [ + SemanticOp.JMP_TRUE, + SemanticOp.JMP_FALSE, + SemanticOp.JMP_NULLISH, + SemanticOp.JMP_UNDEFINED, + ], + { + operandKind: "jump", + stackInput: 1, + stackOutput: 0, + effect: "control", + control: "conditional", + mayThrow: false, + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "control", + } +); +classify( + [ + SemanticOp.JMP_TRUE_KEEP, + SemanticOp.JMP_FALSE_KEEP, + SemanticOp.JMP_NULLISH_KEEP, + SemanticOp.LOGICAL_AND, + SemanticOp.LOGICAL_OR, + SemanticOp.NULLISH_COALESCE, + ], + { + operandKind: "jump", + stackInput: 1, + stackOutput: 1, + effect: "control", + control: "conditional", + mayThrow: false, + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "control", + } +); +classify([SemanticOp.TABLE_SWITCH, SemanticOp.LOOKUP_SWITCH], { + operandKind: "table", + stackInput: 1, + stackOutput: 0, + effect: "control", + control: "conditional", + mayThrow: false, + readsThis: false, + readsScope: false, + syntheticDimensions: 4, + family: "control", +}); +classify([SemanticOp.RETURN], { + operandKind: "none", + stackInput: 1, + stackOutput: 0, + effect: "control", + control: "return", + mayThrow: false, + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "control", +}); +classify([SemanticOp.RETURN_VOID], { + operandKind: "none", + stackInput: 0, + stackOutput: 0, + effect: "control", + control: "return", + mayThrow: false, + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "control", +}); +classify( + [ + SemanticOp.THROW, + SemanticOp.RETHROW, + SemanticOp.THROW_IF_NOT_OBJECT, + SemanticOp.THROW_REF_ERROR, + SemanticOp.THROW_TYPE_ERROR, + SemanticOp.THROW_SYNTAX_ERROR, + ], + { + operandKind: "none", + stackInput: "dynamic", + stackOutput: 0, + effect: "exception", + control: "throw", + mayThrow: true, + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "exception", + } +); +classify([SemanticOp.NOP, SemanticOp.LABEL, SemanticOp.SOURCE_MAP], { + operandKind: "none", + stackInput: 0, + stackOutput: 0, + effect: "pure", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "control", +}); + +// Calls and construction. Negative argument counts denote spread and remain +// dynamic until the canonical operand model is normalized. +classify( + [ + SemanticOp.CALL, + SemanticOp.CALL_NEW, + SemanticOp.CALL_OPTIONAL, + SemanticOp.DIRECT_EVAL, + SemanticOp.CALL_TAGGED_TEMPLATE, + ], + { + operandKind: "argc", + stackInput: callInputs, + stackOutput: 1, + effect: "call", + control: "call", + mayThrow: true, + readsThis: false, + readsScope: false, + syntheticDimensions: 4, + family: "call", + } +); +classify( + [ + SemanticOp.CALL_METHOD, + SemanticOp.CALL_METHOD_OPTIONAL, + SemanticOp.CALL_SUPER_METHOD, + SemanticOp.SUPER_CALL, + ], + { + operandKind: "argc", + stackInput: methodCallInputs, + stackOutput: 1, + effect: "call", + control: "call", + mayThrow: true, + readsThis: true, + readsScope: false, + syntheticDimensions: 4, + family: "call", + } +); +classify([SemanticOp.CALL_0], { + operandKind: "none", + stackInput: 1, + stackOutput: 1, + effect: "call", + control: "call", + mayThrow: true, + readsThis: false, + readsScope: false, + family: "call", +}); +classify([SemanticOp.CALL_1], { + operandKind: "none", + stackInput: 2, + stackOutput: 1, + effect: "call", + control: "call", + mayThrow: true, + readsThis: false, + readsScope: false, + family: "call", +}); +classify([SemanticOp.CALL_2], { + operandKind: "none", + stackInput: 3, + stackOutput: 1, + effect: "call", + control: "call", + mayThrow: true, + readsThis: false, + readsScope: false, + family: "call", +}); +classify([SemanticOp.CALL_3], { + operandKind: "none", + stackInput: 4, + stackOutput: 1, + effect: "call", + control: "call", + mayThrow: true, + readsThis: false, + readsScope: false, + family: "call", +}); + +// Suspension and async boundaries. +classify([SemanticOp.YIELD, SemanticOp.YIELD_DELEGATE], { + operandKind: "none", + stackInput: 1, + stackOutput: 1, + effect: "async", + control: "yield", + mayThrow: true, + readsThis: false, + readsScope: false, + syntheticDimensions: 4, + family: "suspension", +}); +classify([SemanticOp.AWAIT, SemanticOp.FOR_AWAIT_NEXT], { + operandKind: "none", + stackInput: 1, + stackOutput: 1, + effect: "async", + control: "await", + mayThrow: true, + readsThis: false, + readsScope: false, + syntheticDimensions: 4, + family: "suspension", +}); +classify( + [ + SemanticOp.SUSPEND, + SemanticOp.RESUME, + SemanticOp.GENERATOR_RESUME, + SemanticOp.GENERATOR_RETURN, + SemanticOp.GENERATOR_THROW, + SemanticOp.ASYNC_GENERATOR_YIELD, + SemanticOp.ASYNC_GENERATOR_NEXT, + SemanticOp.ASYNC_GENERATOR_RETURN, + SemanticOp.ASYNC_GENERATOR_THROW, + ], + { + operandKind: "none", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "async", + mayThrow: true, + readsThis: false, + readsScope: false, + syntheticDimensions: 4, + family: "suspension", + } +); + +// Structured exception state is explicit even where exact stack shape still +// depends on handler metadata. +classify( + [ + SemanticOp.TRY_PUSH, + SemanticOp.TRY_POP, + SemanticOp.CATCH_BIND, + SemanticOp.CATCH_BIND_PATTERN, + SemanticOp.FINALLY_MARK, + SemanticOp.END_FINALLY, + ], + { + operandKind: "packed", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "exception", + mayThrow: true, + readsThis: false, + readsScope: true, + syntheticDimensions: 4, + family: "exception", + } +); + +// Scope access and closure creation. +classify( + [ + SemanticOp.LOAD_GLOBAL, + SemanticOp.STORE_GLOBAL, + SemanticOp.LOAD_SCOPED, + SemanticOp.STORE_SCOPED, + SemanticOp.DECLARE_VAR, + SemanticOp.DECLARE_LET, + SemanticOp.DECLARE_CONST, + SemanticOp.TDZ_CHECK, + SemanticOp.TDZ_MARK, + SemanticOp.DELETE_SCOPED, + SemanticOp.TYPEOF_GLOBAL, + SemanticOp.INC_SCOPED, + SemanticOp.DEC_SCOPED, + SemanticOp.POST_INC_SCOPED, + SemanticOp.POST_DEC_SCOPED, + SemanticOp.ADD_ASSIGN_SCOPED, + SemanticOp.SUB_ASSIGN_SCOPED, + SemanticOp.MUL_ASSIGN_SCOPED, + SemanticOp.DIV_ASSIGN_SCOPED, + SemanticOp.MOD_ASSIGN_SCOPED, + SemanticOp.POW_ASSIGN_SCOPED, + SemanticOp.BIT_AND_ASSIGN_SCOPED, + SemanticOp.BIT_OR_ASSIGN_SCOPED, + SemanticOp.BIT_XOR_ASSIGN_SCOPED, + SemanticOp.SHL_ASSIGN_SCOPED, + SemanticOp.SHR_ASSIGN_SCOPED, + SemanticOp.USHR_ASSIGN_SCOPED, + SemanticOp.AND_ASSIGN_SCOPED, + SemanticOp.OR_ASSIGN_SCOPED, + SemanticOp.NULLISH_ASSIGN_SCOPED, + SemanticOp.PUSH_CLOSURE_VAR, + SemanticOp.STORE_CLOSURE_VAR, + ], + { + operandKind: "scope-name", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "scope", + readsThis: false, + readsScope: true, + syntheticDimensions: 3, + family: "scope", + } +); +classify([SemanticOp.LOAD_GLOBAL_FAST], { + operandKind: "scope-name", + stackInput: 0, + stackOutput: 1, + effect: "scope", + mayThrow: true, + readsThis: false, + readsScope: true, + family: "scope", +}); +classify( + [ + SemanticOp.PUSH_SCOPE, + SemanticOp.POP_SCOPE, + SemanticOp.PUSH_WITH_SCOPE, + SemanticOp.PUSH_BLOCK_SCOPE, + SemanticOp.PUSH_CATCH_SCOPE, + SemanticOp.PUSH_INDEXED_SCOPE, + SemanticOp.POP_INDEXED_SCOPE, + ], + { + operandKind: "count", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "scope", + readsThis: false, + readsScope: true, + family: "scope", + } +); +classify( + [ + SemanticOp.NEW_CLOSURE, + SemanticOp.NEW_FUNCTION, + SemanticOp.NEW_ARROW, + SemanticOp.NEW_ASYNC, + SemanticOp.NEW_GENERATOR, + SemanticOp.NEW_ASYNC_GENERATOR, + ], + { + operandKind: "unit-ref", + stackInput: 0, + stackOutput: 1, + effect: "local", + mayThrow: true, + readsScope: true, + syntheticDimensions: 4, + family: "closure", + } +); +classify([SemanticOp.NEW_ARROW, SemanticOp.NEW_CLOSURE], { + readsThis: true, +}); + +// Property, aggregate, and iterator operations are conservatively marked as +// potentially throwing while still exposing their candidate-balancing family. +classify( + [ + SemanticOp.GET_PROP_STATIC, + SemanticOp.SET_PROP_STATIC, + SemanticOp.GET_PROP_DYNAMIC, + SemanticOp.SET_PROP_DYNAMIC, + SemanticOp.DELETE_PROP_STATIC, + SemanticOp.DELETE_PROP_DYNAMIC, + SemanticOp.OPT_CHAIN_GET, + SemanticOp.OPT_CHAIN_DYNAMIC, + SemanticOp.GET_SUPER_PROP, + SemanticOp.SET_SUPER_PROP, + SemanticOp.GET_PRIVATE_FIELD, + SemanticOp.SET_PRIVATE_FIELD, + SemanticOp.HAS_PRIVATE_FIELD, + SemanticOp.DEFINE_OWN_PROPERTY, + SemanticOp.FAST_GET_PROP, + SemanticOp.REG_GET_PROP, + SemanticOp.IDX_REG, + SemanticOp.REG_GET_PROP_DYN, + ], + { + operandKind: "packed", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "object", + mayThrow: true, + readsThis: false, + readsScope: false, + syntheticDimensions: 3, + family: "property", + } +); +classify([SemanticOp.GET_SUPER_PROP, SemanticOp.SET_SUPER_PROP], { + readsThis: true, +}); +classify( + [ + SemanticOp.NEW_OBJECT, + SemanticOp.NEW_ARRAY, + SemanticOp.NEW_ARRAY_WITH_SIZE, + SemanticOp.ARRAY_PUSH, + SemanticOp.ARRAY_HOLE, + SemanticOp.SPREAD_ARRAY, + SemanticOp.SPREAD_OBJECT, + SemanticOp.COPY_DATA_PROPERTIES, + SemanticOp.SET_PROTO, + SemanticOp.FREEZE_OBJECT, + SemanticOp.SEAL_OBJECT, + SemanticOp.DEFINE_PROPERTY_DESC, + SemanticOp.CREATE_TEMPLATE_OBJECT, + ], + { + operandKind: "packed", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "object", + mayThrow: true, + readsThis: false, + readsScope: false, + family: "aggregate", + } +); +classify( + [ + SemanticOp.GET_ITERATOR, + SemanticOp.ITER_NEXT, + SemanticOp.ITER_DONE, + SemanticOp.ITER_VALUE, + SemanticOp.ITER_CLOSE, + SemanticOp.ITER_RESULT_UNWRAP, + SemanticOp.FORIN_INIT, + SemanticOp.FORIN_NEXT, + SemanticOp.FORIN_DONE, + SemanticOp.GET_ASYNC_ITERATOR, + SemanticOp.ASYNC_ITER_NEXT, + SemanticOp.ASYNC_ITER_DONE, + SemanticOp.ASYNC_ITER_VALUE, + SemanticOp.ASYNC_ITER_CLOSE, + ], + { + operandKind: "none", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "object", + mayThrow: true, + readsThis: false, + readsScope: false, + family: "iterator", + } +); + +classify( + [ + SemanticOp.TYPEOF, + SemanticOp.VOID, + SemanticOp.TO_NUMBER, + SemanticOp.TO_STRING, + SemanticOp.TO_BOOLEAN, + SemanticOp.TO_OBJECT, + SemanticOp.TO_PROPERTY_KEY, + SemanticOp.TO_NUMERIC, + ], + { + operandKind: "none", + stackInput: 1, + stackOutput: 1, + effect: "pure", + readsThis: false, + readsScope: false, + family: "conversion", + } +); +classify([SemanticOp.TYPEOF, SemanticOp.VOID, SemanticOp.TO_BOOLEAN], { + mayThrow: false, +}); + +classify( + [ + SemanticOp.PUSH_THIS, + SemanticOp.PUSH_NEW_TARGET, + SemanticOp.PUSH_ARGUMENTS, + SemanticOp.PUSH_GLOBAL_THIS, + SemanticOp.PUSH_WELL_KNOWN_SYMBOL, + SemanticOp.IMPORT_META, + ], + { + operandKind: "none", + stackInput: 0, + stackOutput: 1, + effect: "local", + mayThrow: false, + readsThis: false, + readsScope: false, + family: "environment", + } +); +classify([SemanticOp.PUSH_THIS, SemanticOp.PUSH_NEW_TARGET], { + readsThis: true, +}); +classify([SemanticOp.DYNAMIC_IMPORT], { + operandKind: "none", + stackInput: 1, + stackOutput: 1, + effect: "async", + control: "call", + mayThrow: true, + readsThis: false, + readsScope: false, + family: "environment", +}); + +// Legacy-only runtime mutation remains total but intentionally conservative: +// it has no place in canonical Isogloss output and will be removed at cutover. +classify([SemanticOp.MUTATE], { + operandKind: "packed", + stackInput: 0, + stackOutput: 0, + effect: "control", + control: "fallthrough", + mayThrow: true, + readsThis: true, + readsScope: true, + syntheticDimensions: 1, + family: "mutation", + precision: "conservative", +}); + +const INTRINSIC_COERCION_OPS = new Set([ + SemanticOp.NOT, + SemanticOp.TO_BOOLEAN, + SemanticOp.JMP_TRUE, + SemanticOp.JMP_FALSE, + SemanticOp.JMP_TRUE_KEEP, + SemanticOp.JMP_FALSE_KEEP, + SemanticOp.LOGICAL_AND, + SemanticOp.LOGICAL_OR, +]); + +const OBSERVABLE_COERCION_OPS = new Set([ + SemanticOp.ADD, + SemanticOp.SUB, + SemanticOp.MUL, + SemanticOp.DIV, + SemanticOp.MOD, + SemanticOp.POW, + SemanticOp.NEG, + SemanticOp.UNARY_PLUS, + SemanticOp.INC, + SemanticOp.DEC, + SemanticOp.BIT_AND, + SemanticOp.BIT_OR, + SemanticOp.BIT_XOR, + SemanticOp.BIT_NOT, + SemanticOp.SHL, + SemanticOp.SHR, + SemanticOp.USHR, + SemanticOp.EQ, + SemanticOp.NEQ, + SemanticOp.LT, + SemanticOp.LTE, + SemanticOp.GT, + SemanticOp.GTE, + SemanticOp.TO_NUMBER, + SemanticOp.TO_STRING, + SemanticOp.TO_OBJECT, + SemanticOp.TO_PROPERTY_KEY, + SemanticOp.TO_NUMERIC, + SemanticOp.TEMPLATE_LITERAL, + SemanticOp.INC_REG, + SemanticOp.DEC_REG, + SemanticOp.POST_INC_REG, + SemanticOp.POST_DEC_REG, + SemanticOp.ADD_ASSIGN_REG, + SemanticOp.SUB_ASSIGN_REG, + SemanticOp.MUL_ASSIGN_REG, + SemanticOp.DIV_ASSIGN_REG, + SemanticOp.MOD_ASSIGN_REG, + SemanticOp.INC_SLOT, + SemanticOp.DEC_SLOT, + SemanticOp.POST_INC_SLOT, + SemanticOp.POST_DEC_SLOT, + SemanticOp.ADD_ASSIGN_SLOT, + SemanticOp.SUB_ASSIGN_SLOT, + SemanticOp.MUL_ASSIGN_SLOT, + SemanticOp.INC_SCOPED, + SemanticOp.DEC_SCOPED, + SemanticOp.POST_INC_SCOPED, + SemanticOp.POST_DEC_SCOPED, + SemanticOp.ADD_ASSIGN_SCOPED, + SemanticOp.SUB_ASSIGN_SCOPED, + SemanticOp.MUL_ASSIGN_SCOPED, + SemanticOp.DIV_ASSIGN_SCOPED, + SemanticOp.MOD_ASSIGN_SCOPED, + SemanticOp.POW_ASSIGN_SCOPED, + SemanticOp.BIT_AND_ASSIGN_SCOPED, + SemanticOp.BIT_OR_ASSIGN_SCOPED, + SemanticOp.BIT_XOR_ASSIGN_SCOPED, + SemanticOp.SHL_ASSIGN_SCOPED, + SemanticOp.SHR_ASSIGN_SCOPED, + SemanticOp.USHR_ASSIGN_SCOPED, + SemanticOp.ASSIGN_OP, +]); + +const INVOKE_OPS = new Set([ + SemanticOp.CALL, + SemanticOp.CALL_METHOD, + SemanticOp.CALL_OPTIONAL, + SemanticOp.CALL_METHOD_OPTIONAL, + SemanticOp.CALL_TAGGED_TEMPLATE, + SemanticOp.CALL_SUPER_METHOD, + SemanticOp.CALL_0, + SemanticOp.CALL_1, + SemanticOp.CALL_2, + SemanticOp.CALL_3, + SemanticOp.TAGGED_TEMPLATE, +]); + +const CONSTRUCT_OPS = new Set([ + SemanticOp.CALL_NEW, + SemanticOp.SUPER_CALL, +]); + +const HOST_PROTOCOL_OPS = new Set([ + SemanticOp.GET_PROP_STATIC, + SemanticOp.SET_PROP_STATIC, + SemanticOp.GET_PROP_DYNAMIC, + SemanticOp.SET_PROP_DYNAMIC, + SemanticOp.DELETE_PROP_STATIC, + SemanticOp.DELETE_PROP_DYNAMIC, + SemanticOp.OPT_CHAIN_GET, + SemanticOp.OPT_CHAIN_DYNAMIC, + SemanticOp.GET_SUPER_PROP, + SemanticOp.SET_SUPER_PROP, + SemanticOp.IN_OP, + SemanticOp.INSTANCEOF, + SemanticOp.SPREAD_ARRAY, + SemanticOp.SPREAD_OBJECT, + SemanticOp.COPY_DATA_PROPERTIES, + SemanticOp.SET_PROTO, + SemanticOp.FREEZE_OBJECT, + SemanticOp.SEAL_OBJECT, + SemanticOp.DEFINE_PROPERTY_DESC, + SemanticOp.GET_ITERATOR, + SemanticOp.ITER_NEXT, + SemanticOp.ITER_CLOSE, + SemanticOp.FORIN_INIT, + SemanticOp.GET_ASYNC_ITERATOR, + SemanticOp.ASYNC_ITER_NEXT, + SemanticOp.ASYNC_ITER_CLOSE, + SemanticOp.FOR_AWAIT_NEXT, +]); + +const YIELD_OPS = new Set([ + SemanticOp.YIELD, + SemanticOp.YIELD_DELEGATE, + SemanticOp.ASYNC_GENERATOR_YIELD, +]); + +const AWAIT_OPS = new Set([ + SemanticOp.AWAIT, + SemanticOp.FOR_AWAIT_NEXT, +]); + +const RUNTIME_SUSPENSION_OPS = new Set([ + SemanticOp.SUSPEND, + SemanticOp.RESUME, + SemanticOp.GENERATOR_RESUME, + SemanticOp.GENERATOR_RETURN, + SemanticOp.GENERATOR_THROW, + SemanticOp.ASYNC_GENERATOR_NEXT, + SemanticOp.ASYNC_GENERATOR_RETURN, + SemanticOp.ASYNC_GENERATOR_THROW, +]); + +const FRAME_READ_OPS = new Set([ + SemanticOp.LOAD_REG, + SemanticOp.LOAD_ARG, + SemanticOp.LOAD_ARG_OR_DEFAULT, + SemanticOp.GET_ARG_COUNT, + SemanticOp.LOAD_SLOT, +]); + +const FRAME_WRITE_OPS = new Set([ + SemanticOp.STORE_REG, + SemanticOp.STORE_ARG, + SemanticOp.STORE_SLOT, + SemanticOp.DECLARE_SLOT, +]); + +const FRAME_READ_WRITE_OPS = new Set([ + SemanticOp.INC_REG, + SemanticOp.DEC_REG, + SemanticOp.POST_INC_REG, + SemanticOp.POST_DEC_REG, + SemanticOp.ADD_ASSIGN_REG, + SemanticOp.SUB_ASSIGN_REG, + SemanticOp.MUL_ASSIGN_REG, + SemanticOp.DIV_ASSIGN_REG, + SemanticOp.MOD_ASSIGN_REG, + SemanticOp.INC_SLOT, + SemanticOp.DEC_SLOT, + SemanticOp.POST_INC_SLOT, + SemanticOp.POST_DEC_SLOT, + SemanticOp.ADD_ASSIGN_SLOT, + SemanticOp.SUB_ASSIGN_SLOT, + SemanticOp.MUL_ASSIGN_SLOT, +]); + +const GLOBAL_READ_OPS = new Set([ + SemanticOp.LOAD_GLOBAL, + SemanticOp.LOAD_GLOBAL_FAST, + SemanticOp.TYPEOF_GLOBAL, + SemanticOp.PUSH_GLOBAL_THIS, +]); + +const GLOBAL_WRITE_OPS = new Set([ + SemanticOp.STORE_GLOBAL, +]); + +const SCOPE_READ_OPS = new Set([ + SemanticOp.LOAD_SCOPED, + SemanticOp.TDZ_CHECK, + SemanticOp.PUSH_CLOSURE_VAR, +]); + +const SCOPE_WRITE_OPS = new Set([ + SemanticOp.STORE_SCOPED, + SemanticOp.DECLARE_VAR, + SemanticOp.DECLARE_LET, + SemanticOp.DECLARE_CONST, + SemanticOp.TDZ_MARK, + SemanticOp.DELETE_SCOPED, + SemanticOp.STORE_CLOSURE_VAR, +]); + +const SCOPE_READ_WRITE_OPS = new Set([ + SemanticOp.PUSH_SCOPE, + SemanticOp.POP_SCOPE, + SemanticOp.PUSH_WITH_SCOPE, + SemanticOp.PUSH_BLOCK_SCOPE, + SemanticOp.PUSH_CATCH_SCOPE, + SemanticOp.PUSH_INDEXED_SCOPE, + SemanticOp.POP_INDEXED_SCOPE, + SemanticOp.CATCH_BIND, + SemanticOp.CATCH_BIND_PATTERN, + SemanticOp.INC_SCOPED, + SemanticOp.DEC_SCOPED, + SemanticOp.POST_INC_SCOPED, + SemanticOp.POST_DEC_SCOPED, + SemanticOp.ADD_ASSIGN_SCOPED, + SemanticOp.SUB_ASSIGN_SCOPED, + SemanticOp.MUL_ASSIGN_SCOPED, + SemanticOp.DIV_ASSIGN_SCOPED, + SemanticOp.MOD_ASSIGN_SCOPED, + SemanticOp.POW_ASSIGN_SCOPED, + SemanticOp.BIT_AND_ASSIGN_SCOPED, + SemanticOp.BIT_OR_ASSIGN_SCOPED, + SemanticOp.BIT_XOR_ASSIGN_SCOPED, + SemanticOp.SHL_ASSIGN_SCOPED, + SemanticOp.SHR_ASSIGN_SCOPED, + SemanticOp.USHR_ASSIGN_SCOPED, + SemanticOp.AND_ASSIGN_SCOPED, + SemanticOp.OR_ASSIGN_SCOPED, + SemanticOp.NULLISH_ASSIGN_SCOPED, +]); + +const OBJECT_READ_OPS = new Set([ + SemanticOp.GET_PROP_STATIC, + SemanticOp.GET_PROP_DYNAMIC, + SemanticOp.OPT_CHAIN_GET, + SemanticOp.OPT_CHAIN_DYNAMIC, + SemanticOp.GET_SUPER_PROP, + SemanticOp.GET_PRIVATE_FIELD, + SemanticOp.HAS_PRIVATE_FIELD, + SemanticOp.IN_OP, + SemanticOp.INSTANCEOF, + SemanticOp.GET_ITERATOR, + SemanticOp.ITER_NEXT, + SemanticOp.ITER_DONE, + SemanticOp.ITER_VALUE, + SemanticOp.ITER_RESULT_UNWRAP, + SemanticOp.FORIN_INIT, + SemanticOp.FORIN_NEXT, + SemanticOp.FORIN_DONE, + SemanticOp.GET_ASYNC_ITERATOR, + SemanticOp.ASYNC_ITER_NEXT, + SemanticOp.ASYNC_ITER_DONE, + SemanticOp.ASYNC_ITER_VALUE, +]); + +const OBJECT_WRITE_OPS = new Set([ + SemanticOp.SET_PROP_STATIC, + SemanticOp.SET_PROP_DYNAMIC, + SemanticOp.DELETE_PROP_STATIC, + SemanticOp.DELETE_PROP_DYNAMIC, + SemanticOp.SET_SUPER_PROP, + SemanticOp.SET_PRIVATE_FIELD, + SemanticOp.DEFINE_OWN_PROPERTY, + SemanticOp.ARRAY_PUSH, + SemanticOp.ARRAY_HOLE, + SemanticOp.SET_PROTO, + SemanticOp.FREEZE_OBJECT, + SemanticOp.SEAL_OBJECT, + SemanticOp.DEFINE_PROPERTY_DESC, +]); + +const OBJECT_READ_WRITE_OPS = new Set([ + SemanticOp.SPREAD_ARRAY, + SemanticOp.SPREAD_OBJECT, + SemanticOp.COPY_DATA_PROPERTIES, + SemanticOp.ITER_CLOSE, + SemanticOp.ASYNC_ITER_CLOSE, +]); + +const CLOSURE_ALLOCATION_OPS = new Set([ + SemanticOp.NEW_CLOSURE, + SemanticOp.NEW_FUNCTION, + SemanticOp.NEW_ARROW, + SemanticOp.NEW_ASYNC, + SemanticOp.NEW_GENERATOR, + SemanticOp.NEW_ASYNC_GENERATOR, +]); + +const OBJECT_ALLOCATION_OPS = new Set([ + SemanticOp.NEW_OBJECT, + SemanticOp.NEW_CLASS, + SemanticOp.NEW_DERIVED_CLASS, + SemanticOp.CREATE_TEMPLATE_OBJECT, + SemanticOp.CREATE_RAW_STRINGS, + SemanticOp.TO_OBJECT, +]); + +const ARRAY_ALLOCATION_OPS = new Set([ + SemanticOp.NEW_ARRAY, + SemanticOp.NEW_ARRAY_WITH_SIZE, + SemanticOp.CREATE_REST_ARGS, +]); + +const ARGUMENT_ALLOCATION_OPS = new Set([ + SemanticOp.PUSH_ARGUMENTS, + SemanticOp.CREATE_UNMAPPED_ARGS, + SemanticOp.CREATE_MAPPED_ARGS, +]); + +const ITERATOR_ALLOCATION_OPS = new Set([ + SemanticOp.CREATE_GENERATOR, + SemanticOp.CREATE_ASYNC_FROM_SYNC_ITER, +]); + +function accessFor( + op: SemanticOpValue, + reads: ReadonlySet, + writes: ReadonlySet, + readWrites: ReadonlySet +): SemanticAccess { + if (readWrites.has(op)) return "read-write"; + if (reads.has(op)) return "read"; + if (writes.has(op)) return "write"; + return "none"; +} + +function completionFor(control: SemanticControl): SemanticCompletion { + return control === "fallthrough" ? "normal" : control; +} + +function annotateSignature(signature: SemanticSignature): SemanticSignature { + if (signature.precision === "conservative") { + return { + ...signature, + purity: "observable", + completion: completionFor(signature.control), + throwBehavior: signature.mayThrow ? "may-throw" : "never", + coercion: "unknown", + callKind: "unknown", + suspension: "unknown", + frameAccess: "unknown", + scopeAccess: "unknown", + objectAccess: "unknown", + globalAccess: "unknown", + allocation: "unknown", + }; + } + + const op = signature.op; + const coercion: SemanticCoercion = OBSERVABLE_COERCION_OPS.has(op) + ? "observable" + : INTRINSIC_COERCION_OPS.has(op) + ? "intrinsic" + : "none"; + let callKind: SemanticCallKind = INVOKE_OPS.has(op) + ? "invoke" + : CONSTRUCT_OPS.has(op) + ? "construct" + : op === SemanticOp.DIRECT_EVAL + ? "direct-eval" + : op === SemanticOp.DYNAMIC_IMPORT + ? "dynamic-import" + : HOST_PROTOCOL_OPS.has(op) + ? "host-protocol" + : coercion === "observable" + ? "coercion-hook" + : "none"; + const suspension: SemanticSuspensionKind = YIELD_OPS.has(op) + ? "yield" + : AWAIT_OPS.has(op) + ? "await" + : RUNTIME_SUSPENSION_OPS.has(op) + ? "runtime" + : "none"; + const frameAccess = accessFor( + op, + FRAME_READ_OPS, + FRAME_WRITE_OPS, + FRAME_READ_WRITE_OPS + ); + let scopeAccess = GLOBAL_READ_OPS.has(op) || GLOBAL_WRITE_OPS.has(op) + ? "none" + : accessFor(op, SCOPE_READ_OPS, SCOPE_WRITE_OPS, SCOPE_READ_WRITE_OPS); + let globalAccess = accessFor( + op, + GLOBAL_READ_OPS, + GLOBAL_WRITE_OPS, + new Set() + ); + let objectAccess = accessFor( + op, + OBJECT_READ_OPS, + OBJECT_WRITE_OPS, + OBJECT_READ_WRITE_OPS + ); + let allocation: SemanticAllocation = CLOSURE_ALLOCATION_OPS.has(op) + ? "closure" + : OBJECT_ALLOCATION_OPS.has(op) + ? "object" + : ARRAY_ALLOCATION_OPS.has(op) + ? "array" + : ARGUMENT_ALLOCATION_OPS.has(op) + ? "arguments" + : ITERATOR_ALLOCATION_OPS.has(op) + ? "iterator" + : op === SemanticOp.DYNAMIC_IMPORT + ? "promise" + : CONSTRUCT_OPS.has(op) + ? "unknown" + : "none"; + + // Calls, host protocols, and observable conversion hooks may execute + // arbitrary user code. Their mutation footprint is therefore unknown even + // when the operation also has a more specific direct access. + if ( + callKind !== "none" && + callKind !== "construct" && + callKind !== "dynamic-import" + ) { + if (scopeAccess === "none") scopeAccess = "unknown"; + if (globalAccess === "none") globalAccess = "unknown"; + if (objectAccess === "none") objectAccess = "unknown"; + } + if (callKind === "construct" || callKind === "dynamic-import") { + scopeAccess = "unknown"; + globalAccess = "unknown"; + objectAccess = "unknown"; + } + if (signature.effect === "scope" && scopeAccess === "none" && globalAccess === "none") { + scopeAccess = "unknown"; + } + if ( + signature.effect === "object" && + objectAccess === "none" && + allocation === "none" + ) { + objectAccess = "unknown"; + } + if ( + signature.effect === "async" && + suspension === "none" && + callKind === "none" + ) { + callKind = "unknown"; + } + + const completion = completionFor(signature.control); + const throwBehavior: SemanticThrowBehavior = !signature.mayThrow + ? "never" + : signature.control === "throw" && + signature.op !== SemanticOp.THROW_IF_NOT_OBJECT + ? "always-throws" + : "may-throw"; + const observable = + throwBehavior !== "never" || + coercion === "observable" || + callKind !== "none" || + suspension !== "none" || + scopeAccess !== "none" || + objectAccess !== "none" || + globalAccess !== "none" || + allocation !== "none" || + completion === "call" || + completion === "return" || + completion === "throw" || + completion === "yield" || + completion === "await"; + const purity: SemanticPurity = observable + ? "observable" + : frameAccess === "none" && signature.effect !== "local" + ? "pure" + : "frame-local"; + + return { + ...signature, + purity, + completion, + throwBehavior, + coercion, + callKind, + suspension, + frameAccess, + scopeAccess, + objectAccess, + globalAccess, + allocation, + }; +} + +function conservativeSignature(op: SemanticOpValue): SemanticSignature { + return { + op, + operandKind: "packed", + stackInput: "dynamic", + stackOutput: "dynamic", + effect: "object", + purity: "observable", + control: "fallthrough", + completion: "normal", + mayThrow: true, + throwBehavior: "may-throw", + coercion: "unknown", + callKind: "unknown", + suspension: "unknown", + frameAccess: "unknown", + scopeAccess: "unknown", + objectAccess: "unknown", + globalAccess: "unknown", + allocation: "unknown", + readsThis: true, + readsScope: true, + syntheticDimensions: 1, + family: "unclassified", + precision: "conservative", + }; +} + +function buildSignatureTable(): Readonly< + Record +> { + const table = Object.create(null) as Record< + SemanticOpValue, + SemanticSignature + >; + + for (const op of ALL_SEMANTIC_OPS) { + const signature = annotateSignature({ + ...conservativeSignature(op), + ...overrides.get(op), + op, + }); + table[op] = Object.freeze(signature); + } + + return Object.freeze(table); +} + +/** + * Total descriptor table for all real semantic operations. + * + * Its `Record` type makes downstream lookup total at compile + * time; construction from `ALL_SEMANTIC_OPS` and the coverage test make enum + * growth total at runtime as well. + */ +export const SEMANTIC_SIGNATURES: Readonly< + Record +> = buildSignatureTable(); + +/** Look up the descriptor for a canonical semantic operation. */ +export function getSemanticSignature( + op: SemanticOpValue +): SemanticSignature { + const signature = SEMANTIC_SIGNATURES[op]; + if (signature === undefined) { + throw new RangeError(`Missing semantic signature for ${semanticOpName(op)}`); + } + return signature; +} + +/** Resolve a fixed or operand-dependent stack arity. */ +export function resolveStackArity( + arity: StackArity, + operand: number +): ResolvedStackArity { + return typeof arity === "function" ? arity(operand) : arity; +} diff --git a/packages/ruam/src/compiler/slot-analysis.ts b/packages/ruam/src/compiler/slot-analysis.ts deleted file mode 100644 index 658853a..0000000 --- a/packages/ruam/src/compiler/slot-analysis.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Per-unit interpreter-slot usage analysis. - * - * The hoisted (sync) interpreter shares ~17 "slot" variables at IIFE scope - * (S, R, IP, C, O, SC, EX, PE, HPE, CT, CV, U, A, TV, NT, HO, _g). To stay - * recursion-safe, every `exec()` call snapshots these on entry and restores - * them on exit. Most units only touch a subset, so two groups of slots can be - * conditionally skipped — shrinking the per-call save/restore cost (the - * dominant overhead under deep recursion): - * - * - **Exception completion slots** `PE, HPE, CT, CV` — written only by the - * exception machinery (the scaffold `catch` routing and the handlers for - * {@link EXC_OPCODES}) and read only by `END_FINALLY`/`RETHROW`. (`RETURN` - * /`RETURN_VOID` write `CT`/`CV` only when unwinding to an `EX` *finally* - * frame, which can exist only if the unit emitted a `TRY_PUSH` — i.e. an - * EXC opcode.) A unit with none of {@link EXC_OPCODES} therefore never - * reads or writes any of these four slots (`EX` itself is NOT in this group - * — `RETURN`/`RETURN_VOID` read it on every return, so it is always saved). - * - * - **This-context slots** `TV, NT, HO` — read only by the handlers for - * {@link THIS_CTX_OPCODES} (`this`, `new.target`, lexical-`this` closures, - * and `super`). The scaffold's only other use is the entry param-copy and - * the save/restore, both gated on the same flag. A unit with none of these - * opcodes never reads them, so they need not be copied in or saved. - * - * Both sets are validated against the live handler registry by - * `test/security/slot-save-restore.test.ts`, which introspects every handler's - * emitted AST and fails if any opcode outside these sets references a gated - * slot — guaranteeing the sets remain a safe superset as handlers evolve. - * - * `A` (arguments) is deliberately NOT gated: it is read by ordinary parameter - * loads (`LOAD_ARG`) present in almost every function, so it is always saved. - * - * @module compiler/slot-analysis - */ - -import { Op } from "./opcodes.js"; - -/** - * Opcodes that imply the unit needs the exception-completion machinery - * (`PE`/`HPE`/`CT`/`CV` + the scaffold `catch` routing). Any unit containing - * one of these has `usesExceptions = true`. - * - * These are the structural exception opcodes: a `TRY_PUSH` is required for any - * `EX` handler frame to exist (so the `catch` routing and `RETURN`-through- - * `finally` `CT`/`CV` writes are reachable), and `END_FINALLY`/`RETHROW` are - * the only handlers that directly read/write `PE`/`HPE`/`CT`/`CV`. Plain - * `THROW`/`THROW_*` opcodes are excluded: with no `EX` frame they propagate via - * the (gated) `catch`'s bare `throw`, touching none of the gated slots. - */ -export const EXC_OPCODES: ReadonlySet = new Set([ - Op.TRY_PUSH, - Op.TRY_POP, - Op.CATCH_BIND, - Op.CATCH_BIND_PATTERN, - Op.FINALLY_MARK, - Op.END_FINALLY, - Op.RETHROW, -]); - -/** - * Opcodes whose handlers read the this-context slots `TV`/`NT`/`HO`. Any unit - * containing one of these has `usesThisContext = true`. - */ -export const THIS_CTX_OPCODES: ReadonlySet = new Set([ - Op.PUSH_THIS, - Op.PUSH_NEW_TARGET, - Op.NEW_ARROW, - Op.NEW_CLOSURE, - Op.GET_SUPER_PROP, - Op.SET_SUPER_PROP, - Op.CALL_SUPER_METHOD, - Op.SUPER_CALL, -]); - -/** - * Whether a unit's (logical) instructions use the exception-completion - * machinery — i.e. contain any {@link EXC_OPCODES}. - * - * @param instructions - The unit's logical instructions (pre opcode-shuffle). - * @returns `true` when `PE`/`HPE`/`CT`/`CV` may be touched at runtime. - */ -export function computeUsesExceptions( - instructions: { opcode: number }[] -): boolean { - for (const ins of instructions) { - if (EXC_OPCODES.has(ins.opcode as Op)) return true; - } - return false; -} - -/** - * Whether a unit's (logical) instructions reference `this`/`new.target`/ - * `super`/lexical-`this` closures — i.e. contain any {@link THIS_CTX_OPCODES}. - * - * @param instructions - The unit's logical instructions (pre opcode-shuffle). - * @returns `true` when `TV`/`NT`/`HO` may be read at runtime. - */ -export function computeUsesThisContext( - instructions: { opcode: number }[] -): boolean { - for (const ins of instructions) { - if (THIS_CTX_OPCODES.has(ins.opcode as Op)) return true; - } - return false; -} diff --git a/packages/ruam/src/compiler/types.ts b/packages/ruam/src/compiler/types.ts new file mode 100644 index 0000000..02c8328 --- /dev/null +++ b/packages/ruam/src/compiler/types.ts @@ -0,0 +1,89 @@ +/** + * Compiler-internal types shared by canonical semantic IR and source lowering. + * + * These are analysis structures, not a distributable instruction format. + * + * @module compiler/types + */ + +/** Literal data referenced by canonical semantic instructions. */ +export type ConstantPoolEntry = + | { type: "null"; value: null } + | { type: "undefined"; value: undefined } + | { type: "boolean"; value: boolean } + | { type: "number"; value: number } + | { type: "string"; value: string } + | { type: "bigint"; value: string } + | { type: "regex"; value: { pattern: string; flags: string } }; + +/** One temporary visitor emission before canonical CFG construction. */ +export interface EmittedSemanticInstruction { + opcode: number; + operand: number; +} + +/** Temporary exception range emitted while lowering one source function. */ +export interface SemanticExceptionRange { + startIp: number; + endIp: number; + catchIp: number; + finallyIp: number; +} + +/** Opaque identity shared by every semantic unit in one protected root. */ +export type RootGroupId = string; + +/** Opaque identity of a compiled semantic unit. */ +export type SemanticUnitId = string; + +/** + * Function-level facts that are independent of the eventual execution + * representation. + * + * These fields exclude serialization, physical encoding, and dispatch data. + */ +export interface SemanticFunctionMetadata { + /** Number of declared parameters. */ + paramCount: number; + /** Total registers allocated by compiler analysis. */ + registerCount: number; + /** Number of indexed scope slots used by the function. */ + slotCount: number; + /** Whether the source function had a strict-mode directive. */ + isStrict: boolean; + /** Whether the source function is a generator. */ + isGenerator: boolean; + /** Whether the source function is async. */ + isAsync: boolean; + /** Whether the source function is an arrow function. */ + isArrow: boolean; + /** Whether the per-call scope object may be elided. */ + scopeless: boolean; + /** Whether exception-completion state is required. */ + usesExceptions: boolean; + /** Whether `this`, `new.target`, or `super` context is required. */ + usesThisContext: boolean; + /** Constant-pool index of the function name, or `-1` when anonymous. */ + nameConstIndex: number; + /** Names captured from outer lexical scopes. */ + outerNames: string[]; +} + +/** Minimum identity contract for a unit participating in a root group. */ +export interface RootGroupCompatible { + id: SemanticUnitId; + rootGroupId: RootGroupId; +} + +/** + * Private result used while recursive Babel visitors assemble a root group. + * It is discarded after canonical semantic IR is frozen. + */ +export interface SemanticCompileUnit extends SemanticFunctionMetadata { + id: SemanticUnitId; + constants: ConstantPoolEntry[]; + instructions: EmittedSemanticInstruction[]; + jumpTable: Record; + exceptionTable: SemanticExceptionRange[]; + childUnits: SemanticCompileUnit[]; +} diff --git a/packages/ruam/src/compiler/visitors/classes.ts b/packages/ruam/src/compiler/visitors/classes.ts index 9087e80..7d3fc40 100644 --- a/packages/ruam/src/compiler/visitors/classes.ts +++ b/packages/ruam/src/compiler/visitors/classes.ts @@ -2,7 +2,7 @@ * Class expression and declaration compilation. * * Handles the `class` keyword by emitting `NEW_CLASS`, then compiling each - * method / property as a child bytecode unit. Instance field initialisers + * method / property as a child semantic unit. Instance field initialisers * are injected into the constructor body before compilation. * * @module compiler/visitors/classes @@ -10,11 +10,11 @@ import type { NodePath } from "@babel/traverse"; import type * as t from "@babel/types"; -import { Op } from "../opcodes.js"; +import { Op } from "../operations.js"; import type { Emitter } from "../emitter.js"; import type { ScopeAnalyzer } from "../scope.js"; import type { CompileContext } from "../index.js"; -import type { BytecodeUnit } from "../../types.js"; +import type { SemanticCompileUnit } from "../types.js"; import { compileExpression } from "./expressions.js"; /** @@ -28,11 +28,11 @@ export function compileClassExpr( emitter: Emitter, scope: ScopeAnalyzer, ctx: CompileContext, - allUnits: BytecodeUnit[], + allUnits: SemanticCompileUnit[], compileFunctionInner: ( fnPath: NodePath, - allUnits: BytecodeUnit[] - ) => BytecodeUnit + allUnits: SemanticCompileUnit[] + ) => SemanticCompileUnit ): void { const classNode = classPath.node; @@ -176,11 +176,11 @@ function compileClassMethod( emitter: Emitter, scope: ScopeAnalyzer, ctx: CompileContext, - allUnits: BytecodeUnit[], + allUnits: SemanticCompileUnit[], compileFunctionInner: ( fnPath: NodePath, - allUnits: BytecodeUnit[] - ) => BytecodeUnit + allUnits: SemanticCompileUnit[] + ) => SemanticCompileUnit ): void { emitter.emit(Op.DUP, 0); diff --git a/packages/ruam/src/compiler/visitors/expressions.ts b/packages/ruam/src/compiler/visitors/expressions.ts index 63d9b4c..fb06edf 100644 --- a/packages/ruam/src/compiler/visitors/expressions.ts +++ b/packages/ruam/src/compiler/visitors/expressions.ts @@ -1,17 +1,16 @@ /** * Expression compilation visitors. * - * Every JS expression type is compiled into a bytecode sequence that - * leaves exactly one value on the VM stack. Binary/unary operators map - * directly to opcodes; calls, member access, and optional chaining - * require multi-step sequences. + * Accepted JavaScript expressions are lowered into temporary stack-form + * source operations. Canonical CFG construction consumes this form before it + * can become a runtime representation. * * @module compiler/visitors/expressions */ import type { NodePath } from "@babel/traverse"; import type * as t from "@babel/types"; -import { Op } from "../opcodes.js"; +import { Op } from "../operations.js"; import type { Emitter } from "../emitter.js"; import type { ScopeAnalyzer } from "../scope.js"; import type { CompileContext } from "../index.js"; diff --git a/packages/ruam/src/compiler/visitors/statements.ts b/packages/ruam/src/compiler/visitors/statements.ts index 1db95b8..d1ad061 100644 --- a/packages/ruam/src/compiler/visitors/statements.ts +++ b/packages/ruam/src/compiler/visitors/statements.ts @@ -1,17 +1,17 @@ /** * Statement compilation visitors. * - * Each JS statement type has a dedicated compiler function that emits the - * corresponding bytecode sequence. Control flow (if, while, for, switch, + * Each accepted statement has a compiler function that emits temporary source + * operations. Control flow (if, while, for, switch, * try/catch, break/continue, labeled statements) is handled via jump - * instructions and a loop-stack mechanism. + * target markers and a loop-stack mechanism. * * @module compiler/visitors/statements */ import type { NodePath } from "@babel/traverse"; import type * as t from "@babel/types"; -import { Op } from "../opcodes.js"; +import { Op } from "../operations.js"; import type { Emitter } from "../emitter.js"; import type { ScopeAnalyzer } from "../scope.js"; import type { CompileContext } from "../index.js"; diff --git a/packages/ruam/src/index.ts b/packages/ruam/src/index.ts index edc7812..ce87918 100644 --- a/packages/ruam/src/index.ts +++ b/packages/ruam/src/index.ts @@ -1,94 +1,112 @@ /** - * Ruam VM Obfuscator -- public API surface. + * Ruam Isogloss public API. + * * @module index */ -import { obfuscateCode as transformCode } from "./transform.js"; -import type { VmObfuscationOptions } from "./types.js"; import fs from "fs-extra"; -import path from "path"; import { globby } from "globby"; +import path from "node:path"; +import { obfuscateCode, protectCode } from "./transform.js"; +import type { IsoglossSourceBuildResult } from "./isogloss/source-transform.js"; +import type { RuamOptions } from "./isogloss/options.js"; +export { obfuscateCode, protectCode }; export { - type VmObfuscationOptions, - type PresetName, - type TargetEnvironment, -} from "./types.js"; -export { PRESETS } from "./presets.js"; + IsoglossSourceTransformError, + type IsoglossBuildDiagnostic, + type IsoglossBuildDiagnosticCode, + type IsoglossOwnerRegionTrace, + type IsoglossOwnerSidecar, + type IsoglossSourceBuildResult, + type IsoglossSourceBuildStats, +} from "./isogloss/source-transform.js"; export { - OPTION_META, - AUTO_ENABLE_RULES, - OPTION_LABELS, -} from "./option-meta.js"; -export type { - OptionMetaEntry, - AutoEnableRule, - OptionCategory, -} from "./option-meta.js"; - -// --- Single-Source Obfuscation --- + DEFAULT_MINIMUM_EXACT_ATTACK_QUERIES, + DEFAULT_MINIMUM_EXACT_ATTACK_QUERIES_TEXT, + ISOGLOSS_FIXED_LOCAL_BPRF, + REMOVED_LEGACY_VM_OPTION_HINTS, + REMOVED_LEGACY_VM_OPTIONS, + RuamOptionError, + resolveRuamOptions, + type IsoglossAttestationCapability, + type IsoglossBooleanRegionDomain, + type IsoglossCapabilityOptions, + type IsoglossCustodianCapability, + type IsoglossDeploymentProfile, + type IsoglossMaximumCustodyOptions, + type IsoglossNumericRegionDomain, + type IsoglossOptions, + type IsoglossOwnerTrace, + type IsoglossPrivateFunctionCapability, + type IsoglossRegionDomain, + type IsoglossRegionDomains, + type IsoglossTargetEnvironment, + type IsoglossTargetMode, + type ResolvedIsoglossOptions, + type ResolvedRuamOptions, + type RuamOptionErrorCode, + type RuamOptions, +} from "./isogloss/options.js"; +export { + createSourceExpressionMacroregionCallRiskEvidence, + deriveIsoglossMacroregionCallRisk, + digestCanonicalIsoglossBuildValue, + planIsoglossProduct, +} from "./isogloss/plan.js"; +export * from "./isogloss/types.js"; -/** - * Obfuscate a JavaScript source string. Compiles eligible functions to - * bytecode, embeds a VM runtime, and returns the transformed source. - * - * @param source - JavaScript source code to obfuscate. - * @param options - Obfuscation options. - * @returns The obfuscated JavaScript source. - */ -export function obfuscateCode( - source: string, - options?: VmObfuscationOptions -): string { - return transformCode(source, options); +/** Protect one file and return the same honest metadata as {@link protectCode}. */ +export async function protectFile( + inputPath: string, + outputPath?: string, + options: RuamOptions = {} +): Promise { + const source = await fs.readFile(inputPath, "utf8"); + const result = protectCode(source, options); + await fs.writeFile(outputPath ?? inputPath, result.code, "utf8"); + return result; } -// --- File-Level Obfuscation --- - -/** - * Obfuscate a single file on disk. - * - * @param inputPath - Path to the source JS file. - * @param outputPath - Where to write the result (defaults to overwriting the input). - * @param options - Obfuscation options. - */ +/** String-only file alias for callers that do not consume build metadata. */ export async function obfuscateFile( inputPath: string, outputPath?: string, - options?: VmObfuscationOptions + options: RuamOptions = {} ): Promise { - const source = await fs.readFile(inputPath, "utf-8"); - const result = transformCode(source, options); - await fs.writeFile(outputPath ?? inputPath, result, "utf-8"); + await protectFile(inputPath, outputPath, options); } -// --- Directory-Level Obfuscation --- +export interface RunProtectionConfig { + readonly include?: readonly string[]; + readonly exclude?: readonly string[]; + readonly options?: RuamOptions; +} -/** - * Obfuscate all matching JS files in a directory. - * - * @param dir - Root directory to scan. - * @param config - Include/exclude globs and obfuscation options. - */ -export async function runVmObfuscation( - dir: string, - config?: { - include?: string[]; - exclude?: string[]; - options?: VmObfuscationOptions; - } -): Promise { - const include = config?.include ?? ["**/*.js"]; - const exclude = config?.exclude ?? ["**/node_modules/**"]; +export interface ProtectedFileResult { + readonly file: string; + readonly build: IsoglossSourceBuildResult; +} - const files = await globby(include, { +/** Protect matching JavaScript files without any legacy execution fallback. */ +export async function runProtection( + dir: string, + config: RunProtectionConfig = {} +): Promise { + const files = await globby(config.include ?? ["**/*.js"], { cwd: dir, - ignore: exclude, + ignore: [...(config.exclude ?? ["**/node_modules/**"])], absolute: false, }); - + const results: ProtectedFileResult[] = []; for (const file of files) { const filePath = path.join(dir, file); - await obfuscateFile(filePath, filePath, config?.options); + const build = await protectFile(filePath, filePath, config.options); + results.push(Object.freeze({ file, build })); } + return Object.freeze(results); } + +// Keep the function type reachable without exporting implementation internals +// from the transform module's private deterministic-test entry point. +export type ProtectCode = typeof protectCode; diff --git a/packages/ruam/src/isogloss/bprf/generate.ts b/packages/ruam/src/isogloss/bprf/generate.ts new file mode 100644 index 0000000..ba5155c --- /dev/null +++ b/packages/ruam/src/isogloss/bprf/generate.ts @@ -0,0 +1,588 @@ +/** + * Deterministic generator for the bounded pure BPRF reference artifact. + * + * Logical formula tags are consumed here and never copied into the artifact. + * Each realization uses contextual physical wires and longitudinal fragments. + * + * @module isogloss/bprf/generate + */ + +import { BprfRandom, mix32, opaqueId } from "./random.js"; +import type { + BprfArtifact, + BprfFactor, + BprfFragment, + BprfGenerationOptions, + BprfPiece, + BprfPort, + BprfRealization, + BprfTransition, + BprfWireBasis, + PureRegionContract, + PureRegionFormula, + PureValueRef, + PureValueType, +} from "./types.js"; +import { validateBprfArtifact } from "./validate.js"; + +interface AlgebraicFactor { + ref: PureValueRef; + offset: number; +} + +interface AlgebraicTerm { + coefficient: number; + factors: AlgebraicFactor[]; +} + +interface ContractFacts { + types: PureValueType[]; + depths: number[]; +} + +/** Generate K semantically equivalent, structurally diverse realizations. */ +export function generateBprfArtifact( + contract: PureRegionContract, + options: BprfGenerationOptions +): BprfArtifact { + const facts = validateContract(contract); + const realizationCount = options.realizationCount ?? 3; + const fragmentCount = options.fragmentCount ?? 3; + if (!Number.isSafeInteger(realizationCount) || realizationCount < 2) { + throw new Error("RUAM_BPRF_REALIZATION_COUNT_MIN_2"); + } + if (!Number.isSafeInteger(fragmentCount) || fragmentCount < 2) { + throw new Error("RUAM_BPRF_FRAGMENT_COUNT_MIN_2"); + } + + const rootRandom = new BprfRandom(options.seed); + const realizations = Array.from({ length: realizationCount }, (_, index) => + generateRealization( + contract, + facts, + index, + fragmentCount, + mix32(options.seed ^ Math.imul(index + 1, 0x9e3779b9)) + ) + ); + const artifact: BprfArtifact = Object.freeze({ + format: "ruam-bprf-pure-1", + id: opaqueId("r", rootRandom), + selectionSalt: rootRandom.nextUint32(), + realizations: Object.freeze(realizations), + }); + validateBprfArtifact(artifact); + return artifact; +} + +function generateRealization( + contract: PureRegionContract, + facts: ContractFacts, + realizationIndex: number, + fragmentCount: number, + seed: number +): BprfRealization { + const random = new BprfRandom(seed); + const familyCode = (realizationIndex & 1) as 0 | 1; + const logicalValueCount = facts.types.length; + const residualCount = familyCode === 1 ? contract.steps.length : 0; + const contextualWireCount = logicalValueCount + residualCount; + const frameSize = contextualWireCount + contract.outputs.length; + const slots = random.shuffle( + Array.from({ length: frameSize }, (_, slot) => slot) + ); + const logicalSlots = slots.slice(0, logicalValueCount); + const residualSlots = slots.slice( + logicalValueCount, + logicalValueCount + residualCount + ); + const exitSlots = slots.slice(contextualWireCount); + const contextualSlots = [...logicalSlots, ...residualSlots]; + const contextualBases = Array.from({ length: contextualWireCount }, () => + randomBasis(random) + ); + const exitBases = contract.outputs.map((outputRef) => + distinctBasis(random, contextualBases[outputRef]!) + ); + + const phaseByStep = contract.steps.map((_, stepIndex) => + familyCode === 0 + ? facts.depths[contract.inputs.length + stepIndex]! - 1 + : stepIndex * 2 + 1 + ); + const computationPhaseCount = + phaseByStep.length === 0 ? 0 : Math.max(...phaseByStep) + 1; + const boundaryPhase = computationPhaseCount; + const transitionWrites = Array.from( + { length: boundaryPhase + 1 }, + () => [] as number[] + ); + for (let stepIndex = 0; stepIndex < contract.steps.length; stepIndex++) { + const ref = contract.inputs.length + stepIndex; + if (familyCode === 1) { + transitionWrites[stepIndex * 2]!.push(residualSlots[stepIndex]!); + } + transitionWrites[phaseByStep[stepIndex]!]!.push(logicalSlots[ref]!); + } + transitionWrites[boundaryPhase]!.push(...exitSlots); + + const transitions: BprfTransition[] = transitionWrites.map( + (writes, phase) => + Object.freeze({ + id: opaqueId("t", random), + phase, + boundaryCode: phase === boundaryPhase ? 1 : 0, + writes: Object.freeze(writes.slice()), + }) + ); + + const fragments: BprfFragment[] = Array.from( + { length: fragmentCount }, + () => ({ + id: opaqueId("f", random), + pieces: [] as BprfPiece[], + }) + ); + + let destinationOrdinal = 0; + for (let stepIndex = 0; stepIndex < contract.steps.length; stepIndex++) { + const ref = contract.inputs.length + stepIndex; + const step = contract.steps[stepIndex]!; + const terms = termsForFormula(step.formula, familyCode, random); + if (familyCode === 0) { + distributeTerms({ + terms, + phase: phaseByStep[stepIndex]!, + destination: logicalSlots[ref]!, + destinationBasis: contextualBases[ref]!, + logicalSlots: contextualSlots, + logicalBases: contextualBases, + fragments, + random, + rotation: destinationOrdinal + realizationIndex, + }); + destinationOrdinal++; + } else { + const residualRef = logicalValueCount + stepIndex; + const { head, continuation } = splitContinuationTerms( + terms, + residualRef, + random + ); + distributeTerms({ + terms: head, + phase: stepIndex * 2, + destination: residualSlots[stepIndex]!, + destinationBasis: contextualBases[residualRef]!, + logicalSlots: contextualSlots, + logicalBases: contextualBases, + fragments, + random, + rotation: destinationOrdinal + realizationIndex, + }); + destinationOrdinal++; + distributeTerms({ + terms: continuation, + phase: phaseByStep[stepIndex]!, + destination: logicalSlots[ref]!, + destinationBasis: contextualBases[ref]!, + logicalSlots: contextualSlots, + logicalBases: contextualBases, + fragments, + random, + rotation: destinationOrdinal + realizationIndex, + }); + destinationOrdinal++; + } + } + + for (let outputIndex = 0; outputIndex < contract.outputs.length; outputIndex++) { + const outputRef = contract.outputs[outputIndex]!; + distributeTerms({ + terms: [{ coefficient: 1, factors: [{ ref: outputRef, offset: 0 }] }], + phase: boundaryPhase, + destination: exitSlots[outputIndex]!, + destinationBasis: exitBases[outputIndex]!, + logicalSlots: contextualSlots, + logicalBases: contextualBases, + fragments, + random, + rotation: destinationOrdinal + realizationIndex, + }); + destinationOrdinal++; + } + + const inputPorts: BprfPort[] = contract.inputs.map((input, index) => + Object.freeze({ + slot: logicalSlots[index]!, + typeCode: typeCode(input.type), + basis: contextualBases[index]!, + }) + ); + const outputPorts: BprfPort[] = contract.outputs.map((outputRef, index) => + Object.freeze({ + slot: exitSlots[index]!, + typeCode: typeCode(facts.types[outputRef]!), + basis: exitBases[index]!, + }) + ); + + return Object.freeze({ + id: opaqueId("v", random), + familyCode, + contextSalt: random.nextUint32(), + frameSize, + fragmentThreshold: fragmentCount, + inputPorts: Object.freeze(inputPorts), + outputPorts: Object.freeze(outputPorts), + transitions: Object.freeze(transitions), + fragments: Object.freeze( + fragments.map((fragment) => + Object.freeze({ + id: fragment.id, + pieces: Object.freeze(fragment.pieces.slice()), + }) + ) + ), + }); +} + +function distributeTerms(options: { + terms: AlgebraicTerm[]; + phase: number; + destination: number; + destinationBasis: BprfWireBasis; + logicalSlots: readonly number[]; + logicalBases: readonly BprfWireBasis[]; + fragments: BprfFragment[]; + random: BprfRandom; + rotation: number; +}): void { + const { + phase, + destination, + destinationBasis, + logicalSlots, + logicalBases, + fragments, + random, + rotation, + } = options; + const terms = options.terms.slice(); + while (terms.length < fragments.length) { + const mask = random.nonZeroInt(11); + terms.push({ coefficient: mask, factors: [] }); + terms.push({ coefficient: -mask, factors: [] }); + } + + for (let termIndex = 0; termIndex < terms.length; termIndex++) { + const term = terms[termIndex]!; + const fragment = fragments[(termIndex + rotation) % fragments.length]!; + const factors: BprfFactor[] = term.factors.map((factor) => + Object.freeze({ + slot: logicalSlots[factor.ref]!, + offset: factor.offset, + basis: logicalBases[factor.ref]!, + }) + ); + (fragment.pieces as BprfPiece[]).push( + Object.freeze({ + phase, + destination, + destinationBasis, + coefficient: term.coefficient, + factors: Object.freeze(factors), + }) + ); + } +} + +function termsForFormula( + formula: PureRegionFormula, + familyCode: 0 | 1, + random: BprfRandom +): AlgebraicTerm[] { + switch (formula.tag) { + case "literal": + return [ + { + coefficient: + formula.type === "boolean" + ? booleanCoordinate(formula.value, familyCode) + : formula.value, + factors: [], + }, + ]; + case "sum": + return [wireTerm(formula.left), wireTerm(formula.right)]; + case "difference": + return [wireTerm(formula.left), wireTerm(formula.right, -1)]; + case "product": + return splitProduct(formula.left, formula.right, 1, random); + case "negate": + return [wireTerm(formula.value, -1)]; + case "not": + return familyCode === 0 + ? [constantTerm(1), wireTerm(formula.value, -1)] + : [wireTerm(formula.value, -1)]; + case "and": + return familyCode === 0 + ? splitProduct(formula.left, formula.right, 1, random) + : [ + wireTerm(formula.left, 0.5), + wireTerm(formula.right, 0.5), + ...splitProduct(formula.left, formula.right, 0.5, random), + constantTerm(-0.5), + ]; + case "or": + return familyCode === 0 + ? [ + wireTerm(formula.left), + wireTerm(formula.right), + ...splitProduct(formula.left, formula.right, -1, random), + ] + : [ + wireTerm(formula.left, 0.5), + wireTerm(formula.right, 0.5), + ...splitProduct(formula.left, formula.right, -0.5, random), + constantTerm(0.5), + ]; + case "xor": + return familyCode === 0 + ? [ + wireTerm(formula.left), + wireTerm(formula.right), + ...splitProduct(formula.left, formula.right, -2, random), + ] + : splitProduct(formula.left, formula.right, -1, random); + case "select": { + const baseCoefficient = familyCode === 0 ? 1 : 0.5; + return [ + wireTerm(formula.whenFalse, baseCoefficient), + ...(familyCode === 1 + ? [wireTerm(formula.whenTrue, 0.5)] + : []), + ...splitProduct( + formula.gate, + formula.whenTrue, + baseCoefficient, + random + ), + ...splitProduct( + formula.gate, + formula.whenFalse, + -baseCoefficient, + random + ), + ]; + } + } +} + +/** + * Convert one direct polynomial update into a continuation residual followed + * by a separate closure transition. Neither transition owns the full update. + */ +function splitContinuationTerms( + terms: readonly AlgebraicTerm[], + residualRef: PureValueRef, + random: BprfRandom +): { head: AlgebraicTerm[]; continuation: AlgebraicTerm[] } { + if (terms.length === 1) { + const mask = random.nonZeroInt(13); + return { + head: [terms[0]!, constantTerm(mask)], + continuation: [wireTerm(residualRef), constantTerm(-mask)], + }; + } + const cut = Math.max(1, Math.floor(terms.length / 2)); + return { + head: terms.slice(0, cut), + continuation: [wireTerm(residualRef), ...terms.slice(cut)], + }; +} + +function splitProduct( + left: PureValueRef, + right: PureValueRef, + coefficient: number, + random: BprfRandom +): AlgebraicTerm[] { + const leftShift = random.nonZeroInt(5); + const rightShift = random.nonZeroInt(5); + return [ + { + coefficient, + factors: [ + { ref: left, offset: leftShift }, + { ref: right, offset: rightShift }, + ], + }, + { + coefficient: -coefficient * leftShift, + factors: [{ ref: right, offset: 0 }], + }, + { + coefficient: -coefficient * rightShift, + factors: [{ ref: left, offset: 0 }], + }, + constantTerm(-coefficient * leftShift * rightShift), + ]; +} + +function wireTerm(ref: PureValueRef, coefficient = 1): AlgebraicTerm { + return { coefficient, factors: [{ ref, offset: 0 }] }; +} + +function constantTerm(coefficient: number): AlgebraicTerm { + return { coefficient, factors: [] }; +} + +function randomBasis(random: BprfRandom): BprfWireBasis { + return Object.freeze({ + scale: random.nonZeroInt(3), + bias: random.nonZeroInt(17), + }); +} + +function distinctBasis( + random: BprfRandom, + previous: BprfWireBasis +): BprfWireBasis { + for (;;) { + const candidate = randomBasis(random); + if ( + candidate.scale !== previous.scale || + candidate.bias !== previous.bias + ) { + return candidate; + } + } +} + +function typeCode(type: PureValueType): 0 | 1 { + return type === "number" ? 0 : 1; +} + +function booleanCoordinate(value: boolean, familyCode: 0 | 1): number { + if (familyCode === 0) return value ? 1 : 0; + return value ? 1 : -1; +} + +function validateContract(contract: PureRegionContract): ContractFacts { + if (contract.inputs.length === 0) { + throw new Error("RUAM_BPRF_REGION_REQUIRES_INPUT"); + } + if (contract.steps.length < 2) { + throw new Error("RUAM_BPRF_REGION_REQUIRES_BRAIDABLE_STEPS"); + } + if (contract.outputs.length === 0) { + throw new Error("RUAM_BPRF_REGION_REQUIRES_OUTPUT"); + } + + const types = contract.inputs.map((input) => input.type); + const depths = contract.inputs.map(() => 0); + for (let stepIndex = 0; stepIndex < contract.steps.length; stepIndex++) { + const step = contract.steps[stepIndex]!; + const maxRef = contract.inputs.length + stepIndex; + const refs = formulaRefs(step.formula); + for (const ref of refs) assertPriorRef(ref, maxRef); + assertFormulaTypes(step.formula, step.type, types); + if ( + step.formula.tag === "literal" && + step.formula.type === "number" && + !Number.isFinite(step.formula.value) + ) { + throw new Error("RUAM_BPRF_NON_FINITE_LITERAL"); + } + types.push(step.type); + depths.push( + refs.length === 0 ? 1 : Math.max(...refs.map((ref) => depths[ref]!)) + 1 + ); + } + for (const output of contract.outputs) assertPriorRef(output, types.length); + return { types, depths }; +} + +function assertFormulaTypes( + formula: PureRegionFormula, + resultType: PureValueType, + types: readonly PureValueType[] +): void { + const typeAt = (ref: PureValueRef): PureValueType => types[ref]!; + switch (formula.tag) { + case "literal": + if (formula.type !== resultType) throw typeError(); + return; + case "sum": + case "difference": + case "product": + if ( + resultType !== "number" || + typeAt(formula.left) !== "number" || + typeAt(formula.right) !== "number" + ) { + throw typeError(); + } + return; + case "negate": + if (resultType !== "number" || typeAt(formula.value) !== "number") { + throw typeError(); + } + return; + case "not": + if (resultType !== "boolean" || typeAt(formula.value) !== "boolean") { + throw typeError(); + } + return; + case "and": + case "or": + case "xor": + if ( + resultType !== "boolean" || + typeAt(formula.left) !== "boolean" || + typeAt(formula.right) !== "boolean" + ) { + throw typeError(); + } + return; + case "select": + if ( + typeAt(formula.gate) !== "boolean" || + typeAt(formula.whenTrue) !== resultType || + typeAt(formula.whenFalse) !== resultType + ) { + throw typeError(); + } + return; + } +} + +function formulaRefs(formula: PureRegionFormula): PureValueRef[] { + switch (formula.tag) { + case "literal": + return []; + case "negate": + case "not": + return [formula.value]; + case "sum": + case "difference": + case "product": + case "and": + case "or": + case "xor": + return [formula.left, formula.right]; + case "select": + return [formula.gate, formula.whenTrue, formula.whenFalse]; + } +} + +function assertPriorRef(ref: PureValueRef, upperBound: number): void { + if (!Number.isSafeInteger(ref) || ref < 0 || ref >= upperBound) { + throw new Error(`RUAM_BPRF_INVALID_VALUE_REF: ${ref}`); + } +} + +function typeError(): Error { + return new Error("RUAM_BPRF_FORMULA_TYPE_MISMATCH"); +} diff --git a/packages/ruam/src/isogloss/bprf/index.ts b/packages/ruam/src/isogloss/bprf/index.ts new file mode 100644 index 0000000..206b2ff --- /dev/null +++ b/packages/ruam/src/isogloss/bprf/index.ts @@ -0,0 +1,26 @@ +/** Public compiler-side surface for the bounded pure BPRF spike. */ + +export { generateBprfArtifact } from "./generate.js"; +export { validateBprfArtifact } from "./validate.js"; +export type { + BprfArtifact, + BprfCallerContext, + BprfFactor, + BprfFragment, + BprfGenerationOptions, + BprfPiece, + BprfPort, + BprfRealization, + BprfReferenceResult, + BprfReferenceTraceEvent, + BprfTransition, + BprfValidationReport, + BprfWireBasis, + PureRegionContract, + PureRegionFormula, + PureRegionInput, + PureRegionStep, + PureScalar, + PureValueRef, + PureValueType, +} from "./types.js"; diff --git a/packages/ruam/src/isogloss/bprf/random.ts b/packages/ruam/src/isogloss/bprf/random.ts new file mode 100644 index 0000000..95bc05b --- /dev/null +++ b/packages/ruam/src/isogloss/bprf/random.ts @@ -0,0 +1,66 @@ +/** Deterministic, non-cryptographic entropy used only by the BPRF generator. */ +export class BprfRandom { + private state: number; + + constructor(seed: number) { + this.state = mix32(seed >>> 0); + } + + nextUint32(): number { + let value = this.state; + value ^= value << 13; + value ^= value >>> 17; + value ^= value << 5; + this.state = value >>> 0; + return this.state; + } + + int(minInclusive: number, maxExclusive: number): number { + if (maxExclusive <= minInclusive) { + throw new Error("RUAM_BPRF_INVALID_RANDOM_RANGE"); + } + return ( + minInclusive + + (this.nextUint32() % (maxExclusive - minInclusive)) + ); + } + + nonZeroInt(magnitude: number): number { + const absolute = this.int(1, magnitude + 1); + return (this.nextUint32() & 1) === 0 ? absolute : -absolute; + } + + shuffle(values: readonly T[]): T[] { + const result = values.slice(); + for (let index = result.length - 1; index > 0; index--) { + const swapIndex = this.int(0, index + 1); + [result[index], result[swapIndex]] = [ + result[swapIndex]!, + result[index]!, + ]; + } + return result; + } +} + +export function mix32(value: number): number { + let mixed = value >>> 0; + mixed = Math.imul(mixed ^ (mixed >>> 16), 0x7feb352d); + mixed = Math.imul(mixed ^ (mixed >>> 15), 0x846ca68b); + return (mixed ^ (mixed >>> 16)) >>> 0; +} + +export function hashText(value: string, seed = 0x811c9dc5): number { + let hash = seed >>> 0; + for (let index = 0; index < value.length; index++) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 0x01000193) >>> 0; + } + return mix32(hash); +} + +export function opaqueId(prefix: string, random: BprfRandom): string { + return `${prefix}${random.nextUint32().toString(36)}${random + .nextUint32() + .toString(36)}`; +} diff --git a/packages/ruam/src/isogloss/bprf/scalar-source.ts b/packages/ruam/src/isogloss/bprf/scalar-source.ts new file mode 100644 index 0000000..f89fc54 --- /dev/null +++ b/packages/ruam/src/isogloss/bprf/scalar-source.ts @@ -0,0 +1,804 @@ +/** + * Production-shaped local scalar source emission for validated BPRF artifacts. + * + * The emitted client is complete: a local attacker can inspect every emitted + * realization and scalar expression. This removes a universal artifact-walker + * seam; it does not create secrecy or a hardness guarantee. + * + * @module isogloss/bprf/scalar-source + */ + +import { hashText, mix32 } from "./random.js"; +import type { + BprfArtifact, + BprfCallerContext, + BprfFragment, + BprfPiece, + BprfRealization, + BprfTransition, + BprfWireBasis, + PureScalar, +} from "./types.js"; +import { validateBprfArtifact } from "./validate.js"; + +const MAX_EXACT_DYADIC_NUMERATOR = 1n << 53n; + +export type BprfScalarInputDomain = + | { type: "boolean" } + | { type: "number"; min: number; max: number }; + +export interface BprfScalarEmissionStats { + byteLength: number; + realizationCount: number; + transitionCount: number; + physicalSlotLocalCount: number; + fragmentFunctionCount: number; + fragmentContributionLocalCount: number; + pieceExpressionCount: number; + arithmeticProofOperationCount: number; +} + +export interface BprfScalarEmissionCertificate { + artifactValidated: true; + artifactFormat: "ruam-bprf-pure-1"; + abi: "entry(inputs,context)->outputs"; + inputArity: number; + outputArity: number; + contextualRealizationCount: number; + guardedInputDomains: readonly BprfScalarInputDomain[]; + arithmeticStrategy: "static-exact-dyadic-physical-slot-scalarization"; + maxExactDyadicNumeratorMagnitude: bigint; + maxExactDyadicNumeratorBits: number; + physicalSlotScalarization: true; + runtimeArtifactWalker: false; + ownerTrace: false; + completeLocalClient: true; + hardnessClaim: null; + securityNonClaim: "complete-local-client-no-secrecy-or-hardness-claim"; +} + +export interface BprfScalarSourceEmission { + source: string; + entryName: string; + stats: BprfScalarEmissionStats; + certificate: BprfScalarEmissionCertificate; +} + +interface DyadicRange { + /** Every possible value is an integer multiple of 2^exponent. */ + exponent: number; + /** Inclusive upper bound on the absolute integer multiplier. */ + numeratorMagnitude: bigint; +} + +interface RealizationProof { + slotRanges: ReadonlyMap; +} + +interface EmissionCounters { + transitionCount: number; + physicalSlotLocalCount: number; + fragmentFunctionCount: number; + fragmentContributionLocalCount: number; + pieceExpressionCount: number; +} + +interface FragmentDestination { + fragment: BprfFragment; + pieces: readonly BprfPiece[]; +} + +/** + * Emit one deterministic standalone declaration. + * + * The returned entry accepts guarded inputs and a caller context and returns an + * output tuple. There is no tracing parameter or runtime artifact object. + */ +export function emitBprfScalarSource( + artifact: BprfArtifact, + inputDomains: readonly BprfScalarInputDomain[] +): BprfScalarSourceEmission { + validateBprfArtifact(artifact); + const domains = validateAndFreezeDomains(artifact, inputDomains); + const outputArity = validateCrossRealizationAbi(artifact, domains); + const exactness = new ExactDyadicProof(); + const proofs = artifact.realizations.map((realization) => + proveRealization(realization, domains, exactness) + ); + const names = new OpaqueIdentifierAllocator( + artifact.id, + artifact.selectionSalt + ); + const entryName = names.name("entry"); + const mixName = names.name("context-mix"); + const hashName = names.name("context-hash"); + const realizationNames = artifact.realizations.map((realization) => + names.name(`realization:${realization.id}`) + ); + const counters: EmissionCounters = { + transitionCount: 0, + physicalSlotLocalCount: 0, + fragmentFunctionCount: 0, + fragmentContributionLocalCount: 0, + pieceExpressionCount: 0, + }; + const sourceParts = ['"use strict";']; + + for ( + let realizationIndex = 0; + realizationIndex < artifact.realizations.length; + realizationIndex++ + ) { + sourceParts.push( + emitRealization( + artifact.realizations[realizationIndex]!, + proofs[realizationIndex]!, + realizationNames[realizationIndex]!, + names, + counters + ) + ); + } + sourceParts.push(emitContextMixers(mixName, hashName)); + sourceParts.push( + emitEntry( + entryName, + mixName, + hashName, + artifact.selectionSalt, + realizationNames, + domains + ) + ); + + const source = sourceParts.join("\n"); + const stats = Object.freeze({ + byteLength: utf8ByteLength(source), + realizationCount: artifact.realizations.length, + transitionCount: counters.transitionCount, + physicalSlotLocalCount: counters.physicalSlotLocalCount, + fragmentFunctionCount: counters.fragmentFunctionCount, + fragmentContributionLocalCount: + counters.fragmentContributionLocalCount, + pieceExpressionCount: counters.pieceExpressionCount, + arithmeticProofOperationCount: exactness.operationCount, + }); + const certificate = Object.freeze({ + artifactValidated: true, + artifactFormat: "ruam-bprf-pure-1", + abi: "entry(inputs,context)->outputs", + inputArity: domains.length, + outputArity, + contextualRealizationCount: artifact.realizations.length, + guardedInputDomains: domains, + arithmeticStrategy: + "static-exact-dyadic-physical-slot-scalarization", + maxExactDyadicNumeratorMagnitude: + exactness.maxNumeratorMagnitude, + maxExactDyadicNumeratorBits: bitLength( + exactness.maxNumeratorMagnitude + ), + physicalSlotScalarization: true, + runtimeArtifactWalker: false, + ownerTrace: false, + completeLocalClient: true, + hardnessClaim: null, + securityNonClaim: + "complete-local-client-no-secrecy-or-hardness-claim", + }) satisfies BprfScalarEmissionCertificate; + return Object.freeze({ source, entryName, stats, certificate }); +} + +function proveRealization( + realization: BprfRealization, + domains: readonly BprfScalarInputDomain[], + exactness: ExactDyadicProof +): RealizationProof { + const slots = new Map(); + for (let inputIndex = 0; inputIndex < domains.length; inputIndex++) { + const domain = domains[inputIndex]!; + const port = realization.inputPorts[inputIndex]!; + const coordinate = + domain.type === "boolean" + ? exactness.integerRange( + realization.familyCode === 0 ? 1n : 1n, + `input:${inputIndex}` + ) + : exactness.integerRange( + maxAbsInteger(domain.min, domain.max), + `input:${inputIndex}` + ); + exactness.proveAffineRoundTrip( + coordinate, + port.basis, + `input:${inputIndex}` + ); + slots.set(port.slot, coordinate); + } + + for (const transition of realization.transitions) { + const referenceTotals = new Map(); + const fragmentTotals = new Map(); + for (const fragment of realization.fragments) { + for (const destination of transition.writes) { + const pieces = fragment.pieces.filter( + (piece) => + piece.phase === transition.phase && + piece.destination === destination + ); + if (pieces.length === 0) { + throw new Error( + "RUAM_BPRF_SCALAR_MISSING_FRAGMENT_DESTINATION" + ); + } + let fragmentTotal = exactness.zero(); + for (const piece of pieces) { + const contribution = provePiece(piece, slots, exactness); + fragmentTotal = exactness.add( + fragmentTotal, + contribution, + `fragment:${fragment.id}:${destination}` + ); + const priorReference = + referenceTotals.get(destination) ?? exactness.zero(); + referenceTotals.set( + destination, + exactness.add( + priorReference, + contribution, + `reference:${transition.id}:${destination}` + ) + ); + } + fragmentTotals.set( + fragmentDestinationKey(fragment.id, destination), + fragmentTotal + ); + } + } + + for (const destination of transition.writes) { + let emittedTotal = exactness.zero(); + for (const fragment of realization.fragments) { + emittedTotal = exactness.add( + emittedTotal, + fragmentTotals.get( + fragmentDestinationKey(fragment.id, destination) + )!, + `emitted:${transition.id}:${destination}` + ); + } + const referenceTotal = referenceTotals.get(destination); + if (!referenceTotal) { + throw new Error( + "RUAM_BPRF_SCALAR_MISSING_DESTINATION_TOTAL" + ); + } + const basis = destinationBasis( + realization, + transition, + destination + ); + exactness.proveAffineRoundTrip( + referenceTotal, + basis, + `destination:${transition.id}:${destination}` + ); + // Both groupings are exact dyadic arithmetic and therefore equal. + slots.set(destination, emittedTotal); + } + } + + for (let outputIndex = 0; outputIndex < realization.outputPorts.length; outputIndex++) { + const port = realization.outputPorts[outputIndex]!; + const coordinate = slots.get(port.slot); + if (!coordinate) { + throw new Error("RUAM_BPRF_SCALAR_MISSING_OUTPUT_SLOT"); + } + exactness.proveAffineRoundTrip( + coordinate, + port.basis, + `output:${outputIndex}` + ); + } + return Object.freeze({ slotRanges: slots }); +} + +function provePiece( + piece: BprfPiece, + slots: ReadonlyMap, + exactness: ExactDyadicProof +): DyadicRange { + let value = exactness.constant( + piece.coefficient, + `coefficient:${piece.phase}:${piece.destination}` + ); + for (let factorIndex = 0; factorIndex < piece.factors.length; factorIndex++) { + const factor = piece.factors[factorIndex]!; + const coordinate = slots.get(factor.slot); + if (!coordinate) { + throw new Error("RUAM_BPRF_SCALAR_READ_BEFORE_WRITE"); + } + const offset = exactness.constant( + factor.offset, + `offset:${piece.phase}:${piece.destination}:${factorIndex}` + ); + const shifted = exactness.add( + coordinate, + offset, + `factor-add:${piece.phase}:${piece.destination}:${factorIndex}` + ); + value = exactness.multiply( + value, + shifted, + `factor-multiply:${piece.phase}:${piece.destination}:${factorIndex}` + ); + } + return value; +} + +function emitRealization( + realization: BprfRealization, + proof: RealizationProof, + realizationName: string, + names: OpaqueIdentifierAllocator, + counters: EmissionCounters +): string { + const slotNames = Array.from( + { length: realization.frameSize }, + (_, slot) => + names.name(`slot:${realization.id}:${slot}`) + ); + const lines = [`function ${realizationName}(a){`]; + lines.push(`let ${slotNames.join(",")};`); + counters.physicalSlotLocalCount += slotNames.length; + + for (let inputIndex = 0; inputIndex < realization.inputPorts.length; inputIndex++) { + const port = realization.inputPorts[inputIndex]!; + const coordinate = + port.typeCode === 0 + ? `a[${inputIndex}]` + : realization.familyCode === 0 + ? `(a[${inputIndex}]?1:0)` + : `(a[${inputIndex}]?1:-1)`; + lines.push( + `${slotNames[port.slot]}=(${coordinate})*(${numberSource( + port.basis.scale + )})+(${numberSource(port.basis.bias)});` + ); + } + + for (const transition of realization.transitions) { + counters.transitionCount++; + const contributions = new Map(); + for (const destination of transition.writes) { + contributions.set(destination, []); + } + for (const fragment of realization.fragments) { + for (const destination of transition.writes) { + const grouped = groupFragmentDestination( + fragment, + transition, + destination + ); + const functionName = names.name( + `fragment-function:${realization.id}:${transition.id}:${fragment.id}:${destination}` + ); + const contributionName = names.name( + `fragment-contribution:${realization.id}:${transition.id}:${fragment.id}:${destination}` + ); + const expression = grouped.pieces + .map((piece) => { + counters.pieceExpressionCount++; + return emitPieceExpression(piece, slotNames); + }) + .join("+"); + lines.push( + `function ${functionName}(){return 0+${expression};}` + ); + lines.push(`const ${contributionName}=${functionName}();`); + contributions.get(destination)!.push(contributionName); + counters.fragmentFunctionCount++; + counters.fragmentContributionLocalCount++; + } + } + for (const destination of transition.writes) { + const basis = destinationBasis( + realization, + transition, + destination + ); + lines.push( + `${slotNames[destination]}=(0+${contributions + .get(destination)! + .join("+")})*(${numberSource(basis.scale)})+(${numberSource( + basis.bias + )});` + ); + } + } + + const outputs = realization.outputPorts.map((port) => { + const coordinate = `((${slotNames[port.slot]}-(${numberSource( + port.basis.bias + )}))/(${numberSource(port.basis.scale)}))`; + if (!proof.slotRanges.has(port.slot)) { + throw new Error("RUAM_BPRF_SCALAR_MISSING_EMITTED_OUTPUT"); + } + if (port.typeCode === 0) return coordinate; + return realization.familyCode === 0 + ? `(${coordinate}>0.5)` + : `(${coordinate}>0)`; + }); + lines.push(`return [${outputs.join(",")}];`, "}"); + return lines.join("\n"); +} + +function emitPieceExpression( + piece: BprfPiece, + slotNames: readonly string[] +): string { + let expression = `(${numberSource(piece.coefficient)})`; + for (const factor of piece.factors) { + const coordinate = `((${slotNames[factor.slot]}-(${numberSource( + factor.basis.bias + )}))/(${numberSource(factor.basis.scale)}))`; + expression = `(${expression}*(${coordinate}+(${numberSource( + factor.offset + )})))`; + } + return expression; +} + +function emitContextMixers(mixName: string, hashName: string): string { + return [ + `function ${mixName}(x){x>>>=0;x=Math.imul(x^(x>>>16),2146121005);x=Math.imul(x^(x>>>15),2221713035);return(x^(x>>>16))>>>0;}`, + `function ${hashName}(s,x){for(let j=0;j>>0;}return ${mixName}(x);}`, + ].join("\n"); +} + +function emitEntry( + entryName: string, + mixName: string, + hashName: string, + selectionSalt: number, + realizationNames: readonly string[], + domains: readonly BprfScalarInputDomain[] +): string { + const lines = [ + `function ${entryName}(a,c){`, + `if(!Array.isArray(a)||a.length!==${domains.length})throw new Error("RUAM_BPRF_SCALAR_INPUT_ABI");`, + ]; + for (let inputIndex = 0; inputIndex < domains.length; inputIndex++) { + const domain = domains[inputIndex]!; + if (domain.type === "boolean") { + lines.push( + `if(typeof a[${inputIndex}]!=="boolean")throw new Error("RUAM_BPRF_SCALAR_INPUT_GUARD");` + ); + } else { + lines.push( + `if(!Number.isSafeInteger(a[${inputIndex}])||Object.is(a[${inputIndex}],-0)||a[${inputIndex}]<${numberSource(domain.min)}||a[${inputIndex}]>${numberSource(domain.max)})throw new Error("RUAM_BPRF_SCALAR_INPUT_GUARD");` + ); + } + } + lines.push( + 'if(!c||typeof c.caller!=="string"||!Number.isSafeInteger(c.epoch)||c.epoch<0||!Number.isSafeInteger(c.lineage)||c.lineage<0)throw new Error("RUAM_BPRF_SCALAR_CONTEXT_ABI");', + `const n=${mixName}(${hashName}(c.caller,${selectionSalt >>> 0})^Math.imul(c.epoch+1,2654435769)^Math.imul(c.lineage+1,2246822507))%${realizationNames.length};` + ); + for (let index = 0; index < realizationNames.length - 1; index++) { + lines.push(`if(n===${index})return ${realizationNames[index]}(a);`); + } + lines.push( + `return ${realizationNames[realizationNames.length - 1]}(a);`, + "}" + ); + return lines.join("\n"); +} + +function groupFragmentDestination( + fragment: BprfFragment, + transition: BprfTransition, + destination: number +): FragmentDestination { + const pieces = fragment.pieces.filter( + (piece) => + piece.phase === transition.phase && + piece.destination === destination + ); + if (pieces.length === 0) { + throw new Error("RUAM_BPRF_SCALAR_MISSING_FRAGMENT_DESTINATION"); + } + return { fragment, pieces }; +} + +function destinationBasis( + realization: BprfRealization, + transition: BprfTransition, + destination: number +): BprfWireBasis { + for (const fragment of realization.fragments) { + for (const piece of fragment.pieces) { + if ( + piece.phase === transition.phase && + piece.destination === destination + ) { + return piece.destinationBasis; + } + } + } + throw new Error("RUAM_BPRF_SCALAR_MISSING_DESTINATION_BASIS"); +} + +function validateAndFreezeDomains( + artifact: BprfArtifact, + inputDomains: readonly BprfScalarInputDomain[] +): readonly BprfScalarInputDomain[] { + const expectedArity = artifact.realizations[0]!.inputPorts.length; + if (inputDomains.length !== expectedArity) { + throw new Error("RUAM_BPRF_SCALAR_INPUT_DOMAIN_ARITY_MISMATCH"); + } + return Object.freeze( + inputDomains.map((domain, inputIndex) => { + if (domain.type === "boolean") { + return Object.freeze({ type: "boolean" }); + } + if ( + domain.type !== "number" || + !Number.isSafeInteger(domain.min) || + !Number.isSafeInteger(domain.max) || + Object.is(domain.min, -0) || + Object.is(domain.max, -0) || + domain.min > domain.max + ) { + throw new Error( + `RUAM_BPRF_SCALAR_INVALID_INPUT_DOMAIN: ${inputIndex}` + ); + } + return Object.freeze({ + type: "number", + min: domain.min, + max: domain.max, + }); + }) + ); +} + +function validateCrossRealizationAbi( + artifact: BprfArtifact, + domains: readonly BprfScalarInputDomain[] +): number { + const outputArity = artifact.realizations[0]!.outputPorts.length; + const outputTypes = artifact.realizations[0]!.outputPorts.map( + (port) => port.typeCode + ); + for (const realization of artifact.realizations) { + if ( + realization.inputPorts.length !== domains.length || + realization.outputPorts.length !== outputArity + ) { + throw new Error("RUAM_BPRF_SCALAR_REALIZATION_ABI_MISMATCH"); + } + for (let inputIndex = 0; inputIndex < domains.length; inputIndex++) { + const expectedType = + domains[inputIndex]!.type === "number" ? 0 : 1; + if (realization.inputPorts[inputIndex]!.typeCode !== expectedType) { + throw new Error( + `RUAM_BPRF_SCALAR_INPUT_DOMAIN_TYPE_MISMATCH: ${inputIndex}` + ); + } + } + for (let outputIndex = 0; outputIndex < outputArity; outputIndex++) { + if ( + realization.outputPorts[outputIndex]!.typeCode !== + outputTypes[outputIndex] + ) { + throw new Error( + "RUAM_BPRF_SCALAR_REALIZATION_ABI_MISMATCH" + ); + } + } + } + return outputArity; +} + +class ExactDyadicProof { + operationCount = 0; + maxNumeratorMagnitude = 0n; + + zero(): DyadicRange { + return { exponent: 0, numeratorMagnitude: 0n }; + } + + integerRange(magnitude: bigint, label: string): DyadicRange { + return this.assertExact( + { exponent: 0, numeratorMagnitude: magnitude }, + label + ); + } + + constant(value: number, label: string): DyadicRange { + if (!Number.isFinite(value)) { + throw new Error( + `RUAM_BPRF_SCALAR_ARITHMETIC_NOT_EXACT: ${label}:non-finite` + ); + } + return this.assertExact(exactDyadic(value), label); + } + + add(left: DyadicRange, right: DyadicRange, label: string): DyadicRange { + this.operationCount++; + const exponent = Math.min(left.exponent, right.exponent); + const leftShift = BigInt(left.exponent - exponent); + const rightShift = BigInt(right.exponent - exponent); + return this.assertExact( + { + exponent, + numeratorMagnitude: + (left.numeratorMagnitude << leftShift) + + (right.numeratorMagnitude << rightShift), + }, + label + ); + } + + multiply( + left: DyadicRange, + right: DyadicRange, + label: string + ): DyadicRange { + this.operationCount++; + return this.assertExact( + { + exponent: left.exponent + right.exponent, + numeratorMagnitude: + left.numeratorMagnitude * right.numeratorMagnitude, + }, + label + ); + } + + proveAffineRoundTrip( + coordinate: DyadicRange, + basis: BprfWireBasis, + label: string + ): void { + const scale = this.constant(basis.scale, `${label}:scale`); + const bias = this.constant(basis.bias, `${label}:bias`); + const scaled = this.multiply(coordinate, scale, `${label}:encode-scale`); + this.add(scaled, bias, `${label}:encode-bias`); + // The exact encoded value, exact subtraction result, and original exact + // coordinate make the subsequent correctly-rounded division exact. + } + + private assertExact(range: DyadicRange, label: string): DyadicRange { + if ( + range.numeratorMagnitude < 0n || + range.numeratorMagnitude > MAX_EXACT_DYADIC_NUMERATOR || + range.exponent < -1074 || + (range.numeratorMagnitude !== 0n && + bitLength(range.numeratorMagnitude) - 1 + range.exponent > + 1023) + ) { + throw new Error( + `RUAM_BPRF_SCALAR_ARITHMETIC_NOT_EXACT: ${label}` + ); + } + if (range.numeratorMagnitude > this.maxNumeratorMagnitude) { + this.maxNumeratorMagnitude = range.numeratorMagnitude; + } + return range; + } +} + +class OpaqueIdentifierAllocator { + private readonly byKey = new Map(); + private readonly used = new Set(); + + constructor( + private readonly artifactId: string, + private readonly selectionSalt: number + ) {} + + name(key: string): string { + const existing = this.byKey.get(key); + if (existing) return existing; + const first = hashText( + `${this.artifactId}\u0000${key}`, + this.selectionSalt + ); + const second = mix32( + first ^ hashText(key, this.selectionSalt ^ 0x9e3779b9) + ); + const base = `_${first.toString(36)}${second.toString(36)}`; + let candidate = base; + let collision = 0; + while (this.used.has(candidate)) { + collision++; + candidate = `${base}_${collision.toString(36)}`; + } + this.byKey.set(key, candidate); + this.used.add(candidate); + return candidate; + } +} + +function exactDyadic(value: number): DyadicRange { + if (value === 0) return { exponent: 0, numeratorMagnitude: 0n }; + const buffer = new ArrayBuffer(8); + const view = new DataView(buffer); + view.setFloat64(0, Math.abs(value), false); + const high = view.getUint32(0, false); + const low = view.getUint32(4, false); + const exponentBits = (high >>> 20) & 0x7ff; + const fraction = + (BigInt(high & 0x000fffff) << 32n) | BigInt(low); + let numerator = + exponentBits === 0 ? fraction : (1n << 52n) | fraction; + let exponent = + exponentBits === 0 ? -1074 : exponentBits - 1023 - 52; + while ((numerator & 1n) === 0n) { + numerator >>= 1n; + exponent++; + } + return { exponent, numeratorMagnitude: numerator }; +} + +function maxAbsInteger(min: number, max: number): bigint { + const minMagnitude = + min < 0 ? -BigInt(min) : BigInt(min); + const maxMagnitude = + max < 0 ? -BigInt(max) : BigInt(max); + return minMagnitude > maxMagnitude ? minMagnitude : maxMagnitude; +} + +function bitLength(value: bigint): number { + return value === 0n ? 0 : value.toString(2).length; +} + +function fragmentDestinationKey( + fragmentId: string, + destination: number +): string { + return `${fragmentId}\u0000${destination}`; +} + +function numberSource(value: number): string { + if (!Number.isFinite(value)) { + throw new Error("RUAM_BPRF_SCALAR_NON_FINITE_NUMBER"); + } + return Object.is(value, -0) ? "-0" : String(value); +} + +/** Browser-safe UTF-8 byte count with TextEncoder-compatible surrogate repair. */ +function utf8ByteLength(value: string): number { + let bytes = 0; + for (let index = 0; index < value.length; index++) { + const codeUnit = value.charCodeAt(index); + if (codeUnit <= 0x7f) { + bytes++; + } else if (codeUnit <= 0x7ff) { + bytes += 2; + } else if ( + codeUnit >= 0xd800 && + codeUnit <= 0xdbff && + index + 1 < value.length + ) { + const next = value.charCodeAt(index + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + bytes += 4; + index++; + } else { + bytes += 3; + } + } else { + bytes += 3; + } + } + return bytes; +} + +/** Type-only documentation of the emitted narrow ABI. */ +export type BprfScalarEntry = ( + inputs: readonly PureScalar[], + context: BprfCallerContext +) => PureScalar[]; diff --git a/packages/ruam/src/isogloss/bprf/testing-emitter.ts b/packages/ruam/src/isogloss/bprf/testing-emitter.ts new file mode 100644 index 0000000..223ff73 --- /dev/null +++ b/packages/ruam/src/isogloss/bprf/testing-emitter.ts @@ -0,0 +1,295 @@ +/** + * NON-PRODUCT SPECIALIZED SOURCE-EMISSION SPIKE. + * + * This owner-side utility lowers a validated pure BPRF artifact to dedicated + * JavaScript codelets. Generated code contains no artifact walker, piece + * dispatcher, or generic regional evaluator. It exists only to measure whether + * specialization removes the reference evaluator's universal hook surface. + * + * @module isogloss/bprf/testing-emitter + */ + +import type { + BprfArtifact, + BprfFragment, + BprfPiece, + BprfRealization, + BprfTransition, +} from "./types.js"; +import { validateBprfArtifact } from "./validate.js"; + +export interface BprfTestingEmissionOptions { + /** Add owner-only opaque fabric events. Never enable in ordinary output. */ + ownerTracing?: boolean; +} + +export interface BprfTestingEmission { + source: string; + entryName: string; + byteLength: number; + realizationCount: number; + transitionCount: number; + codeletCount: number; + ownerTracing: boolean; +} + +/** + * Emit a standalone function declaration. + * + * Narrow ABI: `entry(inputs, { caller, epoch, lineage }, ownerTrace?)`. + * Ordinary emission omits all tracing code and ignores a third argument. + */ +export function emitBprfTestingSource( + artifact: BprfArtifact, + options: BprfTestingEmissionOptions = {} +): BprfTestingEmission { + validateBprfArtifact(artifact); + const ownerTracing = options.ownerTracing === true; + const entryName = "__ruamBprfSpike"; + const sourceParts: string[] = ['"use strict";']; + let codeletCount = 0; + let transitionCount = 0; + + for (let realizationIndex = 0; realizationIndex < artifact.realizations.length; realizationIndex++) { + const realization = artifact.realizations[realizationIndex]!; + const emitted = emitRealization( + realization, + realizationIndex, + ownerTracing + ); + sourceParts.push(emitted.source); + codeletCount += emitted.codeletCount; + transitionCount += realization.transitions.length; + } + + sourceParts.push(emitContextMixers()); + sourceParts.push( + emitEntry( + entryName, + artifact.selectionSalt, + artifact.realizations.length, + ownerTracing + ) + ); + const source = sourceParts.join("\n"); + return Object.freeze({ + source, + entryName, + byteLength: Buffer.byteLength(source, "utf8"), + realizationCount: artifact.realizations.length, + transitionCount, + codeletCount, + ownerTracing, + }); +} + +function emitRealization( + realization: BprfRealization, + realizationIndex: number, + ownerTracing: boolean +): { source: string; codeletCount: number } { + const sourceParts: string[] = []; + let codeletCount = 0; + + for (const transition of realization.transitions) { + for (let fragmentIndex = 0; fragmentIndex < realization.fragments.length; fragmentIndex++) { + const fragment = realization.fragments[fragmentIndex]!; + sourceParts.push( + emitCodelet( + realization, + realizationIndex, + transition, + fragment, + fragmentIndex, + ownerTracing + ) + ); + codeletCount++; + } + } + + const traceParameter = ownerTracing ? ",z" : ""; + sourceParts.push( + `function v${realizationIndex}(a${traceParameter}){`, + emitInputAbi(realization), + `const q=new Array(${realization.frameSize});`, + ...realization.inputPorts.map((port, inputIndex) => { + const coordinate = + port.typeCode === 0 + ? `a[${inputIndex}]` + : realization.familyCode === 0 + ? `(a[${inputIndex}]?1:0)` + : `(a[${inputIndex}]?1:-1)`; + return `q[${port.slot}]=(${coordinate})*(${numberSource(port.basis.scale)})+(${numberSource(port.basis.bias)});`; + }), + ...realization.transitions.flatMap((transition) => + emitTransitionCalls( + realization, + realizationIndex, + transition, + ownerTracing + ) + ), + `return [${realization.outputPorts + .map((port) => { + const decoded = `((q[${port.slot}]-(${numberSource(port.basis.bias)}))/(${numberSource(port.basis.scale)}))`; + if (port.typeCode === 0) return decoded; + return realization.familyCode === 0 + ? `(${decoded}>0.5)` + : `(${decoded}>0)`; + }) + .join(",")}];`, + "}" + ); + + return { source: sourceParts.join("\n"), codeletCount }; +} + +function emitCodelet( + realization: BprfRealization, + realizationIndex: number, + transition: BprfTransition, + fragment: BprfFragment, + fragmentIndex: number, + ownerTracing: boolean +): string { + const pieces = fragment.pieces.filter( + (piece) => piece.phase === transition.phase + ); + const byDestination = new Map(); + for (const piece of pieces) { + const group = byDestination.get(piece.destination) ?? []; + group.push(piece); + byDestination.set(piece.destination, group); + } + const values = transition.writes.map((destination) => { + const destinationPieces = byDestination.get(destination); + if (!destinationPieces || destinationPieces.length === 0) { + throw new Error("RUAM_BPRF_EMITTER_MISSING_FRAGMENT_DESTINATION"); + } + return destinationPieces.map(pieceExpression).join("+"); + }); + const name = codeletName( + realizationIndex, + transition.phase, + fragmentIndex + ); + const traceParameter = ownerTracing ? ",z" : ""; + const traceStatement = ownerTracing + ? `if(z)z({r:${stringSource(realization.id)},t:${stringSource(transition.id)},f:${stringSource(fragment.id)},p:${transition.phase}});` + : ""; + return `function ${name}(q${traceParameter}){const x=[${values.join(",")}];${traceStatement}return x;}`; +} + +function emitTransitionCalls( + realization: BprfRealization, + realizationIndex: number, + transition: BprfTransition, + ownerTracing: boolean +): string[] { + const traceArgument = ownerTracing ? ",z" : ""; + const callNames = realization.fragments.map( + (_, fragmentIndex) => `p${transition.phase}_${fragmentIndex}` + ); + const lines = realization.fragments.map((_, fragmentIndex) => { + const codelet = codeletName( + realizationIndex, + transition.phase, + fragmentIndex + ); + return `const ${callNames[fragmentIndex]}=${codelet}(q${traceArgument});`; + }); + for (let outputIndex = 0; outputIndex < transition.writes.length; outputIndex++) { + const destination = transition.writes[outputIndex]!; + const pieces = realization.fragments.flatMap((fragment) => + fragment.pieces.filter( + (piece) => + piece.phase === transition.phase && + piece.destination === destination + ) + ); + const basis = pieces[0]?.destinationBasis; + if (!basis) { + throw new Error("RUAM_BPRF_EMITTER_MISSING_DESTINATION_BASIS"); + } + const total = callNames + .map((callName) => `${callName}[${outputIndex}]`) + .join("+"); + lines.push( + `q[${destination}]=(${total})*(${numberSource(basis.scale)})+(${numberSource(basis.bias)});` + ); + } + return lines; +} + +function pieceExpression(piece: BprfPiece): string { + const factors = piece.factors.map( + (factor) => + `(((q[${factor.slot}]-(${numberSource(factor.basis.bias)}))/(${numberSource(factor.basis.scale)}))+(${numberSource(factor.offset)}))` + ); + if (factors.length === 0) return `(${numberSource(piece.coefficient)})`; + return `(${numberSource(piece.coefficient)}*${factors.join("*")})`; +} + +function emitInputAbi(realization: BprfRealization): string { + const checks = [ + `if(!Array.isArray(a)||a.length!==${realization.inputPorts.length})throw new Error("RUAM_BPRF_INPUT_ABI");`, + ]; + for (let index = 0; index < realization.inputPorts.length; index++) { + const port = realization.inputPorts[index]!; + checks.push( + port.typeCode === 0 + ? `if(typeof a[${index}]!=="number"||!Number.isFinite(a[${index}]))throw new Error("RUAM_BPRF_INPUT_ABI");` + : `if(typeof a[${index}]!=="boolean")throw new Error("RUAM_BPRF_INPUT_ABI");` + ); + } + return checks.join(""); +} + +function emitContextMixers(): string { + return [ + "function mx(x){x>>>=0;x=Math.imul(x^(x>>>16),2146121005);x=Math.imul(x^(x>>>15),2221713035);return(x^(x>>>16))>>>0;}", + "function hx(s,x){for(let j=0;j>>0;}return mx(x);}", + ].join("\n"); +} + +function emitEntry( + entryName: string, + selectionSalt: number, + realizationCount: number, + ownerTracing: boolean +): string { + const traceArgument = ownerTracing ? ",z" : ""; + const lines = [ + `function ${entryName}(a,c${ownerTracing ? ",z" : ""}){`, + 'if(!c||typeof c.caller!=="string"||!Number.isSafeInteger(c.epoch)||c.epoch<0||!Number.isSafeInteger(c.lineage)||c.lineage<0)throw new Error("RUAM_BPRF_CONTEXT_ABI");', + `const n=mx(hx(c.caller,${selectionSalt >>> 0})^Math.imul(c.epoch+1,2654435769)^Math.imul(c.lineage+1,2246822507))%${realizationCount};`, + ]; + for (let index = 0; index < realizationCount - 1; index++) { + lines.push(`if(n===${index})return v${index}(a${traceArgument});`); + } + lines.push( + `return v${realizationCount - 1}(a${traceArgument});`, + "}" + ); + return lines.join("\n"); +} + +function codeletName( + realizationIndex: number, + phase: number, + fragmentIndex: number +): string { + return `x${realizationIndex}_${phase}_${fragmentIndex}`; +} + +function numberSource(value: number): string { + if (!Number.isFinite(value)) { + throw new Error("RUAM_BPRF_EMITTER_NON_FINITE_NUMBER"); + } + return Object.is(value, -0) ? "-0" : String(value); +} + +function stringSource(value: string): string { + return JSON.stringify(value); +} diff --git a/packages/ruam/src/isogloss/bprf/testing-reference.ts b/packages/ruam/src/isogloss/bprf/testing-reference.ts new file mode 100644 index 0000000..4b4e126 --- /dev/null +++ b/packages/ruam/src/isogloss/bprf/testing-reference.ts @@ -0,0 +1,176 @@ +/** + * NON-PRODUCT REFERENCE EVALUATOR. + * + * This generic evaluator exists only for differential tests and spike scoring. + * Product execution must emit generated regional codelets instead of shipping + * a universal artifact evaluator. + * + * @module isogloss/bprf/testing-reference + */ + +import { hashText, mix32 } from "./random.js"; +import type { + BprfArtifact, + BprfCallerContext, + BprfPiece, + BprfRealization, + BprfReferenceResult, + BprfReferenceTraceEvent, + BprfWireBasis, + PureScalar, +} from "./types.js"; +import { validateBprfArtifact } from "./validate.js"; + +/** Deterministically select a contextual realization for one caller lineage. */ +export function selectBprfRealization( + artifact: BprfArtifact, + context: BprfCallerContext +): BprfRealization { + if ( + !Number.isSafeInteger(context.epoch) || + context.epoch < 0 || + !Number.isSafeInteger(context.lineage) || + context.lineage < 0 + ) { + throw new Error("RUAM_BPRF_INVALID_CALLER_CONTEXT"); + } + const callerHash = hashText(context.caller, artifact.selectionSalt); + const selection = mix32( + callerHash ^ + Math.imul(context.epoch + 1, 0x9e3779b9) ^ + Math.imul(context.lineage + 1, 0x85ebca6b) + ); + return artifact.realizations[selection % artifact.realizations.length]!; +} + +/** + * Evaluate a generated artifact for differential testing. + * + * Trace records contain only contextual realization/transition/fragment + * identities. They deliberately omit values, formula tags, operand tuples, + * and source identities. + */ +export function evaluateBprfReference( + artifact: BprfArtifact, + inputs: readonly PureScalar[], + context: BprfCallerContext +): BprfReferenceResult { + validateBprfArtifact(artifact); + const realization = selectBprfRealization(artifact, context); + if (inputs.length !== realization.inputPorts.length) { + throw new Error("RUAM_BPRF_INPUT_ARITY_MISMATCH"); + } + + const frame: Array = Array.from( + { length: realization.frameSize }, + () => undefined + ); + for (let index = 0; index < inputs.length; index++) { + const port = realization.inputPorts[index]!; + const coordinate = toCoordinate( + inputs[index]!, + port.typeCode, + realization.familyCode + ); + frame[port.slot] = encodeCoordinate(coordinate, port.basis); + } + + const trace: BprfReferenceTraceEvent[] = []; + for (const transition of realization.transitions) { + const totals = new Map(); + const destinationBases = new Map(); + for (const fragment of realization.fragments) { + let participated = false; + for (const piece of fragment.pieces) { + if (piece.phase !== transition.phase) continue; + participated = true; + const contribution = evaluatePiece(piece, frame); + totals.set( + piece.destination, + (totals.get(piece.destination) ?? 0) + contribution + ); + destinationBases.set(piece.destination, piece.destinationBasis); + } + if (participated) { + trace.push( + Object.freeze({ + realization: realization.id, + transition: transition.id, + fragment: fragment.id, + phase: transition.phase, + }) + ); + } + } + for (const destination of transition.writes) { + const total = totals.get(destination); + const basis = destinationBases.get(destination); + if (total == null || !basis) { + throw new Error("RUAM_BPRF_INCOMPLETE_REFERENCE_TRANSITION"); + } + frame[destination] = encodeCoordinate(total, basis); + } + } + + const outputs = realization.outputPorts.map((port) => { + const encoded = frame[port.slot]; + if (encoded == null) throw new Error("RUAM_BPRF_MISSING_REFERENCE_OUTPUT"); + const coordinate = decodeCoordinate(encoded, port.basis); + return fromCoordinate(coordinate, port.typeCode, realization.familyCode); + }); + return { + outputs, + trace, + realization: realization.id, + }; +} + +function evaluatePiece( + piece: BprfPiece, + frame: readonly (number | undefined)[] +): number { + let value = piece.coefficient; + for (const factor of piece.factors) { + const encoded = frame[factor.slot]; + if (encoded == null) { + throw new Error("RUAM_BPRF_REFERENCE_READ_BEFORE_WRITE"); + } + value *= decodeCoordinate(encoded, factor.basis) + factor.offset; + } + return value; +} + +function toCoordinate( + value: PureScalar, + typeCode: 0 | 1, + familyCode: 0 | 1 +): number { + if (typeCode === 0) { + if (typeof value !== "number" || !Number.isFinite(value)) { + throw new Error("RUAM_BPRF_EXPECTED_FINITE_NUMBER"); + } + return value; + } + if (typeof value !== "boolean") { + throw new Error("RUAM_BPRF_EXPECTED_BOOLEAN"); + } + if (familyCode === 0) return value ? 1 : 0; + return value ? 1 : -1; +} + +function fromCoordinate( + value: number, + typeCode: 0 | 1, + familyCode: 0 | 1 +): PureScalar { + if (typeCode === 0) return value; + return familyCode === 0 ? value > 0.5 : value > 0; +} + +function encodeCoordinate(value: number, basis: BprfWireBasis): number { + return value * basis.scale + basis.bias; +} + +function decodeCoordinate(value: number, basis: BprfWireBasis): number { + return (value - basis.bias) / basis.scale; +} diff --git a/packages/ruam/src/isogloss/bprf/types.ts b/packages/ruam/src/isogloss/bprf/types.ts new file mode 100644 index 0000000..b07a59c --- /dev/null +++ b/packages/ruam/src/isogloss/bprf/types.ts @@ -0,0 +1,145 @@ +/** + * Architecture-neutral contracts for the bounded pure BPRF spike. + * + * Logical contracts are compiler/reference inputs. Generated artifacts use + * only contextual wire, fragment, and transition identities. + * + * @module isogloss/bprf/types + */ + +export type PureScalar = number | boolean; +export type PureValueType = "number" | "boolean"; +export type PureValueRef = number; + +export interface PureRegionInput { + type: PureValueType; +} + +export type PureRegionFormula = + | { tag: "literal"; type: "number"; value: number } + | { tag: "literal"; type: "boolean"; value: boolean } + | { tag: "sum"; left: PureValueRef; right: PureValueRef } + | { tag: "difference"; left: PureValueRef; right: PureValueRef } + | { tag: "product"; left: PureValueRef; right: PureValueRef } + | { tag: "negate"; value: PureValueRef } + | { tag: "not"; value: PureValueRef } + | { tag: "and"; left: PureValueRef; right: PureValueRef } + | { tag: "or"; left: PureValueRef; right: PureValueRef } + | { tag: "xor"; left: PureValueRef; right: PureValueRef } + | { + tag: "select"; + gate: PureValueRef; + whenTrue: PureValueRef; + whenFalse: PureValueRef; + }; + +export interface PureRegionStep { + type: PureValueType; + formula: PureRegionFormula; +} + +/** + * Dense pure-region graph. + * + * Input references occupy `[0, inputs.length)`. Step references follow in + * declaration order, so every formula may reference only an earlier value. + */ +export interface PureRegionContract { + inputs: readonly PureRegionInput[]; + steps: readonly PureRegionStep[]; + outputs: readonly PureValueRef[]; +} + +export interface BprfGenerationOptions { + seed: number; + /** Must be at least two. Defaults to three. */ + realizationCount?: number; + /** Necessary fragments per realization. Must be at least two. */ + fragmentCount?: number; +} + +/** Affine coordinate basis for one contextual physical wire. */ +export interface BprfWireBasis { + scale: number; + bias: number; +} + +export interface BprfPort { + slot: number; + typeCode: 0 | 1; + basis: BprfWireBasis; +} + +export interface BprfFactor { + slot: number; + offset: number; + basis: BprfWireBasis; +} + +/** One partial polynomial contribution; never a complete logical step. */ +export interface BprfPiece { + phase: number; + destination: number; + destinationBasis: BprfWireBasis; + coefficient: number; + factors: readonly BprfFactor[]; +} + +/** A longitudinal fragment braided across several logical destinations. */ +export interface BprfFragment { + id: string; + pieces: readonly BprfPiece[]; +} + +export interface BprfTransition { + id: string; + phase: number; + boundaryCode: 0 | 1; + writes: readonly number[]; +} + +export interface BprfRealization { + id: string; + /** Opaque structural-family code; it is not a language-operation identity. */ + familyCode: 0 | 1; + contextSalt: number; + frameSize: number; + fragmentThreshold: number; + inputPorts: readonly BprfPort[]; + outputPorts: readonly BprfPort[]; + transitions: readonly BprfTransition[]; + fragments: readonly BprfFragment[]; +} + +export interface BprfArtifact { + format: "ruam-bprf-pure-1"; + id: string; + selectionSalt: number; + realizations: readonly BprfRealization[]; +} + +export interface BprfCallerContext { + caller: string; + epoch: number; + lineage: number; +} + +export interface BprfReferenceTraceEvent { + realization: string; + transition: string; + fragment: string; + phase: number; +} + +export interface BprfReferenceResult { + outputs: PureScalar[]; + trace: BprfReferenceTraceEvent[]; + realization: string; +} + +export interface BprfValidationReport { + realizationCount: number; + familyCount: number; + fragmentCountRange: readonly [number, number]; + transitionCountRange: readonly [number, number]; +} diff --git a/packages/ruam/src/isogloss/bprf/validate.ts b/packages/ruam/src/isogloss/bprf/validate.ts new file mode 100644 index 0000000..beaf931 --- /dev/null +++ b/packages/ruam/src/isogloss/bprf/validate.ts @@ -0,0 +1,312 @@ +/** + * Structural verifier for pure BPRF reference artifacts. + * + * The verifier checks fission, longitudinal braiding, contextual diversity, + * physical-frame validity, and absence of semantic-dispatch schema fields. + * + * @module isogloss/bprf/validate + */ + +import type { + BprfArtifact, + BprfPiece, + BprfRealization, + BprfValidationReport, + BprfWireBasis, +} from "./types.js"; + +const FORBIDDEN_SCHEMA_KEYS = new Set([ + "op", + "opcode", + "operand", + "semanticop", + "handler", + "handlerid", + "sourceid", + "sourcenode", + "sourcenodeid", + "nodeid", + "canonicaloperand", +]); + +/** Validate an artifact or throw a stable `RUAM_BPRF_*` diagnostic. */ +export function validateBprfArtifact( + artifact: BprfArtifact +): BprfValidationReport { + if (artifact.format !== "ruam-bprf-pure-1") { + throw new Error("RUAM_BPRF_FORMAT_MISMATCH"); + } + assertNoForbiddenSchemaKeys(artifact); + if (artifact.realizations.length < 2) { + throw new Error("RUAM_BPRF_REALIZATION_COUNT_MIN_2"); + } + + const realizationIds = new Set(); + const familyCodes = new Set(); + const structuralSignatures = new Set(); + const fragmentCounts: number[] = []; + const transitionCounts: number[] = []; + + for (const realization of artifact.realizations) { + if (realizationIds.has(realization.id)) { + throw new Error("RUAM_BPRF_DUPLICATE_REALIZATION_ID"); + } + realizationIds.add(realization.id); + familyCodes.add(realization.familyCode); + validateRealization(realization); + fragmentCounts.push(realization.fragments.length); + transitionCounts.push(realization.transitions.length); + structuralSignatures.add(structuralSignature(realization)); + } + + if (familyCodes.size < 2) { + throw new Error("RUAM_BPRF_REQUIRES_TWO_STRUCTURAL_FAMILIES"); + } + if (structuralSignatures.size !== artifact.realizations.length) { + throw new Error("RUAM_BPRF_DUPLICATE_REALIZATION_STRUCTURE"); + } + + return Object.freeze({ + realizationCount: artifact.realizations.length, + familyCount: familyCodes.size, + fragmentCountRange: Object.freeze([ + Math.min(...fragmentCounts), + Math.max(...fragmentCounts), + ]) as readonly [number, number], + transitionCountRange: Object.freeze([ + Math.min(...transitionCounts), + Math.max(...transitionCounts), + ]) as readonly [number, number], + }); +} + +function validateRealization(realization: BprfRealization): void { + if (realization.familyCode !== 0 && realization.familyCode !== 1) { + throw new Error("RUAM_BPRF_INVALID_FAMILY_CODE"); + } + if (!Number.isSafeInteger(realization.contextSalt)) { + throw new Error("RUAM_BPRF_INVALID_CONTEXT_SALT"); + } + if (!Number.isSafeInteger(realization.frameSize) || realization.frameSize < 1) { + throw new Error("RUAM_BPRF_INVALID_FRAME_SIZE"); + } + if (realization.fragments.length < 2) { + throw new Error("RUAM_BPRF_FRAGMENT_COUNT_MIN_2"); + } + if ( + !Number.isSafeInteger(realization.fragmentThreshold) || + realization.fragmentThreshold < 2 || + realization.fragmentThreshold !== realization.fragments.length + ) { + throw new Error("RUAM_BPRF_FRAGMENT_THRESHOLD_MISMATCH"); + } + if (realization.transitions.length < 2) { + throw new Error("RUAM_BPRF_TRANSITION_COUNT_MIN_2"); + } + if (realization.inputPorts.length === 0 || realization.outputPorts.length === 0) { + throw new Error("RUAM_BPRF_PORTS_REQUIRED"); + } + + const fragmentIds = new Set(); + for (const fragment of realization.fragments) { + if (fragmentIds.has(fragment.id)) { + throw new Error("RUAM_BPRF_DUPLICATE_FRAGMENT_ID"); + } + fragmentIds.add(fragment.id); + if (fragment.pieces.length === 0) { + throw new Error("RUAM_BPRF_EMPTY_FRAGMENT"); + } + } + + const transitionIds = new Set(); + const writtenPhase = new Map(); + const basisBySlot = new Map(); + const inputSlots = new Set(); + for (const port of realization.inputPorts) { + assertSlot(port.slot, realization.frameSize); + assertBasis(port.basis); + if (inputSlots.has(port.slot)) { + throw new Error("RUAM_BPRF_DUPLICATE_INPUT_SLOT"); + } + inputSlots.add(port.slot); + writtenPhase.set(port.slot, -1); + basisBySlot.set(port.slot, port.basis); + } + + for (let index = 0; index < realization.transitions.length; index++) { + const transition = realization.transitions[index]!; + if (transition.phase !== index) { + throw new Error("RUAM_BPRF_NON_DENSE_PHASES"); + } + if (transitionIds.has(transition.id)) { + throw new Error("RUAM_BPRF_DUPLICATE_TRANSITION_ID"); + } + transitionIds.add(transition.id); + if (transition.writes.length === 0) { + throw new Error("RUAM_BPRF_EMPTY_TRANSITION"); + } + if ( + transition.boundaryCode !== + (index === realization.transitions.length - 1 ? 1 : 0) + ) { + throw new Error("RUAM_BPRF_INVALID_BOUNDARY_TRANSITION"); + } + for (const slot of transition.writes) { + assertSlot(slot, realization.frameSize); + if (writtenPhase.has(slot)) { + throw new Error("RUAM_BPRF_SLOT_WRITTEN_MORE_THAN_ONCE"); + } + writtenPhase.set(slot, transition.phase); + } + } + + const piecesByDestination = new Map< + number, + Array<{ fragmentId: string; piece: BprfPiece }> + >(); + for (const fragment of realization.fragments) { + const fragmentDestinations = new Set(); + for (const piece of fragment.pieces) { + assertFinite(piece.coefficient); + assertBasis(piece.destinationBasis); + assertSlot(piece.destination, realization.frameSize); + const expectedPhase = writtenPhase.get(piece.destination); + if (expectedPhase == null || expectedPhase !== piece.phase) { + throw new Error("RUAM_BPRF_PIECE_DESTINATION_PHASE_MISMATCH"); + } + for (const factor of piece.factors) { + assertFinite(factor.offset); + assertBasis(factor.basis); + assertSlot(factor.slot, realization.frameSize); + const factorPhase = writtenPhase.get(factor.slot); + if (factorPhase == null || factorPhase >= piece.phase) { + throw new Error("RUAM_BPRF_FORWARD_OR_UNKNOWN_FACTOR"); + } + } + fragmentDestinations.add(piece.destination); + const destinationPieces = piecesByDestination.get(piece.destination) ?? []; + destinationPieces.push({ fragmentId: fragment.id, piece }); + piecesByDestination.set(piece.destination, destinationPieces); + } + if (fragmentDestinations.size < 2) { + throw new Error("RUAM_BPRF_FRAGMENT_NOT_BRAIDED"); + } + } + + for (const transition of realization.transitions) { + for (const destination of transition.writes) { + const destinationPieces = piecesByDestination.get(destination) ?? []; + const owners = new Set(destinationPieces.map((entry) => entry.fragmentId)); + if (owners.size !== realization.fragmentThreshold) { + throw new Error("RUAM_BPRF_DESTINATION_NOT_FISSIONED"); + } + for (const owner of owners) { + const ownedCount = destinationPieces.filter( + (entry) => entry.fragmentId === owner + ).length; + if (ownedCount === destinationPieces.length) { + throw new Error("RUAM_BPRF_FRAGMENT_OWNS_COMPLETE_DESTINATION"); + } + } + assertConsistentDestinationBasis(destinationPieces.map((entry) => entry.piece)); + basisBySlot.set( + destination, + destinationPieces[0]!.piece.destinationBasis + ); + } + } + + for (const fragment of realization.fragments) { + for (const piece of fragment.pieces) { + for (const factor of piece.factors) { + const expectedBasis = basisBySlot.get(factor.slot); + if (!expectedBasis || !sameBasis(expectedBasis, factor.basis)) { + throw new Error("RUAM_BPRF_FACTOR_BASIS_MISMATCH"); + } + } + } + } + + const finalTransition = + realization.transitions[realization.transitions.length - 1]!; + const finalSlots = new Set(finalTransition.writes); + for (const port of realization.outputPorts) { + assertSlot(port.slot, realization.frameSize); + assertBasis(port.basis); + if (!finalSlots.has(port.slot) || inputSlots.has(port.slot)) { + throw new Error("RUAM_BPRF_OUTPUT_NOT_REBASED_AT_BOUNDARY"); + } + const pieces = piecesByDestination.get(port.slot)!; + if (!sameBasis(pieces[0]!.piece.destinationBasis, port.basis)) { + throw new Error("RUAM_BPRF_OUTPUT_BASIS_MISMATCH"); + } + } +} + +function assertConsistentDestinationBasis(pieces: readonly BprfPiece[]): void { + const expected = pieces[0]?.destinationBasis; + if (!expected) throw new Error("RUAM_BPRF_MISSING_DESTINATION_PIECES"); + for (const piece of pieces) { + if (!sameBasis(piece.destinationBasis, expected)) { + throw new Error("RUAM_BPRF_INCONSISTENT_DESTINATION_BASIS"); + } + } +} + +function assertNoForbiddenSchemaKeys(value: unknown): void { + if (Array.isArray(value)) { + for (const item of value) assertNoForbiddenSchemaKeys(item); + return; + } + if (value === null || typeof value !== "object") return; + for (const [key, child] of Object.entries(value)) { + if (FORBIDDEN_SCHEMA_KEYS.has(key.toLowerCase())) { + throw new Error(`RUAM_BPRF_FORBIDDEN_SCHEMA_KEY: ${key}`); + } + assertNoForbiddenSchemaKeys(child); + } +} + +function structuralSignature(realization: BprfRealization): string { + return JSON.stringify({ + familyCode: realization.familyCode, + frameSize: realization.frameSize, + fragmentThreshold: realization.fragmentThreshold, + inputPorts: realization.inputPorts, + outputPorts: realization.outputPorts, + transitions: realization.transitions.map((transition) => ({ + phase: transition.phase, + boundaryCode: transition.boundaryCode, + writes: transition.writes, + })), + fragments: realization.fragments.map((fragment) => + fragment.pieces.map((piece) => ({ + phase: piece.phase, + destination: piece.destination, + destinationBasis: piece.destinationBasis, + coefficient: piece.coefficient, + factors: piece.factors, + })) + ), + }); +} + +function assertSlot(slot: number, frameSize: number): void { + if (!Number.isSafeInteger(slot) || slot < 0 || slot >= frameSize) { + throw new Error(`RUAM_BPRF_INVALID_SLOT: ${slot}`); + } +} + +function assertBasis(basis: BprfWireBasis): void { + assertFinite(basis.scale); + assertFinite(basis.bias); + if (basis.scale === 0) throw new Error("RUAM_BPRF_ZERO_BASIS_SCALE"); +} + +function assertFinite(value: number): void { + if (!Number.isFinite(value)) throw new Error("RUAM_BPRF_NON_FINITE_VALUE"); +} + +function sameBasis(left: BprfWireBasis, right: BprfWireBasis): boolean { + return left.scale === right.scale && left.bias === right.bias; +} diff --git a/packages/ruam/src/isogloss/csh/chart-custody-protocol.ts b/packages/ruam/src/isogloss/csh/chart-custody-protocol.ts new file mode 100644 index 0000000..d90b3a4 --- /dev/null +++ b/packages/ruam/src/isogloss/csh/chart-custody-protocol.ts @@ -0,0 +1,247 @@ +/** + * Client protocol for a custodied contribution inside a CSH cover transition. + * + * The client can combine a signed set of chart-local additive contributions + * into an already transported local cover. It has no implementation of the + * missing relation and no fallback when a response is absent. + * + * @module isogloss/csh/chart-custody-protocol + */ + +import { createHash, verify } from "node:crypto"; +import { + CSH_FIELD_MODULUS, + type ChartCellTransform, + type ChartCover, + type EncodedChart, +} from "./reference.js"; +import type { CustodyClientState } from "./custody-protocol.js"; + +export interface ChartCustodyClientContract { + readonly sessionId: string; + readonly contractId: string; + readonly fromCoverId: string; + readonly toCoverId: string; + readonly initialLineageCommitment: string; + readonly verificationKey: string; +} + +export interface ChartCustodyRequest { + readonly sessionId: string; + readonly contractId: string; + readonly fromCoverId: string; + readonly toCoverId: string; + readonly epoch: number; + readonly lineageCommitment: string; + readonly nonce: string; + readonly charts: readonly EncodedChart[]; +} + +export interface AdditiveChartContribution { + readonly chartId: string; + readonly cells: readonly number[]; +} + +export interface ChartCustodyResponse { + readonly sessionId: string; + readonly contractId: string; + readonly requestNonce: string; + readonly epoch: number; + readonly nextEpoch: number; + readonly nextLineageCommitment: string; + readonly chartContributions: readonly AdditiveChartContribution[]; + readonly signature: string; +} + +export interface AppliedChartCustody { + readonly charts: readonly EncodedChart[]; + readonly state: CustodyClientState; +} + +export function createChartCustodyClientState( + contract: ChartCustodyClientContract +): CustodyClientState { + return Object.freeze({ + epoch: 0, + lineageCommitment: contract.initialLineageCommitment, + }); +} + +export function prepareChartCustodyRequest( + contract: ChartCustodyClientContract, + state: CustodyClientState, + charts: readonly EncodedChart[], + nonce: string +): ChartCustodyRequest { + if (nonce.length < 8) throw new Error("RUAM_CSH_CUSTODY_NONCE_TOO_SHORT"); + if ( + charts.length === 0 || + charts.some((chart) => chart.coverId !== contract.fromCoverId) + ) { + throw new Error("RUAM_CSH_CHART_CUSTODY_WRONG_SOURCE_COVER"); + } + return Object.freeze({ + sessionId: contract.sessionId, + contractId: contract.contractId, + fromCoverId: contract.fromCoverId, + toCoverId: contract.toCoverId, + epoch: state.epoch, + lineageCommitment: state.lineageCommitment, + nonce, + charts, + }); +} + +/** Apply one authenticated missing gluing contribution to a local transport. */ +export function applyCustodiedChartContribution( + contract: ChartCustodyClientContract, + state: CustodyClientState, + localCharts: readonly EncodedChart[], + targetCover: ChartCover, + response: ChartCustodyResponse | undefined, + expectedNonce: string +): AppliedChartCustody { + if (response === undefined) { + throw new Error("RUAM_CSH_CUSTODIAN_REQUIRED"); + } + if ( + response.sessionId !== contract.sessionId || + response.contractId !== contract.contractId || + response.requestNonce !== expectedNonce || + response.epoch !== state.epoch || + response.nextEpoch !== state.epoch + 1 || + targetCover.id !== contract.toCoverId + ) { + throw new Error("RUAM_CSH_CHART_CUSTODY_RESPONSE_MISMATCH"); + } + if ( + !verify( + null, + Buffer.from(chartCustodySigningPayload(response)), + contract.verificationKey, + Buffer.from(response.signature, "base64") + ) + ) { + throw new Error("RUAM_CSH_CUSTODY_BAD_SIGNATURE"); + } + const localById = new Map(localCharts.map((chart) => [chart.chartId, chart])); + const contributionById = new Map( + response.chartContributions.map((contribution) => [ + contribution.chartId, + contribution, + ]) + ); + if ( + localById.size !== targetCover.charts.length || + contributionById.size !== targetCover.charts.length + ) { + throw new Error("RUAM_CSH_CHART_CUSTODY_INCOMPLETE_CONTRIBUTION"); + } + const combined = targetCover.charts.map((descriptor): EncodedChart => { + const local = localById.get(descriptor.id); + const contribution = contributionById.get(descriptor.id); + if ( + !local || + !contribution || + local.coverId !== targetCover.id || + local.epoch !== targetCover.epoch || + local.cells.length !== targetCover.width || + contribution.cells.length !== targetCover.width + ) { + throw new Error( + `RUAM_CSH_CHART_CUSTODY_INVALID_CONTRIBUTION: ${descriptor.id}` + ); + } + const cells = local.cells.map((stored, lane) => { + const raw = unwrapCell(stored, descriptor.cells[lane]!); + return wrapCell( + add(raw, contribution.cells[lane]!), + descriptor.cells[lane]! + ); + }); + return Object.freeze({ + coverId: targetCover.id, + epoch: targetCover.epoch, + chartId: descriptor.id, + cells: Object.freeze(cells), + }); + }); + return Object.freeze({ + charts: Object.freeze(combined), + state: Object.freeze({ + epoch: response.nextEpoch, + lineageCommitment: response.nextLineageCommitment, + }), + }); +} + +export function chartCustodySigningPayload( + response: + | Omit + | ChartCustodyResponse +): string { + const contributionDigest = createHash("sha256") + .update(JSON.stringify(response.chartContributions)) + .digest("hex"); + return [ + response.sessionId, + response.contractId, + response.requestNonce, + response.epoch, + response.nextEpoch, + response.nextLineageCommitment, + contributionDigest, + ].join("|"); +} + +function wrapCell(raw: number, transform: ChartCellTransform): number { + return power( + add(multiply(transform.scale, raw), transform.offset), + transform.exponent + ); +} + +function unwrapCell( + stored: number, + transform: ChartCellTransform +): number { + return multiply( + subtract(power(stored, transform.inverseExponent), transform.offset), + inverse(transform.scale) + ); +} + +function inverse(value: number): number { + const normalized = normalize(value); + if (normalized === 0) throw new Error("RUAM_CSH_ZERO_HAS_NO_INVERSE"); + return power(normalized, CSH_FIELD_MODULUS - 2); +} + +function power(base: number, exponent: number): number { + let result = 1; + let factor = normalize(base); + let remaining = exponent; + while (remaining > 0) { + if (remaining % 2 === 1) result = multiply(result, factor); + factor = multiply(factor, factor); + remaining = Math.floor(remaining / 2); + } + return result; +} + +function add(left: number, right: number): number { + return normalize(normalize(left) + normalize(right)); +} + +function subtract(left: number, right: number): number { + return normalize(normalize(left) - normalize(right)); +} + +function multiply(left: number, right: number): number { + return normalize(normalize(left) * normalize(right)); +} + +function normalize(value: number): number { + const normalized = Math.trunc(value) % CSH_FIELD_MODULUS; + return normalized < 0 ? normalized + CSH_FIELD_MODULUS : normalized; +} diff --git a/packages/ruam/src/isogloss/csh/custody-protocol.ts b/packages/ruam/src/isogloss/csh/custody-protocol.ts new file mode 100644 index 0000000..e831ae4 --- /dev/null +++ b/packages/ruam/src/isogloss/csh/custody-protocol.ts @@ -0,0 +1,197 @@ +/** + * Client-visible protocol for the experimental CSH direct-custody spike. + * + * The protocol deliberately has no local relation implementation or fallback. + * It can verify and open one lineage-bound, site-specific projection returned + * by a custodian, but it cannot derive that projection from client artifacts. + * + * @module isogloss/csh/custody-protocol + */ + +import { verify } from "node:crypto"; +import { + CSH_FIELD_MODULUS, + type ChartCellTransform, + type EncodedChart, +} from "./reference.js"; + +export interface CustodyClientContract { + readonly sessionId: string; + readonly contractId: string; + readonly coverId: string; + readonly initialLineageCommitment: string; + readonly verificationKey: string; +} + +export interface CustodyClientState { + readonly epoch: number; + readonly lineageCommitment: string; +} + +export interface CustodyRequest { + readonly sessionId: string; + readonly contractId: string; + readonly coverId: string; + readonly epoch: number; + readonly lineageCommitment: string; + readonly nonce: string; + readonly charts: readonly EncodedChart[]; +} + +export interface CustodyResponse { + readonly sessionId: string; + readonly contractId: string; + readonly requestNonce: string; + readonly epoch: number; + readonly nextEpoch: number; + readonly nextLineageCommitment: string; + readonly encodedProjection: number; + /** One-response opening for the declared scalar projection only. */ + readonly projectionOpening: ChartCellTransform; + readonly signature: string; +} + +export interface OpenedCustodyProjection { + readonly projection: number; + readonly state: CustodyClientState; +} + +export function createCustodyClientState( + contract: CustodyClientContract +): CustodyClientState { + return Object.freeze({ + epoch: 0, + lineageCommitment: contract.initialLineageCommitment, + }); +} + +export function prepareCustodyRequest( + contract: CustodyClientContract, + state: CustodyClientState, + charts: readonly EncodedChart[], + nonce: string +): CustodyRequest { + if (nonce.length < 8) throw new Error("RUAM_CSH_CUSTODY_NONCE_TOO_SHORT"); + if ( + charts.length === 0 || + charts.some((chart) => chart.coverId !== contract.coverId) + ) { + throw new Error("RUAM_CSH_CUSTODY_WRONG_COVER"); + } + return Object.freeze({ + sessionId: contract.sessionId, + contractId: contract.contractId, + coverId: contract.coverId, + epoch: state.epoch, + lineageCommitment: state.lineageCommitment, + nonce, + charts, + }); +} + +/** + * Verify and materialize exactly one ordinary value at its declared effect + * boundary. There is intentionally no overload that accepts no response. + */ +export function openCustodiedProjection( + contract: CustodyClientContract, + state: CustodyClientState, + response: CustodyResponse | undefined, + expectedNonce: string +): OpenedCustodyProjection { + if (response === undefined) { + throw new Error("RUAM_CSH_CUSTODIAN_REQUIRED"); + } + if ( + response.sessionId !== contract.sessionId || + response.contractId !== contract.contractId || + response.requestNonce !== expectedNonce || + response.epoch !== state.epoch || + response.nextEpoch !== state.epoch + 1 + ) { + throw new Error("RUAM_CSH_CUSTODY_RESPONSE_MISMATCH"); + } + const signed = custodyResponseSigningPayload(response); + if ( + !verify( + null, + Buffer.from(signed), + contract.verificationKey, + Buffer.from(response.signature, "base64") + ) + ) { + throw new Error("RUAM_CSH_CUSTODY_BAD_SIGNATURE"); + } + return Object.freeze({ + projection: unwrapProjection( + response.encodedProjection, + response.projectionOpening + ), + state: Object.freeze({ + epoch: response.nextEpoch, + lineageCommitment: response.nextLineageCommitment, + }), + }); +} + +export function custodyResponseSigningPayload( + response: Omit | CustodyResponse +): string { + return [ + response.sessionId, + response.contractId, + response.requestNonce, + response.epoch, + response.nextEpoch, + response.nextLineageCommitment, + response.encodedProjection, + response.projectionOpening.scale, + response.projectionOpening.offset, + response.projectionOpening.exponent, + response.projectionOpening.inverseExponent, + ].join("|"); +} + +function unwrapProjection( + stored: number, + transform: ChartCellTransform +): number { + return multiply( + subtract( + powerField(stored, transform.inverseExponent), + transform.offset + ), + inverseField(transform.scale) + ); +} + +function multiply(left: number, right: number): number { + return normalize(normalize(left) * normalize(right)); +} + +function subtract(left: number, right: number): number { + return normalize(normalize(left) - normalize(right)); +} + +function inverseField(value: number): number { + const normalized = normalize(value); + if (normalized === 0) throw new Error("RUAM_CSH_ZERO_HAS_NO_INVERSE"); + return powerField(normalized, CSH_FIELD_MODULUS - 2); +} + +function powerField(base: number, exponent: number): number { + let result = 1; + let factor = normalize(base); + let remaining = exponent; + while (remaining > 0) { + if (remaining % 2 === 1) result = multiply(result, factor); + factor = multiply(factor, factor); + remaining = Math.floor(remaining / 2); + } + return result; +} + +function normalize(value: number): number { + const normalized = Math.trunc(value) % CSH_FIELD_MODULUS; + return normalized < 0 ? normalized + CSH_FIELD_MODULUS : normalized; +} diff --git a/packages/ruam/src/isogloss/csh/masked-custody-protocol.ts b/packages/ruam/src/isogloss/csh/masked-custody-protocol.ts new file mode 100644 index 0000000..e57493f --- /dev/null +++ b/packages/ruam/src/isogloss/csh/masked-custody-protocol.ts @@ -0,0 +1,155 @@ +/** + * Client protocol for statefully masked multi-transition CSH custody. + * + * The client carries charts for `logicalState + custodianMask`. It can perform + * an identity cover transport and combine a signed transition contribution, + * but neither the hidden transition nor the current/next representation mask + * appears in this contract. + * + * @module isogloss/csh/masked-custody-protocol + */ + +import { + applyCustodiedChartContribution, + prepareChartCustodyRequest, + type ChartCustodyClientContract, + type ChartCustodyRequest, + type ChartCustodyResponse, +} from "./chart-custody-protocol.js"; +import { + openCustodiedProjection, + type CustodyClientContract, + type CustodyClientState, + type CustodyRequest, + type CustodyResponse, + type OpenedCustodyProjection, +} from "./custody-protocol.js"; +import type { ChartCover, EncodedChart } from "./reference.js"; + +export interface MaskedCustodyClientContract { + readonly sessionId: string; + readonly contractId: string; + /** Ordered ingress, intermediate, and terminal covers. */ + readonly coverIds: readonly string[]; + readonly initialLineageCommitment: string; + readonly verificationKey: string; +} + +export function createMaskedCustodyClientState( + contract: MaskedCustodyClientContract +): CustodyClientState { + if (contract.coverIds.length < 2) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_COVER_PATH_TOO_SHORT"); + } + return Object.freeze({ + epoch: 0, + lineageCommitment: contract.initialLineageCommitment, + }); +} + +export function prepareMaskedTransitionRequest( + contract: MaskedCustodyClientContract, + state: CustodyClientState, + charts: readonly EncodedChart[], + nonce: string +): ChartCustodyRequest { + const fixed = transitionContract(contract, state.epoch); + return prepareChartCustodyRequest(fixed, state, charts, nonce); +} + +export function applyMaskedTransitionResponse( + contract: MaskedCustodyClientContract, + state: CustodyClientState, + localIdentityTransport: readonly EncodedChart[], + targetCover: ChartCover, + response: ChartCustodyResponse | undefined, + expectedNonce: string +): { + readonly charts: readonly EncodedChart[]; + readonly state: CustodyClientState; +} { + const fixed = transitionContract(contract, state.epoch); + return applyCustodiedChartContribution( + fixed, + state, + localIdentityTransport, + targetCover, + response, + expectedNonce + ); +} + +export function prepareMaskedProjectionRequest( + contract: MaskedCustodyClientContract, + state: CustodyClientState, + charts: readonly EncodedChart[], + nonce: string +): CustodyRequest { + const terminalEpoch = contract.coverIds.length - 1; + if (state.epoch !== terminalEpoch) { + throw new Error( + `RUAM_CSH_MASKED_CUSTODY_NOT_AT_EXIT: ${state.epoch}/${terminalEpoch}` + ); + } + if ( + nonce.length < 8 || + charts.length === 0 || + charts.some( + (chart) => chart.coverId !== contract.coverIds[terminalEpoch] + ) + ) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_INVALID_EXIT_REQUEST"); + } + return Object.freeze({ + sessionId: contract.sessionId, + contractId: contract.contractId, + coverId: contract.coverIds[terminalEpoch]!, + epoch: state.epoch, + lineageCommitment: state.lineageCommitment, + nonce, + charts, + }); +} + +export function openMaskedCustodyProjection( + contract: MaskedCustodyClientContract, + state: CustodyClientState, + response: CustodyResponse | undefined, + expectedNonce: string +): OpenedCustodyProjection { + const terminalEpoch = contract.coverIds.length - 1; + const fixed: CustodyClientContract = Object.freeze({ + sessionId: contract.sessionId, + contractId: contract.contractId, + coverId: contract.coverIds[terminalEpoch]!, + initialLineageCommitment: contract.initialLineageCommitment, + verificationKey: contract.verificationKey, + }); + return openCustodiedProjection( + fixed, + state, + response, + expectedNonce + ); +} + +function transitionContract( + contract: MaskedCustodyClientContract, + epoch: number +): ChartCustodyClientContract { + const fromCoverId = contract.coverIds[epoch]; + const toCoverId = contract.coverIds[epoch + 1]; + if (fromCoverId === undefined || toCoverId === undefined) { + throw new Error( + `RUAM_CSH_MASKED_CUSTODY_NO_TRANSITION: ${epoch}` + ); + } + return Object.freeze({ + sessionId: contract.sessionId, + contractId: contract.contractId, + fromCoverId, + toCoverId, + initialLineageCommitment: contract.initialLineageCommitment, + verificationKey: contract.verificationKey, + }); +} diff --git a/packages/ruam/src/isogloss/csh/padded-masked-plan.ts b/packages/ruam/src/isogloss/csh/padded-masked-plan.ts new file mode 100644 index 0000000..abb2005 --- /dev/null +++ b/packages/ruam/src/isogloss/csh/padded-masked-plan.ts @@ -0,0 +1,183 @@ +/** + * OWNER/SERVER-ONLY fixed-bucket planning for statefully masked custody. + * + * Real logical transitions retain their order but occupy secret slots among + * identity epochs. Every slot still refreshes the custodian representation + * mask and moves to a fresh cover, so the client transcript reveals only the + * declared width/epoch bucket, not the number or positions of real stages. + * + * @module isogloss/csh/padded-masked-plan + */ + +import { createHmac } from "node:crypto"; +import { deriveSeed } from "../../naming/scope.js"; +import { + createChartCover, + type ChartCover, +} from "./reference.js"; +import type { HiddenMaskedTransition } from "./reference-masked-custodian.js"; +export { + MASKED_CUSTODY_TRANSCRIPT_BUCKETS, + type MaskedCustodyTranscriptBucket, +} from "./transcript-buckets.js"; +import { MASKED_CUSTODY_TRANSCRIPT_BUCKETS } from "./transcript-buckets.js"; + +export interface PaddedMaskedCustodyPlanOptions { + readonly realTransitions: readonly HiddenMaskedTransition[]; + readonly width: number; + readonly bucketSize: (typeof MASKED_CUSTODY_TRANSCRIPT_BUCKETS)[number]; + readonly coverSeed: number; + /** Server-only high-entropy key; never included in the client contract. */ + readonly placementSecret: string; +} + +/** + * This complete object is owner/server material. Only `transcriptClassId` and + * the resulting custodian client contract may cross the trust boundary. + */ +export interface PaddedMaskedCustodyPlan { + readonly transcriptClassId: string; + readonly covers: readonly ChartCover[]; + readonly transitions: readonly HiddenMaskedTransition[]; + readonly realTransitionSlots: readonly number[]; + readonly paddingTransitionCount: number; +} + +export function createPaddedMaskedCustodyPlan( + options: PaddedMaskedCustodyPlanOptions +): PaddedMaskedCustodyPlan { + validateOptions(options); + const transcriptClassId = + `csh-masked-v1-w${options.width}-e${options.bucketSize}`; + const realTransitionSlots = selectOrderedSlots( + options.bucketSize, + options.realTransitions.length, + options.placementSecret, + transcriptClassId + ); + const realBySlot = new Map( + realTransitionSlots.map((slot, index) => [ + slot, + options.realTransitions[index]!, + ]) + ); + const identity = createIdentityTransition(options.width); + const transitions = Array.from( + { length: options.bucketSize }, + (_, slot) => realBySlot.get(slot) ?? identity + ); + const covers = Array.from( + { length: options.bucketSize + 1 }, + (_, epoch) => + createChartCover({ + seed: deriveSeed( + options.coverSeed >>> 0, + `${transcriptClassId}:cover` + ), + epoch, + width: options.width, + chartCount: 5, + threshold: 3, + }) + ); + return Object.freeze({ + transcriptClassId, + covers: Object.freeze(covers), + transitions: Object.freeze(transitions), + realTransitionSlots: Object.freeze(realTransitionSlots), + paddingTransitionCount: + options.bucketSize - options.realTransitions.length, + }); +} + +function validateOptions(options: PaddedMaskedCustodyPlanOptions): void { + if ( + !Number.isSafeInteger(options.width) || + options.width < 2 + ) { + throw new Error("RUAM_CSH_PADDED_PLAN_INVALID_WIDTH"); + } + if ( + !MASKED_CUSTODY_TRANSCRIPT_BUCKETS.includes(options.bucketSize) + ) { + throw new Error("RUAM_CSH_PADDED_PLAN_INVALID_BUCKET"); + } + if ( + options.realTransitions.length === 0 || + options.realTransitions.length > options.bucketSize + ) { + throw new Error("RUAM_CSH_PADDED_PLAN_TRANSITION_COUNT"); + } + if (!Number.isSafeInteger(options.coverSeed)) { + throw new Error("RUAM_CSH_PADDED_PLAN_INVALID_SEED"); + } + if (options.placementSecret.length < 16) { + throw new Error("RUAM_CSH_PADDED_PLAN_WEAK_PLACEMENT_SECRET"); + } +} + +function selectOrderedSlots( + bucketSize: number, + realCount: number, + secret: string, + transcriptClassId: string +): number[] { + const nextWord = createKeyedWordStream( + secret, + `${transcriptClassId}:real-count:${realCount}:placement` + ); + const slots = Array.from({ length: bucketSize }, (_, index) => index); + for (let index = slots.length - 1; index > 0; index--) { + const target = uniformBelow(nextWord, index + 1); + [slots[index], slots[target]] = [slots[target]!, slots[index]!]; + } + return slots.slice(0, realCount).sort((left, right) => left - right); +} + +function createKeyedWordStream( + secret: string, + domain: string +): () => number { + let counter = 0; + return () => + createHmac("sha256", secret) + .update(domain) + .update("|") + .update(String(counter++)) + .digest() + .readUInt32LE(0); +} + +function uniformBelow( + nextWord: () => number, + upperExclusive: number +): number { + const wordRange = 0x1_0000_0000; + const limit = + wordRange - (wordRange % upperExclusive); + for (;;) { + const word = nextWord(); + if (word < limit) return word % upperExclusive; + } +} + +function createIdentityTransition( + width: number +): HiddenMaskedTransition { + return Object.freeze({ + linear: Object.freeze( + Array.from( + { length: width }, + (_, row) => + Object.freeze( + Array.from( + { length: width }, + (_, column) => (row === column ? 1 : 0) + ) + ) + ) + ), + bias: Object.freeze(Array.from({ length: width }, () => 0)), + cubicTerms: Object.freeze([]), + }); +} diff --git a/packages/ruam/src/isogloss/csh/reference-chart-custodian.ts b/packages/ruam/src/isogloss/csh/reference-chart-custodian.ts new file mode 100644 index 0000000..7b0d217 --- /dev/null +++ b/packages/ruam/src/isogloss/csh/reference-chart-custodian.ts @@ -0,0 +1,257 @@ +/** + * Server-side missing chart relation for the CSH integration spike. + * + * It evaluates a hidden nonlinear projection and returns only additive chart + * contributions under the next cover. The relation and logical direction are + * absent from the client contract. + * + * @module isogloss/csh/reference-chart-custodian + */ + +import { + createHash, + generateKeyPairSync, + sign, +} from "node:crypto"; +import { deriveSeed } from "../../naming/scope.js"; +import { createSeededRandom } from "../../random/entropy.js"; +import { + CSH_FIELD_MODULUS, + evaluateCertifiedProjection, + type ChartCover, + type CertifiedProjectionSite, +} from "./reference.js"; +import { + chartCustodySigningPayload, + type AdditiveChartContribution, + type ChartCustodyClientContract, + type ChartCustodyRequest, + type ChartCustodyResponse, +} from "./chart-custody-protocol.js"; + +export interface HiddenChartRelation { + readonly inputProjection: CertifiedProjectionSite; + readonly cubic: number; + readonly linear: number; + readonly bias: number; + readonly outputDirection: readonly number[]; +} + +export interface ReferenceChartCustodianOptions { + readonly sessionId: string; + readonly contractId: string; + readonly fromCover: ChartCover; + readonly toCover: ChartCover; + readonly initialLineageCommitment: string; + readonly lineageSecret: string; + readonly sharingSecret: string; + readonly relation: HiddenChartRelation; +} + +export class ReferenceChartRelationCustodian { + readonly clientContract: ChartCustodyClientContract; + + readonly #fromCover: ChartCover; + readonly #toCover: ChartCover; + readonly #relation: HiddenChartRelation; + readonly #lineageSecret: string; + readonly #sharingSecret: string; + readonly #privateKey: ReturnType["privateKey"]; + readonly #consumedNonces = new Set(); + #epoch = 0; + #lineageCommitment: string; + + constructor(options: ReferenceChartCustodianOptions) { + if (options.fromCover.id === options.toCover.id) { + throw new Error("RUAM_CSH_COVER_DID_NOT_CHANGE"); + } + if ( + options.fromCover.width !== options.toCover.width || + options.relation.outputDirection.length !== options.toCover.width + ) { + throw new Error("RUAM_CSH_CHART_CUSTODY_WIDTH_MISMATCH"); + } + const relationScalars = [ + options.relation.cubic, + options.relation.linear, + options.relation.bias, + ...options.relation.outputDirection, + ]; + if ( + relationScalars.some((value) => !Number.isSafeInteger(value)) || + !options.relation.outputDirection.some( + (value) => normalize(value) !== 0 + ) + ) { + throw new Error("RUAM_CSH_CHART_CUSTODY_INVALID_RELATION"); + } + const { privateKey, publicKey } = generateKeyPairSync("ed25519"); + this.#privateKey = privateKey; + this.#fromCover = options.fromCover; + this.#toCover = options.toCover; + this.#relation = options.relation; + this.#lineageSecret = options.lineageSecret; + this.#sharingSecret = options.sharingSecret; + this.#lineageCommitment = options.initialLineageCommitment; + this.clientContract = Object.freeze({ + sessionId: options.sessionId, + contractId: options.contractId, + fromCoverId: options.fromCover.id, + toCoverId: options.toCover.id, + initialLineageCommitment: options.initialLineageCommitment, + verificationKey: publicKey + .export({ type: "spki", format: "pem" }) + .toString(), + }); + } + + evaluate(request: ChartCustodyRequest): ChartCustodyResponse { + if ( + request.sessionId !== this.clientContract.sessionId || + request.contractId !== this.clientContract.contractId || + request.fromCoverId !== this.#fromCover.id || + request.toCoverId !== this.#toCover.id + ) { + throw new Error("RUAM_CSH_CUSTODY_REQUEST_MISMATCH"); + } + if (this.#consumedNonces.has(request.nonce)) { + throw new Error("RUAM_CSH_CUSTODY_REPLAY"); + } + if ( + request.epoch !== this.#epoch || + request.lineageCommitment !== this.#lineageCommitment + ) { + throw new Error("RUAM_CSH_CUSTODY_STALE_LINEAGE"); + } + const projectedInput = evaluateCertifiedProjection( + request.charts, + this.#fromCover, + this.#relation.inputProjection + ); + const cubed = multiply( + multiply(projectedInput, projectedInput), + projectedInput + ); + const residual = add( + add( + multiply(this.#relation.cubic, cubed), + multiply(this.#relation.linear, projectedInput) + ), + this.#relation.bias + ); + const logicalDelta = this.#relation.outputDirection.map((direction) => + multiply(direction, residual) + ); + const mixedDelta = multiplyMatrixVector( + this.#toCover.mixing, + logicalDelta + ); + const chartContributions = createAdditiveShares( + mixedDelta, + this.#toCover, + deriveSharingSeed( + this.#sharingSecret, + this.#epoch, + request.nonce + ) + ); + const nextEpoch = this.#epoch + 1; + const nextLineageCommitment = createHash("sha256") + .update(this.#lineageSecret) + .update("|") + .update(this.#lineageCommitment) + .update("|") + .update(request.nonce) + .update("|") + .update(JSON.stringify(chartContributions)) + .digest("hex"); + const unsigned = { + sessionId: request.sessionId, + contractId: request.contractId, + requestNonce: request.nonce, + epoch: this.#epoch, + nextEpoch, + nextLineageCommitment, + chartContributions, + }; + const signature = sign( + null, + Buffer.from(chartCustodySigningPayload(unsigned)), + this.#privateKey + ).toString("base64"); + this.#consumedNonces.add(request.nonce); + this.#epoch = nextEpoch; + this.#lineageCommitment = nextLineageCommitment; + return Object.freeze({ ...unsigned, signature }); + } +} + +function createAdditiveShares( + constant: readonly number[], + cover: ChartCover, + seed: number +): readonly AdditiveChartContribution[] { + const random = createSeededRandom(seed); + const residuals = Array.from({ length: cover.width }, () => + Array.from({ length: cover.threshold - 1 }, () => + random.nextUint32() % CSH_FIELD_MODULUS + ) + ); + return Object.freeze( + cover.charts.map((chart) => { + const cells = constant.map((value, lane) => { + let shared = normalize(value); + let pointPower = chart.point; + for (const residual of residuals[lane]!) { + shared = add(shared, multiply(residual, pointPower)); + pointPower = multiply(pointPower, chart.point); + } + return shared; + }); + return Object.freeze({ + chartId: chart.id, + cells: Object.freeze(cells), + }); + }) + ); +} + +function deriveSharingSeed( + secret: string, + epoch: number, + nonce: string +): number { + const secretWord = createHash("sha256") + .update(secret) + .update("|") + .update(nonce) + .digest() + .readUInt32LE(0); + return deriveSeed(secretWord, `chart-custody:${epoch}`); +} + +function multiplyMatrixVector( + matrix: readonly (readonly number[])[], + input: readonly number[] +): number[] { + return matrix.map((row) => { + let value = 0; + for (let index = 0; index < row.length; index++) { + value = add(value, multiply(row[index]!, input[index]!)); + } + return value; + }); +} + +function add(left: number, right: number): number { + return normalize(normalize(left) + normalize(right)); +} + +function multiply(left: number, right: number): number { + return normalize(normalize(left) * normalize(right)); +} + +function normalize(value: number): number { + const normalized = Math.trunc(value) % CSH_FIELD_MODULUS; + return normalized < 0 ? normalized + CSH_FIELD_MODULUS : normalized; +} diff --git a/packages/ruam/src/isogloss/csh/reference-custodian.ts b/packages/ruam/src/isogloss/csh/reference-custodian.ts new file mode 100644 index 0000000..2ccacbc --- /dev/null +++ b/packages/ruam/src/isogloss/csh/reference-custodian.ts @@ -0,0 +1,246 @@ +/** + * Server-side direct relation evaluator for the experimental CSH spike. + * + * This module models a process/trust boundary. It must never be bundled into a + * client artifact. It is the direct-remote performance/control baseline that + * precedes a topology-hidden, actively secure PFE implementation. + * + * @module isogloss/csh/reference-custodian + */ + +import { + createHash, + generateKeyPairSync, + sign, +} from "node:crypto"; +import { + CSH_FIELD_MODULUS, + evaluateCertifiedProjection, + type ChartCellTransform, + type ChartCover, + type CertifiedProjectionSite, +} from "./reference.js"; +import { + custodyResponseSigningPayload, + type CustodyClientContract, + type CustodyRequest, + type CustodyResponse, +} from "./custody-protocol.js"; + +export interface CustodiedNonlinearRelation { + readonly projectionSite: CertifiedProjectionSite; + readonly cubic: number; + readonly linear: number; + readonly bias: number; +} + +export interface ReferenceCustodianOptions { + readonly sessionId: string; + readonly contractId: string; + readonly cover: ChartCover; + readonly initialLineageCommitment: string; + readonly relation: CustodiedNonlinearRelation; + readonly lineageSecret: string; + readonly responseSecret: string; +} + +/** + * Stateful one-session evaluator. A successful request consumes its epoch + * before another request can be evaluated, preventing snapshot-and-fork reuse. + */ +export class ReferenceRelationCustodian { + readonly clientContract: CustodyClientContract; + + readonly #cover: ChartCover; + readonly #relation: CustodiedNonlinearRelation; + readonly #lineageSecret: string; + readonly #responseSecret: string; + readonly #privateKey: ReturnType["privateKey"]; + readonly #consumedNonces = new Set(); + #epoch = 0; + #lineageCommitment: string; + + constructor(options: ReferenceCustodianOptions) { + const { privateKey, publicKey } = generateKeyPairSync("ed25519"); + this.#privateKey = privateKey; + this.#cover = options.cover; + this.#relation = options.relation; + this.#lineageSecret = options.lineageSecret; + this.#responseSecret = options.responseSecret; + this.#lineageCommitment = options.initialLineageCommitment; + this.clientContract = Object.freeze({ + sessionId: options.sessionId, + contractId: options.contractId, + coverId: options.cover.id, + initialLineageCommitment: options.initialLineageCommitment, + verificationKey: publicKey + .export({ type: "spki", format: "pem" }) + .toString(), + }); + } + + evaluate(request: CustodyRequest): CustodyResponse { + if ( + request.sessionId !== this.clientContract.sessionId || + request.contractId !== this.clientContract.contractId || + request.coverId !== this.#cover.id + ) { + throw new Error("RUAM_CSH_CUSTODY_REQUEST_MISMATCH"); + } + if (this.#consumedNonces.has(request.nonce)) { + throw new Error("RUAM_CSH_CUSTODY_REPLAY"); + } + if ( + request.epoch !== this.#epoch || + request.lineageCommitment !== this.#lineageCommitment + ) { + throw new Error("RUAM_CSH_CUSTODY_STALE_LINEAGE"); + } + + const inputProjection = evaluateCertifiedProjection( + request.charts, + this.#cover, + this.#relation.projectionSite + ); + const squared = multiply(inputProjection, inputProjection); + const cubed = multiply(squared, inputProjection); + const projected = add( + add( + multiply(this.#relation.cubic, cubed), + multiply(this.#relation.linear, inputProjection) + ), + this.#relation.bias + ); + const projectionOpening = deriveResponseTransform( + this.#responseSecret, + this.#epoch, + request.nonce + ); + const encodedProjection = wrapProjection( + projected, + projectionOpening + ); + const nextEpoch = this.#epoch + 1; + const nextLineageCommitment = createHash("sha256") + .update(this.#lineageSecret) + .update("|") + .update(this.#lineageCommitment) + .update("|") + .update(request.nonce) + .update("|") + .update(String(encodedProjection)) + .digest("hex"); + const unsigned = { + sessionId: request.sessionId, + contractId: request.contractId, + requestNonce: request.nonce, + epoch: this.#epoch, + nextEpoch, + nextLineageCommitment, + encodedProjection, + projectionOpening, + }; + const signature = sign( + null, + Buffer.from(custodyResponseSigningPayload(unsigned)), + this.#privateKey + ).toString("base64"); + + // Advance before returning so re-entrant/forked requests see new state. + this.#consumedNonces.add(request.nonce); + this.#epoch = nextEpoch; + this.#lineageCommitment = nextLineageCommitment; + return Object.freeze({ ...unsigned, signature }); + } +} + +function deriveResponseTransform( + secret: string, + epoch: number, + nonce: string +): ChartCellTransform { + const digest = createHash("sha256") + .update(secret) + .update("|") + .update(String(epoch)) + .update("|") + .update(nonce) + .digest(); + const scale = + 1 + (digest.readUInt32LE(0) % (CSH_FIELD_MODULUS - 1)); + const offset = digest.readUInt32LE(4) % CSH_FIELD_MODULUS; + const order = CSH_FIELD_MODULUS - 1; + let exponent = 3 + (digest.readUInt32LE(8) % (order - 3)); + while (greatestCommonDivisor(exponent, order) !== 1) { + exponent++; + if (exponent >= order) exponent = 3; + } + return Object.freeze({ + scale, + offset, + exponent, + inverseExponent: inverseInteger(exponent, order), + }); +} + +function wrapProjection( + raw: number, + transform: ChartCellTransform +): number { + return powerField( + add(multiply(transform.scale, raw), transform.offset), + transform.exponent + ); +} + +function add(left: number, right: number): number { + return normalize(normalize(left) + normalize(right)); +} + +function multiply(left: number, right: number): number { + return normalize(normalize(left) * normalize(right)); +} + +function powerField(base: number, exponent: number): number { + let result = 1; + let factor = normalize(base); + let remaining = exponent; + while (remaining > 0) { + if (remaining % 2 === 1) result = multiply(result, factor); + factor = multiply(factor, factor); + remaining = Math.floor(remaining / 2); + } + return result; +} + +function inverseInteger(value: number, modulus: number): number { + let oldR = normalizeFor(value, modulus); + let r = modulus; + let oldS = 1; + let s = 0; + while (r !== 0) { + const quotient = Math.floor(oldR / r); + [oldR, r] = [r, oldR - quotient * r]; + [oldS, s] = [s, oldS - quotient * s]; + } + if (oldR !== 1) { + throw new Error(`RUAM_CSH_NONINVERTIBLE_INTEGER: ${value}`); + } + return normalizeFor(oldS, modulus); +} + +function greatestCommonDivisor(left: number, right: number): number { + let a = Math.abs(left); + let b = Math.abs(right); + while (b !== 0) [a, b] = [b, a % b]; + return a; +} + +function normalize(value: number): number { + return normalizeFor(value, CSH_FIELD_MODULUS); +} + +function normalizeFor(value: number, modulus: number): number { + const normalized = Math.trunc(value) % modulus; + return normalized < 0 ? normalized + modulus : normalized; +} diff --git a/packages/ruam/src/isogloss/csh/reference-masked-custodian.ts b/packages/ruam/src/isogloss/csh/reference-masked-custodian.ts new file mode 100644 index 0000000..b87955a --- /dev/null +++ b/packages/ruam/src/isogloss/csh/reference-masked-custodian.ts @@ -0,0 +1,571 @@ +/** + * Server-side statefully masked CSH relation sequence. + * + * Client charts encode `x + mask(epoch)`. A transition response changes an + * identity-transported masked state into `F_epoch(x) + mask(epoch+1)` without + * returning either the logical state, the transition, or either mask. + * + * @module isogloss/csh/reference-masked-custodian + */ + +import { + createHmac, + generateKeyPairSync, + sign, +} from "node:crypto"; +import { + chartCustodySigningPayload, + type AdditiveChartContribution, + type ChartCustodyRequest, + type ChartCustodyResponse, +} from "./chart-custody-protocol.js"; +import { + custodyResponseSigningPayload, + type CustodyRequest, + type CustodyResponse, +} from "./custody-protocol.js"; +import type { MaskedCustodyClientContract } from "./masked-custody-protocol.js"; +import { + CSH_FIELD_MODULUS, + evaluateCertifiedProjection, + type ChartCellTransform, + type ChartCover, + type CertifiedProjectionSite, +} from "./reference.js"; + +export interface HiddenCubicTerm { + readonly inputProjection: readonly number[]; + readonly outputDirection: readonly number[]; + readonly coefficient: number; +} + +export interface HiddenMaskedTransition { + readonly linear: readonly (readonly number[])[]; + readonly bias: readonly number[]; + readonly cubicTerms: readonly HiddenCubicTerm[]; +} + +export interface ReferenceMaskedCustodianOptions { + readonly sessionId: string; + readonly contractId: string; + readonly covers: readonly ChartCover[]; + readonly transitions: readonly HiddenMaskedTransition[]; + readonly exitProjection: CertifiedProjectionSite; + readonly initialLineageCommitment: string; + readonly lineageSecret: string; + readonly maskSecret: string; + readonly sharingSecret: string; + readonly responseSecret: string; +} + +export class ReferenceMaskedChartCustodian { + readonly clientContract: MaskedCustodyClientContract; + + readonly #covers: readonly ChartCover[]; + readonly #transitions: readonly HiddenMaskedTransition[]; + readonly #exitProjection: CertifiedProjectionSite; + readonly #lineageSecret: string; + readonly #maskSecret: string; + readonly #sharingSecret: string; + readonly #responseSecret: string; + readonly #protocolBinding: string; + readonly #privateKey: ReturnType["privateKey"]; + readonly #consumedNonces = new Set(); + #epoch = 0; + #lineageCommitment: string; + #mask: number[]; + #expectedMaskedState: readonly number[] | undefined; + #terminated = false; + + constructor(options: ReferenceMaskedCustodianOptions) { + validateOptions(options); + const { privateKey, publicKey } = generateKeyPairSync("ed25519"); + this.#privateKey = privateKey; + this.#covers = options.covers; + this.#transitions = options.transitions; + this.#exitProjection = options.exitProjection; + this.#lineageSecret = options.lineageSecret; + this.#maskSecret = options.maskSecret; + this.#sharingSecret = options.sharingSecret; + this.#responseSecret = options.responseSecret; + this.#protocolBinding = JSON.stringify([ + options.sessionId, + options.contractId, + ]); + this.#lineageCommitment = options.initialLineageCommitment; + this.#mask = Array.from( + { length: options.covers[0]!.width }, + () => 0 + ); + this.clientContract = Object.freeze({ + sessionId: options.sessionId, + contractId: options.contractId, + coverIds: Object.freeze(options.covers.map((cover) => cover.id)), + initialLineageCommitment: options.initialLineageCommitment, + verificationKey: publicKey + .export({ type: "spki", format: "pem" }) + .toString(), + }); + } + + evaluateTransition(request: ChartCustodyRequest): ChartCustodyResponse { + this.#assertLiveRequest(request); + const transition = this.#transitions[this.#epoch]; + const from = this.#covers[this.#epoch]; + const to = this.#covers[this.#epoch + 1]; + if (!transition || !from || !to) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_NO_TRANSITION"); + } + if ( + request.fromCoverId !== from.id || + request.toCoverId !== to.id + ) { + throw new Error("RUAM_CSH_CUSTODY_REQUEST_MISMATCH"); + } + const masked = recoverVector(request.charts, from); + this.#assertExpectedMaskedState(masked); + const logical = masked.map((value, index) => + subtract(value, this.#mask[index]!) + ); + const nextLogical = evaluateHiddenTransition(transition, logical); + const nextMask = deriveVector( + this.#maskSecret, + this.#protocolBinding, + this.#epoch, + request.nonce, + from.width + ); + const nextMasked = nextLogical.map((value, index) => + add(value, nextMask[index]!) + ); + const delta = nextMasked.map((value, index) => + subtract(value, masked[index]!) + ); + const mixedDelta = multiplyMatrixVector(to.mixing, delta); + const chartContributions = createAdditiveShares( + mixedDelta, + to, + this.#sharingSecret, + this.#protocolBinding, + this.#epoch, + request.nonce + ); + const nextEpoch = this.#epoch + 1; + const nextLineageCommitment = nextLineage( + this.#lineageSecret, + this.#protocolBinding, + this.#lineageCommitment, + request.nonce, + chartContributions + ); + const unsigned = { + sessionId: request.sessionId, + contractId: request.contractId, + requestNonce: request.nonce, + epoch: this.#epoch, + nextEpoch, + nextLineageCommitment, + chartContributions, + }; + const signature = sign( + null, + Buffer.from(chartCustodySigningPayload(unsigned)), + this.#privateKey + ).toString("base64"); + this.#consumedNonces.add(request.nonce); + this.#epoch = nextEpoch; + this.#lineageCommitment = nextLineageCommitment; + this.#mask = nextMask; + this.#expectedMaskedState = Object.freeze(nextMasked); + return Object.freeze({ ...unsigned, signature }); + } + + evaluateExitProjection(request: CustodyRequest): CustodyResponse { + this.#assertLiveRequest(request); + if (this.#epoch !== this.#transitions.length) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_NOT_AT_EXIT"); + } + const terminalCover = this.#covers[this.#epoch]!; + if (request.coverId !== terminalCover.id) { + throw new Error("RUAM_CSH_CUSTODY_REQUEST_MISMATCH"); + } + const masked = recoverVector(request.charts, terminalCover); + this.#assertExpectedMaskedState(masked); + const logical = masked.map((value, index) => + subtract(value, this.#mask[index]!) + ); + const projected = add( + dot(this.#exitProjection.coefficients, logical), + this.#exitProjection.bias + ); + const projectionOpening = deriveResponseTransform( + this.#responseSecret, + this.#protocolBinding, + this.#epoch, + request.nonce + ); + const encodedProjection = wrapProjection( + projected, + projectionOpening + ); + const nextEpoch = this.#epoch + 1; + const nextLineageCommitment = nextLineage( + this.#lineageSecret, + this.#protocolBinding, + this.#lineageCommitment, + request.nonce, + encodedProjection + ); + const unsigned = { + sessionId: request.sessionId, + contractId: request.contractId, + requestNonce: request.nonce, + epoch: this.#epoch, + nextEpoch, + nextLineageCommitment, + encodedProjection, + projectionOpening, + }; + const signature = sign( + null, + Buffer.from(custodyResponseSigningPayload(unsigned)), + this.#privateKey + ).toString("base64"); + this.#consumedNonces.add(request.nonce); + this.#epoch = nextEpoch; + this.#lineageCommitment = nextLineageCommitment; + this.#terminated = true; + return Object.freeze({ ...unsigned, signature }); + } + + #assertLiveRequest( + request: ChartCustodyRequest | CustodyRequest + ): void { + if (this.#terminated) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_TERMINATED"); + } + if ( + request.sessionId !== this.clientContract.sessionId || + request.contractId !== this.clientContract.contractId + ) { + throw new Error("RUAM_CSH_CUSTODY_REQUEST_MISMATCH"); + } + if (this.#consumedNonces.has(request.nonce)) { + throw new Error("RUAM_CSH_CUSTODY_REPLAY"); + } + if ( + request.epoch !== this.#epoch || + request.lineageCommitment !== this.#lineageCommitment + ) { + throw new Error("RUAM_CSH_CUSTODY_STALE_LINEAGE"); + } + } + + #assertExpectedMaskedState(masked: readonly number[]): void { + if ( + this.#expectedMaskedState !== undefined && + (masked.length !== this.#expectedMaskedState.length || + masked.some( + (value, index) => + value !== this.#expectedMaskedState![index] + )) + ) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_STATE_SUBSTITUTION"); + } + } +} + +function validateOptions(options: ReferenceMaskedCustodianOptions): void { + if ( + options.covers.length < 2 || + options.transitions.length !== options.covers.length - 1 + ) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_PATH_MISMATCH"); + } + const width = options.covers[0]!.width; + if ( + options.covers.some((cover) => cover.width !== width) || + new Set(options.covers.map((cover) => cover.id)).size !== + options.covers.length || + options.exitProjection.coefficients.length !== width + ) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_WIDTH_MISMATCH"); + } + for (const transition of options.transitions) { + if ( + transition.linear.length !== width || + transition.linear.some((row) => row.length !== width) || + transition.bias.length !== width + ) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_WIDTH_MISMATCH"); + } + for (const term of transition.cubicTerms) { + if ( + term.inputProjection.length !== width || + term.outputDirection.length !== width + ) { + throw new Error("RUAM_CSH_MASKED_CUSTODY_WIDTH_MISMATCH"); + } + } + } +} + +function evaluateHiddenTransition( + transition: HiddenMaskedTransition, + logical: readonly number[] +): number[] { + const output = multiplyMatrixVector(transition.linear, logical).map( + (value, index) => add(value, transition.bias[index]!) + ); + for (const term of transition.cubicTerms) { + const projected = dot(term.inputProjection, logical); + const cubic = multiply( + multiply(projected, projected), + projected + ); + for (let index = 0; index < output.length; index++) { + output[index] = add( + output[index]!, + multiply( + multiply(term.coefficient, cubic), + term.outputDirection[index]! + ) + ); + } + } + return output; +} + +function recoverVector( + charts: CustodyRequest["charts"], + cover: ChartCover +): number[] { + return Array.from({ length: cover.width }, (_, coordinate) => + evaluateCertifiedProjection(charts, cover, { + id: `server_projection_${coordinate}`, + coefficients: Array.from( + { length: cover.width }, + (_, index) => (index === coordinate ? 1 : 0) + ), + bias: 0, + }) + ); +} + +function createAdditiveShares( + constant: readonly number[], + cover: ChartCover, + secret: string, + protocolBinding: string, + epoch: number, + nonce: string +): readonly AdditiveChartContribution[] { + const residuals = Array.from({ length: cover.width }, (_, lane) => + Array.from({ length: cover.threshold - 1 }, (_, coefficient) => + deriveFieldElement( + secret, + "chart-share", + protocolBinding, + epoch, + nonce, + lane, + coefficient + ) + ) + ); + return Object.freeze( + cover.charts.map((chart) => { + const cells = constant.map((value, lane) => { + let shared = normalize(value); + let pointPower = chart.point; + for (const residual of residuals[lane]!) { + shared = add(shared, multiply(residual, pointPower)); + pointPower = multiply(pointPower, chart.point); + } + return shared; + }); + return Object.freeze({ + chartId: chart.id, + cells: Object.freeze(cells), + }); + }) + ); +} + +function deriveVector( + secret: string, + protocolBinding: string, + epoch: number, + nonce: string, + width: number +): number[] { + return Array.from( + { length: width }, + (_, lane) => + deriveFieldElement( + secret, + "representation-mask", + protocolBinding, + epoch, + nonce, + lane, + 0 + ) + ); +} + +function deriveFieldElement( + secret: string, + domain: string, + protocolBinding: string, + epoch: number, + nonce: string, + lane: number, + coefficient: number +): number { + return createHmac("sha256", secret) + .update(domain) + .update("|") + .update(protocolBinding) + .update("|") + .update(String(epoch)) + .update("|") + .update(nonce) + .update("|") + .update(String(lane)) + .update("|") + .update(String(coefficient)) + .digest() + .readUInt32LE(0) % CSH_FIELD_MODULUS; +} + +function nextLineage( + secret: string, + protocolBinding: string, + current: string, + nonce: string, + value: unknown +): string { + return createHmac("sha256", secret) + .update("lineage") + .update("|") + .update(protocolBinding) + .update("|") + .update(current) + .update("|") + .update(nonce) + .update("|") + .update(JSON.stringify(value)) + .digest("hex"); +} + +function deriveResponseTransform( + secret: string, + protocolBinding: string, + epoch: number, + nonce: string +): ChartCellTransform { + const digest = createHmac("sha256", secret) + .update("projection-opening") + .update("|") + .update(protocolBinding) + .update("|") + .update(String(epoch)) + .update("|") + .update(nonce) + .digest(); + const scale = + 1 + (digest.readUInt32LE(0) % (CSH_FIELD_MODULUS - 1)); + const offset = digest.readUInt32LE(4) % CSH_FIELD_MODULUS; + const order = CSH_FIELD_MODULUS - 1; + let exponent = 3 + (digest.readUInt32LE(8) % (order - 3)); + while (gcd(exponent, order) !== 1) { + exponent++; + if (exponent >= order) exponent = 3; + } + return Object.freeze({ + scale, + offset, + exponent, + inverseExponent: inverseInteger(exponent, order), + }); +} + +function wrapProjection( + raw: number, + transform: ChartCellTransform +): number { + return power( + add(multiply(transform.scale, raw), transform.offset), + transform.exponent + ); +} + +function multiplyMatrixVector( + matrix: readonly (readonly number[])[], + input: readonly number[] +): number[] { + return matrix.map((row) => dot(row, input)); +} + +function dot(left: readonly number[], right: readonly number[]): number { + let value = 0; + for (let index = 0; index < left.length; index++) { + value = add(value, multiply(left[index]!, right[index]!)); + } + return value; +} + +function power(base: number, exponent: number): number { + let result = 1; + let factor = normalize(base); + let remaining = exponent; + while (remaining > 0) { + if (remaining % 2 === 1) result = multiply(result, factor); + factor = multiply(factor, factor); + remaining = Math.floor(remaining / 2); + } + return result; +} + +function inverseInteger(value: number, modulus: number): number { + let oldR = normalizeFor(value, modulus); + let r = modulus; + let oldS = 1; + let s = 0; + while (r !== 0) { + const quotient = Math.floor(oldR / r); + [oldR, r] = [r, oldR - quotient * r]; + [oldS, s] = [s, oldS - quotient * s]; + } + if (oldR !== 1) throw new Error("RUAM_CSH_NONINVERTIBLE_INTEGER"); + return normalizeFor(oldS, modulus); +} + +function gcd(left: number, right: number): number { + let a = Math.abs(left); + let b = Math.abs(right); + while (b !== 0) [a, b] = [b, a % b]; + return a; +} + +function add(left: number, right: number): number { + return normalize(normalize(left) + normalize(right)); +} + +function subtract(left: number, right: number): number { + return normalize(normalize(left) - normalize(right)); +} + +function multiply(left: number, right: number): number { + return normalize(normalize(left) * normalize(right)); +} + +function normalize(value: number): number { + return normalizeFor(value, CSH_FIELD_MODULUS); +} + +function normalizeFor(value: number, modulus: number): number { + const normalized = Math.trunc(value) % modulus; + return normalized < 0 ? normalized + modulus : normalized; +} diff --git a/packages/ruam/src/isogloss/csh/reference.ts b/packages/ruam/src/isogloss/csh/reference.ts new file mode 100644 index 0000000..6b4510e --- /dev/null +++ b/packages/ruam/src/isogloss/csh/reference.ts @@ -0,0 +1,745 @@ +/** + * Experimental moving-cover semantic holography reference model. + * + * This module exists to validate the local CSH mechanics before any production + * artifact or runtime schema is frozen. A chart stores a non-linearly wrapped + * polynomial evaluation of a mixed logical state. No chart contains an + * independently decodable source value, and transitions transport directly + * from incoming chart cells into the next cover without constructing a global + * logical frame. + * + * The model is intentionally limited to fixed-width field vectors and affine + * transitions. BPRF supplies the non-affine regional realizations around this + * transport layer. Do not export this module from the public package entry + * point until the dynamic-attacker gates have passed. + * + * @module isogloss/csh/reference + */ + +import { deriveSeed } from "../../naming/scope.js"; +import { createSeededRandom } from "../../random/entropy.js"; + +/** Largest 16-bit prime; products of field elements remain exact JS integers. */ +export const CSH_FIELD_MODULUS = 65_521; + +export interface ChartCellTransform { + readonly scale: number; + readonly offset: number; + readonly exponent: number; + readonly inverseExponent: number; +} + +export interface ChartDescriptor { + readonly id: string; + readonly owner: string; + readonly point: number; + readonly cells: readonly ChartCellTransform[]; + readonly overlaps: readonly string[]; +} + +export interface ChartCover { + readonly id: string; + readonly epoch: number; + readonly width: number; + readonly threshold: number; + readonly mixing: readonly (readonly number[])[]; + readonly bias: readonly number[]; + readonly charts: readonly ChartDescriptor[]; +} + +export interface EncodedChart { + readonly coverId: string; + readonly epoch: number; + readonly chartId: string; + readonly cells: readonly number[]; +} + +export interface AffineChartTransition { + readonly id: string; + readonly matrix: readonly (readonly number[])[]; + readonly bias: readonly number[]; +} + +export interface CertifiedProjectionSite { + readonly id: string; + readonly coefficients: readonly number[]; + readonly bias: number; +} + +export interface CreateChartCoverOptions { + readonly seed: number; + readonly epoch: number; + readonly width: number; + readonly chartCount?: number; + readonly threshold?: number; +} + +/** + * Create one deterministic cover. The default spike profile is five charts + * with threshold three, as required by the D3 local-holography gate. + */ +export function createChartCover( + options: CreateChartCoverOptions +): ChartCover { + const { + seed, + epoch, + width, + chartCount = 5, + threshold = 3, + } = options; + if (!Number.isSafeInteger(epoch) || epoch < 0) { + throw new Error(`RUAM_CSH_INVALID_EPOCH: ${epoch}`); + } + if (!Number.isSafeInteger(width) || width < 2) { + throw new Error(`RUAM_CSH_INVALID_WIDTH: ${width}`); + } + if (!Number.isSafeInteger(chartCount) || chartCount < 5) { + throw new Error(`RUAM_CSH_INSUFFICIENT_CHARTS: ${chartCount}`); + } + if ( + !Number.isSafeInteger(threshold) || + threshold < 3 || + threshold > chartCount + ) { + throw new Error(`RUAM_CSH_INVALID_THRESHOLD: ${threshold}`); + } + + const random = createSeededRandom( + deriveSeed(seed >>> 0, `csh-cover:${epoch}:${width}:${chartCount}:${threshold}`) + ); + const mixing = createInvertibleMatrix(width, random); + const bias = vector(width, () => randomField(random)); + const points = uniqueNonzeroPoints(chartCount, random); + const coverTag = random.nextUint32().toString(36); + const ids = points.map((_, index) => `c${epoch}_${index}_${coverTag}`); + const charts = points.map((point, index): ChartDescriptor => { + const cells = vector(width, (): ChartCellTransform => { + const exponent = randomPermutationExponent(random); + return freeze({ + scale: randomNonzeroField(random), + offset: randomField(random), + exponent, + inverseExponent: inverseInteger(exponent, CSH_FIELD_MODULUS - 1), + }); + }); + return freeze({ + id: ids[index]!, + owner: `o${(index * 2 + epoch + 1) % chartCount}`, + point, + cells: freeze(cells), + overlaps: freeze(ids.filter((_, other) => other !== index)), + }); + }); + + return freeze({ + id: `cover_${epoch}_${coverTag}`, + epoch, + width, + threshold, + mixing: freezeMatrix(mixing), + bias: freeze(bias), + charts: freeze(charts), + }); +} + +/** + * Encode an observable ingress vector into one cover. + * + * This is an ingress-only reference helper. Regional transitions must use + * {@link transportAffineCharts}, which never reconstructs the logical vector. + */ +export function encodeIngressCharts( + logicalValues: readonly number[], + cover: ChartCover, + seed: number +): readonly EncodedChart[] { + validateCover(cover); + assertVectorWidth(logicalValues, cover.width, "INGRESS"); + const constant = addVectors( + multiplyMatrixVector(cover.mixing, logicalValues), + cover.bias + ); + const residuals = createResiduals( + cover.width, + cover.threshold, + deriveSeed(seed >>> 0, `csh-ingress:${cover.id}`) + ); + const charts = cover.charts.map((descriptor): EncodedChart => { + const cells = vector(cover.width, (lane) => { + const raw = evaluatePolynomial( + constant[lane]!, + residuals[lane]!, + descriptor.point + ); + return wrapCell(raw, descriptor.cells[lane]!); + }); + return freeze({ + coverId: cover.id, + epoch: cover.epoch, + chartId: descriptor.id, + cells: freeze(cells), + }); + }); + return freeze(charts); +} + +/** + * Transport an affine logical transition into a fresh cover. + * + * For x' = A*x+c and encoded constants s=M*x+b, this composes the public + * cover relation s'=M'*A*M^-1*s+d. Each output cell accumulates weighted + * contributions from incoming chart cells directly; no s or x vector is ever + * assembled. + */ +export function transportAffineCharts( + incoming: readonly EncodedChart[], + from: ChartCover, + to: ChartCover, + transition: AffineChartTransition, + seed: number +): readonly EncodedChart[] { + validateCover(from); + validateCover(to); + if (from.id === to.id || from.epoch === to.epoch) { + throw new Error("RUAM_CSH_COVER_DID_NOT_CHANGE"); + } + if (from.width !== to.width) { + throw new Error( + `RUAM_CSH_WIDTH_MISMATCH: ${from.width} -> ${to.width}` + ); + } + assertMatrixShape(transition.matrix, from.width, "TRANSITION"); + assertVectorWidth(transition.bias, from.width, "TRANSITION_BIAS"); + const selected = validateAndSelectCharts(incoming, from); + const weights = interpolationWeightsAtZero( + selected.map(({ descriptor }) => descriptor.point) + ); + const inverseFromMixing = invertMatrix(from.mixing); + const secretTransform = multiplyMatrices( + multiplyMatrices(to.mixing, transition.matrix), + inverseFromMixing + ); + const transformedOldBias = multiplyMatrixVector( + secretTransform, + from.bias + ); + const secretBias = subtractVectors( + addVectors( + multiplyMatrixVector(to.mixing, transition.bias), + to.bias + ), + transformedOldBias + ); + const residuals = createResiduals( + to.width, + to.threshold, + deriveSeed( + seed >>> 0, + `csh-transport:${transition.id}:${from.id}:${to.id}` + ) + ); + + const transported = to.charts.map((target): EncodedChart => { + const cells = vector(to.width, (outputLane) => { + let rawOutput = add( + secretBias[outputLane]!, + evaluatePolynomial(0, residuals[outputLane]!, target.point) + ); + for (let chartIndex = 0; chartIndex < selected.length; chartIndex++) { + const { chart, descriptor } = selected[chartIndex]!; + const chartWeight = weights[chartIndex]!; + for (let inputLane = 0; inputLane < from.width; inputLane++) { + const rawInput = unwrapCell( + chart.cells[inputLane]!, + descriptor.cells[inputLane]! + ); + rawOutput = add( + rawOutput, + multiply( + multiply( + secretTransform[outputLane]![inputLane]!, + chartWeight + ), + rawInput + ) + ); + } + } + return wrapCell(rawOutput, target.cells[outputLane]!); + }); + return freeze({ + coverId: to.id, + epoch: to.epoch, + chartId: target.id, + cells: freeze(cells), + }); + }); + return freeze(transported); +} + +/** + * Materialize one declared scalar effect/return projection. + * + * The implementation fuses interpolation, inverse mixing, and projection into + * a scalar accumulator. It never returns or internally constructs a complete + * logical vector. + */ +export function evaluateCertifiedProjection( + incoming: readonly EncodedChart[], + cover: ChartCover, + site: CertifiedProjectionSite +): number { + validateCover(cover); + assertVectorWidth(site.coefficients, cover.width, "PROJECTION"); + const selected = validateAndSelectCharts(incoming, cover); + const weights = interpolationWeightsAtZero( + selected.map(({ descriptor }) => descriptor.point) + ); + const inverseMixing = invertMatrix(cover.mixing); + const secretCoefficients = multiplyRowByMatrix( + site.coefficients, + inverseMixing + ); + let projected = subtract( + normalize(site.bias), + dot(secretCoefficients, cover.bias) + ); + for (let chartIndex = 0; chartIndex < selected.length; chartIndex++) { + const { chart, descriptor } = selected[chartIndex]!; + const chartWeight = weights[chartIndex]!; + for (let lane = 0; lane < cover.width; lane++) { + const raw = unwrapCell( + chart.cells[lane]!, + descriptor.cells[lane]! + ); + projected = add( + projected, + multiply( + multiply(secretCoefficients[lane]!, chartWeight), + raw + ) + ); + } + } + return projected; +} + +/** Verify structural invariants without deriving a logical value. */ +export function validateCover(cover: ChartCover): void { + if (cover.width < 2 || cover.charts.length < 5) { + throw new Error("RUAM_CSH_INVALID_COVER_SHAPE"); + } + if (cover.threshold < 3 || cover.threshold > cover.charts.length) { + throw new Error("RUAM_CSH_INVALID_COVER_THRESHOLD"); + } + assertMatrixShape(cover.mixing, cover.width, "COVER_MIXING"); + assertVectorWidth(cover.bias, cover.width, "COVER_BIAS"); + invertMatrix(cover.mixing); + const ids = new Set(); + const points = new Set(); + for (const chart of cover.charts) { + if ( + chart.cells.length !== cover.width || + chart.point === 0 || + chart.point >= CSH_FIELD_MODULUS + ) { + throw new Error(`RUAM_CSH_INVALID_CHART: ${chart.id}`); + } + if (ids.has(chart.id) || points.has(chart.point)) { + throw new Error(`RUAM_CSH_DUPLICATE_CHART: ${chart.id}`); + } + ids.add(chart.id); + points.add(chart.point); + for (const cell of chart.cells) { + if ( + cell.scale === 0 || + multiply( + cell.exponent, + cell.inverseExponent, + CSH_FIELD_MODULUS - 1 + ) !== 1 + ) { + throw new Error(`RUAM_CSH_INVALID_CELL_TRANSFORM: ${chart.id}`); + } + } + } + for (const chart of cover.charts) { + const expected = cover.charts + .filter((candidate) => candidate.id !== chart.id) + .map((candidate) => candidate.id); + if ( + chart.overlaps.length !== expected.length || + expected.some((id) => !chart.overlaps.includes(id)) + ) { + throw new Error(`RUAM_CSH_INVALID_OVERLAP_COVER: ${chart.id}`); + } + } +} + +interface SelectedChart { + readonly chart: EncodedChart; + readonly descriptor: ChartDescriptor; +} + +function validateAndSelectCharts( + incoming: readonly EncodedChart[], + cover: ChartCover +): SelectedChart[] { + if (incoming.length < cover.threshold) { + throw new Error( + `RUAM_CSH_THRESHOLD_NOT_MET: ${incoming.length}/${cover.threshold}` + ); + } + const descriptors = new Map( + cover.charts.map((descriptor) => [descriptor.id, descriptor]) + ); + const seen = new Set(); + return incoming.map((chart): SelectedChart => { + if (chart.coverId !== cover.id || chart.epoch !== cover.epoch) { + throw new Error(`RUAM_CSH_WRONG_COVER: ${chart.chartId}`); + } + if (seen.has(chart.chartId)) { + throw new Error(`RUAM_CSH_DUPLICATE_CONTRIBUTION: ${chart.chartId}`); + } + seen.add(chart.chartId); + const descriptor = descriptors.get(chart.chartId); + if (!descriptor || chart.cells.length !== cover.width) { + throw new Error(`RUAM_CSH_UNKNOWN_CHART: ${chart.chartId}`); + } + return { chart, descriptor }; + }); +} + +function createResiduals( + width: number, + threshold: number, + seed: number +): number[][] { + const random = createSeededRandom(seed); + return vector(width, () => + vector(threshold - 1, () => randomField(random)) + ); +} + +function createInvertibleMatrix( + width: number, + random: ReturnType +): number[][] { + for (let attempt = 0; attempt < 128; attempt++) { + const candidate = vector(width, () => + vector(width, () => randomField(random)) + ); + try { + invertMatrix(candidate); + return candidate; + } catch { + // Deterministically continue the stream until a nonsingular basis. + } + } + throw new Error("RUAM_CSH_COULD_NOT_BUILD_MIXING_BASIS"); +} + +function uniqueNonzeroPoints( + count: number, + random: ReturnType +): number[] { + const points: number[] = []; + const seen = new Set(); + while (points.length < count) { + const point = randomNonzeroField(random); + if (!seen.has(point)) { + seen.add(point); + points.push(point); + } + } + return points; +} + +function randomPermutationExponent( + random: ReturnType +): number { + const order = CSH_FIELD_MODULUS - 1; + for (;;) { + const candidate = 3 + (random.nextUint32() % (order - 3)); + if (greatestCommonDivisor(candidate, order) === 1) return candidate; + } +} + +function interpolationWeightsAtZero(points: readonly number[]): number[] { + if (new Set(points).size !== points.length) { + throw new Error("RUAM_CSH_DUPLICATE_INTERPOLATION_POINT"); + } + return points.map((point, index) => { + let numerator = 1; + let denominator = 1; + for (let other = 0; other < points.length; other++) { + if (other === index) continue; + numerator = multiply(numerator, subtract(0, points[other]!)); + denominator = multiply( + denominator, + subtract(point, points[other]!) + ); + } + return multiply(numerator, inverseField(denominator)); + }); +} + +function evaluatePolynomial( + constant: number, + coefficients: readonly number[], + point: number +): number { + let value = normalize(constant); + let power = normalize(point); + for (const coefficient of coefficients) { + value = add(value, multiply(coefficient, power)); + power = multiply(power, point); + } + return value; +} + +function wrapCell(raw: number, transform: ChartCellTransform): number { + return powerField( + add(multiply(transform.scale, raw), transform.offset), + transform.exponent + ); +} + +function unwrapCell(stored: number, transform: ChartCellTransform): number { + return multiply( + subtract( + powerField(stored, transform.inverseExponent), + transform.offset + ), + inverseField(transform.scale) + ); +} + +function multiplyMatrices( + left: readonly (readonly number[])[], + right: readonly (readonly number[])[] +): number[][] { + const width = left.length; + assertMatrixShape(left, width, "LEFT_MATRIX"); + assertMatrixShape(right, width, "RIGHT_MATRIX"); + return vector(width, (row) => + vector(width, (column) => { + let value = 0; + for (let inner = 0; inner < width; inner++) { + value = add( + value, + multiply(left[row]![inner]!, right[inner]![column]!) + ); + } + return value; + }) + ); +} + +function multiplyMatrixVector( + matrix: readonly (readonly number[])[], + input: readonly number[] +): number[] { + assertMatrixShape(matrix, input.length, "MATRIX_VECTOR"); + return matrix.map((row) => dot(row, input)); +} + +function multiplyRowByMatrix( + row: readonly number[], + matrix: readonly (readonly number[])[] +): number[] { + assertMatrixShape(matrix, row.length, "ROW_MATRIX"); + return vector(row.length, (column) => { + let value = 0; + for (let inner = 0; inner < row.length; inner++) { + value = add( + value, + multiply(row[inner]!, matrix[inner]![column]!) + ); + } + return value; + }); +} + +function invertMatrix( + input: readonly (readonly number[])[] +): number[][] { + const width = input.length; + assertMatrixShape(input, width, "INVERSE"); + const augmented = input.map((row, rowIndex) => [ + ...row.map(normalize), + ...vector(width, (column) => (rowIndex === column ? 1 : 0)), + ]); + for (let column = 0; column < width; column++) { + let pivot = column; + while (pivot < width && augmented[pivot]![column] === 0) pivot++; + if (pivot === width) throw new Error("RUAM_CSH_SINGULAR_MIXING_BASIS"); + [augmented[column], augmented[pivot]] = [ + augmented[pivot]!, + augmented[column]!, + ]; + const pivotInverse = inverseField(augmented[column]![column]!); + for (let index = 0; index < width * 2; index++) { + augmented[column]![index] = multiply( + augmented[column]![index]!, + pivotInverse + ); + } + for (let row = 0; row < width; row++) { + if (row === column) continue; + const factor = augmented[row]![column]!; + for (let index = 0; index < width * 2; index++) { + augmented[row]![index] = subtract( + augmented[row]![index]!, + multiply(factor, augmented[column]![index]!) + ); + } + } + } + return augmented.map((row) => row.slice(width)); +} + +function addVectors( + left: readonly number[], + right: readonly number[] +): number[] { + assertVectorWidth(right, left.length, "VECTOR_ADD"); + return left.map((value, index) => add(value, right[index]!)); +} + +function subtractVectors( + left: readonly number[], + right: readonly number[] +): number[] { + assertVectorWidth(right, left.length, "VECTOR_SUBTRACT"); + return left.map((value, index) => subtract(value, right[index]!)); +} + +function dot(left: readonly number[], right: readonly number[]): number { + assertVectorWidth(right, left.length, "DOT"); + let value = 0; + for (let index = 0; index < left.length; index++) { + value = add(value, multiply(left[index]!, right[index]!)); + } + return value; +} + +function assertMatrixShape( + matrix: readonly (readonly number[])[], + width: number, + label: string +): void { + if ( + matrix.length !== width || + matrix.some((row) => row.length !== width) + ) { + throw new Error(`RUAM_CSH_INVALID_${label}_SHAPE`); + } +} + +function assertVectorWidth( + values: readonly number[], + width: number, + label: string +): void { + if (values.length !== width) { + throw new Error( + `RUAM_CSH_INVALID_${label}_WIDTH: ${values.length}/${width}` + ); + } +} + +function randomField( + random: ReturnType +): number { + return random.nextUint32() % CSH_FIELD_MODULUS; +} + +function randomNonzeroField( + random: ReturnType +): number { + return 1 + (random.nextUint32() % (CSH_FIELD_MODULUS - 1)); +} + +function add(left: number, right: number): number { + return normalize(normalize(left) + normalize(right)); +} + +function subtract(left: number, right: number): number { + return normalize(normalize(left) - normalize(right)); +} + +function multiply( + left: number, + right: number, + modulus = CSH_FIELD_MODULUS +): number { + const value = (normalizeFor(left, modulus) * normalizeFor(right, modulus)) % + modulus; + return value < 0 ? value + modulus : value; +} + +function inverseField(value: number): number { + const normalized = normalize(value); + if (normalized === 0) throw new Error("RUAM_CSH_ZERO_HAS_NO_INVERSE"); + return powerField(normalized, CSH_FIELD_MODULUS - 2); +} + +function powerField(base: number, exponent: number): number { + let result = 1; + let factor = normalize(base); + let remaining = exponent; + while (remaining > 0) { + if (remaining % 2 === 1) result = multiply(result, factor); + factor = multiply(factor, factor); + remaining = Math.floor(remaining / 2); + } + return result; +} + +function inverseInteger(value: number, modulus: number): number { + let oldR = normalizeFor(value, modulus); + let r = modulus; + let oldS = 1; + let s = 0; + while (r !== 0) { + const quotient = Math.floor(oldR / r); + [oldR, r] = [r, oldR - quotient * r]; + [oldS, s] = [s, oldS - quotient * s]; + } + if (oldR !== 1) { + throw new Error(`RUAM_CSH_NONINVERTIBLE_INTEGER: ${value}`); + } + return normalizeFor(oldS, modulus); +} + +function greatestCommonDivisor(left: number, right: number): number { + let a = Math.abs(left); + let b = Math.abs(right); + while (b !== 0) [a, b] = [b, a % b]; + return a; +} + +function normalize(value: number): number { + return normalizeFor(value, CSH_FIELD_MODULUS); +} + +function normalizeFor(value: number, modulus: number): number { + const normalized = Math.trunc(value) % modulus; + return normalized < 0 ? normalized + modulus : normalized; +} + +function vector(length: number, create: (index: number) => T): T[] { + return Array.from({ length }, (_, index) => create(index)); +} + +function freeze(value: T): Readonly { + return Object.freeze(value); +} + +function freezeMatrix( + matrix: readonly (readonly number[])[] +): readonly (readonly number[])[] { + return freeze(matrix.map((row) => freeze([...row]))); +} diff --git a/packages/ruam/src/isogloss/csh/testing-bprf-reference.ts b/packages/ruam/src/isogloss/csh/testing-bprf-reference.ts new file mode 100644 index 0000000..8aa80fe --- /dev/null +++ b/packages/ruam/src/isogloss/csh/testing-bprf-reference.ts @@ -0,0 +1,503 @@ +/** + * NON-PRODUCT BPRF/CSH INTEGRATION EVALUATOR. + * + * This differential-test evaluator executes BPRF polynomial fragments over + * chart-local shares. A multiplication raises the sharing polynomial from + * degree two to degree four; five chart contributors then reduce it directly + * into a fresh degree-two sharing under a new cover. No complete BPRF frame is + * reconstructed at a transition. + * + * Product execution must emit specialized chart-local codelets. This generic + * evaluator must never ship in a client artifact. + * + * @module isogloss/csh/testing-bprf-reference + */ + +import { deriveSeed } from "../../naming/scope.js"; +import { createSeededRandom } from "../../random/entropy.js"; +import type { + BprfArtifact, + BprfCallerContext, + BprfPiece, + BprfRealization, + PureScalar, +} from "../bprf/index.js"; +import { validateBprfArtifact } from "../bprf/index.js"; +import { selectBprfRealization } from "../bprf/testing-reference.js"; +import { + CSH_FIELD_MODULUS, + encodeIngressCharts, + evaluateCertifiedProjection, + validateCover, + type ChartCellTransform, + type ChartCover, + type EncodedChart, +} from "./reference.js"; + +export interface BprfCshTraceEvent { + readonly realization: string; + readonly transition: string; + readonly fragment: string; + readonly chart: string; + readonly fromCover: string; + readonly toCover: string; + readonly phase: number; +} + +export interface BprfCshReferenceResult { + readonly outputs: readonly PureScalar[]; + readonly charts: readonly EncodedChart[]; + readonly realization: string; + readonly trace: readonly BprfCshTraceEvent[]; +} + +/** + * Evaluate one bounded integer/boolean BPRF realization over moving CSH + * covers. There must be one ingress cover plus one new cover per transition. + */ +export function evaluateBprfOverCshReference( + artifact: BprfArtifact, + inputs: readonly PureScalar[], + context: BprfCallerContext, + covers: readonly ChartCover[], + seed: number +): BprfCshReferenceResult { + validateBprfArtifact(artifact); + const realization = selectBprfRealization(artifact, context); + validateIntegrationInputs(realization, inputs, covers); + const ingressFrame = createIngressFrame(realization, inputs); + let charts = encodeIngressCharts( + ingressFrame, + covers[0]!, + deriveSeed(seed >>> 0, `bprf-csh-ingress:${realization.id}`) + ); + const trace: BprfCshTraceEvent[] = []; + for ( + let transitionIndex = 0; + transitionIndex < realization.transitions.length; + transitionIndex++ + ) { + const transition = realization.transitions[transitionIndex]!; + const from = covers[transitionIndex]!; + const to = covers[transitionIndex + 1]!; + const transported = transportBprfTransition( + realization, + transition.phase, + charts, + from, + to, + deriveSeed( + seed >>> 0, + `bprf-csh-transition:${realization.id}:${transition.id}` + ) + ); + charts = transported.charts; + for (const event of transported.trace) trace.push(event); + } + + const finalCover = covers[covers.length - 1]!; + const outputs = realization.outputPorts.map((port, outputIndex) => { + const inverseScale = inverse(port.basis.scale); + const coefficients = Array.from( + { length: realization.frameSize }, + (_, slot) => (slot === port.slot ? inverseScale : 0) + ); + const coordinate = evaluateCertifiedProjection(charts, finalCover, { + id: `exit_${outputIndex}`, + coefficients, + bias: subtract(0, multiply(port.basis.bias, inverseScale)), + }); + if (port.typeCode === 0) return fromSignedField(coordinate); + if (realization.familyCode === 0) return coordinate !== 0; + return fromSignedField(coordinate) > 0; + }); + return Object.freeze({ + outputs: Object.freeze(outputs), + charts, + realization: realization.id, + trace: Object.freeze(trace), + }); +} + +function validateIntegrationInputs( + realization: BprfRealization, + inputs: readonly PureScalar[], + covers: readonly ChartCover[] +): void { + if (inputs.length !== realization.inputPorts.length) { + throw new Error("RUAM_BPRF_CSH_INPUT_ARITY_MISMATCH"); + } + if (covers.length !== realization.transitions.length + 1) { + throw new Error("RUAM_BPRF_CSH_COVER_COUNT_MISMATCH"); + } + const ids = new Set(); + for (const cover of covers) { + validateCover(cover); + if (cover.width !== realization.frameSize) { + throw new Error("RUAM_BPRF_CSH_COVER_WIDTH_MISMATCH"); + } + if (cover.charts.length < cover.threshold * 2 - 1) { + throw new Error("RUAM_BPRF_CSH_MULTIPLICATION_QUORUM_TOO_SMALL"); + } + if (ids.has(cover.id)) { + throw new Error("RUAM_BPRF_CSH_COVER_DID_NOT_CHANGE"); + } + ids.add(cover.id); + } +} + +function createIngressFrame( + realization: BprfRealization, + inputs: readonly PureScalar[] +): number[] { + const frame = Array.from({ length: realization.frameSize }, () => 0); + for (let index = 0; index < inputs.length; index++) { + const input = inputs[index]!; + const port = realization.inputPorts[index]!; + let coordinate: number; + if (port.typeCode === 0) { + if ( + typeof input !== "number" || + !Number.isSafeInteger(input) || + Math.abs(input) >= CSH_FIELD_MODULUS / 4 + ) { + throw new Error("RUAM_BPRF_CSH_BOUNDED_INTEGER_REQUIRED"); + } + coordinate = normalize(input); + } else { + if (typeof input !== "boolean") { + throw new Error("RUAM_BPRF_CSH_BOOLEAN_REQUIRED"); + } + coordinate = + realization.familyCode === 0 + ? input + ? 1 + : 0 + : input + ? 1 + : normalize(-1); + } + frame[port.slot] = add( + multiply(coordinate, port.basis.scale), + port.basis.bias + ); + } + return frame; +} + +function transportBprfTransition( + realization: BprfRealization, + phase: number, + incoming: readonly EncodedChart[], + from: ChartCover, + to: ChartCover, + seed: number +): { + charts: readonly EncodedChart[]; + trace: readonly BprfCshTraceEvent[]; +} { + if ( + incoming.length !== from.charts.length || + incoming.length < from.threshold * 2 - 1 + ) { + throw new Error( + `RUAM_BPRF_CSH_MULTIPLICATION_QUORUM: ${incoming.length}/${from.charts.length}` + ); + } + if (from.id === to.id) { + throw new Error("RUAM_BPRF_CSH_COVER_DID_NOT_CHANGE"); + } + const descriptorById = new Map( + from.charts.map((descriptor) => [descriptor.id, descriptor]) + ); + const seen = new Set(); + const selected = incoming.map((chart) => { + if (chart.coverId !== from.id || chart.epoch !== from.epoch) { + throw new Error(`RUAM_BPRF_CSH_WRONG_COVER: ${chart.chartId}`); + } + if (seen.has(chart.chartId)) { + throw new Error( + `RUAM_BPRF_CSH_DUPLICATE_CHART: ${chart.chartId}` + ); + } + seen.add(chart.chartId); + const descriptor = descriptorById.get(chart.chartId); + if (!descriptor) { + throw new Error(`RUAM_BPRF_CSH_UNKNOWN_CHART: ${chart.chartId}`); + } + return { chart, descriptor }; + }); + const inverseMixing = invertMatrix(from.mixing); + const localFrameShares = selected.map(({ chart, descriptor }) => { + const mixedShares = chart.cells.map((cell, lane) => + unwrapCell(cell, descriptor.cells[lane]!) + ); + return multiplyMatrixVector( + inverseMixing, + mixedShares.map((value, lane) => subtract(value, from.bias[lane]!)) + ); + }); + const transition = realization.transitions[phase]; + if (!transition || transition.phase !== phase) { + throw new Error("RUAM_BPRF_CSH_UNKNOWN_TRANSITION_PHASE"); + } + const participating = realization.fragments.filter((fragment) => + fragment.pieces.some((piece) => piece.phase === phase) + ); + const trace: BprfCshTraceEvent[] = []; + const nextFrameShares = localFrameShares.map( + (frameShare, chartIndex): number[] => { + const next = frameShare.slice(); + for (const destination of transition.writes) { + const pieces = participating.flatMap((fragment) => + fragment.pieces.filter( + (piece) => + piece.phase === phase && + piece.destination === destination + ) + ); + if (pieces.length === 0) { + throw new Error("RUAM_BPRF_CSH_MISSING_DESTINATION"); + } + let coordinateShare = 0; + for (const piece of pieces) { + coordinateShare = add( + coordinateShare, + evaluatePieceShare(piece, frameShare) + ); + } + const basis = pieces[0]!.destinationBasis; + next[destination] = add( + multiply(coordinateShare, basis.scale), + basis.bias + ); + } + for (const fragment of participating) { + trace.push( + Object.freeze({ + realization: realization.id, + transition: transition.id, + fragment: fragment.id, + chart: selected[chartIndex]!.descriptor.id, + fromCover: from.id, + toCover: to.id, + phase, + }) + ); + } + return next; + } + ); + + const sourceWeights = interpolationWeightsAtZero( + selected.map(({ descriptor }) => descriptor.point) + ); + const random = createSeededRandom(seed); + const residuals = selected.map(() => + Array.from({ length: to.width }, () => + Array.from({ length: to.threshold - 1 }, () => + random.nextUint32() % CSH_FIELD_MODULUS + ) + ) + ); + const charts = to.charts.map((target): EncodedChart => { + const cells = Array.from({ length: to.width }, (_, mixedLane) => { + let reducedShare = 0; + for ( + let sourceIndex = 0; + sourceIndex < selected.length; + sourceIndex++ + ) { + const sourceFrame = nextFrameShares[sourceIndex]!; + let mixedEvaluation = to.bias[mixedLane]!; + for (let frameLane = 0; frameLane < to.width; frameLane++) { + mixedEvaluation = add( + mixedEvaluation, + multiply( + to.mixing[mixedLane]![frameLane]!, + sourceFrame[frameLane]! + ) + ); + } + let contribution = multiply( + sourceWeights[sourceIndex]!, + mixedEvaluation + ); + let pointPower = target.point; + for (const residual of residuals[sourceIndex]![mixedLane]!) { + contribution = add( + contribution, + multiply(residual, pointPower) + ); + pointPower = multiply(pointPower, target.point); + } + reducedShare = add(reducedShare, contribution); + } + return wrapCell(reducedShare, target.cells[mixedLane]!); + }); + return Object.freeze({ + coverId: to.id, + epoch: to.epoch, + chartId: target.id, + cells: Object.freeze(cells), + }); + }); + return { + charts: Object.freeze(charts), + trace: Object.freeze(trace), + }; +} + +function evaluatePieceShare( + piece: BprfPiece, + frameShare: readonly number[] +): number { + let value = coefficientToField(piece.coefficient); + for (const factor of piece.factors) { + const basisInverse = inverse(factor.basis.scale); + const coordinateShare = multiply( + subtract(frameShare[factor.slot]!, factor.basis.bias), + basisInverse + ); + value = multiply(value, add(coordinateShare, factor.offset)); + } + return value; +} + +function coefficientToField(value: number): number { + if (Number.isSafeInteger(value)) return normalize(value); + const doubled = value * 2; + if (Number.isSafeInteger(doubled)) { + return multiply(normalize(doubled), inverse(2)); + } + throw new Error(`RUAM_BPRF_CSH_UNSUPPORTED_COEFFICIENT: ${value}`); +} + +function interpolationWeightsAtZero(points: readonly number[]): number[] { + if (new Set(points).size !== points.length) { + throw new Error("RUAM_BPRF_CSH_DUPLICATE_POINT"); + } + return points.map((point, index) => { + let numerator = 1; + let denominator = 1; + for (let other = 0; other < points.length; other++) { + if (other === index) continue; + numerator = multiply(numerator, subtract(0, points[other]!)); + denominator = multiply( + denominator, + subtract(point, points[other]!) + ); + } + return multiply(numerator, inverse(denominator)); + }); +} + +function multiplyMatrixVector( + matrix: readonly (readonly number[])[], + input: readonly number[] +): number[] { + return matrix.map((row) => { + let value = 0; + for (let column = 0; column < row.length; column++) { + value = add(value, multiply(row[column]!, input[column]!)); + } + return value; + }); +} + +function invertMatrix( + input: readonly (readonly number[])[] +): number[][] { + const width = input.length; + const augmented = input.map((row, rowIndex) => [ + ...row.map(normalize), + ...Array.from({ length: width }, (_, column) => + rowIndex === column ? 1 : 0 + ), + ]); + for (let column = 0; column < width; column++) { + let pivot = column; + while (pivot < width && augmented[pivot]![column] === 0) pivot++; + if (pivot === width) throw new Error("RUAM_BPRF_CSH_SINGULAR_COVER"); + [augmented[column], augmented[pivot]] = [ + augmented[pivot]!, + augmented[column]!, + ]; + const pivotInverse = inverse(augmented[column]![column]!); + for (let index = 0; index < width * 2; index++) { + augmented[column]![index] = multiply( + augmented[column]![index]!, + pivotInverse + ); + } + for (let row = 0; row < width; row++) { + if (row === column) continue; + const factor = augmented[row]![column]!; + for (let index = 0; index < width * 2; index++) { + augmented[row]![index] = subtract( + augmented[row]![index]!, + multiply(factor, augmented[column]![index]!) + ); + } + } + } + return augmented.map((row) => row.slice(width)); +} + +function wrapCell(raw: number, transform: ChartCellTransform): number { + return power( + add(multiply(transform.scale, raw), transform.offset), + transform.exponent + ); +} + +function unwrapCell( + stored: number, + transform: ChartCellTransform +): number { + return multiply( + subtract(power(stored, transform.inverseExponent), transform.offset), + inverse(transform.scale) + ); +} + +function inverse(value: number): number { + const normalized = normalize(value); + if (normalized === 0) throw new Error("RUAM_BPRF_CSH_ZERO_INVERSE"); + return power(normalized, CSH_FIELD_MODULUS - 2); +} + +function power(base: number, exponent: number): number { + let result = 1; + let factor = normalize(base); + let remaining = exponent; + while (remaining > 0) { + if (remaining % 2 === 1) result = multiply(result, factor); + factor = multiply(factor, factor); + remaining = Math.floor(remaining / 2); + } + return result; +} + +function add(left: number, right: number): number { + return normalize(normalize(left) + normalize(right)); +} + +function subtract(left: number, right: number): number { + return normalize(normalize(left) - normalize(right)); +} + +function multiply(left: number, right: number): number { + return normalize(normalize(left) * normalize(right)); +} + +function normalize(value: number): number { + const normalized = Math.trunc(value) % CSH_FIELD_MODULUS; + return normalized < 0 ? normalized + CSH_FIELD_MODULUS : normalized; +} + +function fromSignedField(value: number): number { + return value > CSH_FIELD_MODULUS / 2 + ? value - CSH_FIELD_MODULUS + : value; +} diff --git a/packages/ruam/src/isogloss/csh/testing-emitter.ts b/packages/ruam/src/isogloss/csh/testing-emitter.ts new file mode 100644 index 0000000..3c4f67e --- /dev/null +++ b/packages/ruam/src/isogloss/csh/testing-emitter.ts @@ -0,0 +1,748 @@ +/** + * NON-PRODUCT SPECIALIZED BPRF/CSH SOURCE-EMISSION SPIKE. + * + * This owner-side utility specializes one caller-selected BPRF realization and + * a complete moving-cover plan into direct JavaScript. Generated code stores + * each chart as a separately named object of scalar fields. It contains no + * artifact walker, chart collection, frame array, global decoder, or generic + * transition evaluator. + * + * @module isogloss/csh/testing-emitter + */ + +import { deriveSeed } from "../../naming/scope.js"; +import { createSeededRandom } from "../../random/entropy.js"; +import type { + BprfArtifact, + BprfCallerContext, + BprfPiece, + BprfRealization, + PureValueType, +} from "../bprf/index.js"; +import { validateBprfArtifact } from "../bprf/index.js"; +import { selectBprfRealization } from "../bprf/testing-reference.js"; +import { + CSH_FIELD_MODULUS, + validateCover, + type ChartCellTransform, + type ChartCover, +} from "./reference.js"; + +export interface BprfCshTestingEmission { + readonly source: string; + readonly entryName: string; + readonly byteLength: number; + readonly realizationId: string; + readonly familyCode: 0 | 1; + readonly coverIds: readonly string[]; + readonly chartLocalCodeletCount: number; + readonly fragmentCodeletCount: number; + readonly contributorCodeletCount: number; + readonly reductionCount: number; + readonly projectionCodeletCount: number; +} + +interface PowerRegistry { + name(exponent: number): string; + source(): string[]; +} + +/** + * Emit one caller-specialized finite-field integer/boolean BPRF-over-CSH + * implementation. + * + * Narrow ABI: `entry(inputs) -> outputs`. Caller context, covers, residual + * entropy, bases, and all routing are owner-side inputs baked into the source. + */ +export function emitBprfOverCshTestingSource( + artifact: BprfArtifact, + context: BprfCallerContext, + covers: readonly ChartCover[], + seed: number +): BprfCshTestingEmission { + validateBprfArtifact(artifact); + const realization = selectBprfRealization(artifact, context); + validatePlan(realization, covers); + + const powers = createPowerRegistry(); + const body: string[] = ['"use strict";', emitNormalizer()]; + let chartLocalCodeletCount = 0; + let fragmentCodeletCount = 0; + let contributorCodeletCount = 0; + let reductionCount = 0; + let projectionCodeletCount = 0; + + const ingressResiduals = residualMatrix( + realization.frameSize, + covers[0]!.threshold - 1, + deriveSeed( + deriveSeed( + seed >>> 0, + `bprf-csh-ingress:${realization.id}` + ), + `csh-ingress:${covers[0]!.id}` + ) + ); + for (let chartIndex = 0; chartIndex < 5; chartIndex++) { + body.push( + emitIngressChart( + realization, + covers[0]!, + chartIndex, + ingressResiduals, + powers + ) + ); + chartLocalCodeletCount++; + } + + const entryLines: string[] = [ + "function __ruamBprfCshSpike(a){", + emitInputAbi(realization), + ...Array.from( + { length: 5 }, + (_, chartIndex) => `let c${chartIndex}=i${chartIndex}(a);` + ), + ]; + + for ( + let transitionIndex = 0; + transitionIndex < realization.transitions.length; + transitionIndex++ + ) { + const transition = realization.transitions[transitionIndex]!; + const from = covers[transitionIndex]!; + const to = covers[transitionIndex + 1]!; + const inverseFromMixing = invertMatrix(from.mixing); + const sourceWeights = interpolationWeightsAtZero( + from.charts.map((chart) => chart.point) + ); + const transitionSeed = deriveSeed( + seed >>> 0, + `bprf-csh-transition:${realization.id}:${transition.id}` + ); + const transitionResiduals = residualCube( + 5, + to.width, + to.threshold - 1, + transitionSeed + ); + + for (let sourceIndex = 0; sourceIndex < 5; sourceIndex++) { + body.push( + emitLocalChartOpening( + transitionIndex, + sourceIndex, + from, + inverseFromMixing, + powers + ) + ); + chartLocalCodeletCount++; + for ( + let fragmentIndex = 0; + fragmentIndex < realization.fragments.length; + fragmentIndex++ + ) { + body.push( + emitChartFragment( + realization, + transitionIndex, + sourceIndex, + fragmentIndex, + transition.phase + ) + ); + fragmentCodeletCount++; + } + body.push( + emitLocalChartMerge( + realization, + transitionIndex, + sourceIndex, + transition.phase + ) + ); + chartLocalCodeletCount++; + } + + for (let targetIndex = 0; targetIndex < 5; targetIndex++) { + for (let sourceIndex = 0; sourceIndex < 5; sourceIndex++) { + body.push( + emitDegreeContribution( + transitionIndex, + targetIndex, + sourceIndex, + to, + sourceWeights[sourceIndex]!, + transitionResiduals[sourceIndex]! + ) + ); + contributorCodeletCount++; + } + body.push( + emitTargetWrap( + transitionIndex, + targetIndex, + to, + powers + ) + ); + reductionCount++; + } + + for (let sourceIndex = 0; sourceIndex < 5; sourceIndex++) { + entryLines.push( + `const U${transitionIndex}_${sourceIndex}=u${transitionIndex}_${sourceIndex}(c${sourceIndex});` + ); + for ( + let fragmentIndex = 0; + fragmentIndex < realization.fragments.length; + fragmentIndex++ + ) { + entryLines.push( + `const G${transitionIndex}_${sourceIndex}_${fragmentIndex}=g${transitionIndex}_${sourceIndex}_${fragmentIndex}(U${transitionIndex}_${sourceIndex});` + ); + } + entryLines.push( + `const L${transitionIndex}_${sourceIndex}=l${transitionIndex}_${sourceIndex}(U${transitionIndex}_${sourceIndex},${Array.from( + { length: realization.fragments.length }, + (_, fragmentIndex) => + `G${transitionIndex}_${sourceIndex}_${fragmentIndex}` + ).join(",")});` + ); + } + for (let targetIndex = 0; targetIndex < 5; targetIndex++) { + for (let sourceIndex = 0; sourceIndex < 5; sourceIndex++) { + entryLines.push( + `const D${transitionIndex}_${targetIndex}_${sourceIndex}=d${transitionIndex}_${targetIndex}_${sourceIndex}(L${transitionIndex}_${sourceIndex});` + ); + } + entryLines.push( + `const z${transitionIndex}_${targetIndex}=w${transitionIndex}_${targetIndex}(${Array.from( + { length: 5 }, + (_, sourceIndex) => + `D${transitionIndex}_${targetIndex}_${sourceIndex}` + ).join(",")});` + ); + } + for (let targetIndex = 0; targetIndex < 5; targetIndex++) { + entryLines.push(`c${targetIndex}=z${transitionIndex}_${targetIndex};`); + } + } + + const finalCover = covers[covers.length - 1]!; + const inverseFinalMixing = invertMatrix(finalCover.mixing); + for (let outputIndex = 0; outputIndex < realization.outputPorts.length; outputIndex++) { + const port = realization.outputPorts[outputIndex]!; + const inverseScale = inverse(port.basis.scale); + const secretCoefficients = inverseFinalMixing[port.slot]!.map( + (value) => multiply(inverseScale, value) + ); + const projectionBase = subtract( + subtract(0, multiply(port.basis.bias, inverseScale)), + dot(secretCoefficients, finalCover.bias) + ); + const weights = interpolationWeightsAtZero( + finalCover.charts.map((chart) => chart.point) + ); + for (let chartIndex = 0; chartIndex < 5; chartIndex++) { + body.push( + emitProjectionContribution( + outputIndex, + chartIndex, + finalCover, + secretCoefficients, + weights[chartIndex]!, + powers + ) + ); + projectionCodeletCount++; + } + entryLines.push( + `const y${outputIndex}=n(${fieldSource(projectionBase)}+${Array.from( + { length: 5 }, + (_, chartIndex) => `j${outputIndex}_${chartIndex}(c${chartIndex})` + ).join("+")});` + ); + } + entryLines.push( + `return [${realization.outputPorts + .map((port, outputIndex) => + outputExpression( + `y${outputIndex}`, + port.typeCode === 0 ? "number" : "boolean", + realization.familyCode + ) + ) + .join(",")}];`, + "}" + ); + + body.push(...powers.source(), ...entryLines); + const source = body.join("\n"); + return Object.freeze({ + source, + entryName: "__ruamBprfCshSpike", + byteLength: Buffer.byteLength(source, "utf8"), + realizationId: realization.id, + familyCode: realization.familyCode, + coverIds: Object.freeze(covers.map((cover) => cover.id)), + chartLocalCodeletCount, + fragmentCodeletCount, + contributorCodeletCount, + reductionCount, + projectionCodeletCount, + }); +} + +function emitIngressChart( + realization: BprfRealization, + cover: ChartCover, + chartIndex: number, + residuals: readonly (readonly number[])[], + powers: PowerRegistry +): string { + const descriptor = cover.charts[chartIndex]!; + const cells = Array.from({ length: cover.width }, (_, mixedLane) => { + const terms = [fieldSource(cover.bias[mixedLane]!)]; + for (let inputIndex = 0; inputIndex < realization.inputPorts.length; inputIndex++) { + const port = realization.inputPorts[inputIndex]!; + const coordinate = + port.typeCode === 0 + ? `n(a[${inputIndex}])` + : realization.familyCode === 0 + ? `(a[${inputIndex}]?1:0)` + : `(a[${inputIndex}]?1:${CSH_FIELD_MODULUS - 1})`; + const encoded = `n((${coordinate})*${fieldSource(port.basis.scale)}+${fieldSource(port.basis.bias)})`; + terms.push( + `${fieldSource(cover.mixing[mixedLane]![port.slot]!)}*(${encoded})` + ); + } + let pointPower = descriptor.point; + for (const residual of residuals[mixedLane]!) { + terms.push(`${fieldSource(residual)}*${fieldSource(pointPower)}`); + pointPower = multiply(pointPower, descriptor.point); + } + const raw = `n(${terms.join("+")})`; + return `s${mixedLane}:${wrapExpression( + raw, + descriptor.cells[mixedLane]!, + powers + )}`; + }); + return `function i${chartIndex}(a){return{${cells.join(",")}};}`; +} + +function emitLocalChartOpening( + transitionIndex: number, + sourceIndex: number, + cover: ChartCover, + inverseMixing: readonly (readonly number[])[], + powers: PowerRegistry +): string { + const descriptor = cover.charts[sourceIndex]!; + const rawNames = Array.from( + { length: cover.width }, + (_, lane) => `r${lane}` + ); + const lines = rawNames.map( + (name, lane) => + `const ${name}=${unwrapExpression( + `c.s${lane}`, + descriptor.cells[lane]!, + powers + )};` + ); + const fields = Array.from({ length: cover.width }, (_, frameLane) => { + const terms = Array.from( + { length: cover.width }, + (_, mixedLane) => + `${fieldSource(inverseMixing[frameLane]![mixedLane]!)}*n(${rawNames[mixedLane]}-${fieldSource(cover.bias[mixedLane]!)})` + ); + return `s${frameLane}:n(${terms.join("+")})`; + }); + return `function u${transitionIndex}_${sourceIndex}(c){${lines.join("")}return{${fields.join(",")}};}`; +} + +function emitChartFragment( + realization: BprfRealization, + transitionIndex: number, + sourceIndex: number, + fragmentIndex: number, + phase: number +): string { + const transition = realization.transitions[phase]!; + const fragment = realization.fragments[fragmentIndex]!; + const fields = transition.writes.map((destination) => { + const pieces = fragment.pieces.filter( + (piece) => + piece.phase === phase && piece.destination === destination + ); + if (pieces.length === 0) { + throw new Error("RUAM_BPRF_CSH_EMITTER_MISSING_FRAGMENT_SHARE"); + } + return `s${destination}:n(${pieces + .map(fieldPieceExpression) + .join("+")})`; + }); + return `function g${transitionIndex}_${sourceIndex}_${fragmentIndex}(q){return{${fields.join(",")}};}`; +} + +function emitLocalChartMerge( + realization: BprfRealization, + transitionIndex: number, + sourceIndex: number, + phase: number +): string { + const transition = realization.transitions[phase]!; + const writes = new Set(transition.writes); + const parameters = [ + "q", + ...Array.from( + { length: realization.fragments.length }, + (_, fragmentIndex) => `p${fragmentIndex}` + ), + ]; + const fields = Array.from({ length: realization.frameSize }, (_, slot) => { + if (!writes.has(slot)) return `s${slot}:q.s${slot}`; + const pieces = realization.fragments.flatMap((fragment) => + fragment.pieces.filter( + (piece) => piece.phase === phase && piece.destination === slot + ) + ); + const basis = pieces[0]!.destinationBasis; + const sum = Array.from( + { length: realization.fragments.length }, + (_, fragmentIndex) => `p${fragmentIndex}.s${slot}` + ).join("+"); + return `s${slot}:n(n(${sum})*${fieldSource(basis.scale)}+${fieldSource(basis.bias)})`; + }); + return `function l${transitionIndex}_${sourceIndex}(${parameters.join(",")}){return{${fields.join(",")}};}`; +} + +function emitDegreeContribution( + transitionIndex: number, + targetIndex: number, + sourceIndex: number, + to: ChartCover, + sourceWeight: number, + residuals: readonly (readonly number[])[], +): string { + const target = to.charts[targetIndex]!; + const fields = Array.from({ length: to.width }, (_, mixedLane) => { + const mixedTerms = [ + fieldSource(to.bias[mixedLane]!), + ...Array.from( + { length: to.width }, + (_, frameLane) => + `${fieldSource(to.mixing[mixedLane]![frameLane]!)}*q.s${frameLane}` + ), + ]; + const terms = [ + `${fieldSource(sourceWeight)}*n(${mixedTerms.join("+")})`, + ]; + let pointPower = target.point; + for (const residual of residuals[mixedLane]!) { + terms.push(`${fieldSource(residual)}*${fieldSource(pointPower)}`); + pointPower = multiply(pointPower, target.point); + } + return `s${mixedLane}:n(${terms.join("+")})`; + }); + return `function d${transitionIndex}_${targetIndex}_${sourceIndex}(q){return{${fields.join(",")}};}`; +} + +function emitTargetWrap( + transitionIndex: number, + targetIndex: number, + to: ChartCover, + powers: PowerRegistry +): string { + const target = to.charts[targetIndex]!; + const parameters = Array.from({ length: 5 }, (_, index) => `d${index}`); + const fields = Array.from({ length: to.width }, (_, lane) => { + const reduced = `n(${parameters + .map((parameter) => `${parameter}.s${lane}`) + .join("+")})`; + return `s${lane}:${wrapExpression( + reduced, + target.cells[lane]!, + powers + )}`; + }); + return `function w${transitionIndex}_${targetIndex}(${parameters.join(",")}){return{${fields.join(",")}};}`; +} + +function emitProjectionContribution( + outputIndex: number, + chartIndex: number, + cover: ChartCover, + secretCoefficients: readonly number[], + weight: number, + powers: PowerRegistry +): string { + const descriptor = cover.charts[chartIndex]!; + const terms = Array.from({ length: cover.width }, (_, lane) => { + const coefficient = multiply(secretCoefficients[lane]!, weight); + return `${fieldSource(coefficient)}*(${unwrapExpression( + `c.s${lane}`, + descriptor.cells[lane]!, + powers + )})`; + }); + return `function j${outputIndex}_${chartIndex}(c){return n(${terms.join("+")});}`; +} + +function fieldPieceExpression(piece: BprfPiece): string { + let expression = fieldSource(coefficientToField(piece.coefficient)); + for (const factor of piece.factors) { + const inverseScale = inverse(factor.basis.scale); + const coordinate = `n(n(q.s${factor.slot}-${fieldSource(factor.basis.bias)})*${fieldSource(inverseScale)})`; + expression += `*n(${coordinate}+${fieldSource(factor.offset)})`; + } + return `n(${expression})`; +} + +function wrapExpression( + raw: string, + transform: ChartCellTransform, + powers: PowerRegistry +): string { + return `${powers.name(transform.exponent)}(n(${fieldSource(transform.scale)}*(${raw})+${fieldSource(transform.offset)}))`; +} + +function unwrapExpression( + stored: string, + transform: ChartCellTransform, + powers: PowerRegistry +): string { + return `n(n(${powers.name(transform.inverseExponent)}(${stored})-${fieldSource(transform.offset)})*${fieldSource(inverse(transform.scale))})`; +} + +function createPowerRegistry(): PowerRegistry { + const names = new Map(); + const definitions: string[] = []; + return { + name(exponent: number): string { + const existing = names.get(exponent); + if (existing) return existing; + const name = `k${names.size}`; + names.set(exponent, name); + const lines = [`function ${name}(x){let b=n(x),r=1;`]; + let remaining = exponent; + while (remaining > 0) { + if (remaining % 2 === 1) lines.push("r=n(r*b);"); + remaining = Math.floor(remaining / 2); + if (remaining > 0) lines.push("b=n(b*b);"); + } + lines.push("return r;}"); + definitions.push(lines.join("")); + return name; + }, + source(): string[] { + return definitions.slice(); + }, + }; +} + +function emitInputAbi(realization: BprfRealization): string { + const checks = [ + `if(!Array.isArray(a)||a.length!==${realization.inputPorts.length})throw new Error("RUAM_CSH_INPUT_ABI");`, + ]; + for (let index = 0; index < realization.inputPorts.length; index++) { + const port = realization.inputPorts[index]!; + checks.push( + port.typeCode === 0 + ? `if(typeof a[${index}]!=="number"||!Number.isSafeInteger(a[${index}])||Math.abs(a[${index}])>=${CSH_FIELD_MODULUS / 4})throw new Error("RUAM_CSH_INPUT_ABI");` + : `if(typeof a[${index}]!=="boolean")throw new Error("RUAM_CSH_INPUT_ABI");` + ); + } + return checks.join(""); +} + +function outputExpression( + fieldName: string, + type: PureValueType, + familyCode: 0 | 1 +): string { + if (type === "number") { + return `(${fieldName}>${Math.floor(CSH_FIELD_MODULUS / 2)}?${fieldName}-${CSH_FIELD_MODULUS}:${fieldName})`; + } + if (familyCode === 0) return `(${fieldName}!==0)`; + return `((${fieldName}>${Math.floor(CSH_FIELD_MODULUS / 2)}?${fieldName}-${CSH_FIELD_MODULUS}:${fieldName})>0)`; +} + +function validatePlan( + realization: BprfRealization, + covers: readonly ChartCover[] +): void { + if (covers.length !== realization.transitions.length + 1) { + throw new Error("RUAM_BPRF_CSH_EMITTER_COVER_COUNT"); + } + const ids = new Set(); + const epochs = new Set(); + for (const cover of covers) { + validateCover(cover); + if ( + cover.width !== realization.frameSize || + cover.charts.length !== 5 || + cover.threshold !== 3 + ) { + throw new Error("RUAM_BPRF_CSH_EMITTER_COVER_PROFILE"); + } + if (ids.has(cover.id)) { + throw new Error("RUAM_BPRF_CSH_EMITTER_COVER_REUSE"); + } + if (epochs.has(cover.epoch)) { + throw new Error("RUAM_BPRF_CSH_EMITTER_EPOCH_REUSE"); + } + ids.add(cover.id); + epochs.add(cover.epoch); + } +} + +function residualCube( + outer: number, + width: number, + degree: number, + seed: number +): number[][][] { + const random = createSeededRandom(seed); + return Array.from({ length: outer }, () => + Array.from({ length: width }, () => + Array.from( + { length: degree }, + () => random.nextUint32() % CSH_FIELD_MODULUS + ) + ) + ); +} + +function residualMatrix( + width: number, + degree: number, + seed: number +): number[][] { + const random = createSeededRandom(seed); + return Array.from({ length: width }, () => + Array.from( + { length: degree }, + () => random.nextUint32() % CSH_FIELD_MODULUS + ) + ); +} + +function emitNormalizer(): string { + return `function n(x){x=Math.trunc(x)%${CSH_FIELD_MODULUS};return x<0?x+${CSH_FIELD_MODULUS}:x;}`; +} + +function fieldSource(value: number): string { + return String(normalize(value)); +} + +function coefficientToField(value: number): number { + if (Number.isSafeInteger(value)) return normalize(value); + const doubled = value * 2; + if (Number.isSafeInteger(doubled)) { + return multiply(normalize(doubled), inverse(2)); + } + throw new Error(`RUAM_BPRF_CSH_UNSUPPORTED_COEFFICIENT: ${value}`); +} + +function interpolationWeightsAtZero(points: readonly number[]): number[] { + return points.map((point, index) => { + let numerator = 1; + let denominator = 1; + for (let other = 0; other < points.length; other++) { + if (other === index) continue; + numerator = multiply(numerator, subtract(0, points[other]!)); + denominator = multiply( + denominator, + subtract(point, points[other]!) + ); + } + return multiply(numerator, inverse(denominator)); + }); +} + +function invertMatrix( + input: readonly (readonly number[])[] +): number[][] { + const width = input.length; + const augmented = input.map((row, rowIndex) => [ + ...row.map(normalize), + ...Array.from({ length: width }, (_, column) => + rowIndex === column ? 1 : 0 + ), + ]); + for (let column = 0; column < width; column++) { + let pivot = column; + while (pivot < width && augmented[pivot]![column] === 0) pivot++; + if (pivot === width) throw new Error("RUAM_BPRF_CSH_SINGULAR_COVER"); + [augmented[column], augmented[pivot]] = [ + augmented[pivot]!, + augmented[column]!, + ]; + const pivotInverse = inverse(augmented[column]![column]!); + for (let index = 0; index < width * 2; index++) { + augmented[column]![index] = multiply( + augmented[column]![index]!, + pivotInverse + ); + } + for (let row = 0; row < width; row++) { + if (row === column) continue; + const factor = augmented[row]![column]!; + for (let index = 0; index < width * 2; index++) { + augmented[row]![index] = subtract( + augmented[row]![index]!, + multiply(factor, augmented[column]![index]!) + ); + } + } + } + return augmented.map((row) => row.slice(width)); +} + +function dot(left: readonly number[], right: readonly number[]): number { + let value = 0; + for (let index = 0; index < left.length; index++) { + value = add(value, multiply(left[index]!, right[index]!)); + } + return value; +} + +function inverse(value: number): number { + const normalized = normalize(value); + if (normalized === 0) throw new Error("RUAM_BPRF_CSH_ZERO_INVERSE"); + return power(normalized, CSH_FIELD_MODULUS - 2); +} + +function power(base: number, exponent: number): number { + let result = 1; + let factor = normalize(base); + let remaining = exponent; + while (remaining > 0) { + if (remaining % 2 === 1) result = multiply(result, factor); + factor = multiply(factor, factor); + remaining = Math.floor(remaining / 2); + } + return result; +} + +function add(left: number, right: number): number { + return normalize(normalize(left) + normalize(right)); +} + +function subtract(left: number, right: number): number { + return normalize(normalize(left) - normalize(right)); +} + +function multiply(left: number, right: number): number { + return normalize(normalize(left) * normalize(right)); +} + +function normalize(value: number): number { + const normalized = Math.trunc(value) % CSH_FIELD_MODULUS; + return normalized < 0 ? normalized + CSH_FIELD_MODULUS : normalized; +} diff --git a/packages/ruam/src/isogloss/csh/transcript-buckets.ts b/packages/ruam/src/isogloss/csh/transcript-buckets.ts new file mode 100644 index 0000000..d3090e8 --- /dev/null +++ b/packages/ruam/src/isogloss/csh/transcript-buckets.ts @@ -0,0 +1,7 @@ +/** Public fixed transcript classes supported by custody product planning. */ +export const MASKED_CUSTODY_TRANSCRIPT_BUCKETS = Object.freeze([ + 4, 8, 16, 32, +] as const); + +export type MaskedCustodyTranscriptBucket = + (typeof MASKED_CUSTODY_TRANSCRIPT_BUCKETS)[number]; diff --git a/packages/ruam/src/isogloss/deployment-eligibility.ts b/packages/ruam/src/isogloss/deployment-eligibility.ts new file mode 100644 index 0000000..ebe3e4e --- /dev/null +++ b/packages/ruam/src/isogloss/deployment-eligibility.ts @@ -0,0 +1,181 @@ +/** + * Conservative deployment-profile eligibility for protected Isogloss regions. + * + * This is an owner/compiler gate. It prevents a remote custody profile from + * silently changing a synchronous API or adding a new suspension point to an + * async function. A remote relation is eligible only when it composes into an + * already-observable remote await contract. + * + * @module isogloss/deployment-eligibility + */ + +export type IsoglossDeploymentProfile = + | "holographic-local" + | "holographic-custodied" + | "holographic-private" + | "holographic-tee"; + +export type IsoglossCustodyBoundary = + | "none" + | "existing-remote-await" + | "in-process-attested"; + +export type IsoglossEligibilityBlocker = + | "NO_PROTECTED_REGION" + | "MISSING_EXACT_DOMAIN_PROOF" + | "REMOTE_BOUNDARY_WOULD_CHANGE_SCHEDULING" + | "MISSING_CUSTODIAN" + | "MISSING_PRIVATE_FUNCTION_PROTOCOL" + | "MISSING_ATTESTED_EXECUTION" + | "UNSUPPORTED_GENERATOR_CUSTODY" + | "LOCAL_FALLBACK_FORBIDDEN"; + +export interface IsoglossDeploymentEligibilityRequest { + readonly profile: IsoglossDeploymentProfile; + readonly protectedRegionCount: number; + readonly hasExactDomainProof: boolean; + readonly isGenerator: boolean; + readonly boundary: IsoglossCustodyBoundary; + readonly custodianAvailable?: boolean; + readonly privateFunctionProtocolAvailable?: boolean; + readonly attestedExecutionAvailable?: boolean; + /** + * Maximum profiles must not carry a complete local relation for outage or + * development fallback. + */ + readonly completeLocalFallbackPresent?: boolean; +} + +export interface IsoglossDeploymentEligibility { + readonly profile: IsoglossDeploymentProfile; + readonly eligible: boolean; + readonly blockers: readonly IsoglossEligibilityBlocker[]; + readonly clientCompleteness: + | "complete" + | "incomplete-under-custodian" + | "incomplete-under-private-protocol" + | "incomplete-under-attestation"; + readonly schedulingContract: + | "no-new-suspension" + | "existing-remote-await-only" + | "in-process-attested-call"; + readonly fallbackPolicy: + | "not-applicable" + | "no-complete-local-fallback"; + readonly securityClaim: + | "client-side-analysis-amplification-only" + | "client-internal-relation-incomplete" + | "client-and-custodian-input-separation" + | "hardware-isolation-dependent"; +} + +export function assessIsoglossDeploymentEligibility( + request: IsoglossDeploymentEligibilityRequest +): IsoglossDeploymentEligibility { + validateRequest(request); + const blockers: IsoglossEligibilityBlocker[] = []; + if (request.protectedRegionCount === 0) { + blockers.push("NO_PROTECTED_REGION"); + } + if (!request.hasExactDomainProof) { + blockers.push("MISSING_EXACT_DOMAIN_PROOF"); + } + + switch (request.profile) { + case "holographic-local": + return result(request.profile, blockers, { + clientCompleteness: "complete", + schedulingContract: "no-new-suspension", + fallbackPolicy: "not-applicable", + securityClaim: "client-side-analysis-amplification-only", + }); + + case "holographic-custodied": + requireRemoteCustody(request, blockers); + if (!request.custodianAvailable) { + blockers.push("MISSING_CUSTODIAN"); + } + return result(request.profile, blockers, { + clientCompleteness: "incomplete-under-custodian", + schedulingContract: "existing-remote-await-only", + fallbackPolicy: "no-complete-local-fallback", + securityClaim: "client-internal-relation-incomplete", + }); + + case "holographic-private": + requireRemoteCustody(request, blockers); + if (!request.custodianAvailable) { + blockers.push("MISSING_CUSTODIAN"); + } + if (!request.privateFunctionProtocolAvailable) { + blockers.push("MISSING_PRIVATE_FUNCTION_PROTOCOL"); + } + return result(request.profile, blockers, { + clientCompleteness: "incomplete-under-private-protocol", + schedulingContract: "existing-remote-await-only", + fallbackPolicy: "no-complete-local-fallback", + securityClaim: "client-and-custodian-input-separation", + }); + + case "holographic-tee": + if ( + request.boundary !== "in-process-attested" || + !request.attestedExecutionAvailable + ) { + blockers.push("MISSING_ATTESTED_EXECUTION"); + } + if (request.completeLocalFallbackPresent) { + blockers.push("LOCAL_FALLBACK_FORBIDDEN"); + } + return result(request.profile, blockers, { + clientCompleteness: "incomplete-under-attestation", + schedulingContract: "in-process-attested-call", + fallbackPolicy: "no-complete-local-fallback", + securityClaim: "hardware-isolation-dependent", + }); + } +} + +function requireRemoteCustody( + request: IsoglossDeploymentEligibilityRequest, + blockers: IsoglossEligibilityBlocker[] +): void { + if (request.isGenerator) { + blockers.push("UNSUPPORTED_GENERATOR_CUSTODY"); + } + if (request.boundary !== "existing-remote-await") { + blockers.push("REMOTE_BOUNDARY_WOULD_CHANGE_SCHEDULING"); + } + if (request.completeLocalFallbackPresent) { + blockers.push("LOCAL_FALLBACK_FORBIDDEN"); + } +} + +function result( + profile: IsoglossDeploymentProfile, + blockers: IsoglossEligibilityBlocker[], + claims: Omit< + IsoglossDeploymentEligibility, + "profile" | "eligible" | "blockers" + > +): IsoglossDeploymentEligibility { + return Object.freeze({ + profile, + eligible: blockers.length === 0, + blockers: Object.freeze(blockers), + ...claims, + }); +} + +function validateRequest( + request: IsoglossDeploymentEligibilityRequest +): void { + if ( + !Number.isSafeInteger(request.protectedRegionCount) || + request.protectedRegionCount < 0 + ) { + throw new Error( + "RUAM_ISOGLOSS_ELIGIBILITY_INVALID_REGION_COUNT" + ); + } +} diff --git a/packages/ruam/src/isogloss/options.ts b/packages/ruam/src/isogloss/options.ts new file mode 100644 index 0000000..6c1a564 --- /dev/null +++ b/packages/ruam/src/isogloss/options.ts @@ -0,0 +1,761 @@ +/** + * Strict public option contract for the Isogloss execution architecture. + * + * This module deliberately has no compatibility alias for the legacy VM + * surface. Removed VM keys are tombstoned with actionable diagnostics, while + * BPRF topology settings remain fixed architecture constants rather than + * user-tunable security knobs. + * + * @module isogloss/options + */ + +import type { IsoglossDeploymentProfile } from "./deployment-eligibility.js"; + +export type { IsoglossDeploymentProfile } from "./deployment-eligibility.js"; + +export type IsoglossOwnerTrace = "off" | "sidecar"; +export type IsoglossTargetMode = "root" | "comment"; +export type IsoglossTargetEnvironment = + | "node" + | "browser" + | "browser-extension"; + +export interface IsoglossNumericRegionDomain { + readonly type: "number"; + readonly min: number; + readonly max: number; +} + +export interface IsoglossBooleanRegionDomain { + readonly type: "boolean"; +} + +export type IsoglossRegionDomain = + | IsoglossNumericRegionDomain + | IsoglossBooleanRegionDomain; + +/** + * Runtime-guard proofs, keyed first by target function name and then by the + * exact local binding name used in that function. Numeric domains are never + * inferred: every numeric binding requires an explicit inclusive range. + */ +export type IsoglossRegionDomains = Readonly< + Record>> +>; + +export interface IsoglossCustodianCapability { + /** Absolute HTTPS endpoint used at an already-observable remote await. */ + readonly endpoint: string; + readonly boundary: "existing-remote-await"; + /** Must be explicitly false: the client must not contain the held relation. */ + readonly completeLocalFallback: false; +} + +export interface IsoglossPrivateFunctionCapability { + readonly protocol: "actively-secure-pfe"; + readonly topology: "padded-universal-circuit"; + /** Non-secret owner identifier for the concrete audited implementation. */ + readonly implementation: string; +} + +export interface IsoglossAttestationCapability { + /** Attestation provider or hardware trust-domain identifier. */ + readonly provider: string; + /** Owner-pinned measurement or policy identifier. */ + readonly expectedMeasurement: string; + readonly boundary: "in-process-attested"; + /** Must be explicitly false: there is no complete non-attested fallback. */ + readonly completeLocalFallback: false; +} + +export interface IsoglossCapabilityOptions { + readonly custodian?: IsoglossCustodianCapability; + readonly privateFunction?: IsoglossPrivateFunctionCapability; + readonly attestation?: IsoglossAttestationCapability; +} + +export interface IsoglossMaximumCustodyOptions { + /** + * Canonical unsigned decimal string. A string keeps JSON/config input exact + * beyond Number.MAX_SAFE_INTEGER; resolution converts it to bigint. + */ + readonly minimumExactAttackQueries?: string; +} + +export interface IsoglossOptions { + /** + * Local is the honest default: it raises analysis cost but remains fully + * reconstructable under unrestricted client instrumentation. + */ + readonly profile?: IsoglossDeploymentProfile; + readonly maximumCustody?: IsoglossMaximumCustodyOptions; + /** Owner-only sidecar metadata is never embedded as a runtime trace hook. */ + readonly ownerTrace?: IsoglossOwnerTrace; + /** Required explicitly for every profile that crosses a trust boundary. */ + readonly capabilities?: IsoglossCapabilityOptions; +} + +export interface RuamOptions { + readonly isogloss?: IsoglossOptions; + /** + * `"root"` protects eligible root functions. `"comment"` protects only a + * function preceded by the exact marker `/* ruam:isogloss *\/`. + */ + readonly targetMode?: IsoglossTargetMode; + /** Probability in the inclusive range [0, 1] for an eligible target. */ + readonly threshold?: number; + readonly preprocessIdentifiers?: boolean; + readonly target?: IsoglossTargetEnvironment; + readonly regionDomains?: IsoglossRegionDomains; +} + +export interface ResolvedIsoglossOptions { + readonly profile: IsoglossDeploymentProfile; + readonly bprf: typeof ISOGLOSS_FIXED_LOCAL_BPRF; + readonly maximumCustody: { + readonly minimumExactAttackQueries: bigint; + }; + readonly ownerTrace: IsoglossOwnerTrace; + readonly capabilities: Readonly; +} + +export interface ResolvedRuamOptions { + readonly isogloss: ResolvedIsoglossOptions; + readonly targetMode: IsoglossTargetMode; + readonly threshold: number; + readonly preprocessIdentifiers: boolean; + readonly target: IsoglossTargetEnvironment; + readonly regionDomains: IsoglossRegionDomains; +} + +export type RuamOptionErrorCode = + | "RUAM_INVALID_ISOGLOSS_OPTIONS" + | "RUAM_INVALID_ISOGLOSS_OPTION" + | "RUAM_UNKNOWN_ISOGLOSS_OPTION" + | "RUAM_REMOVED_VM_OPTION" + | "RUAM_ISOGLOSS_CAPABILITY_REQUIRED" + | "RUAM_ISOGLOSS_PROFILE_CAPABILITY_MISMATCH" + | "RUAM_ISOGLOSS_LOCAL_FALLBACK_FORBIDDEN"; + +export class RuamOptionError extends Error { + override readonly name = "RuamOptionError"; + + constructor( + readonly code: RuamOptionErrorCode, + readonly path: string, + detail: string + ) { + super(`${code}: ${path}: ${detail}`); + } +} + +/** Fixed local BPRF topology. These are intentionally not public inputs. */ +export const ISOGLOSS_FIXED_LOCAL_BPRF = Object.freeze({ + realizationCount: 3 as const, + fragmentCount: 3 as const, +}); + +export const DEFAULT_MINIMUM_EXACT_ATTACK_QUERIES_TEXT = "1000" as const; +export const DEFAULT_MINIMUM_EXACT_ATTACK_QUERIES = 1000n; + +/** One migration instruction for every key removed with the VM architecture. */ +export const REMOVED_LEGACY_VM_OPTION_HINTS = Object.freeze({ + preset: + "Select isogloss.profile explicitly; legacy low/medium/max bundles no longer exist.", + encryptBytecode: + "No Isogloss alias exists; artifact encryption is outside this execution-architecture contract.", + debugProtection: + "No Isogloss alias exists; timing-based anti-debugging is not part of the threat claim.", + debugLogging: + "Use isogloss.ownerTrace='sidecar' for owner-only build metadata; runtime tracing is forbidden.", + dynamicOpcodes: + "Removed with opcode dispatch; Isogloss emits only the semantic structures it needs.", + decoyOpcodes: + "Removed with opcode dispatch; there is no decoy-opcode compatibility behavior.", + deadCodeInjection: + "No Isogloss alias exists; inert semantic corridors require a separately verified transform.", + stackEncoding: + "Removed with the VM stack; Isogloss chart representation is architecture-controlled.", + rollingCipher: + "Removed with the linear instruction stream; there is no rolling-cipher compatibility behavior.", + integrityBinding: + "Use the holographic-tee profile with an attestation capability when hardware integrity is required.", + vmShielding: + "Removed with VM interpreters; protected root groups are isolated by the Isogloss compiler.", + mixedBooleanArithmetic: + "No Isogloss alias exists; use a separately verified engine-independent transform if introduced.", + handlerFragmentation: + "Removed with VM handlers; BPRF fragmentation is fixed and is not a user option.", + stringAtomization: + "No Isogloss alias exists; string protection is outside this execution-architecture contract.", + polymorphicDecoder: + "No Isogloss alias exists; Isogloss does not expose a shared artifact decoder option.", + scatteredKeys: + "Removed with VM key material; custody capabilities describe external trust boundaries instead.", + blockPermutation: + "Removed with bytecode block order; Isogloss topology is not an instruction permutation.", + opcodeMutation: + "Removed with opcode tables; there is no mutation compatibility behavior.", + bytecodeScattering: + "Removed with bytecode artifacts; BPRF fragmentation is fixed and semantically verified.", + incrementalCipher: + "Removed with instruction epochs; Isogloss cover evolution is not a cipher alias.", + semanticOpacity: + "Select an honest Isogloss deployment profile; semantic opacity is not a boolean claim.", + observationResistance: + "Select a custody or attestation profile; local execution cannot promise resistance to full instrumentation.", +} as const); + +export type RemovedLegacyVmOption = + keyof typeof REMOVED_LEGACY_VM_OPTION_HINTS; + +export const REMOVED_LEGACY_VM_OPTIONS: readonly RemovedLegacyVmOption[] = + Object.freeze( + Object.keys(REMOVED_LEGACY_VM_OPTION_HINTS) as RemovedLegacyVmOption[] + ); + +const TOP_LEVEL_KEYS = new Set([ + "isogloss", + "targetMode", + "threshold", + "preprocessIdentifiers", + "target", + "regionDomains", +]); +const ISOGLOSS_KEYS = new Set([ + "profile", + "maximumCustody", + "ownerTrace", + "capabilities", +]); +const MAXIMUM_CUSTODY_KEYS = new Set(["minimumExactAttackQueries"]); +const CAPABILITY_KEYS = new Set([ + "custodian", + "privateFunction", + "attestation", +]); +const CUSTODIAN_KEYS = new Set([ + "endpoint", + "boundary", + "completeLocalFallback", +]); +const PRIVATE_FUNCTION_KEYS = new Set([ + "protocol", + "topology", + "implementation", +]); +const ATTESTATION_KEYS = new Set([ + "provider", + "expectedMeasurement", + "boundary", + "completeLocalFallback", +]); +const NUMBER_DOMAIN_KEYS = new Set(["type", "min", "max"]); +const BOOLEAN_DOMAIN_KEYS = new Set(["type"]); + +const EMPTY_CAPABILITIES: Readonly = Object.freeze({}); +const EMPTY_REGION_DOMAINS: IsoglossRegionDomains = Object.freeze( + Object.create(null) as Record< + string, + Readonly> + > +); + +/** Validate untrusted user input and apply deterministic Isogloss defaults. */ +export function resolveRuamOptions(options: unknown = {}): ResolvedRuamOptions { + const root = requireRecord(options, "options"); + validateTopLevelKeys(root); + + const isogloss = optionalRecord(root.isogloss, "isogloss"); + validateKnownKeys(isogloss, ISOGLOSS_KEYS, "isogloss"); + + const profile = optionalEnum( + isogloss.profile, + "isogloss.profile", + [ + "holographic-local", + "holographic-custodied", + "holographic-private", + "holographic-tee", + ] as const, + "holographic-local" + ); + const ownerTrace = optionalEnum( + isogloss.ownerTrace, + "isogloss.ownerTrace", + ["off", "sidecar"] as const, + "off" + ); + const maximumCustody = resolveMaximumCustody(isogloss.maximumCustody); + const capabilities = resolveCapabilities(isogloss.capabilities); + validateProfileCapabilities(profile, capabilities); + + const targetMode = optionalEnum( + root.targetMode, + "targetMode", + ["root", "comment"] as const, + "root" + ); + const threshold = root.threshold === undefined ? 1 : root.threshold; + if ( + typeof threshold !== "number" || + !Number.isFinite(threshold) || + threshold < 0 || + threshold > 1 + ) { + invalid("threshold", "must be a finite number in the inclusive range [0, 1]"); + } + const preprocessIdentifiers = + root.preprocessIdentifiers === undefined + ? false + : root.preprocessIdentifiers; + if (typeof preprocessIdentifiers !== "boolean") { + invalid("preprocessIdentifiers", "must be a boolean"); + } + const target = optionalEnum( + root.target, + "target", + ["node", "browser", "browser-extension"] as const, + "browser" + ); + const regionDomains = resolveRegionDomains(root.regionDomains); + + return Object.freeze({ + isogloss: Object.freeze({ + profile, + bprf: ISOGLOSS_FIXED_LOCAL_BPRF, + maximumCustody, + ownerTrace, + capabilities, + }), + targetMode, + threshold, + preprocessIdentifiers, + target, + regionDomains, + }); +} + +function resolveMaximumCustody(value: unknown): { + readonly minimumExactAttackQueries: bigint; +} { + const input = optionalRecord(value, "isogloss.maximumCustody"); + validateKnownKeys( + input, + MAXIMUM_CUSTODY_KEYS, + "isogloss.maximumCustody" + ); + const raw = input.minimumExactAttackQueries; + if (raw === undefined) { + return Object.freeze({ + minimumExactAttackQueries: DEFAULT_MINIMUM_EXACT_ATTACK_QUERIES, + }); + } + if (typeof raw !== "string" || !/^(?:0|[1-9][0-9]*)$/.test(raw)) { + invalid( + "isogloss.maximumCustody.minimumExactAttackQueries", + "must be a canonical unsigned decimal string" + ); + } + return Object.freeze({ minimumExactAttackQueries: BigInt(raw) }); +} + +function resolveCapabilities( + value: unknown +): Readonly { + if (value === undefined) return EMPTY_CAPABILITIES; + const input = requireRecord(value, "isogloss.capabilities"); + validateKnownKeys(input, CAPABILITY_KEYS, "isogloss.capabilities"); + + const custodian = + input.custodian === undefined + ? undefined + : resolveCustodian(input.custodian); + const privateFunction = + input.privateFunction === undefined + ? undefined + : resolvePrivateFunction(input.privateFunction); + const attestation = + input.attestation === undefined + ? undefined + : resolveAttestation(input.attestation); + if ( + custodian === undefined && + privateFunction === undefined && + attestation === undefined + ) { + return EMPTY_CAPABILITIES; + } + const resolved: { + custodian?: IsoglossCustodianCapability; + privateFunction?: IsoglossPrivateFunctionCapability; + attestation?: IsoglossAttestationCapability; + } = {}; + if (custodian !== undefined) resolved.custodian = custodian; + if (privateFunction !== undefined) resolved.privateFunction = privateFunction; + if (attestation !== undefined) resolved.attestation = attestation; + return Object.freeze(resolved); +} + +function resolveCustodian(value: unknown): IsoglossCustodianCapability { + const input = requireRecord(value, "isogloss.capabilities.custodian"); + validateKnownKeys( + input, + CUSTODIAN_KEYS, + "isogloss.capabilities.custodian" + ); + const endpoint = requireNonemptyString( + input.endpoint, + "isogloss.capabilities.custodian.endpoint" + ); + let parsed: URL; + try { + parsed = new URL(endpoint); + } catch { + invalid( + "isogloss.capabilities.custodian.endpoint", + "must be an absolute HTTPS URL" + ); + } + if ( + parsed!.protocol !== "https:" || + parsed!.username !== "" || + parsed!.password !== "" + ) { + invalid( + "isogloss.capabilities.custodian.endpoint", + "must be an absolute HTTPS URL without embedded credentials" + ); + } + requireLiteral( + input.boundary, + "existing-remote-await", + "isogloss.capabilities.custodian.boundary" + ); + requireNoLocalFallback( + input.completeLocalFallback, + "isogloss.capabilities.custodian.completeLocalFallback" + ); + return Object.freeze({ + endpoint, + boundary: "existing-remote-await", + completeLocalFallback: false, + }); +} + +function resolvePrivateFunction( + value: unknown +): IsoglossPrivateFunctionCapability { + const input = requireRecord( + value, + "isogloss.capabilities.privateFunction" + ); + validateKnownKeys( + input, + PRIVATE_FUNCTION_KEYS, + "isogloss.capabilities.privateFunction" + ); + requireLiteral( + input.protocol, + "actively-secure-pfe", + "isogloss.capabilities.privateFunction.protocol" + ); + requireLiteral( + input.topology, + "padded-universal-circuit", + "isogloss.capabilities.privateFunction.topology" + ); + const implementation = requireNonemptyString( + input.implementation, + "isogloss.capabilities.privateFunction.implementation" + ); + return Object.freeze({ + protocol: "actively-secure-pfe", + topology: "padded-universal-circuit", + implementation, + }); +} + +function resolveAttestation(value: unknown): IsoglossAttestationCapability { + const input = requireRecord(value, "isogloss.capabilities.attestation"); + validateKnownKeys( + input, + ATTESTATION_KEYS, + "isogloss.capabilities.attestation" + ); + const provider = requireNonemptyString( + input.provider, + "isogloss.capabilities.attestation.provider" + ); + const expectedMeasurement = requireNonemptyString( + input.expectedMeasurement, + "isogloss.capabilities.attestation.expectedMeasurement" + ); + requireLiteral( + input.boundary, + "in-process-attested", + "isogloss.capabilities.attestation.boundary" + ); + requireNoLocalFallback( + input.completeLocalFallback, + "isogloss.capabilities.attestation.completeLocalFallback" + ); + return Object.freeze({ + provider, + expectedMeasurement, + boundary: "in-process-attested", + completeLocalFallback: false, + }); +} + +function validateProfileCapabilities( + profile: IsoglossDeploymentProfile, + capabilities: Readonly +): void { + const hasCustodian = capabilities.custodian !== undefined; + const hasPrivate = capabilities.privateFunction !== undefined; + const hasAttestation = capabilities.attestation !== undefined; + + switch (profile) { + case "holographic-local": + if (hasCustodian || hasPrivate || hasAttestation) { + mismatch(profile, "does not accept remote capability descriptors"); + } + return; + case "holographic-custodied": + if (!hasCustodian) required(profile, "custodian"); + if (hasPrivate || hasAttestation) { + mismatch(profile, "accepts only the custodian capability"); + } + return; + case "holographic-private": + if (!hasCustodian) required(profile, "custodian"); + if (!hasPrivate) required(profile, "privateFunction"); + if (hasAttestation) { + mismatch( + profile, + "accepts custodian and privateFunction capabilities only" + ); + } + return; + case "holographic-tee": + if (!hasAttestation) required(profile, "attestation"); + if (hasCustodian || hasPrivate) { + mismatch(profile, "accepts only the attestation capability"); + } + } +} + +function resolveRegionDomains(value: unknown): IsoglossRegionDomains { + if (value === undefined) return EMPTY_REGION_DOMAINS; + const functions = requireRecord(value, "regionDomains"); + const resolved = Object.create(null) as Record< + string, + Readonly> + >; + for (const functionName of ownStringKeys(functions, "regionDomains")) { + requireNonemptyKey(functionName, `regionDomains.${functionName}`); + const bindings = requireRecord( + functions[functionName], + `regionDomains.${functionName}` + ); + const resolvedBindings = Object.create(null) as Record< + string, + IsoglossRegionDomain + >; + for (const bindingName of ownStringKeys( + bindings, + `regionDomains.${functionName}` + )) { + requireNonemptyKey( + bindingName, + `regionDomains.${functionName}.${bindingName}` + ); + resolvedBindings[bindingName] = resolveRegionDomain( + bindings[bindingName], + `regionDomains.${functionName}.${bindingName}` + ); + } + resolved[functionName] = Object.freeze(resolvedBindings); + } + return Object.freeze(resolved); +} + +function resolveRegionDomain( + value: unknown, + path: string +): IsoglossRegionDomain { + const input = requireRecord(value, path); + if (input.type === "boolean") { + validateKnownKeys(input, BOOLEAN_DOMAIN_KEYS, path); + return Object.freeze({ type: "boolean" }); + } + if (input.type !== "number") { + invalid(`${path}.type`, "must be either 'number' or 'boolean'"); + } + validateKnownKeys(input, NUMBER_DOMAIN_KEYS, path); + const min = input.min; + const max = input.max; + if ( + typeof min !== "number" || + typeof max !== "number" || + !Number.isSafeInteger(min) || + !Number.isSafeInteger(max) || + Object.is(min, -0) || + Object.is(max, -0) || + min > max + ) { + invalid( + path, + "number domains require safe-integer min/max, without negative zero, and min <= max" + ); + } + return Object.freeze({ type: "number", min, max }); +} + +function validateTopLevelKeys(root: Readonly>): void { + for (const key of ownStringKeys(root, "options")) { + if ( + Object.prototype.hasOwnProperty.call( + REMOVED_LEGACY_VM_OPTION_HINTS, + key + ) + ) { + const legacy = key as RemovedLegacyVmOption; + throw new RuamOptionError( + "RUAM_REMOVED_VM_OPTION", + key, + REMOVED_LEGACY_VM_OPTION_HINTS[legacy] + ); + } + if (!TOP_LEVEL_KEYS.has(key)) { + unknown(key); + } + } +} + +function validateKnownKeys( + input: Readonly>, + allowed: ReadonlySet, + path: string +): void { + for (const key of ownStringKeys(input, path)) { + if (!allowed.has(key)) unknown(`${path}.${key}`); + } +} + +function ownStringKeys( + input: Readonly>, + path: string +): string[] { + const keys = Reflect.ownKeys(input); + for (const key of keys) { + if (typeof key === "symbol") unknown(`${path}.[${String(key)}]`); + } + return keys as string[]; +} + +function requireRecord( + value: unknown, + path: string +): Readonly> { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new RuamOptionError( + "RUAM_INVALID_ISOGLOSS_OPTIONS", + path, + "must be a plain object" + ); + } + const prototype = Object.getPrototypeOf(value); + if (prototype !== Object.prototype && prototype !== null) { + throw new RuamOptionError( + "RUAM_INVALID_ISOGLOSS_OPTIONS", + path, + "must be a plain object" + ); + } + return value as Readonly>; +} + +function optionalRecord( + value: unknown, + path: string +): Readonly> { + return value === undefined ? Object.freeze({}) : requireRecord(value, path); +} + +function optionalEnum( + value: unknown, + path: string, + allowed: T, + fallback: T[number] +): T[number] { + if (value === undefined) return fallback; + if (typeof value !== "string" || !allowed.includes(value)) { + invalid(path, `must be one of: ${allowed.join(", ")}`); + } + return value as T[number]; +} + +function requireLiteral( + value: unknown, + expected: T, + path: string +): asserts value is T { + if (value !== expected) invalid(path, `must be ${JSON.stringify(expected)}`); +} + +function requireNonemptyString(value: unknown, path: string): string { + if ( + typeof value !== "string" || + value.length === 0 || + value !== value.trim() + ) { + invalid(path, "must be a nonempty string without surrounding whitespace"); + } + return value; +} + +function requireNonemptyKey(value: string, path: string): void { + if (value.length === 0 || value.trim().length === 0) { + invalid(path, "mapping keys must not be empty or whitespace-only"); + } +} + +function requireNoLocalFallback(value: unknown, path: string): void { + if (value !== false) { + throw new RuamOptionError( + "RUAM_ISOGLOSS_LOCAL_FALLBACK_FORBIDDEN", + path, + "must be explicitly false for a nonlocal profile" + ); + } +} + +function required(profile: IsoglossDeploymentProfile, capability: string): never { + throw new RuamOptionError( + "RUAM_ISOGLOSS_CAPABILITY_REQUIRED", + `isogloss.capabilities.${capability}`, + `${profile} requires an explicit ${capability} capability descriptor` + ); +} + +function mismatch(profile: IsoglossDeploymentProfile, detail: string): never { + throw new RuamOptionError( + "RUAM_ISOGLOSS_PROFILE_CAPABILITY_MISMATCH", + "isogloss.capabilities", + `${profile} ${detail}` + ); +} + +function invalid(path: string, detail: string): never { + throw new RuamOptionError("RUAM_INVALID_ISOGLOSS_OPTION", path, detail); +} + +function unknown(path: string): never { + throw new RuamOptionError( + "RUAM_UNKNOWN_ISOGLOSS_OPTION", + path, + "unknown option" + ); +} diff --git a/packages/ruam/src/isogloss/plan.ts b/packages/ruam/src/isogloss/plan.ts new file mode 100644 index 0000000..a07923b --- /dev/null +++ b/packages/ruam/src/isogloss/plan.ts @@ -0,0 +1,954 @@ +/** + * Fail-closed owner/compiler product planning for Isogloss. + * + * This module consumes only production analyses and generators. Testing + * evaluators and reference custodians are deliberately absent from the build + * schema dependency graph. + * + * @module isogloss/plan + */ + +import { createHash } from "node:crypto"; +import type { + CanonicalCallBoundary, + CanonicalCallGraphInventory, + CanonicalDirectCallEdge, +} from "../compiler/call-graph.js"; +import { + analyzePureRegionLearnability, + assessMaximumCustodyLearnability, + PURE_REGION_LEARNABILITY_NON_CLAIM, +} from "../compiler/pure-region-learnability.js"; +import { + generateBprfArtifact, + type PureRegionContract, +} from "./bprf/index.js"; +import { + MASKED_CUSTODY_TRANSCRIPT_BUCKETS, +} from "./csh/transcript-buckets.js"; +import { + assessIsoglossDeploymentEligibility, + type IsoglossDeploymentProfile, +} from "./deployment-eligibility.js"; +import { + ISOGLOSS_CLIENT_MANIFEST_FORMAT, + ISOGLOSS_ELIGIBILITY_CERTIFICATE_FORMAT, + ISOGLOSS_OWNER_PLAN_FORMAT, + ISOGLOSS_PRODUCT_POLICY_FORMAT, + type IsoglossClientExecution, + type IsoglossClientLearnabilitySummary, + type IsoglossClientManifest, + type IsoglossEligibilityCertificate, + type IsoglossMacroregionCallEvidence, + type IsoglossMacroregionCallRisk, + type IsoglossOwnerBuildPlan, + type IsoglossOwnerMaterial, + type IsoglossPlanBlocker, + type IsoglossPlanningAssessment, + type IsoglossProductPlanRequest, + type IsoglossProductPlanResult, + type IsoglossPureRegionSource, + type IsoglossSourceExpressionCallRiskEvidence, + type IsoglossTranscriptBucket, + type IsoglossTranscriptClass, +} from "./types.js"; + +const PROFILE_ORDER: readonly IsoglossDeploymentProfile[] = Object.freeze([ + "holographic-local", + "holographic-custodied", + "holographic-private", + "holographic-tee", +]); +const PROTECTED_PROFILES: ReadonlySet = new Set([ + "holographic-custodied", + "holographic-private", + "holographic-tee", +]); +const BOUNDARIES = Object.freeze([ + "none", + "existing-remote-await", + "in-process-attested", +] as const); + +interface NormalizedRegion { + readonly id: string; + readonly contract: PureRegionContract; + readonly protectedStageCount: number; + readonly source: IsoglossPureRegionSource; + readonly commitment: Readonly>; +} + +/** + * Build an owner plan and client manifest only after every proof and policy + * gate admits the macroregion. Rejected results carry neither artifact. + */ +export function planIsoglossProduct( + request: IsoglossProductPlanRequest +): IsoglossProductPlanResult { + validateRequest(request); + const region = normalizeRegion(request.region); + const learnability = + request.region.kind === "lowered-contract" + ? analyzePureRegionLearnability(request.region.lowered) + : analyzePureRegionLearnability( + request.region.contract, + request.region.inputDomains + ); + const learnabilityDecision = assessMaximumCustodyLearnability( + learnability, + { + minimumExactAttackQueries: + request.policy.minimumExactAttackQueries, + } + ); + const exactDomainProofComplete = + learnability.issues.length === 0 && + learnability.inputDomains.length === + region.contract.inputs.length && + learnability.inputDomains.every( + (input) => input.domain !== null && input.cardinality !== null + ); + const deployment = assessIsoglossDeploymentEligibility({ + profile: request.profile, + protectedRegionCount: 1, + hasExactDomainProof: exactDomainProofComplete, + isGenerator: request.isGenerator, + boundary: request.boundary, + custodianAvailable: request.capabilities.custodianAvailable, + privateFunctionProtocolAvailable: + request.capabilities.privateFunctionProtocolAvailable, + attestedExecutionAvailable: + request.capabilities.attestedExecutionAvailable, + completeLocalFallbackPresent: + request.completeLocalFallbackPresent, + }); + const callRisk = assessCallEvidence(request.callEvidence, region.id); + const assessment: IsoglossPlanningAssessment = deepFreeze({ + learnability, + learnabilityDecision, + deployment, + callRisk, + exactDomainProofComplete, + hardnessLowerBound: null, + }); + const blockers = collectBlockers(request, assessment, region); + if (blockers.length > 0) { + return deepFreeze({ + decision: "rejected", + blockers, + assessment, + ownerPlan: null, + clientManifest: null, + }); + } + + const bprfArtifact = generateBprfArtifact( + region.contract, + request.policy.bprf + ); + const transcript = createTranscriptClass(request); + const policyDigest = digestCanonical(request.policy); + const regionCommitment = digestCanonical({ + region: region.commitment, + callEvidence: { + evidence: request.callEvidence.evidence, + digest: digestCanonical(request.callEvidence), + }, + }); + const ownerArtifactDigest = digestCanonical(bprfArtifact); + const learnabilitySummary = createLearnabilitySummary(request, assessment); + const certificateBody = { + format: ISOGLOSS_ELIGIBILITY_CERTIFICATE_FORMAT, + eligible: true as const, + profile: request.profile, + policyDigest, + regionCommitment, + ownerArtifactDigest, + learnability: learnabilitySummary, + deployment, + callRisk, + hardnessLowerBound: null, + nonClaim: PURE_REGION_LEARNABILITY_NON_CLAIM, + }; + const certificateDigest = digestCanonical(certificateBody); + const certificate: IsoglossEligibilityCertificate = deepFreeze({ + ...certificateBody, + certificateDigest, + }); + const ownerSecrets = { + placementSecret: + request.ownerSecrets?.placementSecret ?? null, + relationSecret: + request.ownerSecrets?.relationSecret ?? null, + }; + const ownerMaterial: IsoglossOwnerMaterial = deepFreeze({ + regionSource: cloneValue(region.source), + bprfArtifact: cloneValue(bprfArtifact), + ownerSecrets, + transcript, + }); + const ownerMaterialDigest = digestCanonical(ownerMaterial); + const execution = createClientExecution( + request.profile, + bprfArtifact, + ownerArtifactDigest, + transcript + ); + const manifestBody = { + format: ISOGLOSS_CLIENT_MANIFEST_FORMAT, + certificateDigest, + regionCommitment, + profile: request.profile, + clientCompleteness: deployment.clientCompleteness, + schedulingContract: deployment.schedulingContract, + fallbackPolicy: deployment.fallbackPolicy, + securityClaim: deployment.securityClaim, + learnability: learnabilitySummary, + execution, + }; + const planDigest = digestCanonical({ + certificateDigest, + manifest: manifestBody, + }); + const clientManifest: IsoglossClientManifest = deepFreeze({ + ...manifestBody, + planDigest, + }); + assertClientManifestSerializable(clientManifest); + const ownerPlan: IsoglossOwnerBuildPlan = deepFreeze({ + format: ISOGLOSS_OWNER_PLAN_FORMAT, + planDigest, + certificateDigest, + policyDigest, + ownerMaterialDigest, + assessment, + certificate, + ownerMaterial, + }); + return deepFreeze({ + decision: "eligible", + blockers: Object.freeze([]) as readonly [], + assessment, + ownerPlan, + clientManifest, + }); +} + +/** + * Construct the only accepted AST-native call-risk proof. The returned object + * is deeply frozen so callers cannot alter the proof after eligibility is + * assessed. + */ +export function createSourceExpressionMacroregionCallRiskEvidence( + input: Omit< + IsoglossSourceExpressionCallRiskEvidence, + "format" | "evidence" + > +): IsoglossSourceExpressionCallRiskEvidence { + if ( + !input || + typeof input !== "object" || + typeof input.proofId !== "string" || + input.proofId.length === 0 || + input.noCalls !== true || + input.noEffects !== true || + input.noReentrancy !== true || + input.noInterproceduralBoundaries !== true || + input.noRecursiveOrFissionScc !== true + ) { + throw new Error( + "RUAM_ISOGLOSS_PLAN_INVALID_SOURCE_EXPRESSION_PROOF" + ); + } + return deepFreeze({ + format: "ruam-isogloss-source-call-risk-1", + evidence: "source-expression-structural-proof", + proofId: input.proofId, + noCalls: input.noCalls, + noEffects: input.noEffects, + noReentrancy: input.noReentrancy, + noInterproceduralBoundaries: + input.noInterproceduralBoundaries, + noRecursiveOrFissionScc: input.noRecursiveOrFissionScc, + }); +} + +function normalizeRegion( + source: IsoglossPureRegionSource +): NormalizedRegion { + if (source.kind === "lowered-contract") { + return { + id: source.lowered.unitId, + contract: source.lowered.contract, + protectedStageCount: source.lowered.contract.steps.length, + source, + commitment: { + kind: source.kind, + unitId: source.lowered.unitId, + regionIds: [...source.lowered.regionIds], + protectedStageCount: + source.lowered.contract.steps.length, + contractDigest: digestCanonical( + source.lowered.contract + ), + }, + }; + } + return { + id: source.id, + contract: source.contract, + protectedStageCount: source.protectedStageCount, + source, + commitment: { + kind: source.kind, + id: source.id, + protectedStageCount: source.protectedStageCount, + contractDigest: digestCanonical(source.contract), + inputDomainsDigest: digestCanonical(source.inputDomains), + }, + }; +} + +function assessCallEvidence( + callEvidence: IsoglossMacroregionCallEvidence, + regionId: string +): IsoglossMacroregionCallRisk { + if (callEvidence.evidence === "canonical-call-graph") { + return deriveIsoglossMacroregionCallRisk( + callEvidence.inventory, + callEvidence.protectedUnitIds, + regionId + ); + } + + const evidence = callEvidence as unknown as Record; + const noCalls = evidence.noCalls === true; + const noEffects = evidence.noEffects === true; + const noReentrancy = evidence.noReentrancy === true; + const noInterproceduralBoundaries = + evidence.noInterproceduralBoundaries === true; + const noRecursiveOrFissionScc = + evidence.noRecursiveOrFissionScc === true; + const exactProofSchema = + Object.getOwnPropertySymbols(evidence).length === 0 && + Object.getOwnPropertyNames(evidence) + .sort(compareStrings) + .join(",") === + [ + "evidence", + "format", + "noCalls", + "noEffects", + "noInterproceduralBoundaries", + "noRecursiveOrFissionScc", + "noReentrancy", + "proofId", + ].join(","); + const proofComplete = + Object.isFrozen(callEvidence) && + exactProofSchema && + evidence.format === "ruam-isogloss-source-call-risk-1" && + typeof evidence.proofId === "string" && + evidence.proofId.length > 0 && + noCalls && + noEffects && + noReentrancy && + noInterproceduralBoundaries && + noRecursiveOrFissionScc; + return deepFreeze({ + evidence: "source-expression-structural-proof", + proofComplete, + protectedUnitIds: [regionId], + unresolvedBoundaryIds: [ + ...(noCalls + ? [] + : ["source-proof:calls-not-proven-absent"]), + ...(noEffects + ? [] + : ["source-proof:effects-not-proven-absent"]), + ], + reentrantBoundaryIds: noReentrancy + ? [] + : ["source-proof:reentrancy-not-proven-absent"], + interproceduralEdgeIds: noInterproceduralBoundaries + ? [] + : ["source-proof:interprocedural-not-proven-absent"], + recursiveOrFissionSccIds: noRecursiveOrFissionScc + ? [] + : ["source-proof:scc-risk-not-proven-absent"], + }); +} + +/** Derive only facts touching the protected macroregion. */ +export function deriveIsoglossMacroregionCallRisk( + inventory: CanonicalCallGraphInventory, + protectedUnitIds: readonly string[], + loweredUnitId: string +): IsoglossMacroregionCallRisk { + const protectedSet = new Set(protectedUnitIds); + const graphUnits = new Set(inventory.unitIds); + const proofComplete = + inventory.targetPolicy.mode === "canonical-evidence-only" && + protectedSet.has(loweredUnitId) && + [...protectedSet].every( + (unitId) => + graphUnits.has(unitId) && + inventory.sccs.filter((scc) => + scc.unitIds.includes(unitId) + ).length === 1 + ); + const protectedBoundaries = inventory.boundaries.filter((boundary) => + protectedSet.has(boundary.unitId) + ); + const unresolvedBoundaryIds = protectedBoundaries + .filter( + (boundary) => + boundary.resolution.kind === "indirect-or-external" + ) + .map(boundaryId) + .sort(compareStrings); + const reentrantBoundaryIds = protectedBoundaries + .filter( + (boundary) => + boundary.observability.mayReenterRootGroup + ) + .map(boundaryId) + .sort(compareStrings); + const interproceduralEdgeIds = inventory.directEdges + .filter( + (edge) => + edge.unitId !== edge.targetUnitId && + (protectedSet.has(edge.unitId) || + protectedSet.has(edge.targetUnitId)) + ) + .map(edgeId) + .sort(compareStrings); + const recursiveOrFissionSccIds = inventory.sccs + .filter( + (scc) => + scc.unitIds.some((unitId) => protectedSet.has(unitId)) && + (scc.isRecursive || + scc.hasUnresolvedRecursionRisk || + scc.hasUnresolvedMutualRecursionRisk || + scc.reentrancyRelevant || + scc.requiresInterproceduralFission) + ) + .map((scc) => scc.id) + .sort(compareStrings); + return deepFreeze({ + evidence: "canonical-call-graph", + proofComplete, + protectedUnitIds: [...protectedSet].sort(compareStrings), + unresolvedBoundaryIds, + reentrantBoundaryIds, + interproceduralEdgeIds, + recursiveOrFissionSccIds, + }); +} + +/** + * Canonical JSON-compatible serialization used for build and certificate + * digests. BigInts receive an explicit tagged representation. + */ +export function canonicalSerializeIsoglossBuildValue(value: unknown): string { + const active = new Set(); + const serialize = (current: unknown): string => { + if (current === null) return "null"; + switch (typeof current) { + case "string": + case "boolean": + return JSON.stringify(current); + case "number": + if (!Number.isFinite(current)) { + throw new Error( + "RUAM_ISOGLOSS_CANONICAL_NONFINITE_NUMBER" + ); + } + return JSON.stringify( + Object.is(current, -0) ? 0 : current + ); + case "bigint": + return `{"$bigint":${JSON.stringify(String(current))}}`; + case "object": { + if (active.has(current)) { + throw new Error( + "RUAM_ISOGLOSS_CANONICAL_CYCLE" + ); + } + active.add(current); + let serialized: string; + if (Array.isArray(current)) { + if ( + !current.every((_, index) => + Object.hasOwn(current, index) + ) || + !arrayHasEveryIndex(current) || + Object.keys(current).length !== current.length + ) { + throw new Error( + "RUAM_ISOGLOSS_CANONICAL_SPARSE_ARRAY" + ); + } + serialized = `[${current + .map((entry) => serialize(entry)) + .join(",")}]`; + } else { + const prototype = Object.getPrototypeOf(current); + if ( + prototype !== Object.prototype && + prototype !== null + ) { + throw new Error( + "RUAM_ISOGLOSS_CANONICAL_NON_PLAIN_OBJECT" + ); + } + const record = current as Record; + const keys = Object.keys(record).sort(compareStrings); + if (Reflect.ownKeys(record).length !== keys.length) { + throw new Error( + "RUAM_ISOGLOSS_CANONICAL_HIDDEN_PROPERTY" + ); + } + for (const key of keys) { + if (record[key] === undefined) { + throw new Error( + "RUAM_ISOGLOSS_CANONICAL_UNDEFINED" + ); + } + } + serialized = `{${keys + .map( + (key) => + `${JSON.stringify(key)}:${serialize(record[key])}` + ) + .join(",")}}`; + } + active.delete(current); + return serialized; + } + default: + throw new Error( + "RUAM_ISOGLOSS_CANONICAL_UNSUPPORTED_VALUE" + ); + } + }; + return serialize(value); +} + +export function digestCanonicalIsoglossBuildValue(value: unknown): string { + return digestCanonical(value); +} + +function collectBlockers( + request: IsoglossProductPlanRequest, + assessment: IsoglossPlanningAssessment, + region: NormalizedRegion +): IsoglossPlanBlocker[] { + const blockers: IsoglossPlanBlocker[] = []; + const add = ( + code: IsoglossPlanBlocker["code"], + detail: string + ): void => { + if ( + !blockers.some( + (blocker) => + blocker.code === code && blocker.detail === detail + ) + ) { + blockers.push(Object.freeze({ code, detail })); + } + }; + + if (!assessment.exactDomainProofComplete) { + add("INCOMPLETE_DOMAIN_PROOF", region.id); + } + if (assessment.learnability.issues.length > 0) { + add( + "INCOMPLETE_LEARNABILITY_ANALYSIS", + assessment.learnability.issues + .map((issue) => issue.code) + .join(",") + ); + } + for (const reason of assessment.learnabilityDecision.reasons) { + if ( + reason.code === + "RUAM_PURE_REGION_MAXIMUM_CUSTODY_NO_EXACT_ATTACK_BOUND" + ) { + add( + "NO_CONSTRUCTIVE_EXACT_ATTACK_BOUND", + region.id + ); + } + if ( + reason.code === + "RUAM_PURE_REGION_MAXIMUM_CUSTODY_CHEAP_EXACT_ATTACK" + ) { + add( + "CHEAP_KNOWN_EXACT_ATTACK", + `${reason.method}:${reason.queries}<${reason.minimumExactAttackQueries}` + ); + } + } + if (!request.policy.allowedProfiles.includes(request.profile)) { + add("PROFILE_FORBIDDEN_BY_POLICY", request.profile); + } + if (!request.capabilities.supportedProfiles.includes(request.profile)) { + add("UNSUPPORTED_PROFILE_CAPABILITY", request.profile); + } + if (PROTECTED_PROFILES.has(request.profile)) { + const bucket = request.policy.transcriptBucket; + if ( + bucket === null || + !request.capabilities.supportedTranscriptBuckets.includes( + bucket + ) + ) { + add( + "UNSUPPORTED_TRANSCRIPT_BUCKET", + String(bucket) + ); + } else if (region.protectedStageCount > bucket) { + add( + "TRANSCRIPT_BUCKET_TOO_SMALL", + `${region.protectedStageCount}>${bucket}` + ); + } + if (!validOwnerSecret(request.ownerSecrets?.placementSecret)) { + add( + "MISSING_OWNER_PLACEMENT_SECRET", + request.profile + ); + } + if (!validOwnerSecret(request.ownerSecrets?.relationSecret)) { + add("MISSING_OWNER_RELATION_SECRET", request.profile); + } + } else if ( + request.ownerSecrets?.placementSecret !== undefined || + request.ownerSecrets?.relationSecret !== undefined + ) { + add("LOCAL_OWNER_SECRETS_FORBIDDEN", request.profile); + } + if (!assessment.callRisk.proofComplete) { + add("INCOMPLETE_CALL_GRAPH_PROOF", region.id); + } + for (const id of assessment.callRisk.unresolvedBoundaryIds) { + add("UNRESOLVED_CALL_BOUNDARY", id); + } + for (const id of assessment.callRisk.reentrantBoundaryIds) { + add("REENTRANT_CALL_BOUNDARY", id); + } + for (const id of assessment.callRisk.interproceduralEdgeIds) { + add("INTERPROCEDURAL_CALL_BOUNDARY", id); + } + for (const id of assessment.callRisk.recursiveOrFissionSccIds) { + add("RECURSIVE_OR_FISSION_SCC", id); + } + for (const blocker of assessment.deployment.blockers) { + add("DEPLOYMENT_INELIGIBLE", blocker); + } + return blockers; +} + +function createLearnabilitySummary( + request: IsoglossProductPlanRequest, + assessment: IsoglossPlanningAssessment +): IsoglossClientLearnabilitySummary { + const attack = assessment.learnability.cheapestKnownExactAttack; + return deepFreeze({ + minimumExactAttackQueries: + request.policy.minimumExactAttackQueries.toString(), + cheapestKnownExactAttack: attack + ? { + method: attack.method, + queries: attack.queries.toString(), + } + : null, + thresholdTieAccepted: + attack?.queries === + request.policy.minimumExactAttackQueries, + boundInterpretation: + "constructive-exact-attack-upper-bound", + hardnessLowerBound: null, + nonClaim: PURE_REGION_LEARNABILITY_NON_CLAIM, + }); +} + +function createTranscriptClass( + request: IsoglossProductPlanRequest +): IsoglossTranscriptClass | null { + if (request.profile === "holographic-local") return null; + const epochCount = request.policy + .transcriptBucket as IsoglossTranscriptBucket; + return deepFreeze({ + id: `csh-masked-v1-w${request.policy.stateWidth}-e${epochCount}`, + width: request.policy.stateWidth, + epochCount, + coverCount: epochCount + 1, + }); +} + +function createClientExecution( + profile: IsoglossDeploymentProfile, + bprfArtifact: ReturnType, + ownerArtifactDigest: string, + transcript: IsoglossTranscriptClass | null +): IsoglossClientExecution { + if (profile === "holographic-local") { + return deepFreeze({ + mode: "local-bprf", + clientComplete: true, + localFallback: "not-applicable", + artifact: cloneValue(bprfArtifact), + }); + } + if (!transcript) { + throw new Error("RUAM_ISOGLOSS_PLAN_MISSING_TRANSCRIPT"); + } + const mode = + profile === "holographic-custodied" + ? "masked-custody" + : profile === "holographic-private" + ? "private-function-custody" + : "attested-execution"; + return deepFreeze({ + mode, + clientComplete: false, + localFallback: "forbidden", + ownerArtifactDigest, + transcript, + }); +} + +function validateRequest(request: IsoglossProductPlanRequest): void { + if (!request || typeof request !== "object") { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_REQUEST"); + } + if ( + !request.region || + typeof request.region !== "object" || + (request.region.kind !== "lowered-contract" && + request.region.kind !== "pure-contract") + ) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_REGION_SOURCE"); + } + if (request.region.kind === "lowered-contract") { + if ( + !request.region.lowered || + typeof request.region.lowered.unitId !== "string" || + request.region.lowered.unitId.length === 0 || + !Array.isArray(request.region.lowered.regionIds) || + request.region.lowered.regionIds.length === 0 || + !Array.isArray(request.region.lowered.inputBindings) || + !validPureContractShape(request.region.lowered.contract) + ) { + throw new Error( + "RUAM_ISOGLOSS_PLAN_INVALID_LOWERED_CONTRACT" + ); + } + } else if ( + typeof request.region.id !== "string" || + request.region.id.length === 0 || + !validPureContractShape(request.region.contract) || + !Array.isArray(request.region.inputDomains) || + !Number.isSafeInteger(request.region.protectedStageCount) || + request.region.protectedStageCount < 1 || + request.region.protectedStageCount !== + request.region.contract.steps.length + ) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_PURE_CONTRACT"); + } + if (!PROFILE_ORDER.includes(request.profile)) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_PROFILE"); + } + if (!BOUNDARIES.includes(request.boundary)) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_BOUNDARY"); + } + if ( + !request.policy || + typeof request.policy !== "object" || + !request.capabilities || + typeof request.capabilities !== "object" || + !Array.isArray(request.policy.allowedProfiles) || + !request.policy.bprf || + typeof request.policy.bprf !== "object" || + !Array.isArray(request.capabilities.supportedProfiles) || + !Array.isArray( + request.capabilities.supportedTranscriptBuckets + ) || + request.policy.format !== ISOGLOSS_PRODUCT_POLICY_FORMAT || + typeof request.policy.minimumExactAttackQueries !== "bigint" || + request.policy.minimumExactAttackQueries < 0n || + !Number.isSafeInteger(request.policy.stateWidth) || + request.policy.stateWidth < 2 || + !Number.isSafeInteger(request.policy.bprf.seed) || + request.policy.allowedProfiles.length === 0 || + request.policy.allowedProfiles.some( + (profile) => !PROFILE_ORDER.includes(profile) + ) || + request.capabilities.supportedProfiles.some( + (profile) => !PROFILE_ORDER.includes(profile) + ) || + request.capabilities.supportedTranscriptBuckets.some( + (candidate) => + !MASKED_CUSTODY_TRANSCRIPT_BUCKETS.includes(candidate) + ) || + (request.policy.bprf.realizationCount !== undefined && + (!Number.isSafeInteger( + request.policy.bprf.realizationCount + ) || + request.policy.bprf.realizationCount < 2)) || + (request.policy.bprf.fragmentCount !== undefined && + (!Number.isSafeInteger(request.policy.bprf.fragmentCount) || + request.policy.bprf.fragmentCount < 2)) || + typeof request.isGenerator !== "boolean" || + typeof request.completeLocalFallbackPresent !== "boolean" || + typeof request.capabilities.custodianAvailable !== "boolean" || + typeof request.capabilities.privateFunctionProtocolAvailable !== + "boolean" || + typeof request.capabilities.attestedExecutionAvailable !== + "boolean" || + hasDuplicates(request.policy.allowedProfiles) || + hasDuplicates(request.capabilities.supportedProfiles) || + hasDuplicates(request.capabilities.supportedTranscriptBuckets) + ) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_POLICY"); + } + const bucket = request.policy.transcriptBucket; + if ( + (bucket !== null && + !MASKED_CUSTODY_TRANSCRIPT_BUCKETS.includes(bucket)) || + (request.profile === "holographic-local" && bucket !== null) || + (request.profile !== "holographic-local" && bucket === null) + ) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_TRANSCRIPT_BUCKET"); + } + if ( + !request.callEvidence || + typeof request.callEvidence !== "object" || + (request.callEvidence.evidence !== "canonical-call-graph" && + request.callEvidence.evidence !== + "source-expression-structural-proof") + ) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_CALL_EVIDENCE"); + } + if (request.callEvidence.evidence === "canonical-call-graph") { + const inventory = request.callEvidence.inventory; + const protectedUnitIds = + request.callEvidence.protectedUnitIds; + if ( + !Array.isArray(protectedUnitIds) || + protectedUnitIds.length === 0 || + hasDuplicates(protectedUnitIds) || + protectedUnitIds.some( + (unitId) => + typeof unitId !== "string" || + unitId.length === 0 + ) + ) { + throw new Error( + "RUAM_ISOGLOSS_PLAN_INVALID_PROTECTED_UNITS" + ); + } + if ( + !inventory || + inventory.targetPolicy?.mode !== + "canonical-evidence-only" || + !Array.isArray(inventory.unitIds) || + !Array.isArray(inventory.boundaries) || + !Array.isArray(inventory.directEdges) || + !Array.isArray(inventory.sccs) + ) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_CALL_GRAPH"); + } + } + if ( + request.ownerSecrets !== undefined && + (!request.ownerSecrets || + typeof request.ownerSecrets !== "object" || + (request.ownerSecrets.placementSecret !== undefined && + typeof request.ownerSecrets.placementSecret !== + "string") || + (request.ownerSecrets.relationSecret !== undefined && + typeof request.ownerSecrets.relationSecret !== + "string")) + ) { + throw new Error("RUAM_ISOGLOSS_PLAN_INVALID_OWNER_SECRETS"); + } +} + +function validPureContractShape( + contract: PureRegionContract | null | undefined +): contract is PureRegionContract { + return Boolean( + contract && + typeof contract === "object" && + Array.isArray(contract.inputs) && + Array.isArray(contract.steps) && + Array.isArray(contract.outputs) + ); +} + +function assertClientManifestSerializable( + manifest: IsoglossClientManifest +): void { + canonicalSerializeIsoglossBuildValue(manifest); + const serialized = JSON.stringify(manifest); + if ( + typeof serialized !== "string" || + serialized.includes("[object Undefined]") + ) { + throw new Error( + "RUAM_ISOGLOSS_PLAN_CLIENT_MANIFEST_NOT_SERIALIZABLE" + ); + } +} + +function validOwnerSecret(value: string | undefined): boolean { + return typeof value === "string" && value.length >= 16; +} + +function boundaryId(boundary: CanonicalCallBoundary): string { + return `${boundary.unitId}:${boundary.nodeId}`; +} + +function edgeId(edge: CanonicalDirectCallEdge): string { + return `${edge.unitId}:${edge.nodeId}->${edge.targetUnitId}`; +} + +function digestCanonical(value: unknown): string { + return createHash("sha256") + .update(canonicalSerializeIsoglossBuildValue(value)) + .digest("hex"); +} + +function cloneValue(value: T): T { + return structuredClone(value); +} + +function deepFreeze(value: T): T { + if (value && typeof value === "object" && !Object.isFrozen(value)) { + Object.freeze(value); + for (const nested of Object.values( + value as Record + )) { + deepFreeze(nested); + } + } + return value; +} + +function hasDuplicates(values: readonly unknown[]): boolean { + return new Set(values).size !== values.length; +} + +function arrayHasEveryIndex(values: readonly unknown[]): boolean { + for (let index = 0; index < values.length; index++) { + if (!Object.hasOwn(values, index)) return false; + } + return true; +} + +function compareStrings(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} diff --git a/packages/ruam/src/isogloss/source-region.ts b/packages/ruam/src/isogloss/source-region.ts new file mode 100644 index 0000000..61110f4 --- /dev/null +++ b/packages/ruam/src/isogloss/source-region.ts @@ -0,0 +1,621 @@ +/** + * Conservative source-AST lowering for locally guarded pure Isogloss regions. + * + * This is deliberately smaller than JavaScript expression semantics. It + * accepts only local identifiers, finite integer literals, and arithmetic or + * Boolean forms whose complete eager evaluation is observationally + * equivalent to the source form. Every numeric ingress is guarded at runtime + * by the exact domain recorded here. Product lowering replaces the source + * expression completely and rejects values outside the developer-declared + * contract; embedding the original expression as a fallback would hand the + * protected relation back to the client. + * + * @module isogloss/source-region + */ + +import * as t from "@babel/types"; +import type { + PureRegionContract, + PureRegionFormula, + PureValueRef, + PureValueType, +} from "./bprf/index.js"; +import type { PureRegionValueDomain } from "../compiler/pure-region-lowering.js"; + +export interface SourceRegionNumericDomain { + readonly type: "number"; + readonly min: number; + readonly max: number; +} + +export interface SourceRegionBooleanDomain { + readonly type: "boolean"; +} + +export type SourceRegionDomain = + | SourceRegionNumericDomain + | SourceRegionBooleanDomain; + +export interface SourceRegionIngress { + readonly name: string; + readonly type: PureValueType; + readonly domain: PureRegionValueDomain; +} + +export interface SourceRegionValueBounds { + readonly type: PureValueType; + readonly min: number | null; + readonly max: number | null; + readonly mayBeZero: boolean; + readonly mayBeNegative: boolean; +} + +export interface LoweredSourcePureRegion { + readonly contract: PureRegionContract; + readonly ingress: readonly SourceRegionIngress[]; + readonly valueBounds: readonly SourceRegionValueBounds[]; + readonly outputType: PureValueType; + readonly runtimeGuardRequired: true; + readonly domainFailurePolicy: "reject-outside-declared-domain"; +} + +export type SourceRegionRejectionCode = + | "RUAM_SOURCE_REGION_UNSUPPORTED_EXPRESSION" + | "RUAM_SOURCE_REGION_UNBOUND_IDENTIFIER" + | "RUAM_SOURCE_REGION_MISSING_DOMAIN" + | "RUAM_SOURCE_REGION_DOMAIN_TYPE_MISMATCH" + | "RUAM_SOURCE_REGION_INVALID_DOMAIN" + | "RUAM_SOURCE_REGION_IDENTIFIER_TYPE_CONFLICT" + | "RUAM_SOURCE_REGION_UNSAFE_LITERAL" + | "RUAM_SOURCE_REGION_UNSAFE_INTEGER_RANGE" + | "RUAM_SOURCE_REGION_NEGATIVE_ZERO" + | "RUAM_SOURCE_REGION_BRANCH_TYPE_MISMATCH"; + +export interface SourceRegionRejection { + readonly code: SourceRegionRejectionCode; + readonly detail: string; +} + +export type SourceRegionLoweringResult = + | { + readonly accepted: true; + readonly region: LoweredSourcePureRegion; + } + | { + readonly accepted: false; + readonly rejection: SourceRegionRejection; + }; + +export interface SourceRegionLoweringOptions { + /** + * Exact runtime domain guards for local identifiers. The source transform + * must prove locality independently and pass only local binding names. + */ + readonly domains: Readonly>; + /** Names proven by the caller to resolve to local lexical bindings. */ + readonly localBindings: ReadonlySet; +} + +interface MutableIngress { + name: string; + type: PureValueType; + domain: PureRegionValueDomain; +} + +interface LoweringState { + inputs: MutableIngress[]; + inputByName: Map; + steps: Array<{ type: PureValueType; formula: PureRegionFormula }>; + bounds: SourceRegionValueBounds[]; + options: SourceRegionLoweringOptions; +} + +interface LoweredValue { + ref: PureValueRef; + bounds: SourceRegionValueBounds; +} + +class SourceRegionError extends Error { + constructor( + readonly code: SourceRegionRejectionCode, + detail: string + ) { + super(detail); + } +} + +export function lowerSourcePureExpression( + expression: t.Expression, + options: SourceRegionLoweringOptions +): SourceRegionLoweringResult { + const state: LoweringState = { + inputs: [], + inputByName: new Map(), + steps: [], + bounds: [], + options, + }; + try { + // Value references address `[all inputs, then all steps]`. Discover the + // complete ingress prefix before emitting any step so a later first use + // of an input cannot shift already-emitted step references. + if (isSupportedSourceExpressionShape(expression)) { + predeclareSourceInputs(expression, state); + } + const output = lowerExpression(expression, state, null); + const contract: PureRegionContract = Object.freeze({ + inputs: Object.freeze( + state.inputs.map((input) => + Object.freeze({ type: input.type }) + ) + ), + steps: Object.freeze( + state.steps.map((step) => + Object.freeze({ + type: step.type, + formula: Object.freeze({ ...step.formula }), + }) + ) + ), + outputs: Object.freeze([output.ref]), + }); + return Object.freeze({ + accepted: true, + region: Object.freeze({ + contract, + ingress: Object.freeze( + state.inputs.map((input) => + Object.freeze({ + name: input.name, + type: input.type, + domain: freezeDomain(input.domain), + }) + ) + ), + valueBounds: Object.freeze(state.bounds.slice()), + outputType: output.bounds.type, + runtimeGuardRequired: true, + domainFailurePolicy: "reject-outside-declared-domain", + }), + }); + } catch (error) { + if (!(error instanceof SourceRegionError)) throw error; + return Object.freeze({ + accepted: false, + rejection: Object.freeze({ + code: error.code, + detail: error.message, + }), + }); + } +} + +function isSupportedSourceExpressionShape(expression: t.Expression): boolean { + if ( + t.isIdentifier(expression) || + t.isNumericLiteral(expression) || + t.isBooleanLiteral(expression) + ) { + return true; + } + if (t.isParenthesizedExpression(expression)) { + return isSupportedSourceExpressionShape(expression.expression); + } + if ( + t.isUnaryExpression(expression) && + (expression.operator === "-" || expression.operator === "!") && + t.isExpression(expression.argument) + ) { + return isSupportedSourceExpressionShape(expression.argument); + } + if ( + t.isBinaryExpression(expression) && + (expression.operator === "+" || + expression.operator === "-" || + expression.operator === "*") && + t.isExpression(expression.left) && + t.isExpression(expression.right) + ) { + return ( + isSupportedSourceExpressionShape(expression.left) && + isSupportedSourceExpressionShape(expression.right) + ); + } + if ( + t.isLogicalExpression(expression) && + (expression.operator === "&&" || expression.operator === "||") + ) { + return ( + isSupportedSourceExpressionShape(expression.left) && + isSupportedSourceExpressionShape(expression.right) + ); + } + if (t.isConditionalExpression(expression)) { + return ( + isSupportedSourceExpressionShape(expression.test) && + isSupportedSourceExpressionShape(expression.consequent) && + isSupportedSourceExpressionShape(expression.alternate) + ); + } + return false; +} + +function predeclareSourceInputs( + expression: t.Expression, + state: LoweringState +): void { + if (t.isIdentifier(expression)) { + lowerIdentifier(expression, state, null); + return; + } + if ( + t.isNumericLiteral(expression) || + t.isBooleanLiteral(expression) + ) { + return; + } + if (t.isParenthesizedExpression(expression)) { + predeclareSourceInputs(expression.expression, state); + return; + } + if (t.isUnaryExpression(expression) && t.isExpression(expression.argument)) { + predeclareSourceInputs(expression.argument, state); + return; + } + if ( + (t.isBinaryExpression(expression) || + t.isLogicalExpression(expression)) && + t.isExpression(expression.left) && + t.isExpression(expression.right) + ) { + predeclareSourceInputs(expression.left, state); + predeclareSourceInputs(expression.right, state); + return; + } + if (t.isConditionalExpression(expression)) { + predeclareSourceInputs(expression.test, state); + predeclareSourceInputs(expression.consequent, state); + predeclareSourceInputs(expression.alternate, state); + } +} + +function lowerExpression( + expression: t.Expression, + state: LoweringState, + expectedType: PureValueType | null +): LoweredValue { + if (t.isParenthesizedExpression(expression)) { + return lowerExpression(expression.expression, state, expectedType); + } + if (t.isIdentifier(expression)) { + return lowerIdentifier(expression, state, expectedType); + } + if (t.isNumericLiteral(expression)) { + if ( + !Number.isSafeInteger(expression.value) || + Object.is(expression.value, -0) + ) { + fail( + Object.is(expression.value, -0) + ? "RUAM_SOURCE_REGION_NEGATIVE_ZERO" + : "RUAM_SOURCE_REGION_UNSAFE_LITERAL", + String(expression.value) + ); + } + if (expectedType === "boolean") { + fail( + "RUAM_SOURCE_REGION_DOMAIN_TYPE_MISMATCH", + "numeric literal in boolean position" + ); + } + return addStep( + state, + "number", + { tag: "literal", type: "number", value: expression.value }, + numericBounds(expression.value, expression.value) + ); + } + if (t.isBooleanLiteral(expression)) { + if (expectedType === "number") { + fail( + "RUAM_SOURCE_REGION_DOMAIN_TYPE_MISMATCH", + "boolean literal in numeric position" + ); + } + return addStep( + state, + "boolean", + { tag: "literal", type: "boolean", value: expression.value }, + booleanBounds() + ); + } + if ( + t.isUnaryExpression(expression) && + (expression.operator === "-" || expression.operator === "!") + ) { + if (!t.isExpression(expression.argument)) { + fail( + "RUAM_SOURCE_REGION_UNSUPPORTED_EXPRESSION", + expression.type + ); + } + if (expression.operator === "!") { + const value = lowerExpression(expression.argument, state, "boolean"); + return addStep( + state, + "boolean", + { tag: "not", value: value.ref }, + booleanBounds() + ); + } + const value = lowerExpression(expression.argument, state, "number"); + if (value.bounds.mayBeZero) { + fail( + "RUAM_SOURCE_REGION_NEGATIVE_ZERO", + "unary negation may produce -0" + ); + } + return addStep( + state, + "number", + { tag: "negate", value: value.ref }, + checkedNumericBounds( + -value.bounds.max!, + -value.bounds.min!, + "unary-negate" + ) + ); + } + if ( + t.isBinaryExpression(expression) && + (expression.operator === "+" || + expression.operator === "-" || + expression.operator === "*") + ) { + if ( + !t.isExpression(expression.left) || + !t.isExpression(expression.right) + ) { + fail( + "RUAM_SOURCE_REGION_UNSUPPORTED_EXPRESSION", + expression.type + ); + } + const left = lowerExpression(expression.left, state, "number"); + const right = lowerExpression(expression.right, state, "number"); + if ( + expression.operator === "*" && + ((left.bounds.mayBeZero && right.bounds.mayBeNegative) || + (right.bounds.mayBeZero && left.bounds.mayBeNegative)) + ) { + fail( + "RUAM_SOURCE_REGION_NEGATIVE_ZERO", + "multiplication may produce -0" + ); + } + const range = + expression.operator === "+" + ? checkedNumericBounds( + left.bounds.min! + right.bounds.min!, + left.bounds.max! + right.bounds.max!, + "sum" + ) + : expression.operator === "-" + ? checkedNumericBounds( + left.bounds.min! - right.bounds.max!, + left.bounds.max! - right.bounds.min!, + "difference" + ) + : productBounds(left.bounds, right.bounds); + const tag = + expression.operator === "+" + ? "sum" + : expression.operator === "-" + ? "difference" + : "product"; + return addStep( + state, + "number", + { tag, left: left.ref, right: right.ref }, + range + ); + } + if ( + t.isLogicalExpression(expression) && + (expression.operator === "&&" || expression.operator === "||") + ) { + const left = lowerExpression(expression.left, state, "boolean"); + const right = lowerExpression(expression.right, state, "boolean"); + return addStep( + state, + "boolean", + { + tag: expression.operator === "&&" ? "and" : "or", + left: left.ref, + right: right.ref, + }, + booleanBounds() + ); + } + if (t.isConditionalExpression(expression)) { + const gate = lowerExpression(expression.test, state, "boolean"); + const whenTrue = lowerExpression( + expression.consequent, + state, + expectedType + ); + const whenFalse = lowerExpression( + expression.alternate, + state, + whenTrue.bounds.type + ); + if (whenTrue.bounds.type !== whenFalse.bounds.type) { + fail( + "RUAM_SOURCE_REGION_BRANCH_TYPE_MISMATCH", + `${whenTrue.bounds.type}:${whenFalse.bounds.type}` + ); + } + const bounds = + whenTrue.bounds.type === "boolean" + ? booleanBounds() + : checkedNumericBounds( + Math.min( + whenTrue.bounds.min!, + whenFalse.bounds.min! + ), + Math.max( + whenTrue.bounds.max!, + whenFalse.bounds.max! + ), + "select" + ); + return addStep( + state, + whenTrue.bounds.type, + { + tag: "select", + gate: gate.ref, + whenTrue: whenTrue.ref, + whenFalse: whenFalse.ref, + }, + bounds + ); + } + fail("RUAM_SOURCE_REGION_UNSUPPORTED_EXPRESSION", expression.type); +} + +function lowerIdentifier( + identifier: t.Identifier, + state: LoweringState, + expectedType: PureValueType | null +): LoweredValue { + if (!state.options.localBindings.has(identifier.name)) { + fail("RUAM_SOURCE_REGION_UNBOUND_IDENTIFIER", identifier.name); + } + const domain = state.options.domains[identifier.name]; + if (!domain) { + fail("RUAM_SOURCE_REGION_MISSING_DOMAIN", identifier.name); + } + validateDomain(identifier.name, domain); + if (expectedType && domain.type !== expectedType) { + fail( + "RUAM_SOURCE_REGION_DOMAIN_TYPE_MISMATCH", + `${identifier.name}:${domain.type}:${expectedType}` + ); + } + const prior = state.inputByName.get(identifier.name); + if (prior !== undefined) { + const ingress = state.inputs[prior]!; + if (ingress.type !== domain.type) { + fail( + "RUAM_SOURCE_REGION_IDENTIFIER_TYPE_CONFLICT", + identifier.name + ); + } + return { + ref: prior, + bounds: state.bounds[prior]!, + }; + } + const inputIndex = state.inputs.length; + const frozenDomain = freezeDomain(domain); + const bounds = + domain.type === "boolean" + ? booleanBounds() + : numericBounds(domain.min, domain.max); + state.inputs.push({ + name: identifier.name, + type: domain.type, + domain: frozenDomain, + }); + state.inputByName.set(identifier.name, inputIndex); + state.bounds.push(bounds); + return { ref: inputIndex, bounds }; +} + +function addStep( + state: LoweringState, + type: PureValueType, + formula: PureRegionFormula, + bounds: SourceRegionValueBounds +): LoweredValue { + const ref = state.inputs.length + state.steps.length; + state.steps.push({ type, formula }); + state.bounds.push(bounds); + return { ref, bounds }; +} + +function validateDomain(name: string, domain: SourceRegionDomain): void { + if (domain.type === "boolean") return; + if ( + domain.type !== "number" || + !Number.isSafeInteger(domain.min) || + !Number.isSafeInteger(domain.max) || + domain.min > domain.max || + Object.is(domain.min, -0) || + Object.is(domain.max, -0) + ) { + fail("RUAM_SOURCE_REGION_INVALID_DOMAIN", name); + } +} + +function productBounds( + left: SourceRegionValueBounds, + right: SourceRegionValueBounds +): SourceRegionValueBounds { + const products = [ + left.min! * right.min!, + left.min! * right.max!, + left.max! * right.min!, + left.max! * right.max!, + ]; + return checkedNumericBounds( + Math.min(...products), + Math.max(...products), + "product" + ); +} + +function checkedNumericBounds( + min: number, + max: number, + detail: string +): SourceRegionValueBounds { + if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max)) { + fail("RUAM_SOURCE_REGION_UNSAFE_INTEGER_RANGE", detail); + } + return numericBounds(min, max); +} + +function numericBounds( + min: number, + max: number +): SourceRegionValueBounds { + return Object.freeze({ + type: "number", + min, + max, + mayBeZero: min <= 0 && max >= 0, + mayBeNegative: min < 0, + }); +} + +function booleanBounds(): SourceRegionValueBounds { + return Object.freeze({ + type: "boolean", + min: null, + max: null, + mayBeZero: true, + mayBeNegative: false, + }); +} + +function freezeDomain( + domain: SourceRegionDomain | PureRegionValueDomain +): PureRegionValueDomain { + return Object.freeze({ ...domain }) as PureRegionValueDomain; +} + +function fail(code: SourceRegionRejectionCode, detail: string): never { + throw new SourceRegionError(code, detail); +} diff --git a/packages/ruam/src/isogloss/source-sites.ts b/packages/ruam/src/isogloss/source-sites.ts new file mode 100644 index 0000000..d1c36ff --- /dev/null +++ b/packages/ruam/src/isogloss/source-sites.ts @@ -0,0 +1,351 @@ +/** + * Discover source expressions eligible for guarded pure-region replacement. + * + * The returned NodePaths are build-only handles. No source relation, origin, + * or domain map is part of a client artifact. + * + * @module isogloss/source-sites + */ + +import type { NodePath } from "@babel/traverse"; +import * as t from "@babel/types"; +import { traverse } from "../babel-compat.js"; +import { + lowerSourcePureExpression, + type LoweredSourcePureRegion, + type SourceRegionDomain, + type SourceRegionRejection, +} from "./source-region.js"; + +export type SourceTargetMode = "root" | "comment"; + +export interface SourceRegionDiscoveryOptions { + readonly targetMode: SourceTargetMode; + readonly threshold: number; + readonly seed: number; + readonly regionDomains: Readonly< + Record>> + >; +} + +export interface SourceRegionOrigin { + readonly line: number | null; + readonly column: number | null; + readonly start: number | null; + readonly end: number | null; +} + +export interface SourcePureRegionSite { + readonly id: string; + readonly functionName: string; + readonly ordinal: number; + readonly expressionPath: NodePath; + readonly region: LoweredSourcePureRegion; + readonly origin: SourceRegionOrigin; +} + +export type SourceRegionDiscoveryDiagnosticCode = + | "RUAM_SOURCE_TARGET_ANONYMOUS" + | "RUAM_SOURCE_TARGET_MISSING_DOMAINS" + | "RUAM_SOURCE_TARGET_THRESHOLD_SKIPPED" + | "RUAM_SOURCE_REGION_TOO_SMALL" + | "RUAM_SOURCE_REGION_REJECTED"; + +export interface SourceRegionDiscoveryDiagnostic { + readonly code: SourceRegionDiscoveryDiagnosticCode; + readonly functionName: string | null; + readonly origin: SourceRegionOrigin; + readonly rejection: SourceRegionRejection | null; +} + +export interface SourceRegionDiscovery { + readonly sites: readonly SourcePureRegionSite[]; + readonly diagnostics: readonly SourceRegionDiscoveryDiagnostic[]; +} + +export function discoverSourcePureRegions( + ast: t.File, + options: SourceRegionDiscoveryOptions +): SourceRegionDiscovery { + validateOptions(options); + const targetPaths: NodePath[] = []; + traverse(ast, { + Function(path) { + if (shouldTarget(path, options.targetMode)) { + targetPaths.push(path); + } + }, + }); + + const sites: SourcePureRegionSite[] = []; + const diagnostics: SourceRegionDiscoveryDiagnostic[] = []; + const orderedTargets = targetPaths.sort(compareFunctionPaths); + for (let targetOrdinal = 0; targetOrdinal < orderedTargets.length; targetOrdinal++) { + const functionPath = orderedTargets[targetOrdinal]!; + const functionName = inferFunctionName(functionPath); + const functionOrigin = originFor(functionPath.node); + if (!functionName) { + diagnostics.push( + diagnostic( + "RUAM_SOURCE_TARGET_ANONYMOUS", + null, + functionOrigin + ) + ); + continue; + } + const domains = options.regionDomains[functionName]; + if (!domains) { + diagnostics.push( + diagnostic( + "RUAM_SOURCE_TARGET_MISSING_DOMAINS", + functionName, + functionOrigin + ) + ); + continue; + } + if ( + options.threshold < 1 && + selectionUnitInterval( + options.seed, + functionName, + targetOrdinal + ) >= options.threshold + ) { + diagnostics.push( + diagnostic( + "RUAM_SOURCE_TARGET_THRESHOLD_SKIPPED", + functionName, + functionOrigin + ) + ); + continue; + } + + const expressionPaths: NodePath[] = []; + if ( + functionPath.isArrowFunctionExpression() && + !t.isBlockStatement(functionPath.node.body) + ) { + expressionPaths.push( + functionPath.get("body") as NodePath + ); + } + const returnPaths: NodePath[] = []; + functionPath.traverse({ + Function(inner) { + inner.skip(); + }, + ReturnStatement(path) { + returnPaths.push(path); + }, + }); + returnPaths.sort(compareNodePaths); + for (const returnPath of returnPaths) { + const argumentPath = returnPath.get("argument"); + if (argumentPath.node && argumentPath.isExpression()) { + expressionPaths.push(argumentPath); + } + } + expressionPaths.sort(compareNodePaths); + for (let ordinal = 0; ordinal < expressionPaths.length; ordinal++) { + const argumentPath = expressionPaths[ordinal]!; + const localBindings = collectLocalBindings( + argumentPath, + functionPath + ); + const lowering = lowerSourcePureExpression(argumentPath.node, { + domains, + localBindings, + }); + if (!lowering.accepted) { + diagnostics.push( + diagnostic( + "RUAM_SOURCE_REGION_REJECTED", + functionName, + originFor(argumentPath.node), + lowering.rejection + ) + ); + continue; + } + if ( + lowering.region.contract.inputs.length === 0 || + lowering.region.contract.steps.length < 2 + ) { + diagnostics.push( + diagnostic( + "RUAM_SOURCE_REGION_TOO_SMALL", + functionName, + originFor(argumentPath.node) + ) + ); + continue; + } + sites.push( + Object.freeze({ + id: `source-region-${targetOrdinal}-${ordinal}`, + functionName, + ordinal, + expressionPath: argumentPath, + region: lowering.region, + origin: originFor(argumentPath.node), + }) + ); + } + } + + return Object.freeze({ + sites: Object.freeze(sites), + diagnostics: Object.freeze(diagnostics), + }); +} + +function shouldTarget( + path: NodePath, + mode: SourceTargetMode +): boolean { + if (mode === "comment") { + return hasIsoglossMarker(path); + } + let current: NodePath | null = path.parentPath; + while (current) { + if (current.isFunction()) return false; + current = current.parentPath; + } + return true; +} + +function hasIsoglossMarker(path: NodePath): boolean { + const comments = [ + ...(path.node.leadingComments ?? []), + ...(path.parentPath?.node.leadingComments ?? []), + ]; + return comments.some( + (comment) => comment.value.trim() === "ruam:isogloss" + ); +} + +function inferFunctionName(path: NodePath): string | null { + if ( + ("id" in path.node && t.isIdentifier(path.node.id)) || + t.isFunctionDeclaration(path.node) + ) { + const id = "id" in path.node ? path.node.id : null; + if (t.isIdentifier(id)) return id.name; + } + const parent = path.parentPath?.node; + if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) { + return parent.id.name; + } + if ( + (t.isObjectProperty(parent) || t.isObjectMethod(parent)) && + !parent.computed && + t.isIdentifier(parent.key) + ) { + return parent.key.name; + } + return null; +} + +function collectLocalBindings( + expressionPath: NodePath, + functionPath: NodePath +): ReadonlySet { + const names = new Set(); + t.traverseFast(expressionPath.node, (node) => { + if (t.isIdentifier(node)) names.add(node.name); + }); + const local = new Set(); + for (const name of names) { + const binding = expressionPath.scope.getBinding(name); + if (binding && pathIsInside(binding.path, functionPath)) { + local.add(name); + } + } + return local; +} + +function pathIsInside( + path: NodePath, + ancestor: NodePath +): boolean { + let current: NodePath | null = path; + while (current) { + if (current === ancestor) return true; + current = current.parentPath; + } + return false; +} + +function validateOptions(options: SourceRegionDiscoveryOptions): void { + if ( + !Number.isFinite(options.threshold) || + options.threshold < 0 || + options.threshold > 1 + ) { + throw new Error("RUAM_SOURCE_DISCOVERY_INVALID_THRESHOLD"); + } + if (!Number.isSafeInteger(options.seed)) { + throw new Error("RUAM_SOURCE_DISCOVERY_INVALID_SEED"); + } +} + +function diagnostic( + code: SourceRegionDiscoveryDiagnosticCode, + functionName: string | null, + origin: SourceRegionOrigin, + rejection: SourceRegionRejection | null = null +): SourceRegionDiscoveryDiagnostic { + return Object.freeze({ + code, + functionName, + origin, + rejection, + }); +} + +function originFor(node: t.Node): SourceRegionOrigin { + return Object.freeze({ + line: node.loc?.start.line ?? null, + column: node.loc?.start.column ?? null, + start: node.start ?? null, + end: node.end ?? null, + }); +} + +function selectionUnitInterval( + seed: number, + name: string, + ordinal: number +): number { + let hash = (seed ^ Math.imul(ordinal + 1, 0x9e3779b9)) >>> 0; + for (let index = 0; index < name.length; index++) { + hash ^= name.charCodeAt(index); + hash = Math.imul(hash, 0x01000193) >>> 0; + } + hash ^= hash >>> 16; + hash = Math.imul(hash, 0x7feb352d) >>> 0; + hash ^= hash >>> 15; + hash = Math.imul(hash, 0x846ca68b) >>> 0; + hash ^= hash >>> 16; + return (hash >>> 0) / 0x1_0000_0000; +} + +function compareFunctionPaths( + left: NodePath, + right: NodePath +): number { + return compareNodePaths(left, right); +} + +function compareNodePaths( + left: NodePath, + right: NodePath +): number { + return ( + (left.node.start ?? Number.MAX_SAFE_INTEGER) - + (right.node.start ?? Number.MAX_SAFE_INTEGER) + ); +} diff --git a/packages/ruam/src/isogloss/source-transform.ts b/packages/ruam/src/isogloss/source-transform.ts new file mode 100644 index 0000000..88d1cae --- /dev/null +++ b/packages/ruam/src/isogloss/source-transform.ts @@ -0,0 +1,410 @@ +/** + * Product source-to-source orchestration for local Isogloss regions. + * + * Unsupported JavaScript remains ordinary source at distributed native effect + * sites. A configured pure return region is replaced completely by a + * scalarized BPRF closure; the original relation is never embedded as a + * fallback. + * + * Nonlocal profiles are intentionally rejected here. They require an + * explicitly modeled pre-existing remote-await or attested boundary and are + * built through the owner product planner, not silently grafted onto a + * synchronous source expression. + * + * @module isogloss/source-transform + */ + +import { parse } from "@babel/parser"; +import * as t from "@babel/types"; +import { generate, traverse } from "../babel-compat.js"; +import { BABEL_PARSER_PLUGINS } from "../constants.js"; +import { collectIdentifiers } from "../preprocess.js"; +import { generateBprfArtifact } from "./bprf/index.js"; +import { hashText, mix32 } from "./bprf/random.js"; +import { + emitBprfScalarSource, + type BprfScalarSourceEmission, +} from "./bprf/scalar-source.js"; +import type { ResolvedRuamOptions } from "./options.js"; +import { + discoverSourcePureRegions, + type SourcePureRegionSite, + type SourceRegionDiscoveryDiagnostic, + type SourceRegionOrigin, +} from "./source-sites.js"; + +const REQUIRED_EMITTER_INTRINSICS = Object.freeze([ + "Array", + "Error", + "Math", + "Number", + "Object", +]); +const MAX_EMISSION_SEED_ATTEMPTS = 64; + +export type IsoglossBuildDiagnosticCode = + | SourceRegionDiscoveryDiagnostic["code"] + | "RUAM_ISOGLOSS_EMITTER_SEED_REJECTED"; + +export interface IsoglossBuildDiagnostic { + readonly code: IsoglossBuildDiagnosticCode; + readonly functionName: string | null; + readonly origin: SourceRegionOrigin; + readonly detail: string | null; +} + +export interface IsoglossOwnerRegionTrace { + readonly regionId: string; + readonly functionName: string; + readonly origin: SourceRegionOrigin; + readonly emitterCertificate: BprfScalarSourceEmission["certificate"]; +} + +export interface IsoglossOwnerSidecar { + readonly format: "ruam-isogloss-owner-trace-1"; + readonly regions: readonly IsoglossOwnerRegionTrace[]; +} + +export interface IsoglossSourceBuildStats { + readonly engine: "isogloss"; + readonly profile: "holographic-local"; + readonly rootGroupCount: number; + readonly protectedRegionCount: number; + readonly realizationCount: number; + readonly fragmentFunctionCount: number; + readonly originalBytes: number; + readonly outputBytes: number; + readonly expansionRatio: number; + readonly clientCompleteness: "complete"; + readonly hardnessLowerBound: null; +} + +export interface IsoglossSourceBuildResult { + readonly code: string; + readonly diagnostics: readonly IsoglossBuildDiagnostic[]; + readonly stats: IsoglossSourceBuildStats; + readonly ownerTrace?: IsoglossOwnerSidecar; +} + +export class IsoglossSourceTransformError extends Error { + override readonly name = "IsoglossSourceTransformError"; + + constructor( + readonly code: + | "RUAM_ISOGLOSS_SOURCE_PROFILE_REQUIRES_EXTERNAL_BOUNDARY" + | "RUAM_ISOGLOSS_INTRINSIC_SHADOW" + | "RUAM_ISOGLOSS_CONFIGURED_REGION_REJECTED" + | "RUAM_ISOGLOSS_CONFIGURED_TARGET_NOT_FOUND" + | "RUAM_ISOGLOSS_EMISSION_FAILED", + detail: string + ) { + super(`${code}: ${detail}`); + } +} + +export function buildLocalIsoglossSource( + source: string, + options: ResolvedRuamOptions, + fileSeed: number +): IsoglossSourceBuildResult { + if (options.isogloss.profile !== "holographic-local") { + throw new IsoglossSourceTransformError( + "RUAM_ISOGLOSS_SOURCE_PROFILE_REQUIRES_EXTERNAL_BOUNDARY", + `${options.isogloss.profile} must compose at an owner-proven custody or attestation boundary` + ); + } + if (!Number.isSafeInteger(fileSeed)) { + throw new IsoglossSourceTransformError( + "RUAM_ISOGLOSS_EMISSION_FAILED", + "file seed must be a safe integer" + ); + } + + const ast = parse(source, { + sourceType: "unambiguous", + plugins: [...BABEL_PARSER_PLUGINS], + }); + const topLevelBindings = collectTopLevelBindings(ast); + const shadowedIntrinsic = REQUIRED_EMITTER_INTRINSICS.find((name) => + topLevelBindings.has(name) + ); + if (shadowedIntrinsic) { + throw new IsoglossSourceTransformError( + "RUAM_ISOGLOSS_INTRINSIC_SHADOW", + `top-level binding ${shadowedIntrinsic} shadows a required scalar-emitter intrinsic` + ); + } + + const discovery = discoverSourcePureRegions(ast, { + targetMode: options.targetMode, + threshold: options.threshold, + seed: fileSeed, + regionDomains: options.regionDomains, + }); + validateConfiguredTargets(discovery.diagnostics, discovery.sites, options); + + const occupiedNames = collectIdentifiers(source); + const helperStatements: t.Statement[] = []; + const ownerRegions: IsoglossOwnerRegionTrace[] = []; + const diagnostics = discovery.diagnostics.map(buildDiagnostic); + let realizationCount = 0; + let fragmentFunctionCount = 0; + + for (const site of discovery.sites) { + const built = buildSiteEmission(site, fileSeed, occupiedNames); + occupiedNames.add(built.wrapperName); + helperStatements.push(built.wrapperStatement); + site.expressionPath.replaceWith( + t.callExpression(t.identifier(built.wrapperName), [ + t.arrayExpression( + site.region.ingress.map((input) => + t.identifier(input.name) + ) + ), + ]) + ); + realizationCount += built.emission.stats.realizationCount; + fragmentFunctionCount += + built.emission.stats.fragmentFunctionCount; + ownerRegions.push( + Object.freeze({ + regionId: site.id, + functionName: site.functionName, + origin: site.origin, + emitterCertificate: built.emission.certificate, + }) + ); + } + + if (helperStatements.length > 0) { + const insertionIndex = firstNonImportIndex(ast.program.body); + ast.program.body.splice( + insertionIndex, + 0, + ...helperStatements + ); + } + const generated = generate(ast, { + comments: true, + compact: false, + }).code; + const originalBytes = utf8ByteLength(source); + const outputBytes = utf8ByteLength(generated); + const rootGroupCount = new Set( + discovery.sites.map((site) => site.functionName) + ).size; + const stats = Object.freeze({ + engine: "isogloss" as const, + profile: "holographic-local" as const, + rootGroupCount, + protectedRegionCount: discovery.sites.length, + realizationCount, + fragmentFunctionCount, + originalBytes, + outputBytes, + expansionRatio: + originalBytes === 0 ? 1 : outputBytes / originalBytes, + clientCompleteness: "complete" as const, + hardnessLowerBound: null, + }); + const base = { + code: generated, + diagnostics: Object.freeze(diagnostics), + stats, + }; + if (options.isogloss.ownerTrace !== "sidecar") { + return Object.freeze(base); + } + return Object.freeze({ + ...base, + ownerTrace: Object.freeze({ + format: "ruam-isogloss-owner-trace-1", + regions: Object.freeze(ownerRegions), + }), + }); +} + +function buildSiteEmission( + site: SourcePureRegionSite, + fileSeed: number, + occupiedNames: ReadonlySet +): { + readonly emission: BprfScalarSourceEmission; + readonly wrapperName: string; + readonly wrapperStatement: t.Statement; +} { + const domains = site.region.ingress.map((input) => input.domain); + let lastError: unknown; + for (let attempt = 0; attempt < MAX_EMISSION_SEED_ATTEMPTS; attempt++) { + const seed = mix32( + fileSeed ^ + hashText(site.id) ^ + Math.imul(attempt + 1, 0x9e3779b9) + ); + try { + const artifact = generateBprfArtifact(site.region.contract, { + seed, + realizationCount: 3, + fragmentCount: 3, + }); + const emission = emitBprfScalarSource(artifact, domains); + const wrapperName = `${emission.entryName}_${mix32( + seed ^ 0xa5a5a5a5 + ).toString(36)}`; + if (occupiedNames.has(wrapperName)) continue; + const wrapperStatement = parseWrapper( + wrapperName, + emission, + site, + seed + ); + return Object.freeze({ + emission, + wrapperName, + wrapperStatement, + }); + } catch (error) { + lastError = error; + } + } + throw new IsoglossSourceTransformError( + "RUAM_ISOGLOSS_EMISSION_FAILED", + `${site.functionName}:${site.ordinal}: ${ + lastError instanceof Error ? lastError.message : String(lastError) + }` + ); +} + +function parseWrapper( + wrapperName: string, + emission: BprfScalarSourceEmission, + site: SourcePureRegionSite, + seed: number +): t.Statement { + const caller = JSON.stringify(`${site.functionName}:${site.ordinal}`); + const initialLineage = mix32(seed ^ hashText(site.functionName)); + const lineageStep = mix32(seed ^ 0x6d2b79f5) | 1; + const projection = + site.region.outputType === "number" + ? `const r=${emission.entryName}(a,{caller:${caller},epoch:e,lineage:l})[0];return Object.is(r,-0)?0:r;` + : `return ${emission.entryName}(a,{caller:${caller},epoch:e,lineage:l})[0];`; + const wrapperSource = [ + `const ${wrapperName}=(()=>{`, + emission.source, + `let e=0,l=${initialLineage >>> 0};`, + "return function(a){", + `e=(e+1)>>>0;l=(l+e+${lineageStep >>> 0})>>>0;`, + projection, + "};", + "})();", + ].join("\n"); + const wrapperAst = parse(wrapperSource, { + sourceType: "script", + }); + const statement = wrapperAst.program.body[0]; + if ( + wrapperAst.program.body.length !== 1 || + !statement || + !t.isVariableDeclaration(statement) + ) { + throw new IsoglossSourceTransformError( + "RUAM_ISOGLOSS_EMISSION_FAILED", + "scalar wrapper did not parse to one declaration" + ); + } + return statement; +} + +function validateConfiguredTargets( + diagnostics: readonly SourceRegionDiscoveryDiagnostic[], + sites: readonly SourcePureRegionSite[], + options: ResolvedRuamOptions +): void { + const configured = new Set(Object.keys(options.regionDomains)); + const seen = new Set(); + for (const site of sites) seen.add(site.functionName); + for (const diagnostic of diagnostics) { + if (diagnostic.functionName) seen.add(diagnostic.functionName); + if ( + diagnostic.functionName && + configured.has(diagnostic.functionName) && + (diagnostic.code === "RUAM_SOURCE_REGION_REJECTED" || + diagnostic.code === "RUAM_SOURCE_REGION_TOO_SMALL") + ) { + throw new IsoglossSourceTransformError( + "RUAM_ISOGLOSS_CONFIGURED_REGION_REJECTED", + `${diagnostic.functionName}: ${ + diagnostic.rejection?.code ?? diagnostic.code + }` + ); + } + } + for (const functionName of configured) { + if (!seen.has(functionName)) { + throw new IsoglossSourceTransformError( + "RUAM_ISOGLOSS_CONFIGURED_TARGET_NOT_FOUND", + functionName + ); + } + } +} + +function collectTopLevelBindings(ast: t.File): ReadonlySet { + const names = new Set(); + traverse(ast, { + Program(path) { + for (const name of Object.keys(path.scope.bindings)) names.add(name); + path.stop(); + }, + }); + return names; +} + +function buildDiagnostic( + diagnostic: SourceRegionDiscoveryDiagnostic +): IsoglossBuildDiagnostic { + return Object.freeze({ + code: diagnostic.code, + functionName: diagnostic.functionName, + origin: diagnostic.origin, + detail: diagnostic.rejection + ? `${diagnostic.rejection.code}:${diagnostic.rejection.detail}` + : null, + }); +} + +function firstNonImportIndex(body: readonly t.Statement[]): number { + let index = 0; + while (index < body.length && t.isImportDeclaration(body[index]!)) { + index++; + } + return index; +} + +/** Browser-safe UTF-8 byte count. */ +function utf8ByteLength(value: string): number { + let bytes = 0; + for (let index = 0; index < value.length; index++) { + const unit = value.charCodeAt(index); + if (unit <= 0x7f) { + bytes++; + } else if (unit <= 0x7ff) { + bytes += 2; + } else if ( + unit >= 0xd800 && + unit <= 0xdbff && + index + 1 < value.length + ) { + const next = value.charCodeAt(index + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + bytes += 4; + index++; + } else { + bytes += 3; + } + } else { + bytes += 3; + } + } + return bytes; +} diff --git a/packages/ruam/src/isogloss/types.ts b/packages/ruam/src/isogloss/types.ts new file mode 100644 index 0000000..777616b --- /dev/null +++ b/packages/ruam/src/isogloss/types.ts @@ -0,0 +1,293 @@ +/** + * Frozen build-side schemas for Isogloss product planning. + * + * Owner plans may retain compiler contracts, generated relations, and secret + * material. Client manifests are a separate, JSON-serializable type and never + * contain compiler lowering contracts or owner secrets. + * + * @module isogloss/types + */ + +import type { + CanonicalCallGraphInventory, +} from "../compiler/call-graph.js"; +import type { + MaximumCustodyLearnabilityDecision, + PureRegionLearnabilityAnalysis, +} from "../compiler/pure-region-learnability.js"; +import type { LoweredPureRegionContract } from "../compiler/pure-region-lowering.js"; +import type { + BprfArtifact, + BprfGenerationOptions, + PureRegionContract, +} from "./bprf/index.js"; +import type { + IsoglossCustodyBoundary, + IsoglossDeploymentEligibility, + IsoglossDeploymentProfile, +} from "./deployment-eligibility.js"; + +export const ISOGLOSS_PRODUCT_POLICY_FORMAT = + "ruam-isogloss-product-policy-1" as const; +export const ISOGLOSS_CLIENT_MANIFEST_FORMAT = + "ruam-isogloss-client-manifest-1" as const; +export const ISOGLOSS_ELIGIBILITY_CERTIFICATE_FORMAT = + "ruam-isogloss-eligibility-certificate-1" as const; +export const ISOGLOSS_OWNER_PLAN_FORMAT = + "ruam-isogloss-owner-plan-1" as const; + +export type IsoglossTranscriptBucket = 4 | 8 | 16 | 32; + +export interface IsoglossProductPolicy { + readonly format: typeof ISOGLOSS_PRODUCT_POLICY_FORMAT; + readonly allowedProfiles: readonly IsoglossDeploymentProfile[]; + /** + * Reject only when a constructive exact attack is strictly cheaper. + * Equality is an explicit policy tie and is admitted. + */ + readonly minimumExactAttackQueries: bigint; + readonly stateWidth: number; + /** Required for every non-local profile and forbidden for local mode. */ + readonly transcriptBucket: IsoglossTranscriptBucket | null; + readonly bprf: Readonly; +} + +/** Capabilities available in the selected build/deployment environment. */ +export interface IsoglossProfileCapabilities { + readonly supportedProfiles: readonly IsoglossDeploymentProfile[]; + readonly supportedTranscriptBuckets: readonly IsoglossTranscriptBucket[]; + readonly custodianAvailable: boolean; + readonly privateFunctionProtocolAvailable: boolean; + readonly attestedExecutionAvailable: boolean; +} + +/** Raw owner values never accepted by the client-manifest constructor. */ +export interface IsoglossOwnerSecrets { + readonly placementSecret?: string; + readonly relationSecret?: string; +} + +export type IsoglossPureInputDomain = + | { readonly type: "boolean" } + | { + readonly type: "number"; + readonly min: number; + readonly max: number; + }; + +/** + * The AST-native source path uses `pure-contract`; `lowered-contract` remains + * an adapter for the canonical compiler path. + */ +export type IsoglossPureRegionSource = + | { + readonly kind: "lowered-contract"; + readonly lowered: LoweredPureRegionContract; + } + | { + readonly kind: "pure-contract"; + readonly id: string; + readonly contract: PureRegionContract; + readonly inputDomains: readonly IsoglossPureInputDomain[]; + /** Real stages which must fit the fixed transcript bucket. */ + readonly protectedStageCount: number; + }; + +export interface IsoglossCanonicalCallGraphEvidence { + readonly evidence: "canonical-call-graph"; + readonly inventory: CanonicalCallGraphInventory; + readonly protectedUnitIds: readonly string[]; +} + +/** + * Frozen source-lowering evidence for AST-native expressions. Every field is + * positive proof; omission or false values fail closed. + */ +export interface IsoglossSourceExpressionCallRiskEvidence { + readonly format: "ruam-isogloss-source-call-risk-1"; + readonly evidence: "source-expression-structural-proof"; + readonly proofId: string; + readonly noCalls: true; + readonly noEffects: true; + readonly noReentrancy: true; + readonly noInterproceduralBoundaries: true; + readonly noRecursiveOrFissionScc: true; +} + +export type IsoglossMacroregionCallEvidence = + | IsoglossCanonicalCallGraphEvidence + | IsoglossSourceExpressionCallRiskEvidence; + +export interface IsoglossProductPlanRequest { + readonly region: IsoglossPureRegionSource; + readonly callEvidence: IsoglossMacroregionCallEvidence; + readonly profile: IsoglossDeploymentProfile; + readonly boundary: IsoglossCustodyBoundary; + readonly isGenerator: boolean; + readonly completeLocalFallbackPresent: boolean; + readonly policy: IsoglossProductPolicy; + readonly capabilities: IsoglossProfileCapabilities; + readonly ownerSecrets?: IsoglossOwnerSecrets; +} + +export type IsoglossPlanBlockerCode = + | "INCOMPLETE_DOMAIN_PROOF" + | "INCOMPLETE_LEARNABILITY_ANALYSIS" + | "NO_CONSTRUCTIVE_EXACT_ATTACK_BOUND" + | "CHEAP_KNOWN_EXACT_ATTACK" + | "PROFILE_FORBIDDEN_BY_POLICY" + | "UNSUPPORTED_PROFILE_CAPABILITY" + | "UNSUPPORTED_TRANSCRIPT_BUCKET" + | "TRANSCRIPT_BUCKET_TOO_SMALL" + | "MISSING_OWNER_PLACEMENT_SECRET" + | "MISSING_OWNER_RELATION_SECRET" + | "LOCAL_OWNER_SECRETS_FORBIDDEN" + | "INCOMPLETE_CALL_GRAPH_PROOF" + | "UNRESOLVED_CALL_BOUNDARY" + | "REENTRANT_CALL_BOUNDARY" + | "INTERPROCEDURAL_CALL_BOUNDARY" + | "RECURSIVE_OR_FISSION_SCC" + | "DEPLOYMENT_INELIGIBLE"; + +export interface IsoglossPlanBlocker { + readonly code: IsoglossPlanBlockerCode; + readonly detail: string; +} + +export interface IsoglossMacroregionCallRisk { + readonly evidence: + | "canonical-call-graph" + | "source-expression-structural-proof"; + readonly proofComplete: boolean; + readonly protectedUnitIds: readonly string[]; + readonly unresolvedBoundaryIds: readonly string[]; + readonly reentrantBoundaryIds: readonly string[]; + readonly interproceduralEdgeIds: readonly string[]; + readonly recursiveOrFissionSccIds: readonly string[]; +} + +export interface IsoglossPlanningAssessment { + readonly learnability: PureRegionLearnabilityAnalysis; + readonly learnabilityDecision: MaximumCustodyLearnabilityDecision; + readonly deployment: IsoglossDeploymentEligibility; + readonly callRisk: IsoglossMacroregionCallRisk; + readonly exactDomainProofComplete: boolean; + readonly hardnessLowerBound: null; +} + +export interface IsoglossSerializedAttackBound { + readonly method: "input-enumeration" | "dense-interpolation" | "tied"; + readonly queries: string; +} + +/** + * Serializable non-claim. `hardnessLowerBound` is always null: an admitted + * query threshold never becomes a resistance claim. + */ +export interface IsoglossClientLearnabilitySummary { + readonly minimumExactAttackQueries: string; + readonly cheapestKnownExactAttack: IsoglossSerializedAttackBound | null; + readonly thresholdTieAccepted: boolean; + readonly boundInterpretation: "constructive-exact-attack-upper-bound"; + readonly hardnessLowerBound: null; + readonly nonClaim: string; +} + +export interface IsoglossTranscriptClass { + readonly id: string; + readonly width: number; + readonly epochCount: IsoglossTranscriptBucket; + readonly coverCount: number; +} + +export type IsoglossClientExecution = + | { + readonly mode: "local-bprf"; + readonly clientComplete: true; + readonly localFallback: "not-applicable"; + readonly artifact: BprfArtifact; + } + | { + readonly mode: + | "masked-custody" + | "private-function-custody" + | "attested-execution"; + readonly clientComplete: false; + readonly localFallback: "forbidden"; + readonly ownerArtifactDigest: string; + readonly transcript: IsoglossTranscriptClass; + }; + +export interface IsoglossClientManifest { + readonly format: typeof ISOGLOSS_CLIENT_MANIFEST_FORMAT; + readonly planDigest: string; + readonly certificateDigest: string; + readonly regionCommitment: string; + readonly profile: IsoglossDeploymentProfile; + readonly clientCompleteness: + IsoglossDeploymentEligibility["clientCompleteness"]; + readonly schedulingContract: + IsoglossDeploymentEligibility["schedulingContract"]; + readonly fallbackPolicy: + IsoglossDeploymentEligibility["fallbackPolicy"]; + readonly securityClaim: IsoglossDeploymentEligibility["securityClaim"]; + readonly learnability: IsoglossClientLearnabilitySummary; + readonly execution: IsoglossClientExecution; +} + +export interface IsoglossEligibilityCertificate { + readonly format: typeof ISOGLOSS_ELIGIBILITY_CERTIFICATE_FORMAT; + readonly certificateDigest: string; + readonly eligible: true; + readonly profile: IsoglossDeploymentProfile; + readonly policyDigest: string; + readonly regionCommitment: string; + readonly ownerArtifactDigest: string; + readonly learnability: IsoglossClientLearnabilitySummary; + readonly deployment: IsoglossDeploymentEligibility; + readonly callRisk: IsoglossMacroregionCallRisk; + readonly hardnessLowerBound: null; + readonly nonClaim: string; +} + +export interface IsoglossOwnerMaterial { + readonly regionSource: IsoglossPureRegionSource; + readonly bprfArtifact: BprfArtifact; + readonly ownerSecrets: { + readonly placementSecret: string | null; + readonly relationSecret: string | null; + }; + readonly transcript: IsoglossTranscriptClass | null; +} + +/** Never serialize this object as a client artifact. */ +export interface IsoglossOwnerBuildPlan { + readonly format: typeof ISOGLOSS_OWNER_PLAN_FORMAT; + readonly planDigest: string; + readonly certificateDigest: string; + readonly policyDigest: string; + readonly ownerMaterialDigest: string; + readonly assessment: IsoglossPlanningAssessment; + readonly certificate: IsoglossEligibilityCertificate; + readonly ownerMaterial: IsoglossOwnerMaterial; +} + +export interface IsoglossEligibleProductPlan { + readonly decision: "eligible"; + readonly blockers: readonly []; + readonly assessment: IsoglossPlanningAssessment; + readonly ownerPlan: IsoglossOwnerBuildPlan; + readonly clientManifest: IsoglossClientManifest; +} + +export interface IsoglossRejectedProductPlan { + readonly decision: "rejected"; + readonly blockers: readonly IsoglossPlanBlocker[]; + readonly assessment: IsoglossPlanningAssessment; + readonly ownerPlan: null; + readonly clientManifest: null; +} + +export type IsoglossProductPlanResult = + | IsoglossEligibleProductPlan + | IsoglossRejectedProductPlan; diff --git a/packages/ruam/src/naming/claims.ts b/packages/ruam/src/naming/claims.ts deleted file mode 100644 index b36b5ba..0000000 --- a/packages/ruam/src/naming/claims.ts +++ /dev/null @@ -1,248 +0,0 @@ -/** - * @module naming/claims - * Canonical key definitions for all naming scopes. - * Maps RuntimeNames fields and TEMP_NAME_CATALOG entries to descriptive keys. - */ - -// --- Runtime Names (from naming/compat-types.ts RuntimeNames interface) --- - -/** Canonical keys matching the RuntimeNames interface fields (in generation order). */ -export const RUNTIME_KEYS = [ - "bt", - "vm", - "exec", - "execAsync", - "load", - "cache", - "depth", - "callStack", - "fp", - "rc4", - "b64", - "deser", - "dbg", - "dbgOp", - "dbgCfg", - "dbgProt", - "stk", - "stp", - "operand", - "scope", - "regs", - "ip", - "cArr", - "iArr", - "exStk", - "pEx", - "hPEx", - "cType", - "cVal", - "unit", - "args", - "outer", - "tVal", - "nTgt", - "ho", - "phys", - "opVar", - "thresh", - "tdzSentinel", - "strDec", - "fSlots", - "rcState", - "rcDeriveKey", - "rcMix", - "ihash", - "ihashFn", - "keyAnchor", - "router", - "routeMap", - "alpha", - "imul", - "spreadSym", - "hop", - "globalRef", - "icMix", - "icBlockKey", - "orVerify", -] as const; - -/** Runtime keys generated AFTER temps (for LCG sequence compatibility). */ -export const RUNTIME_POST_TEMP_KEYS = [ - "polyDec", - "polyPosSeed", - "strTbl", - "strCache", - "strAcc", - "btDecode", - "stkEnc", - "stkDec", -] as const; - -// --- Temp Names (from naming/claims.ts TEMP_NAME_KEYS) --- - -/** Canonical temp name keys (exact match of TEMP_NAME_CATALOG order). */ -export const TEMP_KEYS = [ - "_a", - "_b", - "_rv", - "_rv2", - "_te", - "_cu", - "_cuid", - "_fu", - "_fuid", - "_tv", - "_tt", - "_tgt", - "_tf", - "_ci", - "_fi", - "_sp", - "_iter", - "_done", - "_value", - "_async", - "_keys", - "_idx", - "_ho", - "_dbgId", - "_count", - "_opNames", - "_uid_", - "_uid", - "_g", - "_il", - "_ri", - "_ks", - "_h", - "_sek", - "_seRaw", - "_t", - "_sev", - "_dm", - "_now", - "_th", - "_tl", - "_pb", - "_fh", - "_o", - "_hr", - "_src", - "_it", - "_act", - "_gk", - "_k", - "_p1", - "_p2", - "_p3", - "_p4", - "_p5", - "_p6", - "_run", - "_s1", - "_s2", - "_e1", - "_e2", - "_ts", - "_i", - "_s", - "_sm", - "_av", - "_vr", - "_d", - "_st", - "_cs", - "_ch", - "_nc", - "_fn", - "_ft", - "_n", - "_det", - "_nx", - "_tid", - "_ki", - "_ue", - "_ji", - "_ps", - "_psv", - "_ht", - "_nf", - "_htd", - "_htk", - "_hti", - "_htv", - "_htw", - "_dr", - "_dv", - "_dof", - "_du8", - "_du16", - "_du32", - "_di32", - "_df64", - "_drs", - "_dfl", - "_dpc", - "_drc", - "_dcc", - "_dcs", - "_dic", - "_din", - "_dtag", - "_del", - "_dea", - "_dei", - "_frs", - "_frv", - "_fdi", - "_fg0", - "_fg1", - "_fg2", - "_fg3", - "_ms", - "_mk", - "_mi", - "_mj", - "_mt", - "_icState", - "_icBk", - "_djtc", - "_detc", - "_dblc", - "_dblm", - "_orRef", - "_orExp", - "_orW", - "_orWv", -] as const; - -/** Fields of RuntimeNames that are shared across all shielding groups. */ -export const SHARED_RUNTIME_KEYS = [ - "bt", - "cache", - "depth", - "callStack", - "fp", - "rc4", - "b64", - "deser", - "dbg", - "dbgOp", - "dbgCfg", - "dbgProt", - "router", - "routeMap", - "tdzSentinel", - "alpha", - "imul", - "spreadSym", - "hop", - "globalRef", - "strTbl", - "strCache", - "strAcc", - "btDecode", - "stkEnc", - "stkDec", -] as const; diff --git a/packages/ruam/src/naming/compat-types.ts b/packages/ruam/src/naming/compat-types.ts deleted file mode 100644 index 23641e0..0000000 --- a/packages/ruam/src/naming/compat-types.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @module naming/compat-types - * RuntimeNames and TempNames interfaces — canonical type definitions. - * These interfaces are used throughout the codebase as the shape of - * randomized identifier mappings produced by the NameRegistry. - */ - -/** - * Mapping of logical role to generated identifier name for all - * runtime-internal variables and functions. - */ -export interface RuntimeNames { - bt: string; - vm: string; - exec: string; - execAsync: string; - load: string; - cache: string; - depth: string; - callStack: string; - fp: string; - rc4: string; - b64: string; - deser: string; - dbg: string; - dbgOp: string; - dbgCfg: string; - dbgProt: string; - stk: string; - stp: string; - operand: string; - scope: string; - regs: string; - ip: string; - cArr: string; - iArr: string; - exStk: string; - pEx: string; - hPEx: string; - cType: string; - cVal: string; - unit: string; - args: string; - outer: string; - tVal: string; - nTgt: string; - ho: string; - phys: string; - opVar: string; - thresh: string; - tdzSentinel: string; - strDec: string; - fSlots: string; - rcState: string; - rcDeriveKey: string; - rcMix: string; - icMix: string; - icBlockKey: string; - orVerify: string; - ihash: string; - ihashFn: string; - keyAnchor: string; - router: string; - routeMap: string; - alpha: string; - imul: string; - spreadSym: string; - hop: string; - globalRef: string; - polyDec: string; - polyPosSeed: string; - strTbl: string; - strCache: string; - strAcc: string; - btDecode: string; - /** Stack-encoding helper: encode a value into a `[tag,payload]` entry (IIFE scope). */ - stkEnc: string; - /** Stack-encoding helper: decode a `[tag,payload]` entry back to a value (IIFE scope). */ - stkDec: string; -} - -/** - * Randomized names for handler/builder temporary variables and - * internal object property keys. - */ -export type TempNames = Readonly>; diff --git a/packages/ruam/src/naming/index.ts b/packages/ruam/src/naming/index.ts deleted file mode 100644 index b6da5f6..0000000 --- a/packages/ruam/src/naming/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @module naming - * Unified naming system — NameRegistry + NameScope + NameToken. - */ - -export { - NameToken, - RestParam, - type Name, - resolveName, - isName, -} from "./token.js"; -export { NameScope, type LengthTier, deriveSeed } from "./scope.js"; -export { NameRegistry } from "./registry.js"; -export { RESERVED_WORDS, EXCLUDED_NAMES } from "./reserved.js"; -export { - setupRegistry, - setupShieldedRegistry, - type RegistryResult, - type ShieldedRegistryResult, -} from "./setup.js"; -export { - RUNTIME_KEYS, - RUNTIME_POST_TEMP_KEYS, - TEMP_KEYS, - SHARED_RUNTIME_KEYS, -} from "./claims.js"; -export type { RuntimeNames, TempNames } from "./compat-types.js"; diff --git a/packages/ruam/src/naming/registry.ts b/packages/ruam/src/naming/registry.ts deleted file mode 100644 index 64ca4e3..0000000 --- a/packages/ruam/src/naming/registry.ts +++ /dev/null @@ -1,264 +0,0 @@ -/** - * @module naming/registry - * Central coordinator for all per-build randomized identifiers. - */ - -import { NameScope, type LengthTier, deriveSeed } from "./scope.js"; -import { RESERVED_WORDS, EXCLUDED_NAMES } from "./reserved.js"; -import { - LCG_MULTIPLIER, - LCG_INCREMENT, - GLOBAL_IDENTIFIERS, -} from "../constants.js"; - -// --- Alphabet --- - -const ALPHABET_BASE = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_$"; - -// --- NameRegistry --- - -export class NameRegistry { - private readonly _seed: number; - private readonly _scopes: NameScope[] = []; - private readonly _globalUsed: Set; - private _resolved = false; - private _alphabet: string | null = null; - - constructor(seed: number) { - this._seed = seed >>> 0; - // Exclude reserved words, excluded short names, AND all well-known - // globals — a generated identifier emitted as a top-level `var X = …` - // must never collide with a host global. Read-only globals the runtime - // does not itself use (e.g. `Bun`, `Deno`, `NaN`, `Infinity`, - // `globalThis`) would throw "assign to readonly property"; runtime-used - // globals (`Object`, `Map`, …) would shadow and break the VM. Excluding - // the canonical GLOBAL_IDENTIFIERS set closes both at the source. - this._globalUsed = new Set([ - ...RESERVED_WORDS, - ...EXCLUDED_NAMES, - ...GLOBAL_IDENTIFIERS, - ]); - } - - /** Whether resolveAll() has been called. */ - get isResolved(): boolean { - return this._resolved; - } - - /** Exclude additional names from generation. Must be called before resolveAll(). */ - exclude(names: Iterable): void { - if (this._resolved) { - throw new Error( - "Registry is frozen — cannot exclude after resolveAll()" - ); - } - for (const name of names) { - this._globalUsed.add(name); - } - } - - /** Create a child scope. If parent is provided, the scope is nested under it. */ - createScope( - id: string, - opts?: { parent?: NameScope; lengthTier?: LengthTier } - ): NameScope { - if (this._resolved) { - throw new Error( - "Registry is frozen — cannot create scope after resolveAll()" - ); - } - const parent = opts?.parent ?? null; - const lengthTier = opts?.lengthTier ?? "medium"; - const scope = new NameScope(id, this._seed, lengthTier, parent); - if (parent) { - parent.children.push(scope); - } - this._scopes.push(scope); - return scope; - } - - /** Resolve all tokens across all scopes. Guarantees no collisions. - * Uses depth-first tree walk (parent before children) — NOT registration order. */ - resolveAll(): void { - if (this._resolved) { - throw new Error("resolveAll() already called"); - } - - // Generate alphabet first (doesn't consume identifier namespace) - this._generateAlphabet(); - - // Depth-first walk: resolve root scopes, then their children recursively - const rootScopes = this._scopes.filter((s) => s.parent === null); - const walkResolve = (scope: NameScope): void => { - this._resolveScope(scope); - scope.freeze(); - for (const child of scope.children) { - walkResolve(child); - } - }; - for (const root of rootScopes) { - walkResolve(root); - } - - this._resolved = true; - } - - /** Get the 64-char encoding alphabet. Only available after resolveAll(). */ - getAlphabet(): string { - if (this._alphabet === null) { - throw new Error( - "Alphabet not yet generated — call resolveAll() first" - ); - } - return this._alphabet; - } - - /** Dump all resolved names for debugging. Returns scopeId:key -> resolved name. */ - dumpAll(): Map { - const result = new Map(); - for (const scope of this._scopes) { - for (const [key, token] of scope.tokens) { - const qualifiedKey = `${scope.id}:${key}`; - try { - result.set(qualifiedKey, token.name); - } catch { - result.set(qualifiedKey, ""); - } - } - } - return result; - } - - /** Total number of tokens across all scopes. */ - get tokenCount(): number { - let count = 0; - for (const scope of this._scopes) { - count += scope.tokens.size; - } - return count; - } - - /** - * Create a dynamic name generator for on-demand identifier allocation. - * - * Unlike `createScope()` + `claim()` + `resolveAll()`, this returns a - * `() => string` closure that generates collision-free names one at a - * time — suitable for systems where the number of names is not known - * until build time (e.g. bytecode scattering, scattered keys). - * - * Uses the registry's global used-name set for collision avoidance, - * and `deriveSeed()` for PRNG isolation. - * - * Must be called after `resolveAll()`. - * - * @param scopeId - Descriptive identifier for seed derivation - * @param lengthTier - Name length range (default "medium") - */ - createDynamicGenerator( - scopeId: string, - lengthTier: LengthTier = "medium" - ): () => string { - if (!this._resolved) { - throw new Error( - "createDynamicGenerator() requires resolveAll() first" - ); - } - const LENGTH_RANGES: Record = { - short: [2, 3], - medium: [3, 4], - long: [4, 5], - }; - const ALPHA = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const ALNUM = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - const [minLen, maxLen] = LENGTH_RANGES[lengthTier]; - let state = deriveSeed(this._seed, scopeId); - const globalUsed = this._globalUsed; - let lengthBump = 0; - const MAX_RETRIES = 50; - - return () => { - let retries = 0; - for (;;) { - state = - (Math.imul(state, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - const effectiveMin = minLen + lengthBump; - const effectiveMax = Math.max( - maxLen + lengthBump, - effectiveMin - ); - const len = - effectiveMin + (state % (effectiveMax - effectiveMin + 1)); - state = - (Math.imul(state, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - let name = ALPHA[state % ALPHA.length]!; - for (let i = 1; i < len; i++) { - state = - (Math.imul(state, LCG_MULTIPLIER) + LCG_INCREMENT) >>> - 0; - name += ALNUM[state % ALNUM.length]!; - } - if (!globalUsed.has(name)) { - globalUsed.add(name); - return name; - } - retries++; - if (retries > MAX_RETRIES) { - lengthBump++; - retries = 0; - } - } - }; - } - - // --- Private --- - - private _resolveScope(scope: NameScope): void { - const MAX_RETRIES = 50; - const ALNUM = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - // Length bump carries across tokens within a scope — if previous - // tokens needed longer names, subsequent tokens start at that - // length instead of burning 50 retries each to rediscover it. - let lengthBump = 0; - - for (const [, token] of scope.tokens) { - let retries = 0; - let candidate: string; - - do { - candidate = scope.generateCandidate(); - // Force longer name when bump is active - if (lengthBump > 0 && candidate.length < 2 + lengthBump) { - while (candidate.length < 2 + lengthBump) { - candidate += ALNUM[scope.nextPrng() % ALNUM.length]!; - } - } - retries++; - if (retries > MAX_RETRIES) { - lengthBump++; - retries = 0; - } - } while (this._globalUsed.has(candidate)); - - this._globalUsed.add(candidate); - token.resolve(candidate); - } - } - - private _generateAlphabet(): void { - const chars = ALPHABET_BASE.split(""); - const codecSeed = deriveSeed(this._seed, "codec"); - let s = codecSeed; - for (let i = chars.length - 1; i > 0; i--) { - s = (Math.imul(s, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - const j = s % (i + 1); - const tmp = chars[i]!; - chars[i] = chars[j]!; - chars[j] = tmp; - } - this._alphabet = chars.join(""); - } -} diff --git a/packages/ruam/src/naming/setup.ts b/packages/ruam/src/naming/setup.ts deleted file mode 100644 index 9729609..0000000 --- a/packages/ruam/src/naming/setup.ts +++ /dev/null @@ -1,282 +0,0 @@ -/** - * @module naming/setup - * Bridge between NameRegistry and the existing RuntimeNames/TempNames interface. - * - * Creates a NameRegistry, claims all required tokens, resolves them, - * and produces RuntimeNames + TempNames objects compatible with the - * existing codebase. This lets us centralize name generation without - * changing every consumer. - */ - -import { NameRegistry } from "./registry.js"; -import { NameScope } from "./scope.js"; -import { NameToken } from "./token.js"; -import type { RuntimeNames, TempNames } from "./compat-types.js"; -import { - RUNTIME_KEYS, - RUNTIME_POST_TEMP_KEYS, - TEMP_KEYS, - SHARED_RUNTIME_KEYS, -} from "./claims.js"; - -// --- Types --- - -export interface RegistryResult { - registry: NameRegistry; - runtime: RuntimeNames; - temps: TempNames; - alphabet: string; -} - -export interface ShieldedRegistryResult { - registry: NameRegistry; - shared: RuntimeNames; - sharedTemps: TempNames; - groups: RuntimeNames[]; - groupTemps: TempNames[]; - alphabet: string; -} - -// --- Helpers --- - -/** Claim RUNTIME_KEYS in a scope and return token map. */ -function claimRuntimeKeys(scope: NameScope): Map { - const map = new Map(); - for (const key of RUNTIME_KEYS) { - map.set(key, scope.claim(key)); - } - return map; -} - -/** Claim RUNTIME_POST_TEMP_KEYS in a scope and add to existing map. */ -function claimPostTempKeys( - scope: NameScope, - map: Map -): void { - for (const key of RUNTIME_POST_TEMP_KEYS) { - map.set(key, scope.claim(key)); - } -} - -/** Claim TEMP_KEYS in a scope and return token map. */ -function claimTempKeys(scope: NameScope): Map { - const map = new Map(); - for (const key of TEMP_KEYS) { - map.set(key, scope.claim(key)); - } - return map; -} - -/** Build a RuntimeNames object from resolved tokens. */ -function buildRuntimeNames(tokens: Map): RuntimeNames { - const get = (key: string): string => { - const t = tokens.get(key); - if (!t) throw new Error(`Missing runtime token: ${key}`); - return t.name; - }; - return { - bt: get("bt"), - vm: get("vm"), - exec: get("exec"), - execAsync: get("execAsync"), - load: get("load"), - cache: get("cache"), - depth: get("depth"), - callStack: get("callStack"), - fp: get("fp"), - rc4: get("rc4"), - b64: get("b64"), - deser: get("deser"), - dbg: get("dbg"), - dbgOp: get("dbgOp"), - dbgCfg: get("dbgCfg"), - dbgProt: get("dbgProt"), - stk: get("stk"), - stp: get("stp"), - operand: get("operand"), - scope: get("scope"), - regs: get("regs"), - ip: get("ip"), - cArr: get("cArr"), - iArr: get("iArr"), - exStk: get("exStk"), - pEx: get("pEx"), - hPEx: get("hPEx"), - cType: get("cType"), - cVal: get("cVal"), - unit: get("unit"), - args: get("args"), - outer: get("outer"), - tVal: get("tVal"), - nTgt: get("nTgt"), - ho: get("ho"), - phys: get("phys"), - opVar: get("opVar"), - thresh: get("thresh"), - tdzSentinel: get("tdzSentinel"), - strDec: get("strDec"), - fSlots: get("fSlots"), - rcState: get("rcState"), - rcDeriveKey: get("rcDeriveKey"), - rcMix: get("rcMix"), - icMix: get("icMix"), - icBlockKey: get("icBlockKey"), - orVerify: get("orVerify"), - ihash: get("ihash"), - ihashFn: get("ihashFn"), - keyAnchor: get("keyAnchor"), - router: get("router"), - routeMap: get("routeMap"), - alpha: get("alpha"), - imul: get("imul"), - spreadSym: get("spreadSym"), - hop: get("hop"), - globalRef: get("globalRef"), - polyDec: get("polyDec"), - polyPosSeed: get("polyPosSeed"), - strTbl: get("strTbl"), - strCache: get("strCache"), - strAcc: get("strAcc"), - btDecode: get("btDecode"), - stkEnc: get("stkEnc"), - stkDec: get("stkDec"), - }; -} - -/** Build a TempNames object from resolved tokens. */ -function buildTempNames(tokens: Map): TempNames { - const result: Record = {}; - for (const key of TEMP_KEYS) { - const t = tokens.get(key); - if (!t) throw new Error(`Missing temp token: ${key}`); - result[key] = t.name; - } - return result as TempNames; -} - -/** Override shared fields on a group's RuntimeNames with shared values. */ -function applySharedOverrides( - groupNames: RuntimeNames, - sharedNames: RuntimeNames -): void { - for (const key of SHARED_RUNTIME_KEYS) { - (groupNames as unknown as Record)[key] = - sharedNames[key]; - } -} - -// --- Public API --- - -/** - * Create a NameRegistry and produce RuntimeNames + TempNames. - * Drop-in replacement for generateRuntimeNames + generateAlphabet. - */ -export function setupRegistry( - seed: number, - additionalExclusions?: Set -): RegistryResult { - const registry = new NameRegistry(seed); - if (additionalExclusions) { - registry.exclude(additionalExclusions); - } - - // Runtime names scope - const runtimeScope = registry.createScope("runtime", { - lengthTier: "short", - }); - const runtimeTokens = claimRuntimeKeys(runtimeScope); - - // Temp names scope (claimed after runtime, before post-temp) - const tempScope = registry.createScope("temps", { lengthTier: "short" }); - const tempTokens = claimTempKeys(tempScope); - - // Post-temp runtime keys (must be after temps for LCG sequence compat) - claimPostTempKeys(runtimeScope, runtimeTokens); - - // Resolve all names - registry.resolveAll(); - - return { - registry, - runtime: buildRuntimeNames(runtimeTokens), - temps: buildTempNames(tempTokens), - alphabet: registry.getAlphabet(), - }; -} - -/** - * Create a NameRegistry for VM Shielding mode. - * Drop-in replacement for generateShieldedNames + generateAlphabet. - */ -export function setupShieldedRegistry( - sharedSeed: number, - groupSeeds: number[], - additionalExclusions?: Set -): ShieldedRegistryResult { - const registry = new NameRegistry(sharedSeed); - if (additionalExclusions) { - registry.exclude(additionalExclusions); - } - - // Shared runtime names - const sharedRtScope = registry.createScope("shared_runtime", { - lengthTier: "short", - }); - const sharedRtTokens = claimRuntimeKeys(sharedRtScope); - - // Shared temp names - const sharedTempScope = registry.createScope("shared_temps", { - lengthTier: "short", - }); - const sharedTempTokens = claimTempKeys(sharedTempScope); - - // Shared post-temp keys - claimPostTempKeys(sharedRtScope, sharedRtTokens); - - // Per-group scopes - const groupRtTokens: Map[] = []; - const groupTempTokens: Map[] = []; - - for (let i = 0; i < groupSeeds.length; i++) { - const gRtScope = registry.createScope(`group${i}_runtime`, { - lengthTier: "short", - }); - const gRtToks = claimRuntimeKeys(gRtScope); - - const gTempScope = registry.createScope(`group${i}_temps`, { - lengthTier: "short", - }); - const gTempToks = claimTempKeys(gTempScope); - - claimPostTempKeys(gRtScope, gRtToks); - - groupRtTokens.push(gRtToks); - groupTempTokens.push(gTempToks); - } - - // Resolve all names - registry.resolveAll(); - - // Build results - const sharedNames = buildRuntimeNames(sharedRtTokens); - const sharedTemps = buildTempNames(sharedTempTokens); - - const groups: RuntimeNames[] = []; - const groupTemps: TempNames[] = []; - - for (let i = 0; i < groupSeeds.length; i++) { - const gNames = buildRuntimeNames(groupRtTokens[i]!); - applySharedOverrides(gNames, sharedNames); - groups.push(gNames); - groupTemps.push(buildTempNames(groupTempTokens[i]!)); - } - - return { - registry, - shared: sharedNames, - sharedTemps, - groups, - groupTemps, - alphabet: registry.getAlphabet(), - }; -} diff --git a/packages/ruam/src/option-meta.ts b/packages/ruam/src/option-meta.ts deleted file mode 100644 index 9b677c3..0000000 --- a/packages/ruam/src/option-meta.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Option metadata — single source of truth for all CLI/UI option definitions. - * - * This file defines option labels, categories, descriptions, and CLI flags. - * The build-time manifest generator reads this to produce a JSON manifest - * that the website consumes, eliminating hardcoded option definitions in - * the Playground component. - * - * @module option-meta - */ - -// --- Types --- - -/** UI category for grouping options. */ -export type OptionCategory = "security" | "obfuscation" | "optimization"; - -/** Metadata for a single boolean option. */ -export interface OptionMetaEntry { - /** Property key on VmObfuscationOptions. */ - key: string; - /** Human-readable label for display. */ - label: string; - /** UI category for grouping. */ - category: OptionCategory; - /** Short description of what the option does. */ - description: string; - /** CLI flag (e.g. "--rolling-cipher"). */ - cliFlag: string; -} - -/** Auto-enable rule: when `when` is enabled, `enables` is forced on. */ -export interface AutoEnableRule { - when: string; - enables: string; -} - -// --- Option definitions --- - -/** - * Complete metadata for all boolean obfuscation options. - * Order determines UI display order within categories. - */ -export const OPTION_META: OptionMetaEntry[] = [ - // Security - { - key: "rollingCipher", - label: "Rolling Cipher", - category: "security", - description: "Position-dependent XOR encryption on every instruction", - cliFlag: "--rolling-cipher", - }, - { - key: "integrityBinding", - label: "Integrity Binding", - category: "security", - description: "Bind bytecode decryption to interpreter source integrity", - cliFlag: "--integrity-binding", - }, - { - key: "debugProtection", - label: "Debug Protection", - category: "security", - description: "Multi-layered anti-debugger with escalating response", - cliFlag: "--debug-protection", - }, - { - key: "vmShielding", - label: "VM Shielding", - category: "security", - description: - "Per-function micro-interpreters with independent opcode shuffle", - cliFlag: "--vm-shielding", - }, - { - key: "incrementalCipher", - label: "Incremental Cipher", - category: "security", - description: "Move instruction decryption into the VM dispatch loop", - cliFlag: "--incremental-cipher", - }, - { - key: "semanticOpacity", - label: "Semantic Opacity", - category: "security", - description: - "Opaque predicates, handler aliasing, and encoding diversity", - cliFlag: "--semantic-opacity", - }, - { - key: "observationResistance", - label: "Observation Resistance", - category: "security", - description: - "Silent computation corruption when instrumentation detected", - cliFlag: "--observation-resistance", - }, - { - key: "encryptBytecode", - label: "Encrypt Bytecode", - category: "security", - description: "RC4 encryption using an environment fingerprint key", - cliFlag: "--encrypt", - }, - - // Obfuscation - { - key: "mixedBooleanArithmetic", - label: "MBA", - category: "obfuscation", - description: "Replace arithmetic/bitwise ops with MBA expressions", - cliFlag: "--mba", - }, - { - key: "stackEncoding", - label: "Stack Encoding", - category: "obfuscation", - description: "XOR-encode VM stack values during execution", - cliFlag: "--stack-encoding", - }, - { - key: "deadCodeInjection", - label: "Dead Code Injection", - category: "obfuscation", - description: "Insert unreachable bytecode sequences after RETURN", - cliFlag: "--dead-code", - }, - { - key: "handlerFragmentation", - label: "Handler Fragmentation", - category: "obfuscation", - description: "Split handlers into interleaved fragments", - cliFlag: "--handler-fragmentation", - }, - { - key: "stringAtomization", - label: "String Atomization", - category: "obfuscation", - description: "Replace string literals with encoded table lookups", - cliFlag: "--string-atomization", - }, - { - key: "blockPermutation", - label: "Block Permutation", - category: "obfuscation", - description: "Shuffle bytecode basic block order", - cliFlag: "--block-permutation", - }, - { - key: "opcodeMutation", - label: "Opcode Mutation", - category: "obfuscation", - description: "Runtime handler table mutations via MUTATE opcodes", - cliFlag: "--opcode-mutation", - }, - { - key: "bytecodeScattering", - label: "Bytecode Scattering", - category: "obfuscation", - description: - "Split bytecode into mixed-type fragments scattered through output", - cliFlag: "--bytecode-scattering", - }, - - // Optimization - { - key: "preprocessIdentifiers", - label: "Rename Identifiers", - category: "optimization", - description: "Rename identifiers before compilation", - cliFlag: "--preprocess", - }, - { - key: "dynamicOpcodes", - label: "Dynamic Opcodes", - category: "optimization", - description: "Filter unused opcode handlers from interpreter", - cliFlag: "--dynamic-opcodes", - }, - { - key: "decoyOpcodes", - label: "Decoy Opcodes", - category: "optimization", - description: "Inject realistic fake opcode handlers", - cliFlag: "--decoy-opcodes", - }, - { - key: "polymorphicDecoder", - label: "Polymorphic Decoder", - category: "optimization", - description: "Per-build random chain of reversible byte operations", - cliFlag: "--polymorphic-decoder", - }, - { - key: "scatteredKeys", - label: "Scattered Keys", - category: "optimization", - description: "Fragment key materials across closure tiers", - cliFlag: "--scattered-keys", - }, -]; - -// --- Auto-enable rules --- - -/** - * Rules that auto-enable dependent options. - * Applied by resolveOptions() in presets.ts. - */ -export const AUTO_ENABLE_RULES: AutoEnableRule[] = [ - { when: "integrityBinding", enables: "rollingCipher" }, - { when: "vmShielding", enables: "rollingCipher" }, - { when: "stringAtomization", enables: "polymorphicDecoder" }, - { when: "opcodeMutation", enables: "rollingCipher" }, - { when: "incrementalCipher", enables: "rollingCipher" }, - { when: "observationResistance", enables: "rollingCipher" }, -]; - -// --- Derived exports for CLI backward compat --- - -/** - * Map of option key → human-readable label. - * Used by CLI for display output. - */ -export const OPTION_LABELS: Record = Object.fromEntries( - OPTION_META.map((m) => [m.key, m.label]) -); diff --git a/packages/ruam/src/presets.ts b/packages/ruam/src/presets.ts deleted file mode 100644 index 9c2d4c1..0000000 --- a/packages/ruam/src/presets.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Built-in preset configurations (low / medium / high) and option resolution. - * @module presets - */ - -import type { - VmObfuscationOptions, - PresetName, - TargetEnvironment, -} from "./types.js"; - -/** - * Preset configurations keyed by name. - * - * - `low` -- VM compilation only. - * - `medium` -- Adds identifier renaming, bytecode encryption, and decoy opcodes. - * - `max` -- Maximum protection: debug protection, dead code injection, stack encoding. - */ -export const PRESETS: Record< - PresetName, - Required> -> = { - low: { - targetMode: "root", - threshold: 1.0, - preprocessIdentifiers: false, - encryptBytecode: false, - debugProtection: false, - dynamicOpcodes: true, - decoyOpcodes: false, - deadCodeInjection: false, - stackEncoding: false, - rollingCipher: false, - integrityBinding: false, - vmShielding: false, - mixedBooleanArithmetic: false, - handlerFragmentation: false, - stringAtomization: false, - polymorphicDecoder: false, - scatteredKeys: false, - blockPermutation: false, - opcodeMutation: false, - bytecodeScattering: false, - incrementalCipher: false, - semanticOpacity: false, - observationResistance: false, - }, - medium: { - targetMode: "root", - threshold: 1.0, - preprocessIdentifiers: true, - encryptBytecode: true, - debugProtection: false, - dynamicOpcodes: true, - decoyOpcodes: true, - deadCodeInjection: false, - stackEncoding: false, - rollingCipher: true, - integrityBinding: false, - vmShielding: false, - mixedBooleanArithmetic: false, - handlerFragmentation: false, - stringAtomization: true, - polymorphicDecoder: true, - scatteredKeys: true, - blockPermutation: false, - opcodeMutation: false, - bytecodeScattering: true, - incrementalCipher: false, - semanticOpacity: false, - observationResistance: false, - }, - max: { - targetMode: "root", - threshold: 1.0, - preprocessIdentifiers: true, - encryptBytecode: true, - debugProtection: true, - dynamicOpcodes: true, - decoyOpcodes: true, - deadCodeInjection: true, - stackEncoding: true, - rollingCipher: true, - integrityBinding: true, - vmShielding: true, - mixedBooleanArithmetic: true, - handlerFragmentation: true, - stringAtomization: true, - polymorphicDecoder: true, - scatteredKeys: true, - blockPermutation: true, - opcodeMutation: true, - bytecodeScattering: true, - incrementalCipher: true, - semanticOpacity: true, - observationResistance: true, - }, -}; - -/** - * Resolved options with internal fields derived from the target environment. - * - * `wrapOutput` is not part of the public API — it is set automatically - * based on {@link VmObfuscationOptions.target}. - */ -export interface ResolvedOptions extends VmObfuscationOptions { - /** @internal Wrap the entire output in an IIFE. Set by `target`. */ - wrapOutput?: boolean; -} - -/** Target-specific default settings. */ -const TARGET_DEFAULTS: Record> = { - node: {}, - browser: {}, - "browser-extension": { wrapOutput: true }, -}; - -/** - * Resolve options by merging a preset (if specified) with explicit overrides, - * then applying target-environment defaults. - * - * Priority: explicit options > preset values > target defaults. - * - * @param options - User-supplied options, optionally referencing a preset. - * @returns Fully resolved options with preset defaults filled in. - */ -export function resolveOptions( - options: VmObfuscationOptions = {} -): ResolvedOptions { - let resolved: ResolvedOptions; - - if (options.preset) { - const preset = PRESETS[options.preset]; - const { preset: _discard, ...explicit } = options; - - resolved = { ...preset }; - for (const [key, value] of Object.entries(explicit)) { - if (value !== undefined) { - (resolved as Record)[key] = value; - } - } - } else { - resolved = { ...options }; - } - - // Apply target-environment defaults (only for fields not explicitly set) - const target = resolved.target ?? "browser"; - const targetDefaults = TARGET_DEFAULTS[target]; - for (const [key, value] of Object.entries(targetDefaults)) { - if ((resolved as Record)[key] === undefined) { - (resolved as Record)[key] = value; - } - } - - // integrityBinding requires rollingCipher — auto-enable it - if (resolved.integrityBinding && !resolved.rollingCipher) { - resolved.rollingCipher = true; - } - - // vmShielding requires rollingCipher (implicit per-unit key derivation) - if (resolved.vmShielding && !resolved.rollingCipher) { - resolved.rollingCipher = true; - } - - // stringAtomization requires polymorphicDecoder - if (resolved.stringAtomization && !resolved.polymorphicDecoder) { - resolved.polymorphicDecoder = true; - } - - // opcodeMutation requires rollingCipher (entangled key derivation) - if (resolved.opcodeMutation && !resolved.rollingCipher) { - resolved.rollingCipher = true; - } - - // incrementalCipher requires rollingCipher - if (resolved.incrementalCipher && !resolved.rollingCipher) { - resolved.rollingCipher = true; - } - - // observationResistance requires rollingCipher - if (resolved.observationResistance && !resolved.rollingCipher) { - resolved.rollingCipher = true; - } - - return resolved; -} diff --git a/packages/ruam/src/random/entropy.ts b/packages/ruam/src/random/entropy.ts new file mode 100644 index 0000000..fcfde6e --- /dev/null +++ b/packages/ruam/src/random/entropy.ts @@ -0,0 +1,72 @@ +/** + * Build-time entropy and deterministic pseudo-random streams. + * + * Production builds draw independent 32-bit values from the platform CSPRNG. + * Tests inject a deterministic source so every randomized build decision can + * be reproduced from one seed. + * + * @module random/entropy + */ + +import { randomBytes } from "node:crypto"; +import { deriveSeed, lcgNext } from "../naming/scope.js"; + +/** Source of labeled 32-bit build-time entropy. */ +export interface BuildEntropy { + /** + * Return the next unsigned 32-bit value for a logical responsibility. + * + * Labels are diagnostic for production entropy and define independent, + * reproducible streams for deterministic test entropy. + */ + nextUint32(label: string): number; +} + +/** Seeded stream used for deterministic selections within one responsibility. */ +export interface SeededRandom { + nextUint32(): number; + nextFloat(): number; +} + +/** Create the production CSPRNG-backed entropy source. */ +export function createCryptoEntropy(): BuildEntropy { + return { + nextUint32(_label: string): number { + return randomBytes(4).readUInt32LE(0); + }, + }; +} + +/** + * Create a deterministic entropy source for tests and failure reproduction. + * + * Each label owns an independent counter-derived stream, so adding entropy use + * in one subsystem does not perturb existing values in another subsystem. + */ +export function createDeterministicEntropy(seed: number): BuildEntropy { + const counters = new Map(); + const root = seed >>> 0; + + return { + nextUint32(label: string): number { + const counter = counters.get(label) ?? 0; + counters.set(label, counter + 1); + return lcgNext(deriveSeed(root, `${label}:${counter}`)); + }, + }; +} + +/** Create a deterministic LCG stream from an already isolated seed. */ +export function createSeededRandom(seed: number): SeededRandom { + let state = seed >>> 0; + return { + nextUint32(): number { + state = lcgNext(state); + return state; + }, + nextFloat(): number { + state = lcgNext(state); + return state / 0x1_0000_0000; + }, + }; +} diff --git a/packages/ruam/src/ruamvm/assembler.ts b/packages/ruam/src/ruamvm/assembler.ts deleted file mode 100644 index 79a0dde..0000000 --- a/packages/ruam/src/ruamvm/assembler.ts +++ /dev/null @@ -1,944 +0,0 @@ -/** - * VM runtime code generator. - * - * Produces a self-contained IIFE that contains: - * - The interpreter cores (sync / async) - * - The dispatch functions - * - A bytecode loader + cache - * - Optional: fingerprint + RC4 decoder, debug protection, debug logging - * - Steganographic watermark (folded into key anchor) - * - * Uses AST-based builders from `ruamvm/builders/` instead of template - * literals, then emits via `ruamvm/emit.ts`. - * - * @module ruamvm/assembler - */ - -import { OPCODE_COUNT } from "../compiler/opcodes.js"; -import type { RuntimeNames, TempNames } from "../naming/compat-types.js"; -import type { JsNode } from "./nodes.js"; -import { - exprStmt, - lit, - varDecl, - arr, - obj, - assign, - bin, - un, - call, - member, - id, - ternary, - BOp, - UOp, -} from "./nodes.js"; -import { emit } from "./emit.js"; -import { buildFingerprintSource } from "./builders/fingerprint.js"; -import { - buildBinaryDecoderSource, - buildRc4Source, - buildStringDecoderSource, -} from "./builders/decoder.js"; -import { buildDebugProtection } from "./builders/debug-protection.js"; -import { buildDebugLogging } from "./builders/debug-logging.js"; -import { buildRollingCipherSource } from "./builders/rolling-cipher.js"; -import { buildIncrementalCipherSource } from "./builders/incremental-cipher.js"; -import { - buildInterpreterFunctions, - buildStackEncodingHelpers, -} from "./builders/interpreter.js"; -import { buildRunners, buildRouter } from "./builders/runners.js"; -import { buildLoader } from "./builders/loader.js"; -import { buildDeserializer } from "./builders/deserializer.js"; -import { buildGlobalExposure } from "./builders/globals.js"; -import { buildDecodeFunction } from "./builders/unpack.js"; -import { - buildIdentityBindings, - buildWitnessCounter, - buildWeakMapCanary, -} from "./observation-resistance.js"; -import { makeConstantSplitter } from "./constant-splitting.js"; -import type { SplitFn } from "./constant-splitting.js"; -import type { StructuralChoices } from "../structural-choices.js"; -import { applyStructuralTransforms } from "./structural-transforms.js"; -import { - generateDecoderChain, - buildDecoderFunctionAST, -} from "./polymorphic-decoder.js"; -import { atomizeStrings } from "./string-atomization.js"; -import { scatterKeyMaterials } from "./scattered-keys.js"; -import type { NameRegistry } from "../naming/registry.js"; -import { deriveSeed } from "../naming/scope.js"; -import { LCG_MULTIPLIER, LCG_INCREMENT } from "../constants.js"; - -/** Result from generating the VM runtime. */ -export interface VmRuntimeResult { - /** The generated JS source string. */ - source: string; - /** Build-time key anchor value (for rolling cipher key derivation). */ - keyAnchorValue: number; -} - -/** - * Generate the complete VM runtime source code. - * - * @returns A VmRuntimeResult containing the JS source string and key anchor value. - */ -export function generateVmRuntime(options: { - opcodeShuffleMap: number[]; - names: RuntimeNames; - temps: TempNames; - encrypt: boolean; - debugProtection: boolean; - debugLogging?: boolean; - dynamicOpcodes?: boolean; - decoyOpcodes?: boolean; - stackEncoding?: boolean; - seed: number; - stringKey?: number; - rollingCipher?: boolean; - integrityBinding?: boolean; - integrityHash?: number; - usedOpcodes?: Set; - cipherSalt?: number; - mixedBooleanArithmetic?: boolean; - handlerFragmentation?: boolean; - /** Generate per-build polymorphic decoder chain for string constants. */ - polymorphicDecoder?: boolean; - /** Atomize interpreter string literals into encoded table lookups. */ - stringAtomization?: boolean; - /** Scatter key material fragments across the output. */ - scatteredKeys?: boolean; - /** Enable runtime opcode mutation (dense handler table required). */ - opcodeMutation?: boolean; - /** Split encoded bytecode into mixed-type fragments. */ - bytecodeScattering?: boolean; - /** Apply incremental cipher encryption (block-epoch keyed). */ - incrementalCipher?: boolean; - /** Apply semantic opacity transforms to handler bodies. */ - semanticOpacity?: boolean; - /** Silently corrupt cipher state when function references are tampered. */ - observationResistance?: boolean; - /** Number of functions to bind for observation resistance (from tuning). */ - identityBindingCount?: number; - /** Tuning: probability (0-100) of witness check per handler. */ - witnessCheckProbability?: number; - /** Shuffled 64-char alphabet for custom binary encoding. */ - alphabet: string; - /** Whether any compiled units are async (controls async interpreter emit). */ - hasAsyncUnits?: boolean; - /** Per-build structural variation choices. */ - structuralChoices?: StructuralChoices; - /** NameRegistry for dynamic name generation (scatter/btScatter). */ - registry: NameRegistry; -}): VmRuntimeResult { - const { - opcodeShuffleMap, - names, - temps, - encrypt, - debugProtection: dbgProt, - debugLogging = false, - dynamicOpcodes = false, - decoyOpcodes = false, - stackEncoding = false, - seed, - stringKey, - rollingCipher = false, - integrityBinding = false, - integrityHash, - usedOpcodes, - cipherSalt, - mixedBooleanArithmetic = false, - handlerFragmentation = false, - polymorphicDecoder = false, - stringAtomization = false, - scatteredKeys = false, - opcodeMutation = false, - bytecodeScattering = false, - incrementalCipher = false, - semanticOpacity = false, - observationResistance = false, - identityBindingCount = 5, - witnessCheckProbability, - alphabet, - hasAsyncUnits = true, - structuralChoices, - registry, - } = options; - - // Create constant splitter — replaces well-known numeric literals with - // computed expressions so attackers can't grep for FNV primes, etc. - const split: SplitFn = makeConstantSplitter(seed); - - // Build reverse map: physical -> logical opcode - const reverseMap = new Array(OPCODE_COUNT); - for (let i = 0; i < opcodeShuffleMap.length; i++) { - reverseMap[opcodeShuffleMap[i]!] = i; - } - - // -- Tier 0: foundational declarations (shuffleable) -------------------- - const tier0Components: JsNode[][] = [ - // 0: imul alias - [varDecl(names.imul, member(id("Math"), "imul"))], - // 1: spread marker symbol - [varDecl(names.spreadSym, call(id("Symbol"), []))], - // 2: hop alias - [ - varDecl( - names.hop, - member(member(id("Object"), "prototype"), "hasOwnProperty") - ), - ], - // 3: globalRef (detection order shuffled per build) - [varDecl(names.globalRef, buildGlobalRefDetection(structuralChoices))], - // 4: TDZ sentinel - [ - varDecl( - names.tdzSentinel, - call(member(id("Object"), "create"), [lit(null)]) - ), - ], - ]; - - // Optional: packed-integer decoder for bytecode scattering - if (bytecodeScattering) { - tier0Components.push(buildDecodeFunction(names.btDecode)); - } - - // -- Tier 1: crypto/encoding primitives (shuffleable) ------------------ - const tier1Components: JsNode[][] = []; - - // Binary decoder (always emitted) - const binaryDecoderNodes = buildBinaryDecoderSource(names, alphabet); - // Deferred nodes: scattered key reassembly + binary decoder rest - // (inserted between tier 1 and tier 2 so the alphabet is available - // before the loader/deserializer in tier 3 reference it) - let scatteredReassemblyNodes: JsNode[] = []; - - if (scatteredKeys) { - // --- Scattered keys: fragment the alphabet literal --- - // All fragments go to tiers 0 and 1 (before the reassembly). - // The reassembly + binary decoder rest go after tier 1 but before - // tier 2 so the decode function is available for the loader. - const scatterNameGen = registry.createDynamicGenerator("scatter"); - const scattered = scatterKeyMaterials( - [{ name: names.alpha, value: alphabet, type: "string" }], - scatterNameGen, - seed - ); - // All fragments go to tier 0 and tier 1 (before reassembly point) - const allFragments = [ - ...scattered.tier0Fragments, - ...scattered.tier1Fragments, - ...scattered.tier3Fragments, - ...scattered.tier4Fragments, - ]; - // Split fragments between tier 0 and tier 1 - const half = Math.ceil(allFragments.length / 2); - tier0Components.push(allFragments.slice(0, half)); - tier1Components.push(allFragments.slice(half)); - // Reassembly + binary decoder rest deferred to between tier 1 and tier 2 - scatteredReassemblyNodes = [ - ...scattered.reassemblyNodes, - ...binaryDecoderNodes.slice(1), - ]; - } else { - tier1Components.push(binaryDecoderNodes); - } - // Optional encryption support - if (encrypt) { - tier1Components.push(buildFingerprintSource(names, split)); - tier1Components.push(buildRc4Source(names, split)); - } - // Optional debug protection - if (dbgProt) { - tier1Components.push(buildDebugProtection(names, temps)); - } - // Optional debug logging - if (debugLogging) { - tier1Components.push(buildDebugLogging(reverseMap, names, temps)); - } - // Optional polymorphic decoder chain (string constant encoding). - // When stringAtomization is also on, the decoder is emitted as part of - // atomization infrastructure (after all tiers) to avoid duplicates. - if (polymorphicDecoder && !stringAtomization) { - const chain = generateDecoderChain(seed); - const decoderNodes = buildDecoderFunctionAST( - chain, - names.polyDec, - names.polyPosSeed - ); - tier1Components.push(decoderNodes); - } - - // -- Tier 2: interpreter machinery (NOT shuffleable — dependency chain) - - // Build interpreter core — also produces handler table init. - const interpResult = buildInterpreterFunctions( - names, - temps, - opcodeShuffleMap, - debugLogging, - rollingCipher, - seed, - { - dynamicOpcodes, - decoyOpcodes, - stackEncoding, - usedOpcodes, - mixedBooleanArithmetic, - handlerFragmentation, - opcodeMutation, - incrementalCipher, - semanticOpacity, - observationResistance, - witnessCheckProbability, - }, - split, - hasAsyncUnits, - structuralChoices, - registry - ); - - const tier2Nodes: JsNode[] = []; - // Handler table + key anchor init (must come before rolling cipher) - tier2Nodes.push(...interpResult.handlerTableInit); - // If integrity binding, fold integrity hash into the key anchor - if (rollingCipher && integrityBinding && integrityHash !== undefined) { - tier2Nodes.push( - exprStmt( - assign( - id(names.keyAnchor), - bin( - BOp.Ushr, - bin( - BOp.BitXor, - id(names.keyAnchor), - split(integrityHash) - ), - lit(0) - ) - ) - ) - ); - } - // Rolling cipher helpers (must come after handler table + key anchor) - if (rollingCipher) { - tier2Nodes.push( - ...buildRollingCipherSource( - names, - true, // hasKeyAnchor — rcDeriveKey references names.keyAnchor - split, - cipherSalt - ) - ); - } - // Incremental cipher helpers (must come after rolling cipher helpers) - if (incrementalCipher) { - tier2Nodes.push(...buildIncrementalCipherSource(names, split)); - } - // Stack-encoding helpers (stkEnc/stkDec) — IIFE-scope, defined once. - if (stackEncoding) { - tier2Nodes.push(...buildStackEncodingHelpers(names, split)); - } - // Interpreter function bodies - tier2Nodes.push(...interpResult.interpreters); - - // -- Tier 3: dispatch layer (shuffleable) -------------------------------- - const tier3Components: JsNode[][] = [ - // 0: Runner dispatch functions - buildRunners(debugLogging, names, temps), - // 1: String constant decoder + Loader - [ - ...(stringKey !== undefined - ? buildStringDecoderSource( - names, - stringKey, - rollingCipher, - split - ) - : []), - ...buildLoader( - encrypt, - names, - stringKey !== undefined, - rollingCipher - ), - ], - // 2: Deserializer - buildDeserializer(names, temps), - ]; - // -- Tier 4: wiring (shuffleable) ---------------------------------------- - const tier4Components: JsNode[][] = [ - // 0: Global exposure - buildGlobalExposure(names.vm), - ]; - - // -- Assemble with shuffled ordering ------------------------------------ - let nodes: JsNode[] = []; - - // "use strict" always first - nodes.push(exprStmt(lit("use strict"))); - - // Apply shuffled tier ordering - const order = structuralChoices?.statementOrder; - function pushShuffled(components: JsNode[][], tierOrder?: number[]) { - if (!tierOrder || !structuralChoices) { - for (const c of components) nodes.push(...c); - return; - } - for (const idx of tierOrder) { - if (idx < components.length) nodes.push(...components[idx]!); - } - // Push any components not covered by the order array - for (let i = 0; i < components.length; i++) { - if (!tierOrder.includes(i)) nodes.push(...components[i]!); - } - } - - // Merge tier 0 and tier 1 into a single preamble pool and shuffle - // together — makes the output beginning vary significantly per build - if (structuralChoices?.preambleOrder) { - const combined = [...tier0Components, ...tier1Components]; - pushShuffled(combined, structuralChoices.preambleOrder); - } else { - pushShuffled(tier0Components, order?.tier0); - pushShuffled(tier1Components, order?.tier1); - } - // Scattered key reassembly: all fragment vars in tier 0 and tier 1 are - // now declared. Reassemble the alphabet + build reverse table + decode - // function before tier 2 (which may depend on them indirectly). - if (scatteredReassemblyNodes.length > 0) { - nodes.push(...scatteredReassemblyNodes); - } - nodes.push(...tier2Nodes); // tier 2 is never shuffled - pushShuffled(tier3Components, order?.tier3); - - // Observation resistance: identity bindings, witness counter, and - // WeakMap canary must go after all bound functions (exec, load, vm, - // deser, rcDeriveKey, rcMix, etc.) are declared (tiers 2 and 3). - // Placed before tier 4 (wiring). - if (observationResistance && rollingCipher) { - const orResult = buildIdentityBindings( - names, - temps, - seed, - identityBindingCount, - { - rollingCipher, - encrypt, - incrementalCipher, - }, - split, - registry - ); - nodes.push(...orResult.declarations, orResult.verifyFn); - - // Witness counter declarations (IIFE scope) - const witnessResult = buildWitnessCounter(names, temps, seed, split); - nodes.push(...witnessResult.declarations); - - // WeakMap canary declarations (IIFE scope) - const canaryResult = buildWeakMapCanary( - names, - temps, - seed, - split, - registry - ); - nodes.push(...canaryResult.declarations); - } - - pushShuffled(tier4Components, order?.tier4); - - // --- String atomization: replace string literals with table lookups --- - if (stringAtomization) { - const atomChain = generateDecoderChain(deriveSeed(seed, "atomization")); - const atomResult = atomizeStrings(nodes, atomChain, { - decoder: names.polyDec, - posSeed: names.polyPosSeed, - table: names.strTbl, - cache: names.strCache, - accessor: names.strAcc, - }); - // Prepend infrastructure (decoder fn + table + cache + accessor) - // after "use strict" but before everything else - const useStrict = atomResult.transformedNodes[0]; // "use strict" - nodes = [ - useStrict!, - ...atomResult.infrastructure, - ...atomResult.transformedNodes.slice(1), - ]; - } - - // Apply structural AST transforms (control flow, declarations, expressions) - const finalNodes = structuralChoices - ? applyStructuralTransforms(nodes, structuralChoices) - : nodes; - - // Wrap in IIFE and emit - return { - source: emitIIFE(finalNodes), - keyAnchorValue: interpResult.keyAnchorValue, - }; -} - -// --------------------------------------------------------------------------- -// VM Shielding: per-group micro-interpreters -// --------------------------------------------------------------------------- - -/** Per-group configuration for shielded runtime generation. */ -export interface ShieldingGroup { - /** Group-specific opcode shuffle map. */ - shuffleMap: number[]; - /** Group-specific randomized identifier names. */ - names: RuntimeNames; - /** Group-specific randomized temp names. */ - temps: TempNames; - /** Group-specific seed (for obfuscateLocals). */ - seed: number; - /** Unit IDs belonging to this group (root + children). */ - unitIds: string[]; - /** Opcodes used by this group's units. */ - usedOpcodes: Set; - /** Per-group integrity hash (if integrityBinding is on). */ - integrityHash?: number; - /** Per-group cipher salt for rolling cipher key derivation. */ - cipherSalt?: number; - /** Whether this group contains async units. */ - hasAsyncUnits?: boolean; -} - -/** Result from generating the shielded VM runtime. */ -export interface ShieldedVmRuntimeResult { - /** The generated JS source string. */ - source: string; - /** Per-group key anchor values (in group order). */ - groupKeyAnchors: number[]; -} - -/** - * Generate a shielded VM runtime with per-group micro-interpreters. - * - * Shared infrastructure (bytecode table, cache, fingerprint, debug, deserializer) - * is emitted once. Each group gets its own interpreter, runners, loader, and - * rolling cipher with unique opcode shuffle and identifier names. - * - * @returns A ShieldedVmRuntimeResult containing the JS source string and - * per-group key anchor values. - */ -export function generateShieldedVmRuntime(options: { - groups: ShieldingGroup[]; - sharedNames: RuntimeNames; - sharedTemps: TempNames; - encrypt: boolean; - debugProtection: boolean; - debugLogging?: boolean; - decoyOpcodes?: boolean; - stackEncoding?: boolean; - integrityBinding?: boolean; - mixedBooleanArithmetic?: boolean; - handlerFragmentation?: boolean; - /** Generate per-build polymorphic decoder chain for string constants. */ - polymorphicDecoder?: boolean; - /** Atomize interpreter string literals into encoded table lookups. */ - stringAtomization?: boolean; - /** Scatter key material fragments across the output. */ - scatteredKeys?: boolean; - /** Enable runtime opcode mutation (dense handler table required). */ - opcodeMutation?: boolean; - /** Split encoded bytecode into mixed-type fragments. */ - bytecodeScattering?: boolean; - /** Apply incremental cipher encryption (block-epoch keyed). */ - incrementalCipher?: boolean; - /** Apply semantic opacity transforms to handler bodies. */ - semanticOpacity?: boolean; - /** Silently corrupt cipher state when function references are tampered. */ - observationResistance?: boolean; - /** Number of functions to bind for observation resistance (from tuning). */ - identityBindingCount?: number; - /** Tuning: probability (0-100) of witness check per handler. */ - witnessCheckProbability?: number; - /** Shuffled 64-char alphabet for custom binary encoding. */ - alphabet: string; - /** NameRegistry for dynamic name generation. */ - registry: NameRegistry; -}): ShieldedVmRuntimeResult { - const { - groups, - sharedNames, - sharedTemps, - encrypt, - debugProtection: dbgProt, - debugLogging = false, - decoyOpcodes = false, - stackEncoding = false, - integrityBinding = false, - mixedBooleanArithmetic = false, - handlerFragmentation = false, - polymorphicDecoder = false, - stringAtomization = false, - scatteredKeys = false, - opcodeMutation = false, - bytecodeScattering = false, - incrementalCipher = false, - semanticOpacity = false, - observationResistance = false, - identityBindingCount = 5, - witnessCheckProbability, - alphabet, - registry, - } = options; - - // Shared constant splitter for shared builders (fingerprint, decoder) - const sharedSplit: SplitFn = makeConstantSplitter( - groups[0]?.seed ?? 0x12345678 - ); - - const nodes: JsNode[] = []; - - // "use strict" directive - nodes.push(exprStmt(lit("use strict"))); - - // Built-in alias — eliminate repeated member chain lookups - nodes.push(varDecl(sharedNames.imul, member(id("Math"), "imul"))); - - // Spread marker symbol — tags spread arrays without object allocation - nodes.push(varDecl(sharedNames.spreadSym, call(id("Symbol"), []))); - - // Cached built-in references — avoid repeated property chain lookups - nodes.push( - varDecl( - sharedNames.hop, - member(member(id("Object"), "prototype"), "hasOwnProperty") - ) - ); - nodes.push(varDecl(sharedNames.globalRef, buildGlobalRefDetection())); - - // TDZ sentinel — shared across all groups - nodes.push( - varDecl( - sharedNames.tdzSentinel, - call(member(id("Object"), "create"), [lit(null)]) - ) - ); - - // Shared: stack-encoding helpers (stkEnc/stkDec) — defined once; the - // per-unit key `_sek` is computed per-exec inside each group's interpreter. - if (stackEncoding) { - nodes.push(...buildStackEncodingHelpers(sharedNames, sharedSplit)); - } - - // Shared: packed-integer decoder for bytecode scattering - if (bytecodeScattering) { - nodes.push(...buildDecodeFunction(sharedNames.btDecode)); - } - - // Shared: custom binary decoder (always emitted) - const shieldedBinDecNodes = buildBinaryDecoderSource(sharedNames, alphabet); - if (scatteredKeys) { - // Scatter the alphabet string: all fragments first, then reassembly - const shieldedScatterGen = - registry.createDynamicGenerator("shieldedScatter"); - const shieldedScattered = scatterKeyMaterials( - [{ name: sharedNames.alpha, value: alphabet, type: "string" }], - shieldedScatterGen, - groups[0]?.seed ?? 0x12345678 - ); - // All fragments emitted first so the reassembly can reference them - nodes.push( - ...shieldedScattered.tier0Fragments, - ...shieldedScattered.tier1Fragments, - ...shieldedScattered.tier3Fragments, - ...shieldedScattered.tier4Fragments, - ...shieldedScattered.reassemblyNodes, - ...shieldedBinDecNodes.slice(1) - ); - } else { - nodes.push(...shieldedBinDecNodes); - } - - // Shared: encryption support (RC4 + fingerprint) - if (encrypt) { - nodes.push(...buildFingerprintSource(sharedNames, sharedSplit)); - nodes.push(...buildRc4Source(sharedNames, sharedSplit)); - } - - // Shared: debug protection - if (dbgProt) { - nodes.push(...buildDebugProtection(sharedNames, sharedTemps)); - } - - // Shared: deserializer - nodes.push(...buildDeserializer(sharedNames, sharedTemps)); - - // Per-group micro-interpreters - const groupRegistrations: { unitIds: string[]; dispatchName: string }[] = - []; - const groupKeyAnchors: number[] = []; - - for (const group of groups) { - const gn = group.names; - const gt = group.temps; - - // Per-group constant splitter — each group gets unique split patterns - const groupSplit: SplitFn = makeConstantSplitter(group.seed); - - // Polymorphic decoder (per-group) — only if stringAtomization is off - // (when on, the decoder is emitted via atomization infrastructure) - if (polymorphicDecoder && !stringAtomization) { - const groupChain = generateDecoderChain(group.seed); - const groupDecoderNodes = buildDecoderFunctionAST( - groupChain, - gn.polyDec, - gn.polyPosSeed - ); - nodes.push(...groupDecoderNodes); - } - - // Debug logging (per-group) - if (debugLogging) { - const reverseMap = new Array(OPCODE_COUNT); - for (let i = 0; i < group.shuffleMap.length; i++) { - reverseMap[group.shuffleMap[i]!] = i; - } - nodes.push(...buildDebugLogging(reverseMap, gn, gt)); - } - - // Build interpreter core (per-group) — produces handler table init - // + key anchor + interpreter function bodies. - // When group has no async units, only the sync interpreter is emitted. - const interpResult = buildInterpreterFunctions( - gn, - gt, - group.shuffleMap, - debugLogging, - true, // rollingCipher always on in shielding mode - group.seed, - { - dynamicOpcodes: true, // always strip unused opcodes in shielding mode - decoyOpcodes, - stackEncoding, - usedOpcodes: group.usedOpcodes, - mixedBooleanArithmetic, - handlerFragmentation, - opcodeMutation, - incrementalCipher, - semanticOpacity, - observationResistance, - witnessCheckProbability, - }, - groupSplit, - group.hasAsyncUnits ?? true, - undefined, // structuralChoices not used in shielded mode - registry - ); - - // Handler table + key anchor init (must come before rolling cipher - // so rcDeriveKey can reference the key anchor closure variable) - nodes.push(...interpResult.handlerTableInit); - - // Fold integrity hash into the key anchor (if integrityBinding is on) - if (integrityBinding && group.integrityHash !== undefined) { - // _ka = (_ka ^ integrityHash) >>> 0; - nodes.push( - exprStmt( - assign( - id(gn.keyAnchor), - bin( - BOp.Ushr, - bin( - BOp.BitXor, - id(gn.keyAnchor), - groupSplit(group.integrityHash) - ), - lit(0) - ) - ) - ) - ); - } - - // Rolling cipher helpers (must come after handler table + key anchor) - nodes.push( - ...buildRollingCipherSource( - gn, - true, // hasKeyAnchor — always true in shielding mode - groupSplit, - group.cipherSalt - ) - ); - - // Incremental cipher helpers (must come after rolling cipher helpers) - if (incrementalCipher) { - nodes.push(...buildIncrementalCipherSource(gn, groupSplit)); - } - - // Interpreter function bodies - nodes.push(...interpResult.interpreters); - - // Save key anchor value for caller - groupKeyAnchors.push(interpResult.keyAnchorValue); - - // Runners (per-group dispatch function) - nodes.push(...buildRunners(debugLogging, gn, gt)); - - // String decoder (per-group, rolling cipher implicit key) - nodes.push(...buildStringDecoderSource(gn, 0, true, groupSplit)); - - // Loader (per-group, skips shared var declarations) - nodes.push( - ...buildLoader(encrypt, gn, true, true, { skipSharedDecls: true }) - ); - - // Observation resistance: per-group identity bindings, witness - // counter, and WeakMap canary (after all bound functions are - // declared for this group) - if (observationResistance) { - const orResult = buildIdentityBindings( - gn, - gt, - group.seed, - identityBindingCount, - { - rollingCipher: true, // always on in shielding mode - encrypt, - incrementalCipher, - }, - groupSplit, - registry - ); - nodes.push(...orResult.declarations, orResult.verifyFn); - - // Witness counter declarations (per-group IIFE scope) - const witnessResult = buildWitnessCounter( - gn, - gt, - group.seed, - groupSplit - ); - nodes.push(...witnessResult.declarations); - - // WeakMap canary declarations (per-group IIFE scope) - const canaryResult = buildWeakMapCanary( - gn, - gt, - group.seed, - groupSplit, - registry - ); - nodes.push(...canaryResult.declarations); - } - - groupRegistrations.push({ - unitIds: group.unitIds, - dispatchName: gn.vm, - }); - } - - // Shared: depth, callStack, cache (emitted once) - nodes.push(varDecl(sharedNames.depth, lit(0))); - nodes.push(varDecl(sharedNames.callStack, arr())); - nodes.push(varDecl(sharedNames.cache, obj())); - - // Router: maps unit IDs to group dispatch functions - nodes.push( - ...buildRouter(sharedNames.router, groupRegistrations, sharedNames) - ); - - // Global exposure: expose the router - nodes.push(...buildGlobalExposure(sharedNames.router)); - - // --- String atomization (shielded): replace string literals with table lookups --- - let finalShieldedNodes = nodes; - if (stringAtomization) { - const atomChain = generateDecoderChain( - deriveSeed(groups[0]?.seed ?? 0x12345678, "shieldedAtomization") - ); - const atomResult = atomizeStrings(finalShieldedNodes, atomChain, { - decoder: sharedNames.polyDec, - posSeed: sharedNames.polyPosSeed, - table: sharedNames.strTbl, - cache: sharedNames.strCache, - accessor: sharedNames.strAcc, - }); - // Prepend infrastructure after "use strict" - const useStrict = atomResult.transformedNodes[0]; - finalShieldedNodes = [ - useStrict!, - ...atomResult.infrastructure, - ...atomResult.transformedNodes.slice(1), - ]; - } - - // Wrap in IIFE and emit - return { - source: emitIIFE(finalShieldedNodes), - groupKeyAnchors, - }; -} - -// --- Helpers --- - -/** - * Build the globalThis detection chain with shuffled check order. - * - * `globalThis` is always checked first — it is the standard way to - * access the global object in all environments (ES2020+) and is the - * only reference guaranteed to resolve correctly in Chrome extension - * content scripts (where `window` is the page's Window, but extension - * globals like `chrome` live on `globalThis`). The remaining globals - * (`window`, `global`, `self`) are shuffled per build for structural - * variation. - */ -function buildGlobalRefDetection(choices?: StructuralChoices): JsNode { - // globalThis is always first — remaining order shuffled - const rest = ["window", "global", "self"]; - - if (choices) { - for (let i = rest.length - 1; i > 0; i--) { - const j = Math.floor(choices.prng() * (i + 1)); - [rest[i], rest[j]] = [rest[j]!, rest[i]!]; - } - } - - const globals = ["globalThis", ...rest]; - - // Build nested ternary chain: check each global, fallback to {} - let result: JsNode = obj(); - for (let i = globals.length - 1; i >= 0; i--) { - const name = globals[i]!; - result = ternary( - bin(BOp.Sneq, un(UOp.Typeof, id(name)), lit("undefined")), - id(name), - result - ); - } - return result; -} - -/** - * Wrap an array of JsNode[] in a self-executing IIFE and emit to string. - * - * Produces: `(function(){...nodes...})();` - */ -function emitIIFE(nodes: JsNode[]): string { - // Emit each node as a top-level statement inside the IIFE. - // We build the IIFE manually to ensure correct formatting. - const parts: string[] = []; - parts.push("(function(){"); - for (const node of nodes) { - const s = emit(node); - if (s.length === 0) continue; - parts.push(s); - // Function declarations don't need semicolons; everything else does. - // We check the node type rather than string endings to avoid - // misclassifying object-literal-ending expressions (e.g. `var x={}`). - if (node.type !== "FnDecl") { - if (!s.endsWith(";")) parts.push(";"); - } - } - parts.push("})();"); - return parts.join("\n"); -} diff --git a/packages/ruam/src/ruamvm/builders/debug-logging.ts b/packages/ruam/src/ruamvm/builders/debug-logging.ts deleted file mode 100644 index 9312df5..0000000 --- a/packages/ruam/src/ruamvm/builders/debug-logging.ts +++ /dev/null @@ -1,397 +0,0 @@ -/** - * Debug logging builder — assembles the debug config, logging function, - * and opcode trace function as AST nodes. - * - * Produces three declarations: - * - Debug config object (enabled flag, log level, opcode name table) - * - General-purpose debug log function (rate-limited console.log wrapper) - * - Opcode trace function (detailed per-instruction trace output) - * - * @module ruamvm/builders/debug-logging - */ - -import type { JsNode } from "../nodes.js"; -import type { RuntimeNames, TempNames } from "../../naming/compat-types.js"; -import { - fn, - varDecl, - id, - lit, - bin, - obj, - arr, - spread, - member, - index, - call, - ifStmt, - exprStmt, - returnStmt, - assign, - ternary, - un, - BOp, - UOp, -} from "../nodes.js"; -import { Op, OPCODE_COUNT } from "../../compiler/opcodes.js"; - -// --- Builder --- - -/** - * Build the debug logging infrastructure as JsNode[]. - * - * Produces three declarations: - * - Debug config object (enabled flag, log level, opcode name table) - * - General-purpose debug log function (rate-limited console.log wrapper) - * - Opcode trace function (detailed per-instruction trace output) - * - * @param reverseMap - Physical-to-logical opcode mapping (index = physical, value = logical). - * @param names - Randomized runtime identifier names. - * @returns An array of JsNode containing the config var and two function declarations. - */ -export function buildDebugLogging( - reverseMap: number[], - names: RuntimeNames, - temps: TempNames -): JsNode[] { - // --- Build opcode name table: physical opcode -> name string --- - - const opNames = Object.entries(Op) - .filter( - ([, v]) => typeof v === "number" && (v as number) < OPCODE_COUNT - ) - .reduce((m, [name, num]) => { - m[num as number] = name; - return m; - }, {} as Record); - - const nameEntries: [string, JsNode][] = []; - for (let phys = 0; phys < reverseMap.length; phys++) { - const logical = reverseMap[phys]!; - const name = opNames[logical] ?? `OP_${logical}`; - nameEntries.push([String(phys), lit(name)]); - } - - // --- Shorthand aliases for readability --- - - const O = names.operand; - const S = names.stk; - const OP = names.opVar; - const cfg = names.dbgCfg; - - // Helper accessors - const cfgId = id(cfg); - const cfgEnabled = member(cfgId, "enabled"); - const cfgCount = member(cfgId, temps["_count"]!); - const cfgMaxLogs = member(cfgId, "maxLogs"); - const cfgOpNames = member(cfgId, temps["_opNames"]!); - const cfgLevel = member(cfgId, "level"); - const cfgLevels = member(cfgId, "levels"); - const console_ = id("console"); - - // --- Config object --- - // var cfg={enabled:true,level:'trace',filter:null,maxLogs:10000,_count:0,_opNames:{...},levels:{trace:0,info:1,warn:2,error:3}} - - const configNode = varDecl( - cfg, - obj( - ["enabled", lit(true)], - ["level", lit("trace")], - ["filter", lit(null)], - ["maxLogs", lit(10000)], - [temps["_count"]!, lit(0)], - [temps["_opNames"]!, obj(...nameEntries)], - [ - "levels", - obj( - ["trace", lit(0)], - ["info", lit(1)], - ["warn", lit(2)], - ["error", lit(3)] - ), - ] - ) - ); - - // --- General debug log function --- - // function dbg(){ - // if(!cfg.enabled)return; - // if(cfg._count>=cfg.maxLogs){ - // if(cfg._count===cfg.maxLogs){console.warn('[VM_DBG] max logs reached ('+cfg.maxLogs+'), silencing');cfg._count++;} - // return; - // } - // cfg._count++; - // var args=Array.prototype.slice.call(arguments); - // console.log.apply(console,['[VM_DBG]'].concat(args)); - // } - - const dbgFn = fn( - names.dbg, - [], - [ - // if(!cfg.enabled)return; - ifStmt(un(UOp.Not, cfgEnabled), [returnStmt()]), - // if(cfg._count>=cfg.maxLogs){...return;} - ifStmt(bin(BOp.Gte, cfgCount, cfgMaxLogs), [ - // if(cfg._count===cfg.maxLogs){console.warn(...);cfg._count++;} - ifStmt(bin(BOp.Seq, cfgCount, cfgMaxLogs), [ - exprStmt( - call(member(console_, "warn"), [ - bin( - BOp.Add, - bin( - BOp.Add, - lit("[VM_DBG] max logs reached ("), - cfgMaxLogs - ), - lit("), silencing") - ), - ]) - ), - exprStmt(assign(cfgCount, bin(BOp.Add, cfgCount, lit(1)))), - ]), - returnStmt(), - ]), - // cfg._count++; - exprStmt(assign(cfgCount, bin(BOp.Add, cfgCount, lit(1)))), - // var args=[...arguments]; - varDecl("args", arr(spread(id("arguments")))), - // console.log('[VM_DBG]',...args); - exprStmt( - call(member(console_, "log"), [ - lit("[VM_DBG]"), - spread(id("args")), - ]) - ), - ] - ); - - // --- Opcode trace function --- - // function dbgOp(OP,O,C,P,S){ - // if(!cfg.enabled||cfg.levels[cfg.level]>0)return; - // if(cfg._count>=cfg.maxLogs)return; - // cfg._count++; - // var name=cfg._opNames[OP]||('OP_'+OP); - // var topStr='(empty)'; - // if(P>=0){ - // var top=S[P]; - // topStr=typeof top==='function'?'[fn'+(top.name?':'+top.name:'')+']':typeof top==='object'&&top!==null?'[obj:'+Object.keys(top).slice(0,3).join(',')+']':String(top); - // if(topStr.length>60)topStr=topStr.slice(0,60)+'...'; - // } - // var constStr=''; - // if(typeof C[O]==='string')constStr=' c="'+C[O].slice(0,30)+'"'; - // else if(typeof C[O]==='number')constStr=' c='+C[O]; - // console.log('[VM_TRACE] '+name+' op='+O+constStr+' sp='+P+' top='+topStr); - // } - - const opId = id(OP); - const oId = id(O); - const cId = id("C"); - const sId = id(S); - const sLen = member(sId, "length"); - const nameVar = id("name"); - const topStrVar = id("topStr"); - const topVar = id("top"); - const constStrVar = id("constStr"); - const cAtO = index(cId, oId); - - const dbgOpFn = fn( - names.dbgOp, - [OP, O, "C", S], - [ - // if(!cfg.enabled||cfg.levels[cfg.level]>0)return; - ifStmt( - bin( - BOp.Or, - un(UOp.Not, cfgEnabled), - bin(BOp.Gt, index(cfgLevels, cfgLevel), lit(0)) - ), - [returnStmt()] - ), - // if(cfg._count>=cfg.maxLogs)return; - ifStmt(bin(BOp.Gte, cfgCount, cfgMaxLogs), [returnStmt()]), - // cfg._count++; - exprStmt(assign(cfgCount, bin(BOp.Add, cfgCount, lit(1)))), - // var name=cfg._opNames[OP]||('OP_'+OP); - varDecl( - "name", - bin( - BOp.Or, - index(cfgOpNames, opId), - bin(BOp.Add, lit("OP_"), opId) - ) - ), - // var topStr='(empty)'; - varDecl("topStr", lit("(empty)")), - // if(S.length>0){...} - ifStmt(bin(BOp.Gt, sLen, lit(0)), [ - // var top=S[S.length-1]; - varDecl("top", index(sId, bin(BOp.Sub, sLen, lit(1)))), - // topStr = typeof top==='function' ? '[fn'+(top.name?':'+top.name:'')+']' - // : typeof top==='object'&&top!==null ? '[obj:'+Object.keys(top).slice(0,3).join(',')+']' - // : String(top); - exprStmt( - assign( - topStrVar, - ternary( - bin( - BOp.Seq, - un(UOp.Typeof, topVar), - lit("function") - ), - // '[fn'+(top.name?':'+top.name:'')+']' - bin( - BOp.Add, - bin( - BOp.Add, - lit("[fn"), - ternary( - member(topVar, "name"), - bin( - BOp.Add, - lit(":"), - member(topVar, "name") - ), - lit("") - ) - ), - lit("]") - ), - // typeof top==='object'&&top!==null ? '[obj:'+...+']' : String(top) - ternary( - bin( - BOp.And, - bin( - BOp.Seq, - un(UOp.Typeof, topVar), - lit("object") - ), - bin(BOp.Sneq, topVar, lit(null)) - ), - // '[obj:'+Object.keys(top).slice(0,3).join(',')+']' - bin( - BOp.Add, - bin( - BOp.Add, - lit("[obj:"), - call( - member( - call( - member( - call( - member( - id("Object"), - "keys" - ), - [topVar] - ), - "slice" - ), - [lit(0), lit(3)] - ), - "join" - ), - [lit(",")] - ) - ), - lit("]") - ), - // String(top) - call(id("String"), [topVar]) - ) - ) - ) - ), - // if(topStr.length>60)topStr=topStr.slice(0,60)+'...'; - ifStmt(bin(BOp.Gt, member(topStrVar, "length"), lit(60)), [ - exprStmt( - assign( - topStrVar, - bin( - BOp.Add, - call(member(topStrVar, "slice"), [ - lit(0), - lit(60), - ]), - lit("...") - ) - ) - ), - ]), - ]), - // var constStr=''; - varDecl("constStr", lit("")), - // if(typeof C[O]==='string')constStr=' c="'+C[O].slice(0,30)+'"'; - ifStmt( - bin(BOp.Seq, un(UOp.Typeof, cAtO), lit("string")), - [ - exprStmt( - assign( - constStrVar, - bin( - BOp.Add, - bin( - BOp.Add, - lit(' c="'), - call(member(cAtO, "slice"), [ - lit(0), - lit(30), - ]) - ), - lit('"') - ) - ) - ), - ], - [ - // else if(typeof C[O]==='number')constStr=' c='+C[O]; - ifStmt(bin(BOp.Seq, un(UOp.Typeof, cAtO), lit("number")), [ - exprStmt( - assign(constStrVar, bin(BOp.Add, lit(" c="), cAtO)) - ), - ]), - ] - ), - // console.log('[VM_TRACE] '+name+' op='+O+constStr+' len='+S.length+' top='+topStr); - exprStmt( - call(member(console_, "log"), [ - bin( - BOp.Add, - bin( - BOp.Add, - bin( - BOp.Add, - bin( - BOp.Add, - bin( - BOp.Add, - bin( - BOp.Add, - bin( - BOp.Add, - bin( - BOp.Add, - lit("[VM_TRACE] "), - nameVar - ), - lit(" op=") - ), - oId - ), - constStrVar - ), - lit(" len=") - ), - sLen - ), - lit(" top=") - ), - topStrVar - ), - ]) - ), - ] - ); - - return [configNode, dbgFn, dbgOpFn]; -} diff --git a/packages/ruam/src/ruamvm/builders/debug-protection.ts b/packages/ruam/src/ruamvm/builders/debug-protection.ts deleted file mode 100644 index 06ae364..0000000 --- a/packages/ruam/src/ruamvm/builders/debug-protection.ts +++ /dev/null @@ -1,803 +0,0 @@ -/** - * Debug protection builder — assembles the multi-layered anti-debugger IIFE as AST. - * - * Replaces the template-literal approach in runtime/templates/debug-protection.ts - * with AST-based construction. All six detection layers, escalating response - * logic, and scheduler are built from structured AST nodes. - * - * No `debugger` statements are used — fully CSP/TrustedScript compatible. - * - * Detection layers: - * 1. Built-in prototype integrity (Object/Array/JSON method monkey-patch detection) - * 2. Environment analysis (--inspect flags, stack trace anomalies) - * 3. Function integrity self-verification (FNV-1a checksum of own source) - * - * Response escalation: - * Level 1-2: Silent bytecode instruction corruption (wrong opcode dispatch) - * Level 3-4: Cache wipe + constants array destruction - * Level 5+: Total bytecode annihilation + infinite busy loop - * - * @module ruamvm/builders/debug-protection - */ - -import type { JsNode } from "../nodes.js"; -import type { RuntimeNames, TempNames } from "../../naming/compat-types.js"; -import { - fn, - varDecl, - id, - lit, - bin, - un, - call, - member, - index, - ifStmt, - exprStmt, - returnStmt, - forStmt, - forIn, - whileStmt, - tryCatch, - ternary, - arr, - obj, - fnExpr, - assign, - newExpr, - getter, - method, - BOp, - UOp, -} from "../nodes.js"; - -// --- Helpers --- - -/** Shorthand: `a.b(args)` */ -function mcall(object: JsNode, prop: string, args: JsNode[]): JsNode { - return call(member(object, prop), args); -} - -/** Shorthand: `a.b` */ -function m(object: JsNode, prop: string): JsNode { - return member(object, prop); -} - -/** Shorthand: `var name; (with no init)` */ -function v(name: string, init?: JsNode): JsNode { - return varDecl(name, init); -} - -/** Shorthand: expression statement */ -function es(expr: JsNode): JsNode { - return exprStmt(expr); -} - -/** Bitwise AND: `left & right` */ -function band(left: JsNode, right: JsNode): JsNode { - return bin(BOp.BitAnd, left, right); -} - -/** Bitwise unsigned right shift: `left >>> right` */ -function ursh(left: JsNode, right: JsNode): JsNode { - return bin(BOp.Ushr, left, right); -} - -// --- Builder --- - -/** - * Build the multi-layered anti-debugger protection IIFE as JsNode[]. - * - * @param names - Per-build randomized runtime identifiers. - * @returns A single-element array containing the IIFE call expression. - */ -export function buildDebugProtection( - names: RuntimeNames, - temps: TempNames -): JsNode[] { - const BT = names.bt; - const CA = names.cache; - - const dbgName = names.dbgProt; - - /** Shorthand for temp name lookup. */ - const Z = (key: string): string => temps[key]!; - - // Reusable ids - const sevId = id(Z("_sev")); - const btId = id(BT); - const caId = id(CA); - const pbId = id(Z("_pb")); - const fhId = id(Z("_fh")); - const dbgId = id(dbgName); - - // --- Body statements --- - const body: JsNode[] = []; - - // var _sev=0; - body.push(v(Z("_sev"), lit(0))); - // var _dc=0; (detection count — must reach threshold before escalating) - body.push(v(Z("_d"), lit(0))); - - // --- function _act() --- - body.push(buildActFunction(sevId, btId, caId, Z)); - - // --- function _p1() (built-in prototype integrity) --- - body.push(buildP1(Z)); - - // --- function _p3() (environment analysis) --- - body.push(buildP3(Z)); - - // --- FNV-1a hash computation of own toString --- - // var _src=.toString(); - body.push(v(Z("_src"), mcall(dbgId, "toString", []))); - // var _fh=0x811C9DC5; - body.push(v(Z("_fh"), lit(0x811c9dc5))); - // for(var _fi=0;_fi<_src.length;_fi++){_fh=((_fh^_src.charCodeAt(_fi))>>>0)*0x01000193>>>0;} - body.push( - forStmt( - v(Z("_fi"), lit(0)), - bin(BOp.Lt, id(Z("_fi")), m(id(Z("_src")), "length")), - assign(id(Z("_fi")), bin(BOp.Add, id(Z("_fi")), lit(1))), - [ - es( - assign( - fhId, - ursh( - bin( - BOp.Mul, - ursh( - bin( - BOp.BitXor, - fhId, - mcall(id(Z("_src")), "charCodeAt", [ - id(Z("_fi")), - ]) - ), - lit(0) - ), - lit(0x01000193) - ), - lit(0) - ) - ) - ), - ] - ) - ); - - // --- function _p4() (integrity self-check) --- - body.push(buildP4(dbgId, fhId, Z)); - - // var _pb=[_p1,_p3,_p4]; - body.push(v(Z("_pb"), arr(id(Z("_p1")), id(Z("_p3")), id(Z("_p4"))))); - - // --- function _run() --- - body.push(buildRun(pbId, sevId, Z)); - - // --- Initial setTimeout --- - // var _it=setTimeout(function(){_run();},500+((Math.random()*1500)|0)); - body.push( - v( - Z("_it"), - call(id("setTimeout"), [ - fnExpr(undefined, [], [es(call(id(Z("_run")), []))]), - bin( - BOp.Add, - lit(500), - bin( - BOp.BitOr, - bin( - BOp.Mul, - mcall(id("Math"), "random", []), - lit(1500) - ), - lit(0) - ) - ), - ]) - ) - ); - - // if(typeof _it==='object'&&_it.unref)_it.unref(); - body.push( - ifStmt( - bin( - BOp.And, - bin(BOp.Seq, un(UOp.Typeof, id(Z("_it"))), lit("object")), - m(id(Z("_it")), "unref") - ), - [es(mcall(id(Z("_it")), "unref", []))] - ) - ); - - // Wrap in IIFE: (function dbgName(){...body...})(); - return [exprStmt(call(fnExpr(dbgName, [], body), []))]; -} - -// --- Sub-builders --- - -/** - * Build _act() — escalating response function. - * - * Level 1-2: Silent bytecode instruction corruption - * Level 3-4: Cache wipe + constants array destruction - * Level 5+: Total bytecode annihilation + infinite busy loop (no debugger statements) - */ -function buildActFunction( - sevId: JsNode, - btId: JsNode, - caId: JsNode, - Z: (key: string) => string -): JsNode { - return fn( - Z("_act"), - [], - [ - // _sev++; - es(assign(sevId, bin(BOp.Add, sevId, lit(1)))), - // if(_sev<=2){...} - ifStmt( - bin(BOp.Lte, sevId, lit(2)), - [ - // try{var _ks=Object.keys(BT);for(var _ki=0;_ki<_ks.length;_ki++){...}}catch(_){} - tryCatch( - [ - v(Z("_ks"), mcall(id("Object"), "keys", [btId])), - forStmt( - v(Z("_ki"), lit(0)), - bin( - BOp.Lt, - id(Z("_ki")), - m(id(Z("_ks")), "length") - ), - assign( - id(Z("_ki")), - bin(BOp.Add, id(Z("_ki")), lit(1)) - ), - [ - v( - Z("_ue"), - index( - btId, - index(id(Z("_ks")), id(Z("_ki"))) - ) - ), - ifStmt( - bin( - BOp.And, - id(Z("_ue")), - m(id(Z("_ue")), "i") - ), - [ - forStmt( - v(Z("_ji"), lit(0)), - bin( - BOp.Lt, - id(Z("_ji")), - m( - m(id(Z("_ue")), "i"), - "length" - ) - ), - assign( - id(Z("_ji")), - bin( - BOp.Add, - id(Z("_ji")), - lit(2) - ) - ), - [ - es( - assign( - index( - m( - id( - Z("_ue") - ), - "i" - ), - id(Z("_ji")) - ), - band( - bin( - BOp.Add, - index( - m( - id( - Z( - "_ue" - ) - ), - "i" - ), - id( - Z( - "_ji" - ) - ) - ), - bin( - BOp.Mul, - sevId, - lit(7) - ) - ), - lit(0xffff) - ) - ) - ), - ] - ), - ] - ), - ] - ), - ], - "_", - [] - ), - ], - // else if(_sev<=4){...} - [ - ifStmt( - bin(BOp.Lte, sevId, lit(4)), - [ - // try{for(var _k in CA)delete CA[_k];}catch(_){} - tryCatch( - [ - forIn(Z("_k"), caId, [ - es( - un( - UOp.Delete, - index(caId, id(Z("_k"))) - ) - ), - ]), - ], - "_", - [] - ), - // try{var _ks=Object.keys(BT);for(var _ki=0;_ki<_ks.length;_ki++){var _ue=BT[_ks[_ki]];if(_ue)_ue.c=[];}}catch(_){} - tryCatch( - [ - v( - Z("_ks"), - mcall(id("Object"), "keys", [btId]) - ), - forStmt( - v(Z("_ki"), lit(0)), - bin( - BOp.Lt, - id(Z("_ki")), - m(id(Z("_ks")), "length") - ), - assign( - id(Z("_ki")), - bin(BOp.Add, id(Z("_ki")), lit(1)) - ), - [ - v( - Z("_ue"), - index( - btId, - index( - id(Z("_ks")), - id(Z("_ki")) - ) - ) - ), - ifStmt(id(Z("_ue")), [ - es( - assign( - m(id(Z("_ue")), "c"), - arr() - ) - ), - ]), - ] - ), - ], - "_", - [] - ), - ], - // else { total annihilation — zero out all instruction arrays + constants + infinite busy loop } - [ - // Wipe all bytecode entries completely - tryCatch( - [ - v( - Z("_ks"), - mcall(id("Object"), "keys", [btId]) - ), - forStmt( - v(Z("_ki"), lit(0)), - bin( - BOp.Lt, - id(Z("_ki")), - m(id(Z("_ks")), "length") - ), - assign( - id(Z("_ki")), - bin(BOp.Add, id(Z("_ki")), lit(1)) - ), - [ - v( - Z("_ue"), - index( - btId, - index( - id(Z("_ks")), - id(Z("_ki")) - ) - ) - ), - ifStmt(id(Z("_ue")), [ - // Zero out instructions - es( - assign( - m(id(Z("_ue")), "i"), - arr() - ) - ), - // Wipe constants - es( - assign( - m(id(Z("_ue")), "c"), - arr() - ) - ), - ]), - ] - ), - ], - "_", - [] - ), - // Wipe cache - tryCatch( - [ - forIn(Z("_k"), caId, [ - es( - un( - UOp.Delete, - index(caId, id(Z("_k"))) - ) - ), - ]), - ], - "_", - [] - ), - // Infinite busy loop — freezes the JS thread - // while(true){_sev++;} - whileStmt(lit(true), [ - es(assign(sevId, bin(BOp.Add, sevId, lit(1)))), - ]), - ] - ), - ] - ), - ] - ); -} - -/** - * Build _p1() — Built-in prototype integrity check. - * - * Detects monkey-patching of core Object/Array/JSON methods, a common - * technique used by reverse engineers to intercept VM operations. - * Complementary to _p5() which checks console methods. - * - * Fully CSP/TrustedScript safe — no console, eval, or debugger usage. - */ -function buildP1(Z: (key: string) => string): JsNode { - return fn( - Z("_p1"), - [], - [ - tryCatch( - [ - // var _nc='[native code]'; - v(Z("_nc"), lit("[native code]")), - // var _fn=[Object.keys,Object.defineProperty,Array.prototype.push,Array.prototype.slice,JSON.stringify]; - v( - Z("_fn"), - arr( - m(id("Object"), "keys"), - m(id("Object"), "defineProperty"), - m(m(id("Array"), "prototype"), "push"), - m(m(id("Array"), "prototype"), "slice"), - m(id("JSON"), "stringify") - ) - ), - // for(var _i=0;_i<_fn.length;_i++){ - forStmt( - v(Z("_i"), lit(0)), - bin(BOp.Lt, id(Z("_i")), m(id(Z("_fn")), "length")), - assign(id(Z("_i")), bin(BOp.Add, id(Z("_i")), lit(1))), - [ - // var _ts=Function.prototype.toString.call(_fn[_i]); - v( - Z("_ts"), - call( - m( - m( - m(id("Function"), "prototype"), - "toString" - ), - "call" - ), - [index(id(Z("_fn")), id(Z("_i")))] - ) - ), - // if(_ts.indexOf(_nc)===-1)return true; - ifStmt( - bin( - BOp.Seq, - mcall(id(Z("_ts")), "indexOf", [ - id(Z("_nc")), - ]), - lit(-1) - ), - [returnStmt(lit(true))] - ), - ] - ), - ], - "_", - [] - ), - // return false; - returnStmt(lit(false)), - ] - ); -} - -/** - * Build _p3() — environment analysis (--inspect flags, stack traces). - */ -function buildP3(Z: (key: string) => string): JsNode { - return fn( - Z("_p3"), - [], - [ - // try{var _st=(new Error()).stack||'';if(/--inspect|--debug/i.test(_st))return true;}catch(_){} - tryCatch( - [ - v( - Z("_st"), - bin( - BOp.Or, - m(newExpr(id("Error"), []), "stack"), - lit("") - ) - ), - ifStmt( - mcall(lit(/--inspect|--debug/i), "test", [ - id(Z("_st")), - ]), - [returnStmt(lit(true))] - ), - ], - "_", - [] - ), - // if(typeof process!=='undefined'){try{if(process.execArgv){for(...)}}catch(_){}} - ifStmt( - bin(BOp.Sneq, un(UOp.Typeof, id("process")), lit("undefined")), - [ - tryCatch( - [ - ifStmt(m(id("process"), "execArgv"), [ - forStmt( - v(Z("_i"), lit(0)), - bin( - BOp.Lt, - id(Z("_i")), - m( - m(id("process"), "execArgv"), - "length" - ) - ), - assign( - id(Z("_i")), - bin(BOp.Add, id(Z("_i")), lit(1)) - ), - [ - ifStmt( - mcall( - lit(/--inspect|--debug/), - "test", - [ - index( - m( - id("process"), - "execArgv" - ), - id(Z("_i")) - ), - ] - ), - [returnStmt(lit(true))] - ), - ] - ), - ]), - ], - "_", - [] - ), - ] - ), - // return false; - returnStmt(lit(false)), - ] - ); -} - -/** - * Build _p4() — function integrity self-verification (FNV-1a). - */ -function buildP4( - dbgId: JsNode, - fhId: JsNode, - Z: (key: string) => string -): JsNode { - return fn( - Z("_p4"), - [], - [ - // var _cs=.toString(); - v(Z("_cs"), mcall(dbgId, "toString", [])), - // var _ch=0x811C9DC5; - v(Z("_ch"), lit(0x811c9dc5)), - // for(var _ci=0;_ci<_cs.length;_ci++){_ch=((_ch^_cs.charCodeAt(_ci))>>>0)*0x01000193>>>0;} - forStmt( - v(Z("_ci"), lit(0)), - bin(BOp.Lt, id(Z("_ci")), m(id(Z("_cs")), "length")), - assign(id(Z("_ci")), bin(BOp.Add, id(Z("_ci")), lit(1))), - [ - es( - assign( - id(Z("_ch")), - bin( - BOp.Ushr, - bin( - BOp.Mul, - bin( - BOp.Ushr, - bin( - BOp.BitXor, - id(Z("_ch")), - mcall(id(Z("_cs")), "charCodeAt", [ - id(Z("_ci")), - ]) - ), - lit(0) - ), - lit(0x01000193) - ), - lit(0) - ) - ) - ), - ] - ), - // return _ch!==_fh; - returnStmt(bin(BOp.Sneq, id(Z("_ch")), fhId)), - ] - ); -} - -/** - * Build _run() — main detection loop with random probe selection and setTimeout recursion. - */ -function buildRun( - pbId: JsNode, - sevId: JsNode, - Z: (key: string) => string -): JsNode { - return fn( - Z("_run"), - [], - [ - // var _n=2+((Math.random()*2)|0); - v( - Z("_n"), - bin( - BOp.Add, - lit(2), - bin( - BOp.BitOr, - bin(BOp.Mul, mcall(id("Math"), "random", []), lit(2)), - lit(0) - ) - ) - ), - // var _det=false; - v(Z("_det"), lit(false)), - // for(var _i=0;_i<_n;_i++){var _idx=(Math.random()*_pb.length)|0;try{if(_pb[_idx]()){_det=true;break;}}catch(_){}} - forStmt( - v(Z("_i"), lit(0)), - bin(BOp.Lt, id(Z("_i")), id(Z("_n"))), - assign(id(Z("_i")), bin(BOp.Add, id(Z("_i")), lit(1))), - [ - v( - Z("_idx"), - bin( - BOp.BitOr, - bin( - BOp.Mul, - mcall(id("Math"), "random", []), - m(pbId, "length") - ), - lit(0) - ) - ), - tryCatch( - [ - ifStmt(call(index(pbId, id(Z("_idx"))), []), [ - es(assign(id(Z("_det")), lit(true))), - { type: "BreakStmt" } as JsNode, - ]), - ], - "_", - [] - ), - ] - ), - // if(_det){_d++;if(_d>=3)_act();}else{_d=0;} - // Require 3 consecutive detection rounds before escalating. - // A single false positive (e.g. DevTools open, sidebar) resets on next clean round. - ifStmt( - id(Z("_det")), - [ - es(assign(id(Z("_d")), bin(BOp.Add, id(Z("_d")), lit(1)))), - ifStmt(bin(BOp.Gte, id(Z("_d")), lit(3)), [ - es(call(id(Z("_act")), [])), - ]), - ], - // else: reset detection counter (transient false positive) - [es(assign(id(Z("_d")), lit(0)))] - ), - // if(_sev<5){var _nx=2000+((Math.random()*5000)|0);var _tid=setTimeout(_run,_nx);if(typeof _tid==='object'&&_tid.unref)_tid.unref();} - ifStmt(bin(BOp.Lt, sevId, lit(5)), [ - v( - Z("_nx"), - bin( - BOp.Add, - lit(2000), - bin( - BOp.BitOr, - bin( - BOp.Mul, - mcall(id("Math"), "random", []), - lit(5000) - ), - lit(0) - ) - ) - ), - v( - Z("_tid"), - call(id("setTimeout"), [id(Z("_run")), id(Z("_nx"))]) - ), - ifStmt( - bin( - BOp.And, - bin( - BOp.Seq, - un(UOp.Typeof, id(Z("_tid"))), - lit("object") - ), - m(id(Z("_tid")), "unref") - ), - [es(mcall(id(Z("_tid")), "unref", []))] - ), - ]), - ] - ); -} diff --git a/packages/ruam/src/ruamvm/builders/decoder.ts b/packages/ruam/src/ruamvm/builders/decoder.ts deleted file mode 100644 index ae8ca72..0000000 --- a/packages/ruam/src/ruamvm/builders/decoder.ts +++ /dev/null @@ -1,466 +0,0 @@ -/** - * Decoder builder — assembles RC4, custom binary decoder, and string - * decoder functions as AST nodes. - * - * All runtime JS is generated via pure AST construction — no raw() nodes. - * Dense bit-manipulation expressions are composed via nested BinOp nodes. - * - * @module ruamvm/builders/decoder - */ - -import type { JsNode } from "../nodes.js"; -import type { RuntimeNames } from "../../naming/compat-types.js"; -import type { SplitFn } from "../constant-splitting.js"; -import { - arr, - assign, - bin, - call, - exprStmt, - fn, - forStmt, - id, - ifStmt, - index, - lit, - member, - newExpr, - obj, - returnStmt, - un, - update, - varDecl, - BOp, - UpOp, - AOp, -} from "../nodes.js"; - -// --- Local helpers for dense bit manipulation --- - -/** `a ^ b` */ -const xor = (a: JsNode, b: JsNode): JsNode => bin(BOp.BitXor, a, b); - -/** `a & b` */ -const band = (a: JsNode, b: JsNode): JsNode => bin(BOp.BitAnd, a, b); - -/** `(expr) >>> 0` — unsigned coercion */ -const u32 = (expr: JsNode): JsNode => bin(BOp.Ushr, expr, lit(0)); - -// --- Custom binary decoder --- - -/** - * Build the custom binary decoder infrastructure as JsNode[]. - * - * Produces: - * 1. Alphabet variable declaration (`var _AL = "shuffled64chars"`) - * 2. Binary decode function (same bit-packing as base64, custom alphabet) - * - * Always emitted — all bytecode units use custom binary encoding. - * - * @param names - Randomized runtime identifier names. - * @param alphabet - The shuffled 64-char alphabet string (from build-time). - * @returns Array of JsNode containing the alphabet var and decode function. - */ -export function buildBinaryDecoderSource( - names: RuntimeNames, - alphabet: string -): JsNode[] { - // Use a name derived from the alpha name for the reverse table - const alphaRevName = names.alpha + "R"; - - return [ - // var _AL = "shuffled64chars"; - varDecl(names.alpha, lit(alphabet)), - // var _ALR = {}; - // for (var k = 0; k < _AL.length; k++) _ALR[_AL.charCodeAt(k)] = k; - varDecl(alphaRevName, obj()), - forStmt( - varDecl("k", lit(0)), - bin(BOp.Lt, id("k"), member(id(names.alpha), "length")), - update(UpOp.Inc, false, id("k")), - [ - exprStmt( - assign( - index( - id(alphaRevName), - call(member(id(names.alpha), "charCodeAt"), [ - id("k"), - ]) - ), - id("k") - ) - ), - ] - ), - buildCustomDecodeFunction(names, alphaRevName), - ]; -} - -/** - * Build the custom cipher function as JsNode[]. - * - * Only emitted when bytecode encryption is enabled. Uses FNV-1a key - * derivation + LCG keystream instead of RC4, avoiding the recognizable - * S-box/KSA/PRGA pattern. Constants go through the splitter. - * - * @param names - Randomized runtime identifier names. - * @param split - Optional constant splitter for numeric obfuscation. - * @returns Single-element array containing the cipher function declaration. - */ -export function buildRc4Source(names: RuntimeNames, split?: SplitFn): JsNode[] { - return [buildCipherFunction(names, split)]; -} - -// --- Custom decode function --- - -/** - * Build the custom binary decode function. - * - * Reverses the custom 64-char alphabet encoding produced at build time. - * Builds a reverse lookup table from the alphabet variable on each call - * (O(64) — negligible since decode runs once per unit load). - * - * ```js - * function _bd(str) { - * var T = {}; - * var A = _AL; - * for (var k = 0; k < A.length; k++) T[A.charCodeAt(k)] = k; - * var n = str.length; - * var out = new Uint8Array((n * 3 >> 2) + 3); - * var j = 0; - * for (var i = 0; i < n; i += 4) { - * var a = T[str.charCodeAt(i)] | 0; - * var b = T[str.charCodeAt(i + 1)] | 0; - * var c = T[str.charCodeAt(i + 2)] | 0; - * var d = T[str.charCodeAt(i + 3)] | 0; - * out[j++] = (a << 2) | (b >> 4); - * if (i + 2 < n) out[j++] = ((b & 15) << 4) | (c >> 2); - * if (i + 3 < n) out[j++] = ((c & 3) << 6) | d; - * } - * return out.subarray(0, j); - * } - * ``` - */ -function buildCustomDecodeFunction( - names: RuntimeNames, - alphaRevName: string -): JsNode { - const str = id("str"); - const T = id("T"); - const n = id("n"); - const out = id("out"); - const j = id("j"); - const i = id("i"); - - // Helper: T[str.charCodeAt(idx)] | 0 - const lookup = (idx: JsNode): JsNode => - bin( - BOp.BitOr, - index(T, call(member(str, "charCodeAt"), [idx])), - lit(0) - ); - - // Helper: out[j++] = expr - const writeOut = (expr: JsNode): JsNode => - exprStmt(assign(index(out, update(UpOp.Inc, false, j)), expr)); - - const body: JsNode[] = [ - // var T = _ALR; (reference the pre-built reverse table) - varDecl("T", id(alphaRevName)), - - // var n = str.length; - varDecl("n", member(str, "length")), - // var out = new Uint8Array((n * 3 >> 2) + 3); - varDecl( - "out", - newExpr(id("Uint8Array"), [ - bin( - BOp.Add, - bin(BOp.Shr, bin(BOp.Mul, n, lit(3)), lit(2)), - lit(3) - ), - ]) - ), - // var j = 0; - varDecl("j", lit(0)), - - // Decode loop: for (var i = 0; i < n; i += 4) { ... } - forStmt( - varDecl("i", lit(0)), - bin(BOp.Lt, i, n), - assign(i, lit(4), AOp.Add), - [ - // var a = T[str.charCodeAt(i)] | 0; - varDecl("a", lookup(i)), - // var b = T[str.charCodeAt(i + 1)] | 0; - varDecl("b", lookup(bin(BOp.Add, i, lit(1)))), - // var c = T[str.charCodeAt(i + 2)] | 0; - varDecl("c", lookup(bin(BOp.Add, i, lit(2)))), - // var d = T[str.charCodeAt(i + 3)] | 0; - varDecl("d", lookup(bin(BOp.Add, i, lit(3)))), - - // out[j++] = (a << 2) | (b >> 4); - writeOut( - bin( - BOp.BitOr, - bin(BOp.Shl, id("a"), lit(2)), - bin(BOp.Shr, id("b"), lit(4)) - ) - ), - - // if (i + 2 < n) out[j++] = ((b & 15) << 4) | (c >> 2); - ifStmt(bin(BOp.Lt, bin(BOp.Add, i, lit(2)), n), [ - writeOut( - bin( - BOp.BitOr, - bin(BOp.Shl, band(id("b"), lit(15)), lit(4)), - bin(BOp.Shr, id("c"), lit(2)) - ) - ), - ]), - - // if (i + 3 < n) out[j++] = ((c & 3) << 6) | d; - ifStmt(bin(BOp.Lt, bin(BOp.Add, i, lit(3)), n), [ - writeOut( - bin( - BOp.BitOr, - bin(BOp.Shl, band(id("c"), lit(3)), lit(6)), - id("d") - ) - ), - ]), - ] - ), - - // return out.subarray(0, j); - returnStmt(call(member(out, "subarray"), [lit(0), j])), - ]; - - return fn(names.b64, ["str"], body); -} - -// --- Custom cipher (replaces RC4) --- - -/** - * Build the custom cipher function — FNV-1a key derivation + LCG keystream. - * - * Avoids RC4's recognizable S-box/KSA/PRGA pattern. Uses the same - * FNV-1a and LCG primitives already present in other runtime code, - * making the cipher blend in as a hash-and-transform utility. - * - * ```js - * function cipher(data, key) { - * var h = FNV_BASIS; - * for (var i = 0; i < key.length; i++) { - * h = Math.imul(h ^ key.charCodeAt(i), FNV_PRIME); - * } - * h = h >>> 0; - * var out = new Uint8Array(data.length); - * for (var i = 0; i < data.length; i++) { - * h = (Math.imul(h, LCG_MULT) + LCG_INC) >>> 0; - * out[i] = data[i] ^ (h >>> 16 & 255); - * } - * return out; - * } - * ``` - */ -function buildCipherFunction(names: RuntimeNames, split?: SplitFn): JsNode { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - const data = id("data"); - const key = id("key"); - const h = id("h"); - const i = id("i"); - const out = id("out"); - - const body: JsNode[] = [ - // var h = FNV_BASIS; - varDecl("h", L(0x811c9dc5)), - - // for(var i=0; i>> 0; - exprStmt(assign(h, u32(h))), - - // var out = new Uint8Array(data.length); - varDecl("out", newExpr(id("Uint8Array"), [member(data, "length")])), - - // for(var i=0; i>> 0; - // out[i] = data[i] ^ (h >>> 16 & 255); - // } - forStmt( - assign(i, lit(0)), - bin(BOp.Lt, i, member(data, "length")), - update(UpOp.Inc, false, i), - [ - // h = (Math.imul(h, LCG_MULT) + LCG_INC) >>> 0; - exprStmt( - assign( - h, - u32( - bin( - BOp.Add, - call(id(names.imul), [h, L(1664525)]), - L(1013904223) - ) - ) - ) - ), - // out[i] = data[i] ^ (h >>> 16 & 255); - exprStmt( - assign( - index(out, i), - xor( - index(data, i), - band(bin(BOp.Ushr, h, lit(16)), lit(255)) - ) - ) - ), - ] - ), - - // return out; - returnStmt(out), - ]; - - return fn(names.rc4, ["data", "key"], body); -} - -// --- String constant decoder --- - -/** - * Build the string constant decoder function as JsNode[]. - * - * The decoder XOR-decodes encoded constant pool strings at load time - * using an LCG key stream. - * - * When `useImplicitKey` is true, the generated function accepts the - * master key as its first parameter (derived at load time from unit - * metadata by the caller). Otherwise the key is embedded as a numeric - * literal. - * - * @param names - Randomized runtime identifier names. - * @param stringKey - The numeric XOR key for string encoding. - * @param useImplicitKey - Whether the key is passed as a parameter (true) or embedded (false). - * @param split - Optional constant splitter for numeric obfuscation. - * @returns An array of JsNode containing the decoder function declaration. - */ -export function buildStringDecoderSource( - names: RuntimeNames, - stringKey: number, - useImplicitKey: boolean, - split?: SplitFn -): JsNode[] { - if (useImplicitKey) { - return [buildStrDecFunction(names, undefined, split)]; - } - return [buildStrDecFunction(names, stringKey, split)]; -} - -/** - * Build the strDec function. - * - * When `embeddedKey` is undefined, the function takes `mk` as first parameter - * (implicit key mode): - * ```js - * function strDec(mk, b, x) { - * var k = (mk ^ (x * 0x9E3779B9)) >>> 0; var s = ''; - * for (var i = 0; i < b.length; i++) { - * k = (k * 1664525 + 1013904223) >>> 0; - * s += String.fromCharCode(b[i] ^ (k & 65535)); - * } - * return s; - * } - * ``` - * - * When `embeddedKey` is provided, the key is embedded as a literal: - * ```js - * function strDec(b, x) { - * var k = (KEY ^ (x * 0x9E3779B9)) >>> 0; var s = ''; - * ... - * } - * ``` - */ -function buildStrDecFunction( - names: RuntimeNames, - embeddedKey: number | undefined, - split?: SplitFn -): JsNode { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - const implicit = embeddedKey === undefined; - const params = implicit ? ["mk", "b", "x"] : ["b", "x"]; - - const b = id("b"); - const x = id("x"); - const k = id("k"); - const i = id("i"); - - // The key source: either the `mk` parameter or the embedded numeric literal - const keySource: JsNode = implicit ? id("mk") : lit(embeddedKey >>> 0); - - const body: JsNode[] = [ - // var k = (keySource ^ (x * 0x9E3779B9)) >>> 0; - varDecl("k", u32(xor(keySource, bin(BOp.Mul, x, L(0x9e3779b9))))), - // var _ca = []; - varDecl("_ca", arr()), - - // for(var i=0; i>> 0; - // _ca.push(b[i] ^ (k & 65535)); - // } - forStmt( - varDecl("i", lit(0)), - bin(BOp.Lt, i, member(b, "length")), - update(UpOp.Inc, false, i), - [ - // k = (k * 1664525 + 1013904223) >>> 0; - exprStmt( - assign( - k, - u32( - bin( - BOp.Add, - bin(BOp.Mul, k, L(1664525)), - L(1013904223) - ) - ) - ) - ), - // _ca.push(b[i] ^ (k & 65535)); - exprStmt( - call(member(id("_ca"), "push"), [ - xor(index(b, i), band(k, lit(65535))), - ]) - ), - ] - ), - - // return String.fromCharCode.apply(null, _ca); - returnStmt( - call(member(member(id("String"), "fromCharCode"), "apply"), [ - lit(null), - id("_ca"), - ]) - ), - ]; - - return fn(names.strDec, params, body); -} diff --git a/packages/ruam/src/ruamvm/builders/deserializer.ts b/packages/ruam/src/ruamvm/builders/deserializer.ts deleted file mode 100644 index ced3149..0000000 --- a/packages/ruam/src/ruamvm/builders/deserializer.ts +++ /dev/null @@ -1,444 +0,0 @@ -/** - * Deserializer builder — assembles the binary bytecode deserializer as AST nodes. - * - * Produces a single function declaration that reads a compact binary - * `Uint8Array` format back into a bytecode unit object at runtime. - * - * The reader is structured as an object with shorthand methods, making it - * look like a utility class rather than a binary parser. All internal names - * (properties, methods, locals) are randomized via TempNames. - * - * @module ruamvm/builders/deserializer - */ - -import type { JsNode, ObjectEntry } from "../nodes.js"; -import type { RuntimeNames, TempNames } from "../../naming/compat-types.js"; -import { - fn, - varDecl, - id, - lit, - bin, - un, - assign, - call, - member, - index, - newExpr, - obj, - method, - exprStmt, - returnStmt, - forStmt, - switchStmt, - caseClause, - breakStmt, - block, - update, - BOp, - UOp, - UpOp, - AOp, -} from "../nodes.js"; - -// --- Builder --- - -/** - * Build the binary bytecode deserializer function as JsNode[]. - * - * Produces a single function declaration that reads a compact binary - * `Uint8Array` format back into a bytecode unit object at runtime. - * Internal structure uses an object with shorthand methods (class-like - * pattern) and all names are randomized via TempNames. - * - * @param names - Per-build randomized runtime identifiers. - * @param temps - Per-build randomized temp name mapping. - * @returns A single-element array containing the deserializer function. - */ -export function buildDeserializer( - names: RuntimeNames, - temps: TempNames -): JsNode[] { - const bytes = id("bytes"); - const TRUE = lit(true); - - // --- Temp name lookups --- - const T = (key: string): string => { - const name = temps[key]; - if (name === undefined) throw new Error(`Unknown temp: ${key}`); - return name; - }; - - // Reader object variable and property/method names - const DR = T("_dr"); // reader object - const DV = T("_dv"); // DataView property - const DOF = T("_dof"); // offset property - const DU8 = T("_du8"); // readU8 method - const DU16 = T("_du16"); // readU16 method - const DU32 = T("_du32"); // readU32 method - const DI32 = T("_di32"); // readI32 method - const DF64 = T("_df64"); // readF64 method - const DRS = T("_drs"); // readStr method - - // Parser local variable names - const FL = T("_dfl"); // flags - const PC = T("_dpc"); // param count - const RC = T("_drc"); // register count - const CC = T("_dcc"); // constant count - const CS = T("_dcs"); // constants array - const IC = T("_dic"); // instruction count - const IN = T("_din"); // instructions array - - // Switch-case locals (must use temp names to avoid collisions - // with obfuscateLocals which can rename 3+ char vars to 2-char - // names that clash with hardcoded locals in the same function scope) - const DTAG = T("_dtag"); // constant tag - const DEL = T("_del"); // encoded string length - const DEA = T("_dea"); // encoded string array - const DEI = T("_dei"); // encoded string index - - // Shorthand: reader.method() call - const rdr = id(DR); - const rcall = (m: string, args: JsNode[] = []): JsNode => - call(member(rdr, m), args); - // Shorthand: this.prop - const tprop = (p: string): JsNode => member(id("this"), p); - // Shorthand: this.method() from within a method body - const tcall = (m: string, args: JsNode[] = []): JsNode => - call(member(id("this"), m), args); - - // --- Function body --- - const body: JsNode[] = []; - - // --- Build reader object with shorthand methods --- - - const readerEntries: ObjectEntry[] = [ - // v: new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) - [ - DV, - newExpr(id("DataView"), [ - member(bytes, "buffer"), - member(bytes, "byteOffset"), - member(bytes, "byteLength"), - ]), - ], - // o: 0 - [DOF, lit(0)], - - // u8() { return this.v.getUint8(this.o++); } - method( - DU8, - [], - [ - returnStmt( - call(member(tprop(DV), "getUint8"), [ - update(UpOp.Inc, false, tprop(DOF)), - ]) - ), - ] - ), - - // u16() { var x=this.v.getUint16(this.o,true); this.o+=2; return x; } - method( - DU16, - [], - [ - varDecl( - "x", - call(member(tprop(DV), "getUint16"), [tprop(DOF), TRUE]) - ), - exprStmt(assign(tprop(DOF), lit(2), AOp.Add)), - returnStmt(id("x")), - ] - ), - - // u32() { var x=this.v.getUint32(this.o,true); this.o+=4; return x; } - method( - DU32, - [], - [ - varDecl( - "x", - call(member(tprop(DV), "getUint32"), [tprop(DOF), TRUE]) - ), - exprStmt(assign(tprop(DOF), lit(4), AOp.Add)), - returnStmt(id("x")), - ] - ), - - // i32() { var x=this.v.getInt32(this.o,true); this.o+=4; return x; } - method( - DI32, - [], - [ - varDecl( - "x", - call(member(tprop(DV), "getInt32"), [tprop(DOF), TRUE]) - ), - exprStmt(assign(tprop(DOF), lit(4), AOp.Add)), - returnStmt(id("x")), - ] - ), - - // f64() { var x=this.v.getFloat64(this.o,true); this.o+=8; return x; } - method( - DF64, - [], - [ - varDecl( - "x", - call(member(tprop(DV), "getFloat64"), [tprop(DOF), TRUE]) - ), - exprStmt(assign(tprop(DOF), lit(8), AOp.Add)), - returnStmt(id("x")), - ] - ), - - // str() { var n=this.u32(); var a=[]; for(var i=0;i - call(member(constants, "push"), [arg]); - const flags = id(FL); - - // Build switch cases for constant pool tags - const cases: JsNode[] = []; - - // case 0: cs.push(null); break; - cases.push(caseClause(lit(0), [exprStmt(push(lit(null))), breakStmt()])); - // case 1: cs.push(void 0); break; - cases.push( - caseClause(lit(1), [exprStmt(push(un(UOp.Void, lit(0)))), breakStmt()]) - ); - // case 2: cs.push(false); break; - cases.push(caseClause(lit(2), [exprStmt(push(lit(false))), breakStmt()])); - // case 3: cs.push(true); break; - cases.push(caseClause(lit(3), [exprStmt(push(lit(true))), breakStmt()])); - // case 4: cs.push(r.v.getInt8(r.o)); r.o+=1; break; - cases.push( - caseClause(lit(4), [ - exprStmt( - push( - call(member(member(rdr, DV), "getInt8"), [member(rdr, DOF)]) - ) - ), - exprStmt(assign(member(rdr, DOF), lit(1), AOp.Add)), - breakStmt(), - ]) - ); - // case 5: cs.push(r.v.getInt16(r.o,true)); r.o+=2; break; - cases.push( - caseClause(lit(5), [ - exprStmt( - push( - call(member(member(rdr, DV), "getInt16"), [ - member(rdr, DOF), - TRUE, - ]) - ) - ), - exprStmt(assign(member(rdr, DOF), lit(2), AOp.Add)), - breakStmt(), - ]) - ); - // case 6: cs.push(r.i32()); break; - cases.push(caseClause(lit(6), [exprStmt(push(rcall(DI32))), breakStmt()])); - // case 7: cs.push(r.f64()); break; - cases.push(caseClause(lit(7), [exprStmt(push(rcall(DF64))), breakStmt()])); - // case 8: cs.push(BigInt(r.str())); break; - cases.push( - caseClause(lit(8), [ - exprStmt(push(call(id("BigInt"), [rcall(DRS)]))), - breakStmt(), - ]) - ); - // case 9: { var p=r.str(); var f=r.str(); cs.push(new RegExp(p,f)); break; } - cases.push( - caseClause(lit(9), [ - block( - varDecl("p", rcall(DRS)), - varDecl("f", rcall(DRS)), - exprStmt(push(newExpr(id("RegExp"), [id("p"), id("f")]))), - breakStmt() - ), - ]) - ); - // case 11: { var _del=r.u16(); var _dea=[]; for(var _dei=0;_dei<_del;_dei++){_dea.push(r.u16());} cs.push(_dea); break; } - cases.push( - caseClause(lit(11), [ - block( - varDecl(DEL, rcall(DU16)), - varDecl(DEA, { type: "ArrayExpr", elements: [] }), - forStmt( - varDecl(DEI, lit(0)), - bin(BOp.Lt, id(DEI), id(DEL)), - update(UpOp.Inc, false, id(DEI)), - [exprStmt(call(member(id(DEA), "push"), [rcall(DU16)]))] - ), - exprStmt(push(id(DEA))), - breakStmt() - ), - ]) - ); - // default: cs.push(r.str()); break; - cases.push(caseClause(null, [exprStmt(push(rcall(DRS))), breakStmt()])); - - // for(var i=0;i[]), - ] - ) - ); - - // --- Instruction array --- - - // var ic=r.u32(); - body.push(varDecl(IC, rcall(DU32))); - - // var ins=new Int32Array(ic*2); - body.push( - varDecl(IN, newExpr(id("Int32Array"), [bin(BOp.Mul, id(IC), lit(2))])) - ); - - // for(var i=0;i - un(UOp.Not, un(UOp.Not, bin(BOp.BitAnd, v, lit(mask)))); - body.push( - returnStmt( - obj( - ["c", constants], - ["i", id(IN)], - ["r", id(RC)], - ["sl", lit(0)], - ["p", id(PC)], - ["g", band(flags, 1)], - ["s", band(flags, 2)], - ["st", band(flags, 4)], - ["a", band(flags, 8)], - ["el", band(flags, 16)], - ["xh", band(flags, 32)], - ["tc", band(flags, 64)], - ["bl", id(DBLM)] - ) - ) - ); - - return [fn(names.deser, ["bytes"], body)]; -} diff --git a/packages/ruam/src/ruamvm/builders/fingerprint.ts b/packages/ruam/src/ruamvm/builders/fingerprint.ts deleted file mode 100644 index ed26d08..0000000 --- a/packages/ruam/src/ruamvm/builders/fingerprint.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Fingerprint builder — assembles the environment fingerprint function as AST. - * - * Produces the same runtime code as {@link generateFingerprintSource} in - * `runtime/fingerprint.ts`, but represented as JsNode[] for the new - * AST-based ruamvm pipeline. - * - * The fingerprint function computes a deterministic hash from built-in - * function `.length` properties using XOR accumulation and Murmur3-style - * mixing. The result is the same for a given JS engine version but differs - * across engines, providing a weak form of environment binding. - * - * @module ruamvm/builders/fingerprint - */ - -import type { JsNode } from "../nodes.js"; -import type { RuntimeNames } from "../../naming/compat-types.js"; -import type { SplitFn } from "../constant-splitting.js"; -import { - fn, - varDecl, - id, - lit, - assign, - bin, - member, - exprStmt, - returnStmt, - BOp, - AOp, -} from "../nodes.js"; - -// --- Probe table --- - -/** Built-in property probes: [object chain, shift amount]. */ -const PROBES: [string, number][] = [ - ["Array.prototype.reduce.length", 0x18], - ["String.prototype.charCodeAt.length", 0x14], - ["Math.floor.length", 0x10], - ["Object.keys.length", 0x0c], - ["JSON.stringify.length", 0x08], - ["parseInt.length", 0x04], -]; - -// --- Local helpers for dense bit manipulation --- - -/** `a ^ b` */ -const xor = (a: JsNode, b: JsNode): JsNode => bin(BOp.BitXor, a, b); - -/** `a >>> n` */ -const ushr = (a: JsNode, n: number): JsNode => bin(BOp.Ushr, a, lit(n)); - -// --- Helpers --- - -/** Emit a dotted property chain as nested MemberExpr nodes. */ -function dotChain(chain: string): JsNode { - const parts = chain.split("."); - let node: JsNode = id(parts[0]!); - for (let i = 1; i < parts.length; i++) { - node = member(node, parts[i]!); - } - return node; -} - -// --- Builder --- - -/** - * Build the environment fingerprint function as JsNode[]. - * - * @param names - Per-build randomized runtime identifiers. - * @param split - Optional constant splitter for numeric obfuscation. - * @returns A single-element array containing the function declaration. - */ -export function buildFingerprintSource( - names: RuntimeNames, - split?: SplitFn -): JsNode[] { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - const h = id("h"); - - // --- Function body --- - - const body: JsNode[] = []; - - // var h = 0x5f3759df; - body.push(varDecl("h", L(0x5f3759df))); - - // h ^= .length << ; - for (const [chain, shift] of PROBES) { - body.push( - exprStmt( - assign(h, bin(BOp.Shl, dotChain(chain), lit(shift)), AOp.BitXor) - ) - ); - } - - // Murmur3-style finalizer - // h = (h ^ (h >>> 16)) * 0x45d9f3b; - body.push( - exprStmt(assign(h, bin(BOp.Mul, xor(h, ushr(h, 16)), L(0x45d9f3b)))) - ); - // h = (h ^ (h >>> 13)) * 0x45d9f3b; - body.push( - exprStmt(assign(h, bin(BOp.Mul, xor(h, ushr(h, 13)), L(0x45d9f3b)))) - ); - // h = h ^ (h >>> 16); - body.push(exprStmt(assign(h, xor(h, ushr(h, 16))))); - - // return h >>> 0; - body.push(returnStmt(ushr(h, 0))); - - return [fn(names.fp, [], body)]; -} diff --git a/packages/ruam/src/ruamvm/builders/globals.ts b/packages/ruam/src/ruamvm/builders/globals.ts deleted file mode 100644 index bd235f0..0000000 --- a/packages/ruam/src/ruamvm/builders/globals.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Global exposure builder — AST-based replacement for the template-literal - * approach in runtime/templates/globals.ts. - * - * Builds the if/else-if chain that assigns the VM dispatch function - * to whichever global object is available (globalThis, window, global, self). - * - * @module ruamvm/builders/globals - */ - -import type { JsNode } from "../nodes.js"; -import { - ifStmt, - exprStmt, - assign, - member, - id, - bin, - un, - lit, - BOp, - UOp, -} from "../nodes.js"; - -// --- Helpers --- - -/** Build `typeof !== 'undefined'` */ -function typeofCheck(name: string): JsNode { - return bin(BOp.Sneq, un(UOp.Typeof, id(name)), lit("undefined")); -} - -/** Build `. = ;` as an ExprStmt */ -function globalAssign(globalName: string, vmName: string): JsNode { - return exprStmt(assign(member(id(globalName), vmName), id(vmName))); -} - -// --- Builder --- - -/** - * Build the global exposure if/else-if chain. - * - * Produces four branches that check for globalThis, window, global, and self - * in order, assigning the VM function to the first available global object. - * - * @param vmName - The VM dispatch function name (already resolved) - * @returns AST nodes representing the if/else-if chain - */ -export function buildGlobalExposure(vmName: string): JsNode[] { - const globals = ["globalThis", "window", "global", "self"] as const; - - // Build the chain from the inside out (last else-if has no else clause) - // Start with the last branch: if(typeof self !== 'undefined') { self.vm = vm; } - let current = ifStmt(typeofCheck(globals[3]), [ - globalAssign(globals[3], vmName), - ]); - - // Build remaining branches in reverse: global, window, globalThis - for (let i = globals.length - 2; i >= 0; i--) { - const name = globals[i]!; - current = ifStmt( - typeofCheck(name), - [globalAssign(name, vmName)], - [current] - ); - } - - return [current]; -} diff --git a/packages/ruam/src/ruamvm/builders/incremental-cipher.ts b/packages/ruam/src/ruamvm/builders/incremental-cipher.ts deleted file mode 100644 index 8881720..0000000 --- a/packages/ruam/src/ruamvm/builders/incremental-cipher.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Incremental cipher builder — assembles icBlockKey and icMix functions as AST nodes. - * - * Emits the runtime counterparts of the build-time functions in - * `compiler/incremental-cipher.ts`. Both functions must produce - * IDENTICAL results to their build-time equivalents so that the - * encrypting and decrypting sides stay in sync. - * - * All runtime JS is generated via pure AST construction — no raw() nodes. - * Dense bit-manipulation expressions are composed via nested BinOp nodes - * with file-local helpers (xor, ushr, imul, xorAssign). - * - * @module ruamvm/builders/incremental-cipher - */ - -import type { JsNode } from "../nodes.js"; -import type { RuntimeNames } from "../../naming/compat-types.js"; -import type { SplitFn } from "../constant-splitting.js"; -import { - assign, - bin, - call, - exprStmt, - fn, - id, - lit, - returnStmt, - varDecl, - BOp, - AOp, -} from "../nodes.js"; -import { - FNV_PRIME, - GOLDEN_RATIO_PRIME, - MIX_PRIME1, - MIX_PRIME2, -} from "../../constants.js"; - -// --- Local helpers for dense bit manipulation --- - -/** `a ^ b` */ -const xor = (a: JsNode, b: JsNode): JsNode => bin(BOp.BitXor, a, b); - -/** `a >>> n` */ -const ushr = (a: JsNode, n: number): JsNode => bin(BOp.Ushr, a, lit(n)); - -/** `imulAlias(a, b)` — uses the IIFE-scope alias for Math.imul */ -const makeImul = - (imulName: string) => - (a: JsNode, b: JsNode): JsNode => - call(id(imulName), [a, b]); - -/** `h ^= expr` — shorthand for `exprStmt(assign(id("h"), expr, AOp.BitXor))` */ -const xorAssign = (target: string, value: JsNode): JsNode => - exprStmt(assign(id(target), value, AOp.BitXor)); - -/** - * Build the runtime incremental cipher helper functions as JsNode[]. - * - * Emits two function declarations: - * - `icBlockKey(mk, bid)` — derives a per-block base key from the master key - * and block ID using FNV-1a-style mixing with Murmur3 finalization. - * Mirrors build-time `deriveBlockKey()` in `compiler/incremental-cipher.ts`. - * - `icMix(s, op, od)` — advances chain feedback state by mixing in the - * decrypted opcode and operand values. - * Mirrors build-time `chainMix()` in `compiler/incremental-cipher.ts`. - * - * @param names Runtime identifier mapping. - * @param split Optional constant splitter for numeric obfuscation. - * @returns Array of JsNode representing both function declarations. - */ -export function buildIncrementalCipherSource( - names: RuntimeNames, - split?: SplitFn -): JsNode[] { - const imulId = names.imul; - return [ - buildBlockKeyFunction(names, split, imulId), - buildMixFunction(names, split, imulId), - ]; -} - -// --- icBlockKey --- - -/** - * Build the icBlockKey(mk, bid) function. - * - * Derives a per-block base key from the master key and block ID: - * 1. Mix masterKey with blockId via FNV-1a-style multiply-xor. - * 2. Mix again with the golden-ratio-scrambled blockId. - * 3. Apply Murmur3 avalanche finalization (16-bit shift + multiply + 13-bit shift). - * - * Must produce the EXACT same output as build-time `deriveBlockKey()`. - */ -function buildBlockKeyFunction( - names: RuntimeNames, - split?: SplitFn, - imulId?: string -): JsNode { - const L = (n: number): JsNode => (split ? split(n) : lit(n)); - const imul = makeImul(imulId ?? "Math.imul"); - const h = id("h"); - const mk = id("mk"); - const bid = id("bid"); - - return fn( - names.icBlockKey, - ["mk", "bid"], - [ - // var h = mk; - varDecl("h", mk), - // h = (Math.imul(h ^ bid, 0x01000193) >>> 0); - exprStmt(assign(h, ushr(imul(xor(h, bid), L(FNV_PRIME)), 0))), - // h = (Math.imul(h ^ (Math.imul(bid, 0x9e3779b9) >>> 0), 0x85EBCA6B) >>> 0); - exprStmt( - assign( - h, - ushr( - imul( - xor(h, ushr(imul(bid, L(GOLDEN_RATIO_PRIME)), 0)), - L(MIX_PRIME1) - ), - 0 - ) - ) - ), - // h ^= h >>> 16; - xorAssign("h", ushr(h, 16)), - // h = (Math.imul(h, 0xC2B2AE35) >>> 0); - exprStmt(assign(h, ushr(imul(h, L(MIX_PRIME2)), 0))), - // h ^= h >>> 13; - xorAssign("h", ushr(h, 13)), - // return h >>> 0; - returnStmt(ushr(h, 0)), - ] - ); -} - -// --- icMix --- - -/** - * Build the icMix(s, op, od) function. - * - * Advances the chain feedback state by mixing in the decrypted opcode - * and operand values, creating sequential dependency within a basic block: - * 1. Mix state with opcode via multiply-xor (MIX_PRIME1). - * 2. Mix with operand via multiply-xor (MIX_PRIME2). - * 3. Apply partial avalanche finalization (16-bit shift). - * - * Must produce the EXACT same output as build-time `chainMix()`. - */ -function buildMixFunction( - names: RuntimeNames, - split?: SplitFn, - imulId?: string -): JsNode { - const L = (n: number): JsNode => (split ? split(n) : lit(n)); - const imul = makeImul(imulId ?? "Math.imul"); - const h = id("h"); - const op = id("op"); - const od = id("od"); - - return fn( - names.icMix, - ["s", "op", "od"], - [ - // var h = s; - varDecl("h", id("s")), - // h = (Math.imul(h ^ op, 0x85EBCA6B) >>> 0); - exprStmt(assign(h, ushr(imul(xor(h, op), L(MIX_PRIME1)), 0))), - // h = (Math.imul(h ^ od, 0xC2B2AE35) >>> 0); - exprStmt(assign(h, ushr(imul(xor(h, od), L(MIX_PRIME2)), 0))), - // h ^= h >>> 16; - xorAssign("h", ushr(h, 16)), - // return h >>> 0; - returnStmt(ushr(h, 0)), - ] - ); -} diff --git a/packages/ruam/src/ruamvm/builders/interpreter.ts b/packages/ruam/src/ruamvm/builders/interpreter.ts deleted file mode 100644 index 526517c..0000000 --- a/packages/ruam/src/ruamvm/builders/interpreter.ts +++ /dev/null @@ -1,2800 +0,0 @@ -/** - * Interpreter builder — assembles exec functions from the handler registry. - * - * Builds the complete interpreter as a pure AST tree. The switch cases come - * from the handler registry; the surrounding scaffolding (dispatch loop, - * exception handling, etc.) is constructed directly as JsNode trees. - * - * Tree-based `obfuscateLocals()` from transforms.ts renames case-local - * variables. No string-based post-processing is needed. - * - * @module ruamvm/builders/interpreter - */ - -import type { CaseClause } from "../nodes.js"; -import type { JsNode } from "../nodes.js"; -import type { RuntimeNames, TempNames } from "../../naming/compat-types.js"; -import type { SplitFn } from "../constant-splitting.js"; -import { - caseClause, - lit, - switchStmt, - id, - breakStmt, - continueStmt, - fn, - varDecl, - exprStmt, - ifStmt, - forStmt, - whileStmt, - tryCatch, - returnStmt, - throwStmt, - newExpr, - bin, - un, - assign, - update, - call, - member, - index, - obj, - arr, - ternary, - fnExpr, - seq, - mapChildren, - BOp, - UOp, - UpOp, - AOp, -} from "../nodes.js"; -import { registry, makeHandlerCtx } from "../handlers/index.js"; -import { - VM_MAX_RECURSION_DEPTH, - LCG_MULTIPLIER, - LCG_INCREMENT, - WATERMARK_MAGIC, -} from "../../constants.js"; -import { deriveSeed } from "../../naming/scope.js"; -import type { NameRegistry } from "../../naming/registry.js"; -import { obfuscateLocals } from "../transforms.js"; -import { applyMBA } from "../mba.js"; -import { fragmentCases } from "../handler-fragmentation.js"; -import { aliasHandlerBody } from "../handler-aliasing.js"; -import { - generateOpaquePredicate, - injectOpaquePredicate, -} from "../opaque-predicates.js"; -import type { StructuralChoices } from "../../structural-choices.js"; -import { - buildWitnessCounter, - buildWeakMapCanary, - buildStackProbe, -} from "../observation-resistance.js"; - -/** Options for interpreter filtering and hardening. */ -export interface InterpreterBuildOptions { - dynamicOpcodes?: boolean; - decoyOpcodes?: boolean; - stackEncoding?: boolean; - usedOpcodes?: Set; - mixedBooleanArithmetic?: boolean; - handlerFragmentation?: boolean; - opcodeMutation?: boolean; - incrementalCipher?: boolean; - semanticOpacity?: boolean; - observationResistance?: boolean; - /** Tuning: probability (0-100) of witness check per handler. */ - witnessCheckProbability?: number; -} - -/** Result from building interpreter functions. */ -export interface InterpreterBuildResult { - /** Sync and async interpreter function AST nodes. */ - interpreters: JsNode[]; - /** Handler table + key anchor initialization AST (for IIFE scope). */ - handlerTableInit: JsNode[]; - /** Build-time key anchor value (for rolling cipher key derivation). */ - keyAnchorValue: number; -} - -/** - * Build both sync and async interpreter functions as JsNode[]. - * - * Returns interpreter function AST nodes plus the handler table - * initialization code (to be placed at IIFE scope). The handler table - * is shared between sync and async interpreters and is packed as an - * XOR-encoded array to resist regex extraction. - * - * Also computes a "key anchor" — a checksum of the packed handler table - * data — stored as a closure variable so that rcDeriveKey cannot be - * extracted via `new Function()`. - */ -export function buildInterpreterFunctions( - names: RuntimeNames, - temps: TempNames, - shuffleMap: number[], - debug: boolean, - rollingCipher: boolean, - seed: number, - interpOpts: InterpreterBuildOptions = {}, - split?: SplitFn, - hasAsyncUnits = true, - structuralChoices?: StructuralChoices, - registry?: NameRegistry -): InterpreterBuildResult { - // Function table dispatch replaces the switch — disable handler - // fragmentation since handlers are naturally isolated in separate - // function expressions (each handler is its own closure). - const effectiveOpts: InterpreterBuildOptions = { - ...interpOpts, - handlerFragmentation: false, - }; - - // Build handler table metadata (shared between sync and async interpreters). - // This produces the packed XOR-encoded handler table, key anchor, and the - // shuffled handler indices — but NOT the case bodies (those depend on - // isAsync and must be built per-mode). - const htMeta = buildHandlerTableMeta( - names, - temps, - shuffleMap, - seed, - effectiveOpts, - split - ); - - // Build sync case clauses (always needed). - const syncCases = buildCasesForMode( - names, - temps, - shuffleMap, - seed, - effectiveOpts, - false, - debug, - htMeta.handlerIndices, - split - ); - - const syncResult = buildExecFunction(names, temps, syncCases.cases, { - isAsync: false, - debug, - rollingCipher, - seed, - interpOpts: effectiveOpts, - split, - fragmentLabelMap: syncCases.fragmentLabelMap, - structuralChoices, - registry, - }); - - // Hoist sentinel/return-value declarations to IIFE scope - // (shared across all exec invocations — avoids per-call allocation) - const iifeDecls = [...syncResult.iifeDecls]; - - // IIFE-scope slot variable declarations for sync handler hoisting. - // These are the 17 variables that handler closures reference — - // declared once at IIFE scope, set/restored per exec() call. - const T = (key: string): string => { - const name = temps[key]; - if (name === undefined) throw new Error(`Unknown temp: ${key}`); - return name; - }; - const slotNames = [ - names.stk, - names.regs, - names.ip, - names.cArr, - names.operand, - names.scope, - names.exStk, - names.pEx, - names.hPEx, - names.cType, - names.cVal, - names.unit, - names.args, - names.tVal, - names.nTgt, - names.ho, - T("_g"), - ]; - // Stack-encoding key slot — declared at IIFE scope so hoisted sync handlers - // (which reference `_sek` via stkDec/stkEnc) can see it; set per exec call. - if (effectiveOpts.stackEncoding) { - slotNames.push(T("_sek")); - } - for (const name of slotNames) { - iifeDecls.push(varDecl(name, un(UOp.Void, lit(0)))); - } - - // When no async units exist, skip the full async interpreter. - // Emit a simple alias: var execAsync = exec - // The closure IIFE code references execAsync by name but never - // reaches that branch (all unit.s flags are falsy), so the alias - // is safe dead-code plumbing. - if (!hasAsyncUnits) { - const alias = varDecl(names.execAsync, id(names.exec)); - return { - interpreters: [syncResult.fnNode, alias], - handlerTableInit: [...htMeta.initNodes, ...iifeDecls], - keyAnchorValue: htMeta.keyAnchorValue, - }; - } - - // Async units exist — build the async interpreter with structural - // differentiation: different group count than sync to avoid the - // "two identical interpreter" signature. - const asyncCases = buildCasesForMode( - names, - temps, - shuffleMap, - seed, - effectiveOpts, - true, - debug, - htMeta.handlerIndices, - split - ); - - const asyncResult = buildExecFunction(names, temps, asyncCases.cases, { - isAsync: true, - debug, - rollingCipher, - seed, - interpOpts: effectiveOpts, - split, - fragmentLabelMap: asyncCases.fragmentLabelMap, - // Structural differentiation: async uses a different group count - // so it doesn't look like a carbon copy of the sync interpreter - asyncGroupOffset: 1, - structuralChoices, - registry, - }); - - // Async iifeDecls are the same sentinel/return-value vars — already declared - // by sync, so we don't re-emit them (they share the same temp names). - - return { - interpreters: [syncResult.fnNode, asyncResult.fnNode], - handlerTableInit: [...htMeta.initNodes, ...iifeDecls], - keyAnchorValue: htMeta.keyAnchorValue, - }; -} - -/** Handler table entry: physical opcode → handler index mapping. */ -interface HtEntry { - physicalOp: number; - handlerIdx: number; -} - -/** Handler table metadata (shared between sync/async interpreters). */ -interface HandlerTableMeta { - /** IIFE-scope initialization AST: packed data array, decode loop, key anchor. */ - initNodes: JsNode[]; - /** Shuffled handler indices (one per included opcode, in iteration order). */ - handlerIndices: number[]; - /** Build-time key anchor value (checksum of packed handler table data). */ - keyAnchorValue: number; -} - -/** Result from building case clauses for a specific mode (sync/async). */ -interface CaseBuildResult { - /** Switch case clauses (using handler indices as labels). */ - cases: CaseClause[]; - /** Fragment label map (handler fragmentation) or undefined. */ - fragmentLabelMap?: Map; -} - -/** - * Build handler table metadata: shuffle handler indices, pack as XOR-encoded - * array, compute key anchor, and generate IIFE-scope initialization AST. - * - * This is shared between sync and async interpreters — only the handler - * index mapping and table encoding is needed, not the case bodies. - */ -function buildHandlerTableMeta( - names: RuntimeNames, - temps: TempNames, - shuffleMap: number[], - seed: number, - interpOpts: InterpreterBuildOptions, - split?: SplitFn -): HandlerTableMeta { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - - // Count how many handlers will be included (real + decoy) - let includedCount = 0; - const includedPhysicalOps: number[] = []; - for (const [op] of registry) { - if ( - interpOpts.dynamicOpcodes && - interpOpts.usedOpcodes && - !interpOpts.usedOpcodes.has(op) - ) { - continue; - } - includedPhysicalOps.push(shuffleMap[op]!); - includedCount++; - } - - // Count decoy handlers - let decoyPhysicalOps: number[] = []; - if (interpOpts.decoyOpcodes && interpOpts.usedOpcodes) { - decoyPhysicalOps = getDecoyPhysicalOps( - shuffleMap, - interpOpts.usedOpcodes - ); - includedCount += decoyPhysicalOps.length; - } - - // --- Handler index shuffling --- - const handlerIndices = Array.from({ length: includedCount }, (_, i) => i); - let hs = seed >>> 0; - for (let i = handlerIndices.length - 1; i > 0; i--) { - hs = (hs * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - const j = hs % (i + 1); - [handlerIndices[i]!, handlerIndices[j]!] = [ - handlerIndices[j]!, - handlerIndices[i]!, - ]; - } - - // --- Collect handler table entries --- - const allPhysicalOps = [...includedPhysicalOps, ...decoyPhysicalOps]; - const entries: HtEntry[] = []; - for (let i = 0; i < allPhysicalOps.length; i++) { - entries.push({ - physicalOp: allPhysicalOps[i]!, - handlerIdx: handlerIndices[i]!, - }); - } - - // When opcode mutation is active, the MUTATE handler swaps _ht entries - // at runtime. Sparse holes (positions with no handler) become undefined, - // and swapping undefined into a valid position corrupts dispatch — the - // exec loop's try/catch catches the resulting TypeError but continues - // the for(;;) loop, hanging forever. Fix: pad _ht with dummy entries - // for every physical opcode not already included, mapping them to - // handler index 0. The decode loop then produces a fully dense array. - if (interpOpts.opcodeMutation) { - const includedSet = new Set(allPhysicalOps); - for (let p = 0; p < shuffleMap.length; p++) { - if (!includedSet.has(p)) { - entries.push({ physicalOp: p, handlerIdx: 0 }); - } - } - } - - // If handler fragmentation will be applied, we need to compute the - // fragment label map from a dummy set of cases with the same handler - // indices and statement counts. Since sync and async handlers produce - // the same number of statements (await wraps expressions, doesn't add - // statements), we use sync mode for the fragmentation preview. - if (interpOpts.handlerFragmentation) { - const nfName = temps["_nf"]; - if (nfName === undefined) throw new Error("Missing temp: _nf"); - - // Build a throwaway set of cases just for fragmentation mapping - const previewCases = buildCasesForModeInternal( - names, - temps, - shuffleMap, - seed, - interpOpts, - false, // sync mode for preview - false, // no debug for preview - handlerIndices - ); - - const fragResult = fragmentCases(previewCases, nfName, seed); - - // Remap handler table entries: handlerIdx → first-fragment ID - for (const entry of entries) { - const newIdx = fragResult.labelMap.get(entry.handlerIdx); - if (newIdx !== undefined) { - entry.handlerIdx = newIdx; - } - } - } - - // --- Pack handler table as XOR-encoded array --- - const htName = temps["_ht"]; - if (htName === undefined) throw new Error("Missing temp: _ht"); - const htdName = temps["_htd"]; - if (htdName === undefined) throw new Error("Missing temp: _htd"); - const htkName = temps["_htk"]; - if (htkName === undefined) throw new Error("Missing temp: _htk"); - const htiName = temps["_hti"]; - if (htiName === undefined) throw new Error("Missing temp: _hti"); - - // Derive encode key from seed - const htEncodeKey = Math.imul(seed, 0x45d9f3b) >>> 0; - - // Build encoded data array - const encodedData: number[] = []; - let rk = htEncodeKey; - for (const { physicalOp, handlerIdx } of entries) { - encodedData.push((physicalOp ^ (rk & 0xffff)) & 0xffff); - encodedData.push((handlerIdx ^ ((rk >>> 16) & 0xffff)) & 0xffff); - rk = (Math.imul(rk ^ physicalOp, 0x45d9f3b) ^ handlerIdx) >>> 0; - } - - // Compute key anchor: FNV-1a checksum of encoded data. - // Offset basis is FNV_OFFSET_BASIS ^ WATERMARK_MAGIC (steganographic - // watermark — alters the FNV seed so the watermark is provably present - // but invisible in the output; no dedicated variable or string). - const WM_OFFSET = (0x811c9dc5 ^ WATERMARK_MAGIC) >>> 0; - let anchor = WM_OFFSET; - for (const v of encodedData) { - anchor = Math.imul(anchor ^ v, 0x01000193) >>> 0; - } - - // --- Build IIFE-scope initialization AST --- - const initNodes: JsNode[] = []; - - // var _htd = [encoded values...]; - initNodes.push(varDecl(htdName, arr(...encodedData.map((v) => lit(v))))); - - // var _ht = []; - initNodes.push(varDecl(htName, arr())); - - // Decode loop: - // var _htk = HT_ENCODE_KEY; - // for(var _hti=0; _hti<_htd.length; _hti+=2) { - // var _v = _htd[_hti] ^ (_htk & 0xFFFF); - // var _w = _htd[_hti+1] ^ ((_htk >>> 16) & 0xFFFF); - // _ht[_v] = _w; - // _htk = (Math.imul(_htk ^ _v, 0x45D9F3B) ^ _w) >>> 0; - // } - initNodes.push(varDecl(htkName, L(htEncodeKey))); - initNodes.push( - forStmt( - varDecl(htiName, lit(0)), - bin(BOp.Lt, id(htiName), member(id(htdName), "length")), - assign(id(htiName), lit(2), AOp.Add), - (() => { - const htvName = temps["_htv"]; - const htwName = temps["_htw"]; - if (htvName === undefined || htwName === undefined) - throw new Error("Missing temps: _htv/_htw"); - return [ - // var htv = _htd[_hti] ^ (_htk & 0xFFFF); - varDecl( - htvName, - bin( - BOp.BitXor, - index(id(htdName), id(htiName)), - bin(BOp.BitAnd, id(htkName), lit(0xffff)) - ) - ), - // var htw = _htd[_hti+1] ^ ((_htk >>> 16) & 0xFFFF); - varDecl( - htwName, - bin( - BOp.BitXor, - index( - id(htdName), - bin(BOp.Add, id(htiName), lit(1)) - ), - bin( - BOp.BitAnd, - bin(BOp.Ushr, id(htkName), lit(16)), - lit(0xffff) - ) - ) - ), - // _ht[htv] = htw; - exprStmt( - assign(index(id(htName), id(htvName)), id(htwName)) - ), - // _htk = (Math.imul(_htk ^ htv, 0x45D9F3B) ^ htw) >>> 0; - exprStmt( - assign( - id(htkName), - bin( - BOp.Ushr, - bin( - BOp.BitXor, - call(id(names.imul), [ - bin( - BOp.BitXor, - id(htkName), - id(htvName) - ), - L(0x45d9f3b), - ]), - id(htwName) - ), - lit(0) - ) - ) - ), - ]; - })() - ) - ); - - // Key anchor: FNV-1a checksum with watermarked offset basis. - // Uses WM_OFFSET (= FNV_OFFSET_BASIS ^ WATERMARK_MAGIC) instead of - // the standard FNV offset basis. The watermark is invisible — just - // a non-standard starting value. Provably present: computing with - // the standard basis would break all rolling cipher decryption. - initNodes.push(varDecl(names.keyAnchor, L(WM_OFFSET))); - initNodes.push( - forStmt( - assign(id(htiName), lit(0)), - bin(BOp.Lt, id(htiName), member(id(htdName), "length")), - update(UpOp.Inc, false, id(htiName)), - [ - exprStmt( - assign( - id(names.keyAnchor), - bin( - BOp.Ushr, - call(id(names.imul), [ - bin( - BOp.BitXor, - id(names.keyAnchor), - index(id(htdName), id(htiName)) - ), - L(0x01000193), - ]), - lit(0) - ) - ) - ), - ] - ) - ); - - return { initNodes, handlerIndices, keyAnchorValue: anchor }; -} - -/** - * Build case clauses for a specific interpreter mode (sync or async). - * - * Uses the pre-computed handler indices from buildHandlerTableMeta so - * that both modes share the same physical-opcode → handler-index mapping. - * Some handlers (AWAIT, iterators, closures) produce different AST nodes - * depending on ctx.isAsync. - */ -function buildCasesForMode( - names: RuntimeNames, - temps: TempNames, - shuffleMap: number[], - seed: number, - interpOpts: InterpreterBuildOptions, - isAsync: boolean, - debug: boolean, - handlerIndices: number[], - split?: SplitFn -): CaseBuildResult { - let cases = buildCasesForModeInternal( - names, - temps, - shuffleMap, - seed, - interpOpts, - isAsync, - debug, - handlerIndices - ); - - // --- Observation resistance: witness counter + stack probes --- - // Prepend witness counter increments to every handler body and - // append verification checks + stack probes to a random subset. - // Runs before aliasing/MBA/fragmentation so the injected code - // gets further transformed by those passes. - if (interpOpts.observationResistance) { - const witnessResult = buildWitnessCounter(names, temps, seed, split); - const probeResult = buildStackProbe( - names, - temps, - seed, - split, - interpOpts.stackEncoding - ); - - // PRNG for witness check selection - let witnessSeed = deriveSeed(seed, "witnessCheck") >>> 0; - const witnessLcg = (): number => { - witnessSeed = - (Math.imul(witnessSeed, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - return witnessSeed; - }; - - // PRNG for stack probe selection - let probeSeed = deriveSeed(seed, "stackProbe") >>> 0; - const probeLcg = (): number => { - probeSeed = - (Math.imul(probeSeed, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - return probeSeed; - }; - - cases = cases.map((c) => { - if (c.label === null) return c; - const body = [...c.body]; - // Prepend witness counter increment to every handler - body.unshift(witnessResult.incrementStmt()); - // Witness verification check (probability from tuning) - if ( - witnessLcg() % 100 < - (interpOpts.witnessCheckProbability ?? 25) - ) { - body.push(...witnessResult.verifyStmts()); - } - // Stack integrity probe (~5% of handlers) - if (probeLcg() % 100 < 5) { - body.push(...probeResult.probeStmts()); - } - return caseClause(c.label, body); - }); - } - - // --- Handler aliasing --- - // Apply structural transforms to a subset of handler bodies so that - // different builds produce structurally different code for the same opcode. - // Must run before MBA and fragmentation (those further transform the aliased bodies). - if (interpOpts.semanticOpacity) { - let aliasSeed = deriveSeed(seed, "aliasSelect"); - cases = cases.map((c, idx) => { - if (c.label === null) return c; - // Each handler gets a deterministic coin flip - aliasSeed = - (Math.imul(aliasSeed, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - // Handler aliasing (~40% of handlers) - if (aliasSeed % 100 < 40) { - return caseClause(c.label, aliasHandlerBody(c.body, seed, idx)); - } - return c; - }); - } - - // --- Opaque predicate injection --- - // Wrap eligible handler bodies behind always-true/always-false predicates - // so the real code is hidden in one branch and dead code in the other. - // Must run after aliasing (so aliased bodies get wrapped) but before MBA - // and fragmentation (those further transform the wrapped bodies). - if (interpOpts.semanticOpacity) { - let predSeed = deriveSeed(seed, "opaquePredSelect"); - cases = cases.map((c, idx) => { - if (c.label === null) return c; - // Only inject into handlers with 3+ statements (small handlers - // aren't worth obfuscating — the predicate would dominate) - if (c.body.length < 3) return c; - // Opaque predicate injection (~50% of eligible handlers) - predSeed = - (Math.imul(predSeed, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - if (predSeed % 100 >= 50) return c; - - // Use the instruction pointer (always an integer) as the - // predicate input — bitwise OR with 0 ensures int32 semantics - const inputExpr = bin(BOp.BitOr, id(names.ip), lit(0)); - const predicate = generateOpaquePredicate( - inputExpr, - deriveSeed(seed, "opaquePred_" + idx), - idx - ); - - // Dead code for the never-taken branch: a few harmless - // statements that look plausible but never execute. - // Use a deterministic pick of dead code patterns. - const deadSeed = - (Math.imul(predSeed, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - const deadBody = generateDeadBody(deadSeed); - - return caseClause( - c.label, - injectOpaquePredicate(c.body, deadBody, predicate) - ); - }); - } - - // Default case - cases.push(caseClause(null, [breakStmt()])); - - // --- MBA --- - if (interpOpts.mixedBooleanArithmetic) { - cases = cases.map((c, idx) => { - if (c.label === null) return c; - // When semanticOpacity is on, derive a distinct seed per handler so - // that the same logical operation (e.g. XOR) looks structurally - // different across handlers — per-handler variant selection. - const mbaSeed = interpOpts.semanticOpacity - ? deriveSeed(seed, "mbaVariant_" + idx) - : seed; - return caseClause(c.label, applyMBA(c.body, mbaSeed)); - }); - } - - // --- Handler fragmentation --- - let fragmentLabelMap: Map | undefined; - if (interpOpts.handlerFragmentation) { - const nfName = temps["_nf"]; - if (nfName === undefined) throw new Error("Missing temp: _nf"); - - const fragResult = fragmentCases(cases, nfName, seed); - cases = fragResult.cases; - fragmentLabelMap = fragResult.labelMap; - } - - return { cases, fragmentLabelMap }; -} - -/** - * Internal helper: build raw case clauses (without default, MBA, or - * fragmentation) for a specific isAsync mode. - */ -function buildCasesForModeInternal( - names: RuntimeNames, - temps: TempNames, - shuffleMap: number[], - seed: number, - interpOpts: InterpreterBuildOptions, - isAsync: boolean, - debug: boolean, - handlerIndices: number[] -): CaseClause[] { - const ctx = makeHandlerCtx( - names, - temps, - isAsync, - debug, - interpOpts.stackEncoding - ); - - // Build raw cases from the handler registry - const rawCases: CaseClause[] = []; - for (const [op, handler] of registry) { - if ( - interpOpts.dynamicOpcodes && - interpOpts.usedOpcodes && - !interpOpts.usedOpcodes.has(op) - ) { - continue; - } - rawCases.push(caseClause(lit(shuffleMap[op]!), handler(ctx))); - } - - // Decoy handlers (isAsync-independent — use default ctx) - if (interpOpts.decoyOpcodes && interpOpts.usedOpcodes) { - rawCases.push( - ...generateDecoyHandlers(names, shuffleMap, interpOpts.usedOpcodes) - ); - } - - // Remap to handler indices - const cases: CaseClause[] = []; - for (let i = 0; i < rawCases.length; i++) { - const handlerIdx = handlerIndices[i]!; - cases.push(caseClause(lit(handlerIdx), rawCases[i]!.body)); - } - - return cases; -} - -/** - * Get physical opcodes for decoy handlers (without building case bodies). - * Must return the same set in the same order as generateDecoyHandlers. - */ -function getDecoyPhysicalOps( - shuffleMap: number[], - usedOpcodes: Set -): number[] { - // Collect unused logical opcodes - const unused: number[] = []; - for (let i = 0; i < shuffleMap.length; i++) { - if (!usedOpcodes.has(i)) unused.push(i); - } - if (unused.length === 0) return []; - - // Select 8-16 decoys (same logic as generateDecoyHandlers) - const count = Math.min(unused.length, 8 + (shuffleMap[0]! % 9)); - const selected: number[] = []; - for (let i = 0; i < count; i++) { - const idx = - (shuffleMap[i % shuffleMap.length]! + i * 7) % unused.length; - const op = unused[idx]!; - if (!selected.includes(op)) selected.push(op); - } - - return selected.map((logicalOp) => shuffleMap[logicalOp]!); -} - -/** - * Build a single interpreter function (sync or async) as a JsNode. - * - * Takes pre-built case clauses (from buildHandlerTableData) and - * constructs the scaffold as AST with tree-based obfuscateLocals(). - */ -function buildExecFunction( - names: RuntimeNames, - temps: TempNames, - cases: CaseClause[], - opts: { - isAsync: boolean; - debug: boolean; - rollingCipher: boolean; - seed: number; - interpOpts: InterpreterBuildOptions; - split?: SplitFn; - fragmentLabelMap?: Map; - /** Offset to apply to group count for structural differentiation. */ - asyncGroupOffset?: number; - structuralChoices?: StructuralChoices; - /** NameRegistry for collision-safe obfuscateLocals renaming. */ - registry?: NameRegistry; - } -): { fnNode: JsNode; iifeDecls: JsNode[] } { - const ctx = makeHandlerCtx( - names, - temps, - opts.isAsync, - opts.debug, - opts.interpOpts.stackEncoding - ); - const htName = temps["_ht"]; - if (htName === undefined) throw new Error("Missing temp: _ht"); - - // Select dispatch style based on structural choices (default: function-table) - const dispatchStyle = - opts.structuralChoices?.dispatchStyle ?? "function-table"; - const returnMech = opts.structuralChoices?.returnMechanism ?? "sentinel"; - const returnTag = opts.structuralChoices?.returnTag ?? 1; - - // --- Decode-once execution cache (Phase 1) --- - // When enabled, the unit's instruction stream is materialized once into a - // per-unit Int32Array of [resolved handler index, decrypted operand], cached - // on the unit. The steady-state dispatch loop then reads it directly — no - // per-instruction `_ht` indirection and (under rolling cipher) no - // per-iteration re-decryption. Gated OFF for the features whose threat model - // is runtime-memory instruction secrecy / tamper response (caching would - // defeat them). At-rest bytecode security is fully preserved (the serialized - // string and key derivation are untouched); this trades only runtime-memory - // instruction secrecy, consistent with the already-cached plaintext pool U.c. - // Gated to rolling-cipher builds: that is where the cache is a decisive win - // (it amortizes the per-instruction keystream derivation the slow path repeats - // every loop iteration). Without rolling cipher the operand stream is already - // plaintext, so caching would only duplicate memory for a marginal saving. - const io = opts.interpOpts; - const decodeCache = - opts.rollingCipher && - !io.incrementalCipher && - !io.opcodeMutation && - !io.observationResistance; - - let ftResult: { - preLoopDecls: JsNode[]; - iifeDecls: JsNode[]; - dispatchNodes: JsNode[]; - }; - - if (dispatchStyle === "direct-array") { - ftResult = buildDirectArrayDispatch( - cases, - temps, - htName, - ctx.PH as string, - opts.isAsync, - returnMech, - returnTag, - decodeCache - ); - } else if (dispatchStyle === "object-lookup") { - ftResult = buildObjectLookupDispatch( - cases, - temps, - htName, - ctx.PH as string, - opts.isAsync, - returnMech, - returnTag, - decodeCache - ); - } else { - // "function-table" — the original grouped dispatch - ftResult = buildFunctionTableDispatch( - cases, - temps, - htName, - ctx.PH as string, - opts.isAsync, - opts.asyncGroupOffset, - decodeCache - ); - } - - // For sync mode, hoist handler closures to IIFE scope. Async keeps - // closures per-call because concurrent async calls interleave state. - const hoistHandlers = !opts.isAsync; - - const fnNode = buildScaffoldAST( - names, - temps, - opts.isAsync, - opts.debug, - opts.rollingCipher, - opts.interpOpts, - ftResult.dispatchNodes, - ftResult.preLoopDecls, - opts.split, - hoistHandlers, - opts.seed, - decodeCache - ); - - // Apply tree-based obfuscation of local variable names. - // Build reserved set from RuntimeNames + TempNames values so - // genShort() avoids collisions with identifiers in the same scope. - const reserved = new Set([ - ...Object.values(names), - ...Object.values(temps), - ]); - const [obfuscated] = obfuscateLocals( - [fnNode], - opts.seed, - reserved, - opts.registry - ); - - // Also obfuscate handler closures when hoisted to IIFE scope - // (they contain handler-local variables like `name`, `val`, etc.) - let iifeDecls = ftResult.iifeDecls; - if (hoistHandlers && iifeDecls.length > 0) { - iifeDecls = obfuscateLocals( - iifeDecls, - deriveSeed(opts.seed, "iifeLocals"), - reserved, - opts.registry - ); - } - - return { fnNode: obfuscated!, iifeDecls }; -} - -// --- Scaffolding (AST) --- - -/** - * Build the interpreter function as a complete FnDecl AST node. - * - * Constructs the full exec/execAsync function body: function signature, - * depth tracking, variable declarations, dispatch loop, exception - * handling, rolling cipher, stack encoding, and debug trace. - * - * @param n - Runtime identifier names - * @param isAsync - Whether to build the async variant - * @param debug - Whether debug logging is enabled - * @param rollingCipher - Whether rolling cipher decryption is enabled - * @param interpOpts - Interpreter build options - * @param dispatchNodes - The dispatch AST nodes (switch or fragmented for-loop) - * @param htInit - Handler table initialization statements (dispatch indirection) - * @param split - Optional constant splitter for numeric obfuscation - * @returns FnDecl AST node for the complete interpreter function - */ -function buildScaffoldAST( - n: RuntimeNames, - temps: TempNames, - isAsync: boolean, - debug: boolean, - rollingCipher: boolean, - interpOpts: InterpreterBuildOptions, - dispatchNodes: JsNode[], - htInit?: JsNode[], - split?: SplitFn, - hoistHandlers = false, - seed = 0, - decodeCache = false -): JsNode { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - const fnName = isAsync ? n.execAsync : n.exec; - const fnLabel = isAsync ? n.execAsync : n.exec; - - const S = n.stk; - const O = n.operand, - SC = n.scope, - R = n.regs, - IP = n.ip; - const C = n.cArr, - I = n.iArr, - EX = n.exStk; - const PE = n.pEx, - HPE = n.hPEx, - CT = n.cType, - CV = n.cVal; - const U = n.unit, - A = n.args, - OS = n.outer; - const PH = n.phys; - // Scope property names removed — prototypal scope uses Object.create chain - - /** Temp name lookup shorthand. */ - const T = (key: string): string => { - const name = temps[key]; - if (name === undefined) throw new Error(`Unknown temp: ${key}`); - return name; - }; - - // --- Hoisted handler slot variables --- - // When hoistHandlers is true (sync mode), handler closures live at IIFE - // scope and reference these variables directly. On each exec call we save - // the current values to a local array, set new values, and restore in - // finally. This eliminates ~290 closure allocations per call. - // - // The 17 slot variables: S, R, IP, C, O, SC, EX, PE, HPE, CT, CV, - // U, A, TV, NT, HO, _g. All are referenced by handler code. - const slotNames = hoistHandlers - ? [ - S, - R, - IP, - C, - O, - SC, - EX, - PE, - HPE, - CT, - CV, - U, - A, - n.tVal, - n.nTgt, - n.ho, - T("_g"), - ] - : []; - // The stack-encoding key `_sek` is a slot too (hoisted handlers read it). - if (hoistHandlers && interpOpts.stackEncoding) { - slotNames.push(T("_sek")); - } - - // --- Per-unit minimal save/restore partition --- - // The per-call slot snapshot/restore is the dominant overhead under deep - // recursion. Most units touch only a subset of slots, so two groups are - // saved/restored only when the unit actually uses them (flags packed on U - // at compile time — see compiler/slot-analysis.ts): - // • EXC group {PE,HPE,CT,CV} — gated on `U.xh` (exception machinery). - // • TC group {TV,NT,HO} — gated on `U.tc` (this/super/new.target/closure). - // Everything else (incl. EX, which RETURN reads on every return, and A, - // read by ordinary param loads) is CORE — always saved. The snapshot uses - // scalar locals (not a per-call array) to avoid an allocation each call. - const EXC_SET = new Set([PE, HPE, CT, CV]); - const TC_SET = new Set([n.tVal, n.nTgt, n.ho]); - const coreSlots = slotNames.filter( - (s) => !EXC_SET.has(s) && !TC_SET.has(s) - ); - const excSlots = slotNames.filter((s) => EXC_SET.has(s)); - const tcSlots = slotNames.filter((s) => TC_SET.has(s)); - // Snapshot-local name per slot (renamed to a short name by obfuscateLocals). - const snapName = new Map(); - slotNames.forEach((s, i) => snapName.set(s, "vmSnap" + i)); - const snap = (s: string): JsNode => id(snapName.get(s)!); - // Runtime flag locals (read once from U). `unitP` is the unit param. - const fXh = "vmUsesXh"; - const fTc = "vmUsesTc"; - /** Partitioned slot restore (used by the recursion guard + the outer finally). */ - const buildSlotRestore = (): JsNode[] => { - const out: JsNode[] = coreSlots.map((s) => - exprStmt(assign(id(s), snap(s))) - ); - if (excSlots.length > 0) { - out.push( - ifStmt( - id(fXh), - excSlots.map((s) => exprStmt(assign(id(s), snap(s)))) - ) - ); - } - if (tcSlots.length > 0) { - out.push( - ifStmt( - id(fTc), - tcSlots.map((s) => exprStmt(assign(id(s), snap(s)))) - ) - ); - } - return out; - }; - - // When hoisting, use distinct parameter names so they don't shadow - // the IIFE-scope slot variables. These names (length >= 3, no _ prefix) - // will be renamed by obfuscateLocals to short 2-char names. - const paramU = hoistHandlers ? "unitP" : U; - const paramA = hoistHandlers ? "argsP" : A; - const paramOS = hoistHandlers ? "osP" : OS; - const paramTV = hoistHandlers ? "tvP" : n.tVal; - const paramNT = hoistHandlers ? "ntP" : n.nTgt; - const paramHO = hoistHandlers ? "hoP" : n.ho; - - // --- Outer body --- - const outerBody: JsNode[] = []; - - // depth++ - outerBody.push(exprStmt(update(UpOp.Inc, false, id(n.depth)))); - - // When hoisting, snapshot current IIFE-scope slot values and copy params. - if (hoistHandlers) { - // Read the per-unit slot-usage flags once (packed on U at compile time). - outerBody.push(varDecl(fXh, member(id(paramU), "xh"))); - outerBody.push(varDecl(fTc, member(id(paramU), "tc"))); - // CORE snapshot — scalar locals (no per-call array allocation). - for (const s of coreSlots) { - outerBody.push(varDecl(snapName.get(s)!, id(s))); - } - // EXC + TC snapshot vars are declared unconditionally (so the finally - // restore can reference them) but only assigned when the unit uses them. - for (const s of [...excSlots, ...tcSlots]) { - outerBody.push(varDecl(snapName.get(s)!)); - } - if (excSlots.length > 0) { - outerBody.push( - ifStmt( - id(fXh), - excSlots.map((s) => - exprStmt(assign(id(snapName.get(s)!), id(s))) - ) - ) - ); - } - if (tcSlots.length > 0) { - outerBody.push( - ifStmt( - id(fTc), - tcSlots.map((s) => - exprStmt(assign(id(snapName.get(s)!), id(s))) - ) - ) - ); - } - // Copy params to IIFE-scope slots. U/A are CORE (always). The - // this-context params are copied only when the unit reads them. - outerBody.push(exprStmt(assign(id(U), id(paramU)))); - outerBody.push(exprStmt(assign(id(A), id(paramA)))); - if (tcSlots.length > 0) { - outerBody.push( - ifStmt(id(fTc), [ - exprStmt(assign(id(n.tVal), id(paramTV))), - exprStmt(assign(id(n.nTgt), id(paramNT))), - exprStmt(assign(id(n.ho), id(paramHO))), - ]) - ); - } - } - - // callStack tracking — only in debug mode (avoids per-call array push/pop) - if (debug) { - // var _uid_=(U._dbgId||'?') - outerBody.push( - varDecl( - T("_uid_"), - bin(BOp.Or, member(id(U), T("_dbgId")), lit("?")) - ) - ); - // callStack.push(_uid_) - outerBody.push( - exprStmt(call(member(id(n.callStack), "push"), [id(T("_uid_"))])) - ); - } - - // Recursion guard: if(depth>500){depth--;throw new RangeError(...)} - const guardBody: JsNode[] = [ - exprStmt(update(UpOp.Dec, false, id(n.depth))), - ]; - if (debug) { - guardBody.push(exprStmt(call(member(id(n.callStack), "pop"), []))); - } - if (hoistHandlers) { - // Restore snapshot values before throwing (snapshot locals still in scope) - guardBody.push(...buildSlotRestore()); - } - guardBody.push( - throwStmt( - newExpr(id("RangeError"), [ - bin( - BOp.Add, - bin(BOp.Add, lit("Maximum call "), lit("s")), - lit("tack size exceeded") - ), - ]) - ) - ); - outerBody.push( - ifStmt(bin(BOp.Gt, id(n.depth), lit(VM_MAX_RECURSION_DEPTH)), guardBody) - ); - - // --- Try body (main interpreter logic) --- - const tryBody: JsNode[] = []; - - // Helper: declare or assign depending on hoisting mode. - // Slot variables are IIFE-scope when hoisting (assigned, not declared). - const slotSet = new Set(slotNames); - const declOrAssign = (name: string, init: JsNode): JsNode => - slotSet.has(name) - ? exprStmt(assign(id(name), init)) - : varDecl(name, init); - - // Variable declarations (or assignments for hoisted slots) - tryBody.push(declOrAssign(S, arr())); - // Build packed register array (no holes) for V8 fast element access - tryBody.push(declOrAssign(R, arr())); - tryBody.push( - forStmt( - varDecl("_rl", member(id(U), "r")), - bin(BOp.Gt, id("_rl"), lit(0)), - update(UpOp.Dec, false, id("_rl")), - [exprStmt(call(member(id(R), "push"), [un(UOp.Void, lit(0))]))] - ) - ); - tryBody.push(declOrAssign(IP, lit(0))); - tryBody.push(declOrAssign(C, member(id(U), "c"))); - // I is not a slot (dispatch-only). With the decode-once cache, I instead - // points at the materialized stream (built below, after rcState init). - if (!decodeCache) { - tryBody.push(varDecl(I, member(id(U), "i"))); - } - tryBody.push(declOrAssign(EX, lit(null))); - if (hoistHandlers) { - // PE/HPE/CT/CV are touched only by exception units; initialize them - // (and save/restore them) only when `U.xh` is set. A non-exception unit - // never reads or writes them, so the caller's values pass through - // untouched and need no snapshot. - tryBody.push( - ifStmt(id(fXh), [ - exprStmt(assign(id(PE), lit(null))), - exprStmt(assign(id(HPE), lit(false))), - exprStmt(assign(id(CT), lit(0))), - exprStmt(assign(id(CV), un(UOp.Void, lit(0)))), - ]) - ); - } else { - tryBody.push(declOrAssign(PE, lit(null))); - tryBody.push(declOrAssign(HPE, lit(false))); - tryBody.push(declOrAssign(CT, lit(0))); - tryBody.push(declOrAssign(CV, un(UOp.Void, lit(0)))); - } - // Scope-object elision: units whose compiled body never mutates, reassigns, - // or captures their scope (`U.el`) can reuse the outer scope directly, - // avoiding a per-call `Object.create` allocation + a prototype-chain hop. - // A missing/false flag falls back to the always-correct `Object.create`. - const osRef = (): JsNode => (hoistHandlers ? id(paramOS) : id(OS)); - tryBody.push( - declOrAssign( - SC, - ternary( - member(id(U), "el"), - osRef(), - call(member(id("Object"), "create"), [osRef()]) - ) - ) - ); - // Stack pointer (P) eliminated — stack uses Array.push/pop/length - - // _g = — resolved once at IIFE scope - tryBody.push(declOrAssign(T("_g"), id(n.globalRef))); - - // Optional: debug entry logging - if (debug) { - tryBody.push( - varDecl( - T("_uid"), - bin(BOp.Or, member(id(U), T("_dbgId")), lit("?")) - ) - ); - tryBody.push( - exprStmt( - call(id(n.dbg), [ - lit("ENTER"), - lit(fnLabel), - bin(BOp.Add, lit("unit="), id(T("_uid"))), - bin(BOp.Add, lit("params="), member(id(U), "p")), - bin(BOp.Add, lit("args="), member(id(A), "length")), - bin( - BOp.Add, - lit("async="), - un(UOp.Not, un(UOp.Not, member(id(U), "s"))) - ), - bin(BOp.Add, lit("regs="), member(id(U), "r")), - bin(BOp.Add, lit("depth="), id(n.depth)), - ]) - ) - ); - } - - // Optional: rolling cipher init. rcDeriveKey(U) is a pure function of the - // unit's constant metadata (folded with the closure key anchor), so it is - // invariant across every exec() of a given unit — memoize it on U.k to avoid - // recomputing the FNV derivation per call (a hot cost under recursion). Only - // the per-exec LOCAL rcState is ever poisoned (observation resistance); the - // memoized U.k stays the clean master key, so tamper response is unaffected. - if (rollingCipher) { - tryBody.push( - varDecl( - n.rcState, - ternary( - bin(BOp.Sneq, member(id(U), "k"), un(UOp.Void, lit(0))), - member(id(U), "k"), - assign(member(id(U), "k"), call(id(n.rcDeriveKey), [id(U)])) - ) - ) - ); - } - - // Optional: incremental cipher init - // var _icState = icBlockKey(rcState, 0) — start in block 0 - if (interpOpts.incrementalCipher && rollingCipher) { - tryBody.push( - varDecl( - T("_icState"), - call(id(n.icBlockKey), [id(n.rcState), lit(0)]) - ) - ); - } - - // Optional: stack encoding — bind the per-exec key to the `_sek` slot (it is - // a save/restored IIFE-scope slot so hoisted handler closures can see it; the - // stkEnc/stkDec helpers are defined once at IIFE scope; the stack stays plain). - if (interpOpts.stackEncoding) { - tryBody.push( - declOrAssign(T("_sek"), buildStackEncodingKeyExpr(n, split)) - ); - } - - // --- Decode-once execution cache (Phase 1) --- - // Materialize, once per unit, an Int32Array holding [resolved handler index, - // decrypted operand] for every instruction, cached on U.d. The keystream is a - // pure function of instruction index (position-keyed), so a single forward - // pass yields the correct plaintext for EVERY position regardless of how the - // loop later reaches it (sequential, jump, or exception route). I then points - // at the cache; the dispatch loop reads handler index + operand directly. - if (decodeCache) { - const HT = T("_ht"); - const DI = "d"; // unit-object property holding the decoded stream - const SRC = "mvSrc", - LEN = "mvLen", - ARR = "mvArr", - POS = "mvPos", - IDX = "mvIdx", - KS = "mvKs"; - - // Per-instruction body: resolve handler index + decode operand. - const loopBody: JsNode[] = []; - if (rollingCipher) { - // var mvIdx = mvPos >>> 1; (instruction index = keystream position) - loopBody.push(varDecl(IDX, bin(BOp.Ushr, id(POS), lit(1)))); - // Inline rcMix(rcState, mvIdx, mvIdx ^ GOLDEN): byte-identical to the - // per-instruction decrypt the slow path performs. - loopBody.push(varDecl(KS, id(n.rcState))); - loopBody.push( - exprStmt( - assign( - id(KS), - bin( - BOp.Ushr, - call(id(n.imul), [ - bin(BOp.BitXor, id(KS), id(IDX)), - L(0x85ebca6b), - ]), - lit(0) - ) - ) - ) - ); - loopBody.push( - exprStmt( - assign( - id(KS), - bin( - BOp.Ushr, - call(id(n.imul), [ - bin( - BOp.BitXor, - id(KS), - bin(BOp.BitXor, id(IDX), L(0x9e3779b9)) - ), - L(0xc2b2ae35), - ]), - lit(0) - ) - ) - ) - ); - loopBody.push( - exprStmt( - assign( - id(KS), - bin(BOp.BitXor, id(KS), bin(BOp.Ushr, id(KS), lit(16))) - ) - ) - ); - loopBody.push( - exprStmt(assign(id(KS), bin(BOp.Ushr, id(KS), lit(0)))) - ); - // mvArr[mvPos] = _ht[ (mvSrc[mvPos] ^ (mvKs & 0xFFFF)) & 0xFFFF ]; - loopBody.push( - exprStmt( - assign( - index(id(ARR), id(POS)), - index( - id(HT), - bin( - BOp.BitAnd, - bin( - BOp.BitXor, - index(id(SRC), id(POS)), - bin(BOp.BitAnd, id(KS), lit(0xffff)) - ), - lit(0xffff) - ) - ) - ) - ) - ); - // mvArr[mvPos+1] = (mvSrc[mvPos+1] ^ mvKs) | 0; - loopBody.push( - exprStmt( - assign( - index(id(ARR), bin(BOp.Add, id(POS), lit(1))), - bin( - BOp.BitOr, - bin( - BOp.BitXor, - index(id(SRC), bin(BOp.Add, id(POS), lit(1))), - id(KS) - ), - lit(0) - ) - ) - ) - ); - } else { - // Light variant (no rolling cipher): pre-resolve handler indices, - // operand passthrough. mvSrc is already plaintext, so the cache - // exposes nothing new — purely removes the _ht indirection. - loopBody.push( - exprStmt( - assign( - index(id(ARR), id(POS)), - index(id(HT), index(id(SRC), id(POS))) - ) - ) - ); - loopBody.push( - exprStmt( - assign( - index(id(ARR), bin(BOp.Add, id(POS), lit(1))), - index(id(SRC), bin(BOp.Add, id(POS), lit(1))) - ) - ) - ); - } - - // if (!U.d) { var mvSrc=U.i; var mvLen=mvSrc.length; var mvArr=new - // Int32Array(mvLen); for(var mvPos=0; mvPos>>1 - whileBody.push( - varDecl( - T("_ri"), - bin(BOp.Ushr, bin(BOp.Sub, id(IP), lit(2)), lit(1)) - ) - ); - } - - // Optional: observation resistance — periodic identity binding + canary check. - // Every ~256 instructions, XOR orVerify() into rcState (identity binding) - // and also check the WeakMap canary. When clean (untampered), both return - // 0 and XOR is a no-op. When a bound function has been replaced or the - // canary has been tampered with, a non-zero corruption constant silently - // poisons all subsequent instruction decryption. - if (interpOpts.observationResistance && rollingCipher) { - const checkBody: JsNode[] = [ - // rcState = (rcState ^ orVerify()) >>> 0; - exprStmt( - assign( - id(n.rcState), - bin( - BOp.Ushr, - bin( - BOp.BitXor, - id(n.rcState), - call(id(n.orVerify), []) - ), - lit(0) - ) - ) - ), - ]; - - // WeakMap canary check — XOR a corruption constant if canary is - // tampered, or 0 if clean. Uses temps _orRef (canary object) and - // _orExp (WeakMap instance). - const canaryRef = temps["_orRef"]; - const canaryWm = temps["_orExp"]; - if (canaryRef !== undefined && canaryWm !== undefined) { - // Derive per-build canary corruption constant - const canaryCorruptSeed = deriveSeed(seed, "canaryCorrupt"); - const canaryCorrupt = (canaryCorruptSeed || 0xcafebabe) >>> 0; - - // rcState = (rcState ^ ((!(_orExp instanceof WeakMap) || _orExp.get(_orRef) !== true) ? CORRUPT : 0)) >>> 0; - checkBody.push( - exprStmt( - assign( - id(n.rcState), - bin( - BOp.Ushr, - bin( - BOp.BitXor, - id(n.rcState), - ternary( - bin( - BOp.Or, - un( - UOp.Not, - bin( - BOp.Instanceof, - id(canaryWm), - id("WeakMap") - ) - ), - bin( - BOp.Sneq, - call(member(id(canaryWm), "get"), [ - id(canaryRef), - ]), - lit(true) - ) - ), - split - ? split(canaryCorrupt) - : lit(canaryCorrupt), - lit(0) - ) - ), - lit(0) - ) - ) - ) - ); - } - - // if ((_ri & 0xFF) === 0) { ...checks... } - whileBody.push( - ifStmt( - bin(BOp.Seq, bin(BOp.BitAnd, id(T("_ri")), lit(0xff)), lit(0)), - checkBody - ) - ); - } - - // Optional: incremental cipher decrypt (outer layer — must come BEFORE rolling cipher) - // At block boundaries, reset the chain state to the block's base key. - // Then XOR-decrypt PH and O using the current chain state. - // Finally advance the chain using the DECRYPTED (plaintext) values. - if (interpOpts.incrementalCipher && rollingCipher) { - const icState = T("_icState"); - const ri = T("_ri"); - - // Block boundary check: if(U.bl[_ri]!==void 0){_icState=icBlockKey(rcState,U.bl[_ri])} - whileBody.push( - ifStmt( - bin( - BOp.Sneq, - index(member(id(U), "bl"), id(ri)), - un(UOp.Void, lit(0)) - ), - [ - exprStmt( - assign( - id(icState), - call(id(n.icBlockKey), [ - id(n.rcState), - index(member(id(U), "bl"), id(ri)), - ]) - ) - ), - ] - ) - ); - - // Decrypt: PH = (PH ^ (_icState & 0xFFFF)) & 0xFFFF - whileBody.push( - exprStmt( - assign( - id(PH), - bin( - BOp.BitAnd, - bin( - BOp.BitXor, - id(PH), - bin(BOp.BitAnd, id(icState), lit(0xffff)) - ), - lit(0xffff) - ) - ) - ) - ); - - // Decrypt: O = (O ^ _icState) | 0 - whileBody.push( - exprStmt( - assign( - id(O), - bin(BOp.BitOr, bin(BOp.BitXor, id(O), id(icState)), lit(0)) - ) - ) - ); - - // Advance chain: _icState = icMix(_icState, PH, O) - whileBody.push( - exprStmt( - assign( - id(icState), - call(id(n.icMix), [id(icState), id(PH), id(O)]) - ) - ) - ); - } - - // Optional: rolling cipher decrypt (inlined rcMix for performance). - // Skipped under the decode cache — the materialized stream is already decrypted. - if (rollingCipher && !decodeCache) { - // Inline rcMix(rcState, _ri, _ri ^ 0x9E3779B9): - // var _ks = rcState; - whileBody.push(varDecl(T("_ks"), id(n.rcState))); - // _ks = imul(_ks ^ _ri, 0x85EBCA6B) >>> 0; - whileBody.push( - exprStmt( - assign( - id(T("_ks")), - bin( - BOp.Ushr, - call(id(n.imul), [ - bin(BOp.BitXor, id(T("_ks")), id(T("_ri"))), - L(0x85ebca6b), - ]), - lit(0) - ) - ) - ) - ); - // _ks = imul(_ks ^ (_ri ^ 0x9E3779B9), 0xC2B2AE35) >>> 0; - whileBody.push( - exprStmt( - assign( - id(T("_ks")), - bin( - BOp.Ushr, - call(id(n.imul), [ - bin( - BOp.BitXor, - id(T("_ks")), - bin(BOp.BitXor, id(T("_ri")), L(0x9e3779b9)) - ), - L(0xc2b2ae35), - ]), - lit(0) - ) - ) - ) - ); - // _ks ^= _ks >>> 16; - whileBody.push( - exprStmt( - assign( - id(T("_ks")), - bin( - BOp.BitXor, - id(T("_ks")), - bin(BOp.Ushr, id(T("_ks")), lit(16)) - ) - ) - ) - ); - // _ks = _ks >>> 0; - whileBody.push( - exprStmt(assign(id(T("_ks")), bin(BOp.Ushr, id(T("_ks")), lit(0)))) - ); - // PH=(PH^(_ks&0xFFFF))&0xFFFF - whileBody.push( - exprStmt( - assign( - id(PH), - bin( - BOp.BitAnd, - bin( - BOp.BitXor, - id(PH), - bin(BOp.BitAnd, id(T("_ks")), lit(0xffff)) - ), - lit(0xffff) - ) - ) - ) - ); - // O=(O^_ks)|0 - whileBody.push( - exprStmt( - assign( - id(O), - bin(BOp.BitOr, bin(BOp.BitXor, id(O), id(T("_ks"))), lit(0)) - ) - ) - ); - } - - // Optional: debug trace - if (debug) { - whileBody.push( - exprStmt(call(id(n.dbgOp), [id(PH), id(O), id(C), id(S)])) - ); - } - - // Dispatch: either a plain switch or fragmented for-loop + switch - whileBody.push(...dispatchNodes); - - // Inner try body: while(IP<_il){...}; return void 0; - const innerTryBody: JsNode[] = [ - whileStmt(bin(BOp.Lt, id(IP), id(T("_il"))), whileBody), - returnStmt(un(UOp.Void, lit(0))), - ]; - - // --- Inner catch handler --- - const catchBody: JsNode[] = []; - - // Optional: debug exception logging - if (debug) { - catchBody.push( - exprStmt( - call(id(n.dbg), [ - lit("EXCEPTION"), - lit("error="), - ternary( - bin(BOp.And, id("e"), member(id("e"), "message")), - member(id("e"), "message"), - id("e") - ), - bin( - BOp.Add, - lit(EX + "="), - ternary(id(EX), member(id(EX), "length"), lit(0)) - ), - ]) - ) - ); - } - - // The exception-completion machinery (PE/HPE/CT/CV resets + EX routing) is - // collected here so it can be gated on `U.xh` when hoisting: a non-exception - // unit never touches these slots, so its caught native exceptions must - // propagate via the bare `throw e` below without disturbing them. - const excCatch: JsNode[] = []; - - // HPE=false; PE=null; CT=0; CV=void 0; - excCatch.push(exprStmt(assign(id(HPE), lit(false)))); - excCatch.push(exprStmt(assign(id(PE), lit(null)))); - excCatch.push(exprStmt(assign(id(CT), lit(0)))); - excCatch.push(exprStmt(assign(id(CV), un(UOp.Void, lit(0))))); - - // if(EX&&EX.length>0){...} - const exHandlerBody: JsNode[] = []; - - // var _h=EX.pop() - exHandlerBody.push(varDecl(T("_h"), call(member(id(EX), "pop"), []))); - - // if(_h._ci>=0){ ... catch routing ... } - const catchRouteBody: JsNode[] = []; - if (debug) { - catchRouteBody.push( - exprStmt( - call(id(n.dbg), [ - lit("CATCH"), - bin(BOp.Add, lit("ip="), member(id(T("_h")), T("_ci"))), - bin(BOp.Add, lit("sp="), member(id(T("_h")), T("_sp"))), - ]) - ) - ); - } - // S.length=_h._sp (restore stack to saved depth) - catchRouteBody.push( - exprStmt(assign(member(id(S), "length"), member(id(T("_h")), T("_sp")))) - ); - // Push error onto stack: S.push(stkEnc?(e, S.length, _sek)) - // (scaffold-level push — must encode like ctx.push when stackEncoding, or a - // handler peek would decode a raw Error as e[1] === undefined.) - catchRouteBody.push( - exprStmt( - call(member(id(S), "push"), [ - interpOpts.stackEncoding - ? call(id(n.stkEnc), [ - id("e"), - member(id(S), "length"), - id(T("_sek")), - ]) - : id("e"), - ]) - ) - ); - // IP=_h._ci*2 - catchRouteBody.push( - exprStmt( - assign(id(IP), bin(BOp.Mul, member(id(T("_h")), T("_ci")), lit(2))) - ) - ); - // continue - catchRouteBody.push(continueStmt()); - - exHandlerBody.push( - ifStmt( - bin(BOp.Gte, member(id(T("_h")), T("_ci")), lit(0)), - catchRouteBody - ) - ); - - // if(_h._fi>=0){ ... finally routing ... } - const finallyRouteBody: JsNode[] = []; - if (debug) { - finallyRouteBody.push( - exprStmt( - call(id(n.dbg), [ - lit("FINALLY"), - bin(BOp.Add, lit("ip="), member(id(T("_h")), T("_fi"))), - bin(BOp.Add, lit("sp="), member(id(T("_h")), T("_sp"))), - ]) - ) - ); - } - // S.length=_h._sp (restore stack to saved depth) - finallyRouteBody.push( - exprStmt(assign(member(id(S), "length"), member(id(T("_h")), T("_sp")))) - ); - // PE=e; HPE=true - finallyRouteBody.push(exprStmt(assign(id(PE), id("e")))); - finallyRouteBody.push(exprStmt(assign(id(HPE), lit(true)))); - // IP=_h._fi*2 - finallyRouteBody.push( - exprStmt( - assign(id(IP), bin(BOp.Mul, member(id(T("_h")), T("_fi")), lit(2))) - ) - ); - // continue - finallyRouteBody.push(continueStmt()); - - exHandlerBody.push( - ifStmt( - bin(BOp.Gte, member(id(T("_h")), T("_fi")), lit(0)), - finallyRouteBody - ) - ); - - excCatch.push( - ifStmt( - bin(BOp.And, id(EX), bin(BOp.Gt, member(id(EX), "length"), lit(0))), - exHandlerBody - ) - ); - - // Emit the exception machinery: gated on `U.xh` when hoisting (so - // non-exception units leave PE/HPE/CT/CV — which they never save — alone), - // inline otherwise (async path uses per-call locals, always needed). - if (hoistHandlers) { - catchBody.push(ifStmt(id(fXh), excCatch)); - } else { - catchBody.push(...excCatch); - } - - // Optional: debug uncaught logging - if (debug) { - catchBody.push( - exprStmt( - call(id(n.dbg), [ - lit("UNCAUGHT"), - lit("error="), - ternary( - bin(BOp.And, id("e"), member(id("e"), "message")), - member(id("e"), "message"), - id("e") - ), - ]) - ) - ); - } - - // throw e - catchBody.push(throwStmt(id("e"))); - - // Inner try-catch wrapped in for(;;) - const innerTryCatch = tryCatch(innerTryBody, "e", catchBody); - const foreverLoop = forStmt(null, null, null, [innerTryCatch]); - - // Handler table initialization (dispatch indirection) - if (htInit) { - tryBody.push(...htInit); - } - - tryBody.push(foreverLoop); - - // --- Outer finally --- - const finallyBody: JsNode[] = [ - exprStmt(update(UpOp.Dec, false, id(n.depth))), - ]; - if (debug) { - finallyBody.push(exprStmt(call(member(id(n.callStack), "pop"), []))); - } - // Restore snapshot slot values when hoisting (partitioned: CORE always, - // EXC/TC only when the unit uses them — matching what was snapshotted). - if (hoistHandlers) { - finallyBody.push(...buildSlotRestore()); - } - - // Outer try-finally wrapping the whole body - outerBody.push(tryCatch(tryBody, undefined, undefined, finallyBody)); - - return fn( - fnName, - [paramU, paramA, paramOS, paramTV, paramNT, paramHO], - outerBody, - { - async: isAsync, - } - ); -} - -// --- Decoy handlers --- - -/** - * Generate fake case handlers for unused opcode slots. - * - * These are never called but make the interpreter appear more complex, - * hardening against static analysis. All bodies are pure AST nodes. - */ -function generateDecoyHandlers( - n: RuntimeNames, - shuffleMap: number[], - usedOpcodes: Set -): CaseClause[] { - const S = n.stk, - C = n.cArr, - O = n.operand; - const R = n.regs, - SC = n.scope; - - /** S[S.length-1] — peek at top of stack */ - const tos = () => - index(id(S), bin(BOp.Sub, member(id(S), "length"), lit(1))); - - // AST decoy body factories — look like real array push/pop/length ops - const decoyBodyFactories: (() => JsNode[])[] = [ - // var b=S.pop(); S[S.length-1]=S[S.length-1]+b - () => [ - varDecl("b", call(member(id(S), "pop"), [])), - exprStmt(assign(tos(), bin(BOp.Add, tos(), id("b")))), - ], - // var b=S.pop(); S[S.length-1]=S[S.length-1]-b - () => [ - varDecl("b", call(member(id(S), "pop"), [])), - exprStmt(assign(tos(), bin(BOp.Sub, tos(), id("b")))), - ], - // var b=S.pop(); S[S.length-1]=S[S.length-1]*b - () => [ - varDecl("b", call(member(id(S), "pop"), [])), - exprStmt(assign(tos(), bin(BOp.Mul, tos(), id("b")))), - ], - // S[S.length-1]=~S[S.length-1] - () => [exprStmt(assign(tos(), un(UOp.BitNot, tos())))], - // S.push(C[O]) - () => [exprStmt(call(member(id(S), "push"), [index(id(C), id(O))]))], - // R[O]=S.pop() - () => [ - exprStmt( - assign(index(id(R), id(O)), call(member(id(S), "pop"), [])) - ), - ], - // S.push(R[O]) - () => [exprStmt(call(member(id(S), "push"), [index(id(R), id(O))]))], - // var s=SC; if(s&&C[O]in s){s[C[O]]=S.pop();} - () => [ - varDecl("s", id(SC)), - ifStmt( - bin( - BOp.And, - id("s"), - bin(BOp.In, index(id(C), id(O)), id("s")) - ), - [ - exprStmt( - assign( - index(id("s"), index(id(C), id(O))), - call(member(id(S), "pop"), []) - ) - ), - ] - ), - ], - // S[S.length-1]=!S[S.length-1] - () => [exprStmt(assign(tos(), un(UOp.Not, tos())))], - // var b=S.pop(); S[S.length-1]=S[S.length-1]&b - () => [ - varDecl("b", call(member(id(S), "pop"), [])), - exprStmt(assign(tos(), bin(BOp.BitAnd, tos(), id("b")))), - ], - // var b=S.pop(); S[S.length-1]=S[S.length-1]|b - () => [ - varDecl("b", call(member(id(S), "pop"), [])), - exprStmt(assign(tos(), bin(BOp.BitOr, tos(), id("b")))), - ], - // S[S.length-1]=-S[S.length-1] - () => [exprStmt(assign(tos(), un(UOp.Neg, tos())))], - // S[S.length-1]=+S[S.length-1]+1 - () => [ - exprStmt(assign(tos(), bin(BOp.Add, un(UOp.Pos, tos()), lit(1)))), - ], - // S[S.length-1]=typeof S[S.length-1] - () => [exprStmt(assign(tos(), un(UOp.Typeof, tos())))], - // S.pop() - () => [exprStmt(call(member(id(S), "pop"), []))], - // S.push(S[S.length-1]) - () => [exprStmt(call(member(id(S), "push"), [tos()]))], - ]; - - // Collect unused logical opcodes - const unused: number[] = []; - for (let i = 0; i < shuffleMap.length; i++) { - if (!usedOpcodes.has(i)) unused.push(i); - } - if (unused.length === 0) return []; - - // Select 8-16 decoys (deterministic based on shuffleMap) - const count = Math.min(unused.length, 8 + (shuffleMap[0]! % 9)); - const selected: number[] = []; - for (let i = 0; i < count; i++) { - const idx = - (shuffleMap[i % shuffleMap.length]! + i * 7) % unused.length; - const op = unused[idx]!; - if (!selected.includes(op)) selected.push(op); - } - - return selected.map((logicalOp, i) => { - const factory = - decoyBodyFactories[(logicalOp + i) % decoyBodyFactories.length]!; - const physicalOp = shuffleMap[logicalOp]!; - return caseClause(lit(physicalOp), [...factory(), breakStmt()]); - }); -} - -// --- Opaque predicate dead code --- - -/** - * Generate a dead code body for the never-taken branch of an opaque predicate. - * - * Produces 2-4 harmless statements that look plausible (variable declarations, - * void expressions, arithmetic) but never execute. Uses a deterministic seed - * so the dead code pattern is reproducible. - * - * @param seed - Deterministic seed for pattern selection - * @returns Array of dead code JsNode statements - */ -function generateDeadBody(seed: number): JsNode[] { - const pattern = seed % 4; - switch (pattern) { - case 0: - // var _d = 0; void 0; - return [varDecl("_d", lit(0)), exprStmt(un(UOp.Void, lit(0)))]; - case 1: - // var _d = 0; _d = _d + 1; - return [ - varDecl("_d", lit(0)), - exprStmt(assign(id("_d"), bin(BOp.Add, id("_d"), lit(1)))), - ]; - case 2: - // var _d = 0; var _e = 1; void (_d + _e); - return [ - varDecl("_d", lit(0)), - varDecl("_e", lit(1)), - exprStmt(un(UOp.Void, bin(BOp.Add, id("_d"), id("_e")))), - ]; - case 3: - default: - // var _d = 0 | 0; void _d; - return [ - varDecl("_d", bin(BOp.BitOr, lit(0), lit(0))), - exprStmt(un(UOp.Void, id("_d"))), - ]; - } -} - -// --- Function table dispatch (Sig 3: eliminates switch pattern) --- - -/** Maximum number of handler groups for function table dispatch. */ -const FT_MAX_GROUPS = 4; - -/** - * Walk a JsNode tree bottom-up, applying a visitor to each node. - * Skips nested function bodies (FnDecl, FnExpr, ArrowFn) so that - * break/return transforms only affect the handler scope. - */ -function walkSkipFns( - node: JsNode, - visitor: (n: JsNode) => JsNode | null -): JsNode { - if ( - node.type === "FnDecl" || - node.type === "FnExpr" || - node.type === "ArrowFn" - ) { - return node; - } - const walked = mapChildren(node, (child) => walkSkipFns(child, visitor)); - return visitor(walked) ?? walked; -} - -/** - * Transform a handler case body for use inside a function table closure. - * - * - `BreakStmt` → `ReturnStmt()` (exit handler, continue dispatch loop) - * - `ReturnStmt(expr)` → `ReturnStmt(seq(assign(_frv, expr), _frs))` - * (signal exec to return _frv via the sentinel) - * - Nested function bodies (FnDecl/FnExpr/ArrowFn) are NOT walked - * (their break/return are for their own scope) - */ -function transformBodyForFT( - body: JsNode[], - frsName: string, - frvName: string -): JsNode[] { - return body.map((node) => - walkSkipFns(node, (n) => { - if (n.type === "BreakStmt") { - return returnStmt(); - } - if (n.type === "ReturnStmt") { - const value = - (n as { type: "ReturnStmt"; value?: JsNode }).value ?? - un(UOp.Void, lit(0)); - return returnStmt(seq(assign(id(frvName), value), id(frsName))); - } - return null; - }) - ); -} - -/** - * Build function table dispatch — grouped handler function arrays - * with if-else routing. Replaces the giant switch statement. - * - * Each handler body becomes a closure (function expression) that - * captures interpreter locals (S, R, C, I, IP, etc.). Handlers - * are split into groups stored as separate arrays, with a balanced - * if-else tree routing to the correct group by handler index. - * - * Return control flow uses a sentinel pattern: - * - Normal handlers return `undefined` → dispatch loop continues - * - Return-type handlers do `return (_frv = value, _frs)` → dispatch - * loop detects the sentinel and returns `_frv` from exec - * - * @param cases - Switch case clauses (handler index labels + bodies) - * @param temps - Per-build randomized temp name mapping - * @param htName - Handler table variable name - * @param phName - Physical opcode variable name - * @param isAsync - Whether this is the async interpreter (handler functions - * must be async and calls must be awaited) - * @returns preLoopDecls (goes before while loop) and dispatchNodes - * (goes inside while loop body) - */ -function buildFunctionTableDispatch( - cases: CaseClause[], - temps: TempNames, - htName: string, - phName: string, - isAsync: boolean, - groupCountOffset = 0, - decodeCache = false -): { preLoopDecls: JsNode[]; iifeDecls: JsNode[]; dispatchNodes: JsNode[] } { - const FRS = temps["_frs"]; - const FRV = temps["_frv"]; - const FDI = temps["_fdi"]; - if (!FRS || !FRV || !FDI) { - throw new Error("Missing function table temp names"); - } - - const groupTempNames = [ - temps["_fg0"], - temps["_fg1"], - temps["_fg2"], - temps["_fg3"], - ]; - - // Filter default case, sort by handler index (label value) - const handlerCases = cases - .filter((c): c is CaseClause & { label: JsNode } => c.label !== null) - .sort((a, b) => { - const aVal = (a.label as { type: "Literal"; value: number }).value; - const bVal = (b.label as { type: "Literal"; value: number }).value; - return aVal - bVal; - }); - - // Transform each handler body and wrap in a function expression. - // Async interpreter handlers must be async functions so that - // `await` expressions inside handler bodies remain valid. - const handlerFns: JsNode[] = handlerCases.map((c) => { - const body = transformBodyForFT(c.body, FRS, FRV); - return fnExpr( - undefined, - [], - body, - isAsync ? { async: true } : undefined - ); - }); - - // Determine group count based on total handler count. - // Sync interpreter uses a single flat array — all call sites are - // megamorphic regardless, so grouped routing adds overhead without - // benefiting V8 inline caching. Async interpreter keeps 2-4 groups - // for structural differentiation (so it doesn't look like the sync). - const totalHandlers = handlerFns.length; - let numGroups: number; - if (isAsync) { - // Async: grouped for structural differentiation - const baseGroups = totalHandlers < 80 ? 2 : totalHandlers < 160 ? 3 : 4; - if (groupCountOffset !== 0) { - const shifted = - baseGroups + groupCountOffset <= FT_MAX_GROUPS - ? baseGroups + groupCountOffset - : baseGroups - groupCountOffset; - numGroups = Math.max(2, Math.min(FT_MAX_GROUPS, shifted)); - } else { - numGroups = Math.min(FT_MAX_GROUPS, baseGroups); - } - } else { - // Sync: single flat array — eliminates if-else routing overhead - numGroups = 1; - } - const groupSize = Math.ceil(totalHandlers / numGroups); - - // IIFE-scope declarations: sentinel object (identity-checked via ===) - // Shared across all exec calls — safe because it's a constant identity marker. - const iifeDecls: JsNode[] = [varDecl(FRS, obj())]; - - // For sync: FRV goes to IIFE scope (handlers reference it and they're hoisted). - // For async: FRV stays exec-local (interleaving can clobber shared state). - const preLoopDecls: JsNode[] = []; - if (isAsync) { - preLoopDecls.push(varDecl(FRV, un(UOp.Void, lit(0)))); - } else { - iifeDecls.push(varDecl(FRV, un(UOp.Void, lit(0)))); - } - - const activeGroupNames: string[] = []; - // For sync: handler group arrays go to IIFE scope (closures created once). - // For async: handler group arrays stay in exec (closures recreated per call - // because they capture exec-local async state that can interleave). - const handlerTarget = isAsync ? preLoopDecls : iifeDecls; - for (let i = 0; i < numGroups; i++) { - const name = groupTempNames[i]; - if (!name) - throw new Error(`Missing function table group temp: _fg${i}`); - activeGroupNames.push(name); - const start = i * groupSize; - const end = Math.min(start + groupSize, totalHandlers); - const group = handlerFns.slice(start, end); - handlerTarget.push(varDecl(name, arr(...group))); - } - - // Dispatch nodes (inside while loop body) - const dispatchNodes: JsNode[] = []; - - // var _fdi = _ht[PH] - // Under the decode cache, PH already holds the resolved handler index - // (pre-computed during materialization); otherwise resolve via _ht. - dispatchNodes.push( - varDecl( - FDI, - decodeCache ? id(phName) : index(id(htName), id(phName)) - ) - ); - - // Build if-else routing tree with grouped dispatch calls. - // Each branch: if (_fgN[_fdi - offset]() === _frs) return _frv; - // For async: if ((await _fgN[_fdi - offset]()) === _frs) return _frv; - const awaitNode = (expr: JsNode): JsNode => - ({ type: "AwaitExpr", expr } as JsNode); - - const buildCallCheck = (groupName: string, offset: number): JsNode[] => { - const indexExpr = - offset === 0 ? id(FDI) : bin(BOp.Sub, id(FDI), lit(offset)); - const callExpr = call(index(id(groupName), indexExpr), []); - const result = isAsync ? awaitNode(callExpr) : callExpr; - return [ifStmt(bin(BOp.Seq, result, id(FRS)), [returnStmt(id(FRV))])]; - }; - - if (numGroups === 1) { - dispatchNodes.push(...buildCallCheck(activeGroupNames[0]!, 0)); - } else { - // Build if-else chain: route to correct group by handler index - let current: JsNode[] = buildCallCheck( - activeGroupNames[numGroups - 1]!, - (numGroups - 1) * groupSize - ); - - for (let i = numGroups - 2; i >= 0; i--) { - const threshold = (i + 1) * groupSize; - const body = buildCallCheck(activeGroupNames[i]!, i * groupSize); - current = [ - ifStmt(bin(BOp.Lt, id(FDI), lit(threshold)), body, current), - ]; - } - - dispatchNodes.push(...current); - } - - return { preLoopDecls, iifeDecls, dispatchNodes }; -} - -// --- Return mechanism transforms --- - -/** - * Transform handler body for "tagged" return mechanism. - * - `BreakStmt` → `ReturnStmt()` (exit handler) - * - `ReturnStmt(expr)` → `ReturnStmt({ t: TAG, v: expr })` - */ -function transformBodyForTagged(body: JsNode[], tag: number): JsNode[] { - return body.map((node) => - walkSkipFns(node, (n) => { - if (n.type === "BreakStmt") return returnStmt(); - if (n.type === "ReturnStmt") { - const value = - (n as { type: "ReturnStmt"; value?: JsNode }).value ?? - un(UOp.Void, lit(0)); - return returnStmt(obj([lit("t"), lit(tag)], [lit("v"), value])); - } - return null; - }) - ); -} - -/** - * Transform handler body for "flag" return mechanism. - * - `BreakStmt` → `ReturnStmt()` (exit handler) - * - `ReturnStmt(expr)` → `_done = true; _rv = expr; return;` - * (sequence: assign flag, assign value, return void) - */ -function transformBodyForFlag( - body: JsNode[], - doneName: string, - rvName: string -): JsNode[] { - return body.map((node) => - walkSkipFns(node, (n) => { - if (n.type === "BreakStmt") return returnStmt(); - if (n.type === "ReturnStmt") { - const value = - (n as { type: "ReturnStmt"; value?: JsNode }).value ?? - un(UOp.Void, lit(0)); - // Use a sequence expression: (_done = true, _rv = value, void 0) - // then return. This keeps it as a single return statement. - return returnStmt( - seq( - assign(id(doneName), lit(true)), - assign(id(rvName), value) - ) - ); - } - return null; - }) - ); -} - -// --- Direct array dispatch --- - -/** - * Direct array dispatch — flat handler array indexed by handler index. - * No grouping, no if-else chain. Just `_ha[_fdi]()`. - * - * Structurally different from function-table: one array instead of - * 2-4 groups, no routing tree. - */ -function buildDirectArrayDispatch( - cases: CaseClause[], - temps: TempNames, - htName: string, - phName: string, - isAsync: boolean, - returnMech: string, - returnTag: number, - decodeCache = false -): { preLoopDecls: JsNode[]; iifeDecls: JsNode[]; dispatchNodes: JsNode[] } { - const FRS = temps["_frs"]; - const FRV = temps["_frv"]; - const FDI = temps["_fdi"]; - if (!FRS || !FRV || !FDI) { - throw new Error("Missing function table temp names"); - } - - const groupName = temps["_fg0"]; - if (!groupName) throw new Error("Missing temp: _fg0"); - - const handlerCases = cases - .filter((c): c is CaseClause & { label: JsNode } => c.label !== null) - .sort((a, b) => { - const aVal = (a.label as { type: "Literal"; value: number }).value; - const bVal = (b.label as { type: "Literal"; value: number }).value; - return aVal - bVal; - }); - - // Transform and wrap each handler - const handlerFns: JsNode[] = handlerCases.map((c) => { - let body: JsNode[]; - if (returnMech === "tagged") { - body = transformBodyForTagged(c.body, returnTag); - } else if (returnMech === "flag") { - body = transformBodyForFlag(c.body, FRS, FRV); - } else { - body = transformBodyForFT(c.body, FRS, FRV); - } - return fnExpr( - undefined, - [], - body, - isAsync ? { async: true } : undefined - ); - }); - - const iifeDecls: JsNode[] = []; - const preLoopDecls: JsNode[] = []; - - // For sentinel/flag: declare sentinel/flag + return value at appropriate scope. - // For async mode, both go to preLoopDecls to avoid clobbering from - // concurrent async calls that interleave on the same IIFE scope. - if (returnMech === "flag") { - const flagTarget = isAsync ? preLoopDecls : iifeDecls; - flagTarget.push(varDecl(FRS, lit(false))); - } else if (returnMech === "tagged") { - // Tagged: no sentinel needed - } else { - // Sentinel (identity-constant, safe at IIFE scope even for async) - iifeDecls.push(varDecl(FRS, obj())); - } - - if (isAsync) { - preLoopDecls.push(varDecl(FRV, un(UOp.Void, lit(0)))); - } else { - iifeDecls.push(varDecl(FRV, un(UOp.Void, lit(0)))); - } - - // Single flat array — no grouping - const handlerTarget = isAsync ? preLoopDecls : iifeDecls; - handlerTarget.push(varDecl(groupName, arr(...handlerFns))); - - // Dispatch nodes - const dispatchNodes: JsNode[] = []; - // Under the decode cache, PH already holds the resolved handler index - // (pre-computed during materialization); otherwise resolve via _ht. - dispatchNodes.push( - varDecl( - FDI, - decodeCache ? id(phName) : index(id(htName), id(phName)) - ) - ); - - const awaitNode = (expr: JsNode): JsNode => - ({ type: "AwaitExpr", expr } as JsNode); - - const callExpr = call(index(id(groupName), id(FDI)), []); - const result = isAsync ? awaitNode(callExpr) : callExpr; - - if (returnMech === "tagged") { - // var _r = fn(); if (_r && _r.t === TAG) return _r.v; - // Reuse FRV to hold tagged result (declared above) - dispatchNodes.push(exprStmt(assign(id(FRV), result))); - dispatchNodes.push( - ifStmt( - bin( - BOp.And, - id(FRV), - bin(BOp.Seq, member(id(FRV), "t"), lit(returnTag)) - ), - [returnStmt(member(id(FRV), "v"))] - ) - ); - } else if (returnMech === "flag") { - // fn(); if (_frs) return _frv; - dispatchNodes.push(exprStmt(result)); - dispatchNodes.push( - ifStmt(id(FRS), [ - exprStmt(assign(id(FRS), lit(false))), - returnStmt(id(FRV)), - ]) - ); - } else { - // Sentinel: if (fn() === _frs) return _frv; - dispatchNodes.push( - ifStmt(bin(BOp.Seq, result, id(FRS)), [returnStmt(id(FRV))]) - ); - } - - return { preLoopDecls, iifeDecls, dispatchNodes }; -} - -// --- Object lookup dispatch --- - -/** - * Object lookup dispatch — handlers stored as properties on a plain - * object, keyed by handler index. Dispatch is a property access + call. - * - * Structurally different from function-table: uses an object literal - * instead of array grouping, property access instead of index access. - */ -function buildObjectLookupDispatch( - cases: CaseClause[], - temps: TempNames, - htName: string, - phName: string, - isAsync: boolean, - returnMech: string, - returnTag: number, - decodeCache = false -): { preLoopDecls: JsNode[]; iifeDecls: JsNode[]; dispatchNodes: JsNode[] } { - const FRS = temps["_frs"]; - const FRV = temps["_frv"]; - const FDI = temps["_fdi"]; - if (!FRS || !FRV || !FDI) { - throw new Error("Missing function table temp names"); - } - - const groupName = temps["_fg0"]; - if (!groupName) throw new Error("Missing temp: _fg0"); - - const handlerCases = cases - .filter((c): c is CaseClause & { label: JsNode } => c.label !== null) - .sort((a, b) => { - const aVal = (a.label as { type: "Literal"; value: number }).value; - const bVal = (b.label as { type: "Literal"; value: number }).value; - return aVal - bVal; - }); - - // Build object entries: { 0: function(){...}, 1: function(){...}, ... } - const entries: [string | JsNode, JsNode][] = handlerCases.map((c, i) => { - let body: JsNode[]; - if (returnMech === "tagged") { - body = transformBodyForTagged(c.body, returnTag); - } else if (returnMech === "flag") { - body = transformBodyForFlag(c.body, FRS, FRV); - } else { - body = transformBodyForFT(c.body, FRS, FRV); - } - const handler = fnExpr( - undefined, - [], - body, - isAsync ? { async: true } : undefined - ); - return [String(i), handler] as [string, JsNode]; - }); - - const handlerObj: JsNode = { type: "ObjectExpr", entries }; - - const iifeDecls: JsNode[] = []; - const preLoopDecls: JsNode[] = []; - - if (returnMech === "flag") { - const flagTarget = isAsync ? preLoopDecls : iifeDecls; - flagTarget.push(varDecl(FRS, lit(false))); - } else if (returnMech === "tagged") { - // No sentinel needed - } else { - iifeDecls.push(varDecl(FRS, obj())); - } - - if (isAsync) { - preLoopDecls.push(varDecl(FRV, un(UOp.Void, lit(0)))); - } else { - iifeDecls.push(varDecl(FRV, un(UOp.Void, lit(0)))); - } - - const handlerTarget = isAsync ? preLoopDecls : iifeDecls; - handlerTarget.push(varDecl(groupName, handlerObj)); - - // Dispatch nodes - const dispatchNodes: JsNode[] = []; - // Under the decode cache, PH already holds the resolved handler index - // (pre-computed during materialization); otherwise resolve via _ht. - dispatchNodes.push( - varDecl( - FDI, - decodeCache ? id(phName) : index(id(htName), id(phName)) - ) - ); - - const awaitNode = (expr: JsNode): JsNode => - ({ type: "AwaitExpr", expr } as JsNode); - - const callExpr = call(index(id(groupName), id(FDI)), []); - const result = isAsync ? awaitNode(callExpr) : callExpr; - - if (returnMech === "tagged") { - // Reuse FRV to hold tagged result (declared above) - dispatchNodes.push(exprStmt(assign(id(FRV), result))); - dispatchNodes.push( - ifStmt( - bin( - BOp.And, - id(FRV), - bin(BOp.Seq, member(id(FRV), "t"), lit(returnTag)) - ), - [returnStmt(member(id(FRV), "v"))] - ) - ); - } else if (returnMech === "flag") { - dispatchNodes.push(exprStmt(result)); - dispatchNodes.push( - ifStmt(id(FRS), [ - exprStmt(assign(id(FRS), lit(false))), - returnStmt(id(FRV)), - ]) - ); - } else { - dispatchNodes.push( - ifStmt(bin(BOp.Seq, result, id(FRS)), [returnStmt(id(FRV))]) - ); - } - - return { preLoopDecls, iifeDecls, dispatchNodes }; -} - -// --- Stack encoding (AST) --- - -/** - * Build the per-exec stack-encoding key expression: `(U.i.length ^ U.r ^ const) >>> 0`. - * - * This is the per-unit key base folded into the position-dependent XOR mask. - * The caller binds it to the `_sek` slot via `declOrAssign` (so it is an - * IIFE-scope slot variable, saved/restored per exec for recursion safety, just - * like `S`/`C`/`O` — hoisted handler closures reference it directly). The stack - * array itself stays a plain `Array`; values are encoded/decoded via the - * IIFE-scope `stkEnc`/`stkDec` helpers ({@link buildStackEncodingHelpers}) - * instead of a `Proxy`. Far faster (no per-access trap, V8 keeps the array's - * fast element path) while preserving the exact in-memory representation - * (`[tag,payload]` entries with int32s position-XOR-masked) — so at-rest - * stack-memory secrecy is identical. - * - * @param n - Runtime identifier names - * @param split - Optional constant splitter for numeric obfuscation - * @returns The `_sek` key expression node - */ -function buildStackEncodingKeyExpr(n: RuntimeNames, split?: SplitFn): JsNode { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - const U = n.unit; - // (U.i.length^U.r^0x5A3C96E1)>>>0 - return bin( - BOp.Ushr, - bin( - BOp.BitXor, - bin( - BOp.BitXor, - member(member(id(U), "i"), "length"), - member(id(U), "r") - ), - L(0x5a3c96e1) - ), - lit(0) - ); -} - -/** - * Build the IIFE-scope stack-encoding helper functions `stkEnc` / `stkDec`. - * - * These replace the legacy `Proxy` get/set traps with plain function calls and - * use an allocation-free representation on the int32 hot path: - * - * stkEnc(v, i, k) -> the stored entry for value v at slot i (key base k): - * int32 -> v ^ ((k ^ (i*GR))>>>0) (BARE masked number — NO allocation) - * else -> [v] (boxed: floats, bool, string, object, - * null, undefined, function, symbol, …) - * stkDec(e, i, k) -> the decoded value from entry e at slot i (inverse): - * typeof e === 'number' -> e ^ ((k ^ (i*GR))>>>0) (masked int, incl. 0) - * else -> e[0] (unbox) - * - * The ONLY bare numbers on the stack are masked int32s, so a `typeof` test - * unambiguously distinguishes them from boxed values. A float is also - * `typeof === 'number'` but is NOT an int32 ((v|0)===v fails), so it is boxed — - * never bare — preventing stkDec from wrongly unmasking it. -0 masks-as-int and - * round-trips to +0 (`-0 ^ k === k`, `k ^ k === 0`), matching JS `-0|0===0` and - * the old `[tag,payload]` scheme exactly. NaN/Infinity/2^31..2^32 are boxed. - * - * The key formula and per-unit `_sek` key are byte-identical to the old Proxy, - * so int32 stack values remain position-XOR-masked with the same keystream; - * non-ints remain exposed (boxed, unmasked) exactly as before. The eliminated - * allocation is the per-push `[tag,payload]` array, which dominated push/pop. - * Defined once at IIFE scope and shared across all exec calls; the per-unit key - * is passed in as `k` (a per-exec local) rather than captured, so recursion with - * differing units stays correct. - * - * @param n - Runtime identifier names (provides stkEnc/stkDec) - * @param split - Optional constant splitter for numeric obfuscation - * @returns Two `var stkEnc = function(...){}` / `var stkDec = ...` declarations - */ -export function buildStackEncodingHelpers( - n: RuntimeNames, - split?: SplitFn -): JsNode[] { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - const ENC = n.stkEnc; - const DEC = n.stkDec; - - // (k^(i*0x9E3779B9))>>>0 — position key, k is the per-unit base param. - const xorKey = (iVar: JsNode): JsNode => - bin( - BOp.Ushr, - bin(BOp.BitXor, id("k"), bin(BOp.Mul, iVar, L(0x9e3779b9))), - lit(0) - ); - - // stkEnc(v,i,k) - const encBody: JsNode[] = [ - // if(typeof v==='number'&&(v|0)===v)return v^key — bare masked int, no alloc - ifStmt( - bin( - BOp.And, - bin(BOp.Seq, un(UOp.Typeof, id("v")), lit("number")), - bin(BOp.Seq, bin(BOp.BitOr, id("v"), lit(0)), id("v")) - ), - [returnStmt(bin(BOp.BitXor, id("v"), xorKey(id("i"))))] - ), - // return[v] — box everything else (floats/bool/string/object/null/…) - returnStmt(arr(id("v"))), - ]; - - // stkDec(e,i,k) - const decBody: JsNode[] = [ - // if(typeof e==='number')return e^key — masked int (incl. 0) - ifStmt(bin(BOp.Seq, un(UOp.Typeof, id("e")), lit("number")), [ - returnStmt(bin(BOp.BitXor, id("e"), xorKey(id("i")))), - ]), - // if(!e)return void 0 — empty/undefined slot (e is not a number here) - ifStmt(un(UOp.Not, id("e")), [returnStmt(un(UOp.Void, lit(0)))]), - // return e[0] — unbox - returnStmt(index(id("e"), lit(0))), - ]; - - return [ - varDecl(ENC, fnExpr(undefined, ["v", "i", "k"], encBody)), - varDecl(DEC, fnExpr(undefined, ["e", "i", "k"], decBody)), - ]; -} diff --git a/packages/ruam/src/ruamvm/builders/loader.ts b/packages/ruam/src/ruamvm/builders/loader.ts deleted file mode 100644 index df5bebc..0000000 --- a/packages/ruam/src/ruamvm/builders/loader.ts +++ /dev/null @@ -1,206 +0,0 @@ -/** - * Loader builder — assembles the bytecode loader function and shared - * declarations as AST nodes. - * - * All bytecode units are always in binary format (custom-alphabet - * encoded strings). The loader decodes, optionally RC4-decrypts, - * deserializes, and caches each unit on first access. - * - * @module ruamvm/builders/loader - */ - -import type { JsNode } from "../nodes.js"; -import type { RuntimeNames } from "../../naming/compat-types.js"; -import { - arr, - assign, - bin, - call, - exprStmt, - fn, - forStmt, - id, - ifStmt, - index, - lit, - member, - newExpr, - obj, - returnStmt, - update, - varDecl, - BOp, - UpOp, -} from "../nodes.js"; - -// --- Builder --- - -/** - * Build the bytecode loader function and optional shared declarations - * as JsNode[]. - * - * Returns an array containing: - * 1. Shared variable declarations (depth, callStack, cache) — - * omitted when `options.skipSharedDecls` is true. - * 2. The `load(id)` function declaration. - * - * The loader always decodes from custom binary encoding, optionally - * RC4-decrypts, deserializes via the binary deserializer, decodes - * XOR-encoded strings when string encoding is enabled, and converts - * instruction arrays to Int32Array for performance. - * - * @param encrypt Whether bytecode is RC4-encrypted. - * @param names Runtime identifier mapping. - * @param hasStringEncoding Whether constant pool strings are XOR-encoded. - * @param rollingCipher Whether rolling cipher is enabled (affects string decode key derivation). - * @param options Additional options. - * @returns Array of JsNode representing the shared declarations and loader function. - */ -export function buildLoader( - encrypt: boolean, - names: RuntimeNames, - hasStringEncoding: boolean = false, - rollingCipher: boolean = false, - options?: { skipSharedDecls?: boolean } -): JsNode[] { - const nodes: JsNode[] = []; - - // --- Shared declarations --- - - if (!options?.skipSharedDecls) { - nodes.push( - varDecl(names.depth, lit(0)), - varDecl(names.callStack, arr()), - varDecl(names.cache, obj()) - ); - } - - // --- Load function --- - - nodes.push( - buildLoadFunction(encrypt, names, hasStringEncoding, rollingCipher) - ); - - return nodes; -} - -// --- Internals --- - -/** Shorthand: `cache[id]` index expression */ -function cacheId(names: RuntimeNames): JsNode { - return index(id(names.cache), id("id")); -} - -/** - * Build the string decode loop for encoded constant pool strings. - * - * In binary format, encoded strings are deserialized as number arrays. - * This loop detects them via `Array.isArray(cv)` and decodes in-place. - * - * Produces: - * ```js - * for(var j=0; j bin(BOp.BitXor, a, b); - -/** `a >>> n` */ -const ushr = (a: JsNode, n: number): JsNode => bin(BOp.Ushr, a, lit(n)); - -/** `imulAlias(a, b)` — uses the IIFE-scope alias for Math.imul */ -const makeImul = - (imulName: string) => - (a: JsNode, b: JsNode): JsNode => - call(id(imulName), [a, b]); - -/** `h ^= expr` — shorthand for `exprStmt(assign(id("h"), expr, AOp.BitXor))` */ -const xorAssign = (target: string, value: JsNode): JsNode => - exprStmt(assign(id(target), value, AOp.BitXor)); - -/** - * Build the runtime rolling cipher helper functions as JsNode[]. - * - * Emits two function declarations: - * - `rcDeriveKey(unit)` — derives the implicit master key from unit metadata - * using FNV-1a, with the key anchor XOR folded in as the final step. - * - `rcMix(state, a, b)` — rolling state update for position-dependent decryption. - * - * @param names Runtime identifier mapping. - * @param hasKeyAnchor Whether to fold the key anchor closure variable into the derived key. - * @param split Optional constant splitter for numeric obfuscation. - * @param cipherSalt Optional per-build salt folded into the derived key via an extra FNV round. - * @returns Array of JsNode representing both function declarations. - */ -export function buildRollingCipherSource( - names: RuntimeNames, - hasKeyAnchor: boolean, - split?: SplitFn, - cipherSalt?: number -): JsNode[] { - const imulId = names.imul; - return [ - buildDeriveKeyFunction(names, hasKeyAnchor, split, cipherSalt, imulId), - buildMixFunction(names, split, imulId), - ]; -} - -// --- rcDeriveKey --- - -/** - * Build the rcDeriveKey(unit) function. - * - * Derives the implicit master key from a bytecode unit's structural - * properties (instruction count, register count, param count, constant count) - * via FNV-1a hashing. When the key anchor is enabled, it is XOR-folded - * into the key before returning. The key anchor is a closure variable - * (names.keyAnchor) that combines the handler table checksum and optional - * integrity hash — this prevents extraction via `new Function()`. - */ -function buildDeriveKeyFunction( - names: RuntimeNames, - hasKeyAnchor: boolean, - split?: SplitFn, - cipherSalt?: number, - imulId?: string -): JsNode { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - const imul = makeImul(imulId ?? "Math.imul"); - const h = id("h"); - const u = id("u"); - const k = id("k"); - const FNV_PRIME = L(0x01000193); - - // h = imul(h ^ expr, FNV_PRIME) - const fnvRound = (expr: JsNode): JsNode => - exprStmt(assign(h, imul(xor(h, expr), FNV_PRIME))); - - const body: JsNode[] = [ - // var h = 0x811C9DC5; - varDecl("h", L(0x811c9dc5)), - // h = Math.imul(h ^ (u.i.length >>> 1), 0x01000193); - fnvRound(ushr(member(member(u, "i"), "length"), 1)), - // h = Math.imul(h ^ u.r, 0x01000193); - fnvRound(member(u, "r")), - // h = Math.imul(h ^ u.p, 0x01000193); - fnvRound(member(u, "p")), - // h = Math.imul(h ^ u.c.length, 0x01000193); - fnvRound(member(member(u, "c"), "length")), - ]; - - // Optional: fold in per-build cipher salt via extra FNV round - // Must come BEFORE avalanche finalization to match build-time deriveImplicitKey - if (cipherSalt !== undefined) { - body.push(fnvRound(L(cipherSalt))); - } - - // Avalanche finalization - body.push( - // h ^= h >>> 16; - xorAssign("h", ushr(h, 16)), - // h = Math.imul(h, 0x45D9F3B); - exprStmt(assign(h, imul(h, L(0x45d9f3b)))), - // h ^= h >>> 13; - xorAssign("h", ushr(h, 13)), - // var k = h >>> 0; - varDecl("k", ushr(h, 0)) - ); - - // Fold in the key anchor (closure variable — blocks new Function() extraction). - // The key anchor combines the handler table checksum and optional integrity hash. - if (hasKeyAnchor) { - body.push(exprStmt(assign(k, ushr(xor(k, id(names.keyAnchor)), 0)))); - } - - // return k; - body.push(returnStmt(k)); - - return fn(names.rcDeriveKey, ["u"], body); -} - -// --- rcMix --- - -/** - * Build the rcMix(state, a, b) function. - * - * Advances the rolling cipher state by mixing in the decrypted opcode - * and operand values using two multiply-xor rounds with avalanche shift. - */ -function buildMixFunction( - names: RuntimeNames, - split?: SplitFn, - imulId?: string -): JsNode { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - const imul = makeImul(imulId ?? "Math.imul"); - const h = id("h"); - const a = id("a"); - const b = id("b"); - - return fn( - names.rcMix, - ["s", "a", "b"], - [ - // var h = s; - varDecl("h", id("s")), - // h = imul(h ^ a, 0x85EBCA6B) >>> 0; - exprStmt(assign(h, ushr(imul(xor(h, a), L(0x85ebca6b)), 0))), - // h = imul(h ^ b, 0xC2B2AE35) >>> 0; - exprStmt(assign(h, ushr(imul(xor(h, b), L(0xc2b2ae35)), 0))), - // h ^= h >>> 16; - xorAssign("h", ushr(h, 16)), - // return h >>> 0; - returnStmt(ushr(h, 0)), - ] - ); -} diff --git a/packages/ruam/src/ruamvm/builders/runners.ts b/packages/ruam/src/ruamvm/builders/runners.ts deleted file mode 100644 index 7d4524a..0000000 --- a/packages/ruam/src/ruamvm/builders/runners.ts +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Runners builder — assembles VM dispatch and router functions as AST nodes. - * - * Produces structured AST nodes for the VM dispatch function, its .call - * variant, and the shielding router — no raw() escape hatches. - * - * @module ruamvm/builders/runners - */ - -import type { JsNode } from "../nodes.js"; -import type { RuntimeNames, TempNames } from "../../naming/compat-types.js"; -import { - id, - lit, - bin, - un, - assign, - call, - member, - index, - fn, - fnExpr, - varDecl, - exprStmt, - ifStmt, - returnStmt, - obj, - arr, - BOp, - UOp, -} from "../nodes.js"; - -// --- VM dispatch functions --- - -/** - * Build the VM dispatch function and its `.call` variant as JsNode[]. - * - * The dispatch function loads a bytecode unit by ID and routes to the - * sync or async interpreter. The `.call` variant handles sloppy-mode - * this-boxing (null/undefined -> globalThis, primitives -> Object()). - * - * @param debug - Whether to emit debug trace calls at dispatch entry. - * @param names - Randomized runtime identifier names. - * @returns An array of JsNode containing the dispatch function and its .call property. - */ -export function buildRunners( - debug: boolean, - names: RuntimeNames, - temps: TempNames -): JsNode[] { - const U = names.unit; - const A = names.args; - const OS = names.outer; - const TV = names.tVal; - const NT = names.nTgt; - const HO = names.ho; - - /** Common args: (unit, args||[], outer||null, thisVal, newTarget, homeObject) */ - const execArgs: JsNode[] = [ - id(U), - bin(BOp.Or, id(A), arr()), - bin(BOp.Or, id(OS), lit(null)), - id(TV), - id(NT), - id(HO), - ]; - - /** Shared: var U = load(id); */ - const loadUnit: JsNode = varDecl(U, call(id(names.load), [id("id")])); - - /** Optional debug trace statements */ - const dbgStmts: JsNode[] = debug - ? [ - exprStmt( - call(id(names.dbg), [ - lit("VM_DISPATCH"), - bin(BOp.Add, lit("id="), id("id")), - bin( - BOp.Add, - lit("async="), - un(UOp.Not, un(UOp.Not, member(id(U), "s"))) - ), - bin(BOp.Add, lit("params="), member(id(U), "p")), - ]) - ), - exprStmt(assign(member(id(U), temps["_dbgId"]!), id("id"))), - ] - : []; - - // --- Main dispatch function: function vm(id, A, OS, TV, NT, HO) { ... } --- - // Includes this-boxing: if TV is provided and the unit is not - // arrow/strict, box null/undefined → globalThis, primitives → Object(). - // This allows function stubs to call vm(id, args, scope, this) directly - // without needing the separate vm.call pattern. - const mainThisBoxing: JsNode = ifStmt( - bin( - BOp.And, - bin(BOp.Sneq, id(TV), un(UOp.Void, lit(0))), - un(UOp.Not, bin(BOp.Or, member(id(U), "a"), member(id(U), "st"))) - ), - [ - ifStmt( - bin(BOp.Eq, id(TV), lit(null)), - [exprStmt(assign(id(TV), id("globalThis")))], - [ - varDecl(temps["_t"]!, un(UOp.Typeof, id(TV))), - ifStmt( - bin( - BOp.And, - bin(BOp.Sneq, id(temps["_t"]!), lit("object")), - bin(BOp.Sneq, id(temps["_t"]!), lit("function")) - ), - [exprStmt(assign(id(TV), call(id("Object"), [id(TV)])))] - ), - ] - ), - ] - ); - - const dispatchFn: JsNode = fn( - names.vm, - ["id", A, OS, TV, NT, HO], - [ - loadUnit, - ...dbgStmts, - mainThisBoxing, - // if (U.s) return execAsync(U, A||[], OS||null, TV, NT, HO); - ifStmt(member(id(U), "s"), [ - returnStmt(call(id(names.execAsync), execArgs)), - ]), - // return exec(U, A||[], OS||null, TV, NT, HO); - returnStmt(call(id(names.exec), execArgs)), - ] - ); - - // --- vm.call = function(TV, id, A, OS, HO) { ... }; --- - // This-boxing: if not arrow and not strict: - // if (TV == null) TV = globalThis; - // else { var _t = typeof TV; if (_t !== "object" && _t !== "function") TV = Object(TV); } - const thisBoxing: JsNode = ifStmt( - un(UOp.Not, bin(BOp.Or, member(id(U), "a"), member(id(U), "st"))), - [ - ifStmt( - bin(BOp.Eq, id(TV), lit(null)), - // then: TV = globalThis - [exprStmt(assign(id(TV), id("globalThis")))], - // else: type check + box - [ - varDecl(temps["_t"]!, un(UOp.Typeof, id(TV))), - ifStmt( - bin( - BOp.And, - bin(BOp.Sneq, id(temps["_t"]!), lit("object")), - bin(BOp.Sneq, id(temps["_t"]!), lit("function")) - ), - [exprStmt(assign(id(TV), call(id("Object"), [id(TV)])))] - ), - ] - ), - ] - ); - - /** exec args for .call — newTarget is void 0 */ - const callExecArgs: JsNode[] = [ - id(U), - bin(BOp.Or, id(A), arr()), - bin(BOp.Or, id(OS), lit(null)), - id(TV), - un(UOp.Void, lit(0)), - id(HO), - ]; - - const callFn: JsNode = exprStmt( - assign( - member(id(names.vm), "call"), - fnExpr( - undefined, - [TV, "id", A, OS, HO], - [ - loadUnit, - ...dbgStmts, - thisBoxing, - // if (U.s) return execAsync(U, A||[], OS||null, TV, void 0, HO); - ifStmt(member(id(U), "s"), [ - returnStmt(call(id(names.execAsync), callExecArgs)), - ]), - // return exec(U, A||[], OS||null, TV, void 0, HO); - returnStmt(call(id(names.exec), callExecArgs)), - ] - ) - ) - ); - - return [dispatchFn, callFn]; -} - -// --- Shielding router --- - -/** - * Build the router function for VM Shielding mode as JsNode[]. - * - * The router maps unit IDs to their group's dispatch function and - * serves as the single global entry point. External function bodies - * call the router, which delegates to the correct micro-interpreter. - * - * @param routerName - The global router function name. - * @param groupRegistrations - Per-group unit ID lists and dispatch function names. - * @param names - Shared RuntimeNames (for parameter names). - * @returns An array of JsNode containing the route map, router function, and its .call property. - */ -export function buildRouter( - routerName: string, - groupRegistrations: { unitIds: string[]; dispatchName: string }[], - names: RuntimeNames -): JsNode[] { - const RM = names.routeMap; - const A = names.args; - const OS = names.outer; - const TV = names.tVal; - const NT = names.nTgt; - const HO = names.ho; - - // var RM = {}; - const routeMapDecl: JsNode = varDecl(RM, obj()); - - // RM["id1"] = dispatch1; RM["id2"] = dispatch1; ... - const registrations: JsNode[] = []; - for (const { unitIds, dispatchName } of groupRegistrations) { - for (const uid of unitIds) { - registrations.push( - exprStmt(assign(index(id(RM), lit(uid)), id(dispatchName))) - ); - } - } - - // function routerName(id, A, OS, TV, NT, HO) { return RM[id](id, A, OS, TV, NT, HO); } - const routerFn: JsNode = fn( - routerName, - ["id", A, OS, TV, NT, HO], - [ - returnStmt( - call(index(id(RM), id("id")), [ - id("id"), - id(A), - id(OS), - id(TV), - id(NT), - id(HO), - ]) - ), - ] - ); - - // routerName.call = function(TV, id, A, OS, HO) { return RM[id].call(TV, id, A, OS, HO); }; - const routerCall: JsNode = exprStmt( - assign( - member(id(routerName), "call"), - fnExpr( - undefined, - [TV, "id", A, OS, HO], - [ - returnStmt( - call(member(index(id(RM), id("id")), "call"), [ - id(TV), - id("id"), - id(A), - id(OS), - id(HO), - ]) - ), - ] - ) - ) - ); - - return [routeMapDecl, ...registrations, routerFn, routerCall]; -} diff --git a/packages/ruam/src/ruamvm/builders/unpack.ts b/packages/ruam/src/ruamvm/builders/unpack.ts deleted file mode 100644 index d7d59f3..0000000 --- a/packages/ruam/src/ruamvm/builders/unpack.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Packed-integer decoder for bytecode scattering. - * - * Builds a function that converts packed 32-bit integer(s) into a string. - * Handles both single integers (4 chars) and arrays of integers (N*4 chars). - * Single ints are wrapped in an array internally so one code path handles both. - * - * @module ruamvm/builders/unpack - */ - -import type { JsNode } from "../nodes.js"; -import type { Name } from "../../naming/index.js"; -import { - id, - lit, - bin, - un, - assign, - call, - member, - index, - varDecl, - arr, - exprStmt, - ifStmt, - forStmt, - returnStmt, - fnExpr, - update, - BOp, - UOp, - AOp, - UpOp, -} from "../nodes.js"; - -/** - * Build the decode helper function as JsNode[]. - * - * Equivalent JS: - * ```js - * var D = function(v) { - * if (typeof v === "number") v = [v]; - * var s = "", i, n; - * for (i = 0; i < v.length; i++) { - * n = v[i]; - * s += String.fromCharCode(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255); - * } - * return s; - * }; - * ``` - * - * @param decodeName - Randomized name for the function - * @returns JsNode[] containing a single var declaration - */ -export function buildDecodeFunction(decodeName: Name): JsNode[] { - const v = "v", - s = "s", - i = "i", - n = "n"; - - // n >>> 24 & 255 - const byte0 = bin(BOp.BitAnd, bin(BOp.Ushr, id(n), lit(24)), lit(255)); - // n >>> 16 & 255 - const byte1 = bin(BOp.BitAnd, bin(BOp.Ushr, id(n), lit(16)), lit(255)); - // n >>> 8 & 255 - const byte2 = bin(BOp.BitAnd, bin(BOp.Ushr, id(n), lit(8)), lit(255)); - // n & 255 - const byte3 = bin(BOp.BitAnd, id(n), lit(255)); - - const fromCharCode = call(member(id("String"), "fromCharCode"), [ - byte0, - byte1, - byte2, - byte3, - ]); - - const body: JsNode[] = [ - // if (typeof v === "number") v = [v]; - ifStmt(bin(BOp.Seq, un(UOp.Typeof, id(v)), lit("number")), [ - exprStmt(assign(id(v), arr(id(v)))), - ]), - varDecl(s, lit("")), - varDecl(i), - varDecl(n), - forStmt( - assign(id(i), lit(0)), - bin(BOp.Lt, id(i), member(id(v), "length")), - update(UpOp.Inc, false, id(i)), - [ - exprStmt(assign(id(n), index(id(v), id(i)))), - exprStmt(assign(id(s), fromCharCode, AOp.Add)), - ] - ), - returnStmt(id(s)), - ]; - - return [varDecl(decodeName, fnExpr(undefined, [v], body))]; -} diff --git a/packages/ruam/src/ruamvm/bytecode-scatter.ts b/packages/ruam/src/ruamvm/bytecode-scatter.ts deleted file mode 100644 index cdf8bf6..0000000 --- a/packages/ruam/src/ruamvm/bytecode-scatter.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Bytecode scattering engine. - * - * Splits encoded bytecode strings into heterogeneous typed fragments: - * - String literals (blend with other string vars in the runtime) - * - Packed 32-bit integers (look like handler table data or constants) - * - Packed integer arrays (look like opcode maps or lookup tables) - * - * Fragment declarations are scattered individually among runtime - * statements. A single compact reassembly expression per unit joins - * them: `frag1 + D(frag2) + frag3 + D(frag4)` — mixing raw strings - * with decode calls on numeric fragments. - * - * @module ruamvm/bytecode-scatter - */ - -import type { JsNode } from "./nodes.js"; -import { id, lit, bin, varDecl, arr, call, BOp } from "./nodes.js"; -import { deriveSeed, lcgNext } from "../naming/scope.js"; - -// --- Fragment types --- - -/** A typed fragment from the scatter engine. */ -export interface ScatterFragment { - /** Variable name (from NameRegistry dynamic generator). */ - name: string; - /** AST node for the variable declaration. */ - decl: JsNode; - /** AST node for the reassembly expression (may involve a decode call). */ - reassemblyExpr: JsNode; - /** Whether this fragment requires the decode function. */ - needsDecode: boolean; -} - -/** Result of scattering a single bytecode unit. */ -export interface ScatterResult { - /** Individual fragment declarations (to be scattered among runtime stmts). */ - fragments: ScatterFragment[]; - /** Single reassembly expression: frag1 + D(frag2) + frag3 + ... */ - reassembly: JsNode; - /** Whether any fragment requires the decode function. */ - needsDecode: boolean; -} - -/** - * Pack a string chunk into an array of 32-bit unsigned integers. - * Each int encodes 4 chars (big-endian byte order). - * Chunk length MUST be divisible by 4. - */ -function packToInts(chunk: string): number[] { - const ints: number[] = []; - for (let j = 0; j < chunk.length; j += 4) { - const packed = - ((chunk.charCodeAt(j) & 0xff) << 24) | - ((chunk.charCodeAt(j + 1) & 0xff) << 16) | - ((chunk.charCodeAt(j + 2) & 0xff) << 8) | - (chunk.charCodeAt(j + 3) & 0xff); - ints.push(packed >>> 0); - } - return ints; -} - -/** - * Split an encoded bytecode string into heterogeneous typed fragments. - * - * @param encoded - The encoded bytecode string to scatter - * @param seed - Per-build seed for deterministic LCG choices - * @param nameGen - Name generator (from NameRegistry.createDynamicGenerator) - * @param decodeName - Runtime name of the decode function - * @param minFragments - Minimum fragment count (default 2) - * @param maxFragments - Maximum fragment count (default 6) - * @returns ScatterResult with typed fragments and reassembly expression - */ -export function scatterBytecodeUnit( - encoded: string, - seed: number, - nameGen: () => string, - decodeName: string, - minFragments = 2, - maxFragments = 6 -): ScatterResult { - let state = deriveSeed(seed, "btScatterFrag"); - - // Short strings: no split — single string literal - if (encoded.length < 8) { - const name = nameGen(); - return { - fragments: [ - { - name, - decl: varDecl(name, lit(encoded)), - reassemblyExpr: id(name), - needsDecode: false, - }, - ], - reassembly: id(name), - needsDecode: false, - }; - } - - // Determine fragment count - state = lcgNext(state); - const range = maxFragments - minFragments + 1; - const fragCount = minFragments + ((state >>> 16) % range); - - // Split into variable-length chunks - const rawChunks: string[] = []; - let offset = 0; - for (let i = 0; i < fragCount && offset < encoded.length; i++) { - const left = fragCount - i; - const baseLen = Math.ceil((encoded.length - offset) / left); - state = lcgNext(state); - const jitter = ((state >>> 16) % 51) - 25; - const len = Math.max( - 4, - Math.min( - encoded.length - offset, - baseLen + Math.floor((baseLen * jitter) / 100) - ) - ); - rawChunks.push(encoded.slice(offset, offset + len)); - offset += len; - } - if (offset < encoded.length && rawChunks.length > 0) { - rawChunks[rawChunks.length - 1] += encoded.slice(offset); - } - - // Assign types to each chunk - const fragments: ScatterFragment[] = []; - let anyNeedsDecode = false; - - for (const chunk of rawChunks) { - state = lcgNext(state); - const name = nameGen(); - - // Choose fragment type based on LCG and chunk properties - // Type 0: string literal - // Type 1: single packed int (exactly 4 chars) - // Type 2: packed int array (length divisible by 4) - const typeRoll = (state >>> 16) % 100; - const canPack = chunk.length % 4 === 0; - - if (canPack && chunk.length === 4 && typeRoll < 30) { - // Single packed integer — looks like a constant - const packed = packToInts(chunk)[0]!; - fragments.push({ - name, - decl: varDecl(name, lit(packed)), - reassemblyExpr: call(id(decodeName), [id(name)]), - needsDecode: true, - }); - anyNeedsDecode = true; - } else if (canPack && typeRoll < 55) { - // Packed integer array — looks like handler/opcode data - const ints = packToInts(chunk); - fragments.push({ - name, - decl: varDecl(name, arr(...ints.map((n) => lit(n)))), - reassemblyExpr: call(id(decodeName), [id(name)]), - needsDecode: true, - }); - anyNeedsDecode = true; - } else { - // String literal — looks like any other string var - fragments.push({ - name, - decl: varDecl(name, lit(chunk)), - reassemblyExpr: id(name), - needsDecode: false, - }); - } - } - - // Build reassembly: frag1 + D(frag2) + frag3 + ... - let reassembly: JsNode = fragments[0]!.reassemblyExpr; - for (let i = 1; i < fragments.length; i++) { - reassembly = bin(BOp.Add, reassembly, fragments[i]!.reassemblyExpr); - } - - return { fragments, reassembly, needsDecode: anyNeedsDecode }; -} diff --git a/packages/ruam/src/ruamvm/constant-splitting.ts b/packages/ruam/src/ruamvm/constant-splitting.ts deleted file mode 100644 index 144fbb7..0000000 --- a/packages/ruam/src/ruamvm/constant-splitting.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Constant splitting — replaces numeric literals with computed expressions. - * - * Each call to the returned function generates a unique expression that - * evaluates to the given value at runtime. This prevents attackers from - * grepping for well-known constants (FNV primes, golden ratios, etc.). - * - * @module ruamvm/constant-splitting - */ - -import type { JsNode } from "./nodes.js"; -import { bin, lit, BOp } from "./nodes.js"; -import { LCG_MULTIPLIER, LCG_INCREMENT } from "../constants.js"; - -// --- Types --- - -/** A function that takes a 32-bit numeric constant and returns a JsNode expression that computes it. */ -export type SplitFn = (value: number) => JsNode; - -// --- Splitter factory --- - -/** - * Create a constant splitter seeded from the build seed. - * - * Returns a function that converts numeric literals into computed - * expressions. Each call advances the internal PRNG state, so - * consecutive calls produce different split patterns. - * - * @param seed - Per-build CSPRNG seed. - * @returns A function `split(value) => JsNode` that creates obfuscated constants. - */ -export function makeConstantSplitter(seed: number): SplitFn { - let s = seed >>> 0; - - function lcg(): number { - s = (s * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - return s; - } - - let callCount = 0; - - return function split(value: number): JsNode { - const v = value >>> 0; // ensure unsigned 32-bit - const mask = lcg(); - const strategy = callCount++ % 3; - - switch (strategy) { - case 0: { - // XOR split: mask ^ (mask ^ value) = value - const other = (mask ^ v) >>> 0; - return bin(BOp.BitXor, lit(mask), lit(other)); - } - case 1: { - // SUB split: (value + offset) - offset = value - // Use smaller offset to avoid overflow issues - const offset = (mask & 0x7fffffff) >>> 0; - const sum = (v + offset) >>> 0; - return bin( - BOp.Ushr, - bin(BOp.Sub, lit(sum), lit(offset)), - lit(0) - ); - } - case 2: - default: { - // Double XOR: (a ^ b ^ c) where a ^ b ^ c = value - const a = lcg(); - const b = (a ^ mask ^ v) >>> 0; - return bin( - BOp.BitXor, - bin(BOp.BitXor, lit(a), lit(mask)), - lit(b) - ); - } - } - }; -} diff --git a/packages/ruam/src/ruamvm/emit.ts b/packages/ruam/src/ruamvm/emit.ts deleted file mode 100644 index a14cdd5..0000000 --- a/packages/ruam/src/ruamvm/emit.ts +++ /dev/null @@ -1,414 +0,0 @@ -/** - * AST → JS source emitter. - * - * Single recursive function that serializes JsNode trees to minified JS. - * Produces compact output with precedence-aware parenthesization. - * - * @module ruamvm/emit - */ - -import type { - JsNode, - VarDecl, - ReturnStmt, - ObjectEntry, - BOpKind, - UOpKind, -} from "./nodes.js"; -import { - assertNever, - BOp, - UOp, - BOP_STR, - UOP_STR, - AOP_STR, - UPOP_STR, -} from "./nodes.js"; -import { - NameToken, - RestParam, - resolveName, - isName, - type Name, -} from "../naming/index.js"; - -/** Emit a parameter list (Name | RestParam) to a comma-separated string. */ -function emitParams(params: (Name | RestParam)[]): string { - return params - .map((p) => (p instanceof RestParam ? p.toString() : resolveName(p))) - .join(","); -} - -// --- Operator precedence table (higher = tighter binding) --- - -const BIN_PREC: Record = { - [BOp.Or]: 4, - [BOp.Nullish]: 4, - [BOp.And]: 5, - [BOp.BitOr]: 6, - [BOp.BitXor]: 7, - [BOp.BitAnd]: 8, - [BOp.Eq]: 9, - [BOp.Neq]: 9, - [BOp.Seq]: 9, - [BOp.Sneq]: 9, - [BOp.Lt]: 10, - [BOp.Gt]: 10, - [BOp.Lte]: 10, - [BOp.Gte]: 10, - [BOp.In]: 10, - [BOp.Instanceof]: 10, - [BOp.Shl]: 11, - [BOp.Shr]: 11, - [BOp.Ushr]: 11, - [BOp.Add]: 12, - [BOp.Sub]: 12, - [BOp.Mul]: 13, - [BOp.Div]: 13, - [BOp.Mod]: 13, - [BOp.Pow]: 14, -}; -const PREC_COMMA = 1; -const PREC_ASSIGN = 2; -const PREC_TERNARY = 3; - -/** Keyword binary operators that need spaces around them. */ -const KEYWORD_BINOP = new Set([BOp.In, BOp.Instanceof]); - -/** Keyword unary operators that need a space after them. */ -const KEYWORD_UNOP = new Set([UOp.Typeof, UOp.Void, UOp.Delete]); - -/** - * Whether a unary operator's operand needs parentheses. - * - * Unary `!`, `~`, `+`, `-` bind tighter than any binary operator, so - * `!a - resolveName(d.name) + - (d.init ? "=" + emit(d.init) : "") - ) - .join(",") - ); - } - return `var ${resolveName(node.name)}${ - node.init ? "=" + emit(node.init) : "" - }`; - } - case "ConstDecl": - return `const ${resolveName(node.name)}${ - node.init ? "=" + emit(node.init) : "" - }`; - case "FnDecl": - return `${node.async ? "async " : ""}function ${resolveName( - node.name - )}(${emitParams(node.params)}){${emitBody(node.body)}}`; - - // --- Statements --- - case "ExprStmt": - return emit(node.expr) + ";"; - case "Block": - return `{${emitBody(node.body)}}`; - case "IfStmt": - return `if(${emit(node.test)}){${emitBody(node.then)}}${ - node.else ? `else{${emitBody(node.else)}}` : "" - }`; - case "WhileStmt": - return `while(${emit(node.test)}){${emitBody(node.body)}}`; - case "ForStmt": - return `for(${node.init ? emit(node.init) : ""};${ - node.test ? emit(node.test) : "" - };${node.update ? emit(node.update) : ""}){${emitBody(node.body)}}`; - case "ForInStmt": - return `for(var ${resolveName(node.decl)} in ${emit( - node.obj - )}){${emitBody(node.body)}}`; - case "SwitchStmt": - return `switch(${emit(node.disc)}){${node.cases - .map((c) => emit(c)) - .join("")}}`; - case "CaseClause": - return node.label === null - ? `default:{${emitBody(node.body)}}` - : `case ${emit(node.label)}:{${emitBody(node.body)}}`; - case "BreakStmt": - return "break;"; - case "ContinueStmt": - return "continue;"; - case "ReturnStmt": - return node.value ? `return ${emit(node.value)};` : "return;"; - case "ThrowStmt": - return `throw ${emit(node.value)};`; - case "TryCatchStmt": { - let s = `try{${emitBody(node.body)}}`; - if (node.handler) { - s += node.param - ? `catch(${resolveName(node.param)}){${emitBody( - node.handler - )}}` - : `catch{${emitBody(node.handler)}}`; - } - if (node.finalizer) s += `finally{${emitBody(node.finalizer)}}`; - return s; - } - case "DebuggerStmt": - return "debugger;"; - - // --- Expressions --- - case "Id": - return resolveName(node.name); - case "Literal": - return emitLiteral(node.value); - case "BinOp": { - const opStr = BOP_STR[node.op]; - const left = needsParens(node.left, node.op, false) - ? `(${emit(node.left)})` - : emit(node.left); - const right = needsParens(node.right, node.op, true) - ? `(${emit(node.right)})` - : emit(node.right); - if (KEYWORD_BINOP.has(node.op)) return `${left} ${opStr} ${right}`; - return `${left}${opStr}${right}`; - } - case "UnaryOp": { - const uopStr = UOP_STR[node.op]; - if (KEYWORD_UNOP.has(node.op)) { - const inner = emit(node.expr); - return `${uopStr} ${ - needsUnaryParens(node.expr) ? `(${inner})` : inner - }`; - } - if ( - node.op === UOp.Neg && - node.expr.type === "UnaryOp" && - node.expr.op === UOp.Neg - ) - return `-(${emit(node.expr)})`; - const inner = emit(node.expr); - return `${uopStr}${ - needsUnaryParens(node.expr) ? `(${inner})` : inner - }`; - } - case "UpdateExpr": - return node.prefix - ? `${UPOP_STR[node.op]}${emit(node.arg)}` - : `${emit(node.arg)}${UPOP_STR[node.op]}`; - case "AssignExpr": - return `${emit(node.target)}${ - node.op != null ? AOP_STR[node.op] : "" - }=${emit(node.value)}`; - case "CallExpr": { - let callee = emit(node.callee); - // Wrap function/arrow expressions in parens when used as callee (IIFE) - if (node.callee.type === "FnExpr" || node.callee.type === "ArrowFn") - callee = `(${callee})`; - return `${callee}(${node.args.map((a) => emit(a)).join(",")})`; - } - case "MemberExpr": - return `${emitObj(node.obj)}.${resolveName(node.prop)}`; - case "IndexExpr": - return `${emitObj(node.obj)}[${emit(node.index)}]`; - case "TernaryExpr": - return `${emit(node.test)}?${emit(node.then)}:${emit(node.else)}`; - case "ArrayExpr": - return `[${node.elements.map((e) => emit(e)).join(",")}]`; - case "ObjectExpr": - return `{${node.entries.map(emitObjectEntry).join(",")}}`; - case "FnExpr": - return `${node.async ? "async " : ""}function${ - node.name ? " " + resolveName(node.name) : "" - }(${emitParams(node.params)}){${emitBody(node.body)}}`; - case "ArrowFn": { - const p0 = node.params[0]; - const isRest = - p0 instanceof RestParam || - (typeof p0 === "string" && p0.startsWith("...")); - const params = - node.params.length === 1 && !isRest && !node.async - ? resolveName(p0 as Name) - : `(${emitParams(node.params)})`; - let body: string; - if ( - node.body.length === 1 && - node.body[0]!.type === "ReturnStmt" && - (node.body[0] as ReturnStmt).value - ) { - const val = (node.body[0] as ReturnStmt).value!; - const expr = emit(val); - body = val.type === "ObjectExpr" ? `(${expr})` : expr; - } else { - body = `{${emitBody(node.body)}}`; - } - return `${node.async ? "async " : ""}${params}=>${body}`; - } - case "NewExpr": - return `new ${emit(node.callee)}(${node.args - .map((a) => emit(a)) - .join(",")})`; - case "SequenceExpr": - return `(${node.exprs.map((e) => emit(e)).join(",")})`; - case "AwaitExpr": - return `await ${emit(node.expr)}`; - case "ImportExpr": - return `import(${emit(node.specifier)})`; - case "SpreadElement": - return `...${emit(node.arg)}`; - case "StackPush": - return `${resolveName(node.S)}.push(${emit(node.value)})`; - case "StackPop": - return `${resolveName(node.S)}.pop()`; - case "StackPeek": { - const sName = resolveName(node.S); - return `${sName}[${sName}.length-1]`; - } - default: - return assertNever(node); - } -} - -/** Emit a statement list (body of function, block, etc). */ -function emitBody(stmts: JsNode[]): string { - return stmts.map((s) => emitStmt(s)).join(""); -} - -/** Emit a single statement — adds semicolons where needed. */ -function emitStmt(node: JsNode): string { - switch (node.type) { - case "VarDecl": - case "ConstDecl": - return emit(node) + ";"; - case "FnDecl": - case "Block": - case "IfStmt": - case "WhileStmt": - case "ForStmt": - case "ForInStmt": - case "SwitchStmt": - case "TryCatchStmt": - // These already include their own structure (no trailing semicolon needed) - return emit(node); - case "ExprStmt": - case "BreakStmt": - case "ContinueStmt": - case "ReturnStmt": - case "ThrowStmt": - case "DebuggerStmt": - // These already emit with trailing semicolons - return emit(node); - case "CaseClause": - return emit(node); - default: - // Expression used as statement — wrap in ExprStmt logic - return emit(node) + ";"; - } -} - -/** Emit an object expression part, wrapping in parens if needed. */ -function emitObj(obj: JsNode): string { - const s = emit(obj); - // Numeric literals need parens before .prop: (0).toString - if (obj.type === "Literal" && typeof obj.value === "number") - return `(${s})`; - // Call expressions and other low-precedence expressions are fine as-is - return s; -} - -/** Serialize a literal value. */ -function emitLiteral(value: string | number | boolean | null | RegExp): string { - if (value === null) return "null"; - if (value === true) return "true"; - if (value === false) return "false"; - if (typeof value === "number") { - if (Object.is(value, -0)) return "-0"; - if (value === Infinity) return "Infinity"; - if (value === -Infinity) return "-Infinity"; - if (Number.isNaN(value)) return "NaN"; - return String(value); - } - if (typeof value === "string") { - // Escape and single-quote - return ( - "'" + - value - .replace(/\\/g, "\\\\") - .replace(/'/g, "\\'") - .replace(/\n/g, "\\n") - .replace(/\r/g, "\\r") - .replace(/\t/g, "\\t") - .replace(/\0/g, "\\0") - .replace(/\u2028/g, "\\u2028") - .replace(/\u2029/g, "\\u2029") + - "'" - ); - } - if (value instanceof RegExp) return value.toString(); - return String(value); -} - -/** Serialize a single object literal entry. */ -function emitObjectEntry(entry: ObjectEntry): string { - // Legacy tuple: [key, value] → key:value - if (Array.isArray(entry)) { - const [k, v] = entry; - return `${isName(k) ? resolveName(k) : `[${emit(k)}]`}:${emit(v)}`; - } - switch (entry.kind) { - case "get": - return `get ${resolveName(entry.name)}(){${emitBody(entry.body)}}`; - case "set": - return `set ${resolveName(entry.name)}(${resolveName( - entry.param - )}){${emitBody(entry.body)}}`; - case "method": { - const key = isName(entry.name) - ? resolveName(entry.name) - : `[${emit(entry.name)}]`; - return `${entry.async ? "async " : ""}${key}(${emitParams( - entry.params - )}){${emitBody(entry.body)}}`; - } - case "spread": - return `...${emit(entry.arg)}`; - } -} diff --git a/packages/ruam/src/ruamvm/handler-aliasing.ts b/packages/ruam/src/ruamvm/handler-aliasing.ts deleted file mode 100644 index dfd6574..0000000 --- a/packages/ruam/src/ruamvm/handler-aliasing.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Handler aliasing — create structurally different implementations - * of the same logical opcode. - * - * For high-value opcodes, generates structurally different handler bodies - * by applying AST transforms (if/else inversion, sequence wrapping, - * no-op injection). The build seed selects which variant is used, - * so different builds produce different interpreter code for the same - * opcode. - * - * All transforms are semantics-preserving: - * - If/else inversion: negate condition, swap branches (always safe) - * - Sequence wrapping: `expr` → `(0, expr)` (always safe) - * - No-op prefix: prepend `void 0;` statement (always safe) - * - * @module ruamvm/handler-aliasing - */ - -import { deriveSeed, lcgNext } from "../naming/scope.js"; -import type { JsNode, IfStmt, ExprStmt } from "./nodes.js"; -import { exprStmt, un, lit, seq, ifStmt, UOp, mapChildren } from "./nodes.js"; - -// --- Transforms --- - -/** - * Invert an if/else statement: negate condition, swap then/else branches. - * Only applied when both branches exist (otherwise the inversion would - * create an else-only if statement, which changes semantics for bare if). - */ -function invertIfElse(node: IfStmt): IfStmt { - if (!node.else || node.else.length === 0) return node; - return ifStmt(un(UOp.Not, node.test), node.else, node.then); -} - -/** - * Wrap an expression statement's expr in a sequence expression: `expr` → `(0, expr)`. - * Always safe — the leading `0` is a no-op value that gets discarded. - */ -function wrapExprInSequence(node: ExprStmt): ExprStmt { - // Don't double-wrap sequence expressions - if (node.expr.type === "SequenceExpr") return node; - return exprStmt(seq(lit(0), node.expr)); -} - -/** - * Create a void-0 no-op statement: `void 0;` - */ -function makeNoop(): JsNode { - return exprStmt(un(UOp.Void, lit(0))); -} - -// --- Deep walk --- - -/** - * Walk a single node, applying if/else inversion and expression wrapping - * based on the PRNG state. - * - * @param node - The node to transform - * @param state - Current LCG state (mutated via closure) - * @returns Object with transformed node and updated state - */ -function walkWithState(node: JsNode, state: { s: number }): JsNode { - // First recurse into children - const walked = mapChildren(node, (child) => walkWithState(child, state)); - - // Apply transforms based on node type - if (walked.type === "IfStmt") { - state.s = lcgNext(state.s); - // ~50% chance to invert when both branches exist - if ((state.s & 1) === 0 && walked.else && walked.else.length > 0) { - return invertIfElse(walked); - } - } - - if (walked.type === "ExprStmt") { - state.s = lcgNext(state.s); - // ~25% chance to wrap in sequence expression - if ((state.s & 3) === 0) { - return wrapExprInSequence(walked); - } - } - - return walked; -} - -// --- Public API --- - -/** - * Apply structural aliasing transforms to a handler body. - * - * Derives a per-opcode seed and uses it to deterministically apply - * semantics-preserving transforms: if/else inversion, expression - * sequence wrapping, and no-op statement injection. - * - * @param body - The handler body AST nodes - * @param seed - Build seed - * @param opcodeIndex - The opcode index (for seed derivation) - * @returns Transformed handler body (new array, no mutation) - */ -export function aliasHandlerBody( - body: JsNode[], - seed: number, - opcodeIndex: number -): JsNode[] { - const localSeed = deriveSeed(seed, "handlerAlias_" + opcodeIndex); - const state = { s: localSeed }; - - // Walk each body statement applying transforms - const result: JsNode[] = []; - - // Possibly prepend a no-op statement - state.s = lcgNext(state.s); - if ((state.s & 3) === 0) { - result.push(makeNoop()); - } - - for (const stmt of body) { - result.push(walkWithState(stmt, state)); - } - - return result; -} diff --git a/packages/ruam/src/ruamvm/handler-fragmentation.ts b/packages/ruam/src/ruamvm/handler-fragmentation.ts deleted file mode 100644 index 28aede1..0000000 --- a/packages/ruam/src/ruamvm/handler-fragmentation.ts +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Handler fragmentation — splits opcode handlers into interleaved fragments. - * - * Each handler's case body is split into 2-3 fragments assigned unique - * case labels from a shuffled pool. Non-terminal fragments chain to the - * next via a next-fragment variable and `continue`; terminal fragments - * `break` out normally. All fragments from all handlers are shuffled. - * - * The result is a flat state machine with hundreds of interleaved - * micro-states — looking at any single `case` reveals only a fraction - * of what an opcode does. - * - * @module ruamvm/handler-fragmentation - */ - -import type { CaseClause, JsNode } from "./nodes.js"; -import { - caseClause, - lit, - exprStmt, - assign, - id, - breakStmt, - continueStmt, -} from "./nodes.js"; -import { LCG_MULTIPLIER, LCG_INCREMENT } from "../constants.js"; - -// --- LCG PRNG --- - -function makeLcg(seed: number) { - let s = seed >>> 0; - return { - next(): number { - s = (s * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - return s; - }, - }; -} - -// --- Fragment splitting --- - -/** - * Determine how many fragments a handler body should be split into. - * Bodies with <=1 real statement → no split. - * Bodies with 2 real statements → 2 fragments. - * Bodies with 3+ real statements → 2 or 3 fragments (seeded random). - */ -function chooseFragmentCount( - realStmtCount: number, - lcg: { next(): number } -): number { - if (realStmtCount <= 1) return 1; - if (realStmtCount === 2) return 2; - return 2 + (lcg.next() % 2); -} - -/** - * Split statements into N roughly-equal groups. - * Guarantees no empty groups (absorbs leftovers into last group). - */ -function splitIntoGroups(stmts: JsNode[], n: number): JsNode[][] { - if (n <= 1 || stmts.length === 0) return [stmts]; - const groups: JsNode[][] = []; - const base = Math.floor(stmts.length / n); - const rem = stmts.length % n; - let off = 0; - for (let i = 0; i < n; i++) { - const sz = base + (i < rem ? 1 : 0); - groups.push(stmts.slice(off, off + sz)); - off += sz; - } - return groups.filter((g) => g.length > 0); -} - -// --- Main API --- - -/** Result of fragmenting handler cases. */ -export interface FragmentResult { - /** Fragmented + shuffled case clauses (with default case last). */ - cases: CaseClause[]; - /** - * Map from original handler-index label value to the first-fragment ID. - * Used to update the handler table init statements. - */ - labelMap: Map; -} - -/** - * Fragment handler case clauses into interleaved micro-states. - * - * @param cases - Original handler case clauses (with handler-index labels). - * Must include a default case (label === null) as last entry. - * @param nfName - Variable name for the next-fragment dispatch variable. - * @param seed - LCG seed for deterministic splitting and shuffling. - * @returns Fragmented cases and a label remapping table. - */ -export function fragmentCases( - cases: CaseClause[], - nfName: string, - seed: number -): FragmentResult { - const lcg = makeLcg(seed); - - // Separate real cases from default - const realCases: CaseClause[] = []; - let defaultCase: CaseClause | undefined; - for (const c of cases) { - if (c.label === null) defaultCase = c; - else realCases.push(c); - } - - // Plan fragments for each handler - type FragPlan = { - originalLabel: number; // handler-index value - fragments: JsNode[][]; // statement groups (last includes break) - }; - - const plans: FragPlan[] = []; - let totalFragments = 0; - - for (const c of realCases) { - const labelVal = - c.label!.type === "Literal" ? (c.label!.value as number) : -1; - const body = c.body; - - // Separate break from logic - const stmts = body.filter((s) => s.type !== "BreakStmt"); - const nFrags = chooseFragmentCount(stmts.length, lcg); - - if (nFrags <= 1) { - // Keep as single fragment with break - plans.push({ originalLabel: labelVal, fragments: [body] }); - totalFragments += 1; - } else { - const groups = splitIntoGroups(stmts, nFrags); - plans.push({ originalLabel: labelVal, fragments: groups }); - totalFragments += groups.length; - } - } - - // Assign shuffled fragment IDs - const fragIds = Array.from({ length: totalFragments }, (_, i) => i); - for (let i = fragIds.length - 1; i > 0; i--) { - const j = lcg.next() % (i + 1); - [fragIds[i]!, fragIds[j]!] = [fragIds[j]!, fragIds[i]!]; - } - - // Build fragment case clauses and label map - const labelMap = new Map(); - const fragCases: CaseClause[] = []; - let cursor = 0; - - for (const plan of plans) { - const firstFragId = fragIds[cursor]!; - labelMap.set(plan.originalLabel, firstFragId); - - for (let f = 0; f < plan.fragments.length; f++) { - const fragId = fragIds[cursor]!; - const fragBody = plan.fragments[f]!; - const isTerminal = f === plan.fragments.length - 1; - - if (isTerminal) { - // Terminal: keep body as-is (single-fragment handlers - // already have break; multi-fragment terminals need break added) - const hasBreak = fragBody.some((s) => s.type === "BreakStmt"); - fragCases.push( - caseClause( - lit(fragId), - hasBreak ? fragBody : [...fragBody, breakStmt()] - ) - ); - } else { - // Non-terminal: chain to next fragment - const nextFragId = fragIds[cursor + 1]!; - fragCases.push( - caseClause(lit(fragId), [ - ...fragBody, - exprStmt(assign(id(nfName), lit(nextFragId))), - continueStmt(), - ]) - ); - } - cursor++; - } - } - - // Shuffle all fragment cases - for (let i = fragCases.length - 1; i > 0; i--) { - const j = lcg.next() % (i + 1); - [fragCases[i]!, fragCases[j]!] = [fragCases[j]!, fragCases[i]!]; - } - - // Append default case last - if (defaultCase) { - fragCases.push(defaultCase); - } else { - fragCases.push(caseClause(null, [breakStmt()])); - } - - return { cases: fragCases, labelMap }; -} diff --git a/packages/ruam/src/ruamvm/handlers/arithmetic.ts b/packages/ruam/src/ruamvm/handlers/arithmetic.ts deleted file mode 100644 index 9129a02..0000000 --- a/packages/ruam/src/ruamvm/handlers/arithmetic.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Arithmetic and bitwise opcode handlers in AST node form. - * - * Covers 17 opcodes across two categories: - * - Arithmetic: ADD, SUB, MUL, DIV, MOD, POW, NEG, UNARY_PLUS, INC, DEC - * - Bitwise: BIT_AND, BIT_OR, BIT_XOR, BIT_NOT, SHL, SHR, USHR - * - * @module ruamvm/handlers/arithmetic - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - id, - varDecl, - exprStmt, - assign, - bin, - un, - lit, - breakStmt, - type JsNode, - BOp, - UOp, - type BOpKind, - type UOpKind, -} from "../nodes.js"; -import { registry, type HandlerCtx, type HandlerFn } from "./registry.js"; - -// --- Helpers --- - -/** - * Build a handler for binary ops: `{var b=S[P--];S[P]=S[P] op b;break;}` - * - * @param op - The JS binary operator string (e.g. `'+'`, `'&'`, `'<<'`) - * @returns A handler function producing the AST for the case body - */ -function binaryHandler(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("rhs"), ctx.pop()), - exprStmt(ctx.setTop(bin(op, ctx.peek(), id(ctx.local("rhs"))))), - breakStmt(), - ]; -} - -/** - * Build a handler for unary ops: `S[P]=op S[P];break;` - * - * @param op - The JS unary operator string (e.g. `'-'`, `'~'`) - * @returns A handler function producing the AST for the case body - */ -function unaryHandler(op: UOpKind): HandlerFn { - return (ctx) => [ - exprStmt(ctx.setTop(un(op, ctx.peek()))), - breakStmt(), - ]; -} - -// --- Arithmetic handlers --- - -registry.set(Op.ADD, binaryHandler(BOp.Add)); -registry.set(Op.SUB, binaryHandler(BOp.Sub)); -registry.set(Op.MUL, binaryHandler(BOp.Mul)); -registry.set(Op.DIV, binaryHandler(BOp.Div)); -registry.set(Op.MOD, binaryHandler(BOp.Mod)); -registry.set(Op.POW, binaryHandler(BOp.Pow)); - -registry.set(Op.NEG, unaryHandler(UOp.Neg)); -registry.set(Op.UNARY_PLUS, unaryHandler(UOp.Pos)); - -/** INC: `S[P]=+S[P]+1;break;` -- unary `+` for ToNumber coercion */ -registry.set(Op.INC, (ctx) => [ - exprStmt(ctx.setTop(bin(BOp.Add, un(UOp.Pos, ctx.peek()), lit(1)))), - breakStmt(), -]); - -/** DEC: `S[P]=+S[P]-1;break;` -- unary `+` for ToNumber coercion */ -registry.set(Op.DEC, (ctx) => [ - exprStmt(ctx.setTop(bin(BOp.Sub, un(UOp.Pos, ctx.peek()), lit(1)))), - breakStmt(), -]); - -// --- Bitwise handlers --- - -registry.set(Op.BIT_AND, binaryHandler(BOp.BitAnd)); -registry.set(Op.BIT_OR, binaryHandler(BOp.BitOr)); -registry.set(Op.BIT_XOR, binaryHandler(BOp.BitXor)); -registry.set(Op.BIT_NOT, unaryHandler(UOp.BitNot)); -registry.set(Op.SHL, binaryHandler(BOp.Shl)); -registry.set(Op.SHR, binaryHandler(BOp.Shr)); -registry.set(Op.USHR, binaryHandler(BOp.Ushr)); diff --git a/packages/ruam/src/ruamvm/handlers/calls.ts b/packages/ruam/src/ruamvm/handlers/calls.ts deleted file mode 100644 index a893668..0000000 --- a/packages/ruam/src/ruamvm/handlers/calls.ts +++ /dev/null @@ -1,717 +0,0 @@ -/** - * Call opcode handlers using pure AST nodes. - * - * Covers 14 opcodes: - * - Basic calls: CALL, CALL_METHOD, CALL_NEW, SUPER_CALL - * - Spread: SPREAD_ARGS - * - Optional: CALL_OPTIONAL, CALL_METHOD_OPTIONAL - * - Eval: DIRECT_EVAL - * - Templates: CALL_TAGGED_TEMPLATE - * - Super methods: CALL_SUPER_METHOD - * - Fast-path: CALL_0, CALL_1, CALL_2, CALL_3 - * - * All handlers use pure AST nodes — no raw() escape hatch. - * - * @module ruamvm/handlers/calls - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - bin, - un, - assign, - call, - member, - index, - varDecl, - exprStmt, - ifStmt, - forStmt, - breakStmt, - newExpr, - ternary, - update, - arr, - obj, - spread, - BOp, - UOp, - UpOp, -} from "../nodes.js"; -import { registry, type HandlerCtx } from "./registry.js"; -import { debugTrace, superProto } from "./helpers.js"; - -// --- Helpers --- - -/** - * Build the common spread-flattening preamble for call handlers as AST nodes. - * - * Generates: - * ``` - * var argc=O;var hasSpread=argc<0;if(hasSpread)argc=-argc; - * var callArgs=new Array(argc);for(var ai=argc-1;ai>=0;ai--)callArgs[ai]=X(); - * if(hasSpread){var flat=[];for(var ai=0;ai=0;ai--)callArgs[ai]=S[P--]; - forStmt( - varDecl( - ctx.local("argIndex"), - bin(BOp.Sub, id(ctx.local("argc")), lit(1)) - ), - bin(BOp.Gte, id(ctx.local("argIndex")), lit(0)), - update(UpOp.Dec, false, id(ctx.local("argIndex"))), - [ - exprStmt( - assign( - index( - id(ctx.local("callArgs")), - id(ctx.local("argIndex")) - ), - ctx.pop() - ) - ), - ] - ), - // if(hasSpread){...flatten spread markers...} - ifStmt(id(ctx.local("hasSpread")), [ - varDecl(ctx.local("flatArgs"), arr()), - forStmt( - varDecl(ctx.local("argIndex"), lit(0)), - bin( - BOp.Lt, - id(ctx.local("argIndex")), - member(id(ctx.local("callArgs")), "length") - ), - update(UpOp.Inc, false, id(ctx.local("argIndex"))), - [ - ifStmt( - bin( - BOp.And, - index( - id(ctx.local("callArgs")), - id(ctx.local("argIndex")) - ), - index( - index( - id(ctx.local("callArgs")), - id(ctx.local("argIndex")) - ), - id(ctx.spreadSym) - ) - ), - [ - // Spread value IS the array now — iterate directly - forStmt( - varDecl(ctx.local("spreadIdx"), lit(0)), - bin( - BOp.Lt, - id(ctx.local("spreadIdx")), - member( - index( - id(ctx.local("callArgs")), - id(ctx.local("argIndex")) - ), - "length" - ) - ), - update( - UpOp.Inc, - false, - id(ctx.local("spreadIdx")) - ), - [ - exprStmt( - call( - member( - id(ctx.local("flatArgs")), - "push" - ), - [ - index( - index( - id( - ctx.local( - "callArgs" - ) - ), - id( - ctx.local( - "argIndex" - ) - ) - ), - id(ctx.local("spreadIdx")) - ), - ] - ) - ), - ] - ), - ], - [ - exprStmt( - call( - member(id(ctx.local("flatArgs")), "push"), - [ - index( - id(ctx.local("callArgs")), - id(ctx.local("argIndex")) - ), - ] - ) - ), - ] - ), - ] - ), - exprStmt( - assign(id(ctx.local("callArgs")), id(ctx.local("flatArgs"))) - ), - ]), - ]; -} - -/** - * Build a simple (no-spread) argument collection preamble as AST nodes. - * - * Generates: - * ``` - * var argc=O;var callArgs=new Array(argc); - * for(var ai=argc-1;ai>=0;ai--)callArgs[ai]=X(); - * ``` - */ -function simplePreamble(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("argc"), id(ctx.O)), - varDecl( - ctx.local("callArgs"), - newExpr(id("Array"), [id(ctx.local("argc"))]) - ), - forStmt( - varDecl( - ctx.local("argIndex"), - bin(BOp.Sub, id(ctx.local("argc")), lit(1)) - ), - bin(BOp.Gte, id(ctx.local("argIndex")), lit(0)), - update(UpOp.Dec, false, id(ctx.local("argIndex"))), - [ - exprStmt( - assign( - index( - id(ctx.local("callArgs")), - id(ctx.local("argIndex")) - ), - ctx.pop() - ) - ), - ] - ), - ]; -} - -// --- Call handlers --- - -/** - * CALL: pop arguments, pop function, apply with spread flattening. - * Includes debug trace when debug mode is enabled. - */ -function CALL(ctx: HandlerCtx): JsNode[] { - return [ - ...spreadPreamble(ctx), - varDecl(ctx.local("func"), ctx.pop()), - ...debugTrace( - ctx, - "CALL", - lit("fn="), - un(UOp.Typeof, id(ctx.local("func"))), - bin( - BOp.Add, - lit("argc="), - member(id(ctx.local("callArgs")), "length") - ), - ternary( - bin( - BOp.And, - id(ctx.local("func")), - member(id(ctx.local("func")), "name") - ), - bin( - BOp.Add, - lit("name="), - member(id(ctx.local("func")), "name") - ), - lit("") - ) - ), - ...(ctx.debug - ? [ - ifStmt( - bin( - BOp.Sneq, - un(UOp.Typeof, id(ctx.local("func"))), - lit("function") - ), - [ - exprStmt( - call(id(ctx.dbg), [ - lit("CALL_ERR"), - lit("NOT A FUNCTION:"), - id(ctx.local("func")), - bin( - BOp.Add, - lit(ctx.S + " depth="), - member(id(ctx.S), "length") - ), - ]) - ), - ] - ), - ] - : []), - exprStmt( - ctx.push( - call(member(id(ctx.local("func")), "apply"), [ - un(UOp.Void, lit(0)), - id(ctx.local("callArgs")), - ]) - ) - ), - breakStmt(), - ]; -} - -/** - * CALL_METHOD: pop arguments, pop receiver, pop function, apply with receiver. - * Includes debug trace when debug mode is enabled. - */ -function CALL_METHOD(ctx: HandlerCtx): JsNode[] { - return [ - ...spreadPreamble(ctx), - varDecl(ctx.local("receiver"), ctx.pop()), - varDecl(ctx.local("func"), ctx.pop()), - ...debugTrace( - ctx, - "CALL_METHOD", - lit("fn="), - un(UOp.Typeof, id(ctx.local("func"))), - lit("recv="), - un(UOp.Typeof, id(ctx.local("receiver"))), - bin( - BOp.Add, - lit("argc="), - member(id(ctx.local("callArgs")), "length") - ), - ternary( - bin( - BOp.And, - id(ctx.local("func")), - member(id(ctx.local("func")), "name") - ), - bin( - BOp.Add, - lit("name="), - member(id(ctx.local("func")), "name") - ), - lit("") - ) - ), - ...(ctx.debug - ? [ - ifStmt( - bin( - BOp.Sneq, - un(UOp.Typeof, id(ctx.local("func"))), - lit("function") - ), - [ - exprStmt( - call(id(ctx.dbg), [ - lit("CALL_METHOD_ERR"), - lit("NOT A FUNCTION:"), - id(ctx.local("func")), - lit("recv="), - id(ctx.local("receiver")), - ]) - ), - ] - ), - ] - : []), - exprStmt( - ctx.push( - call(member(id(ctx.local("func")), "apply"), [ - id(ctx.local("receiver")), - id(ctx.local("callArgs")), - ]) - ) - ), - breakStmt(), - ]; -} - -/** - * CALL_NEW: pop arguments, pop constructor, invoke with `new`. - * - * ``` - * var argc=O;var callArgs=new Array(argc); - * for(var ai=argc-1;ai>=0;ai--)callArgs[ai]=X(); - * var Ctor=X();W(new Ctor(...callArgs));break; - * ``` - */ -function CALL_NEW(ctx: HandlerCtx): JsNode[] { - return [ - ...simplePreamble(ctx), - varDecl(ctx.local("Ctor"), ctx.pop()), - exprStmt( - ctx.push( - newExpr(id(ctx.local("Ctor")), [ - spread(id(ctx.local("callArgs"))), - ]) - ) - ), - breakStmt(), - ]; -} - -/** - * SUPER_CALL: pop arguments, resolve super constructor via home object, apply to this. - * Includes debug trace when debug mode is enabled. - */ -function SUPER_CALL(ctx: HandlerCtx): JsNode[] { - return [ - ...simplePreamble(ctx), - varDecl(ctx.local("superProto"), superProto(ctx)), - ...debugTrace( - ctx, - "SUPER_CALL", - bin(BOp.Add, lit("argc="), id(ctx.local("argc"))), - lit("superProto="), - un(UOp.Not, un(UOp.Not, id(ctx.local("superProto")))), - lit("superCtor="), - bin( - BOp.And, - id(ctx.local("superProto")), - un( - UOp.Typeof, - member(id(ctx.local("superProto")), "constructor") - ) - ) - ), - ifStmt( - bin( - BOp.And, - id(ctx.local("superProto")), - member(id(ctx.local("superProto")), "constructor") - ), - [ - exprStmt( - call( - member( - member(id(ctx.local("superProto")), "constructor"), - "apply" - ), - [id(ctx.TV), id(ctx.local("callArgs"))] - ) - ), - ] - ), - exprStmt(ctx.push(id(ctx.TV))), - breakStmt(), - ]; -} - -// --- Spread --- - -/** - * SPREAD_ARGS: wrap top-of-stack value in a spread marker object. - * - * `S[P]={__spread__:true,items:Array.from(S[P])};break;` - */ -function SPREAD_ARGS(ctx: HandlerCtx): JsNode[] { - // Tag the array with a Symbol instead of wrapping in a marker object - return [ - varDecl( - ctx.local("spreadArr"), - call(member(id("Array"), "from"), [ctx.peek()]) - ), - exprStmt( - assign( - index(id(ctx.local("spreadArr")), id(ctx.spreadSym)), - lit(true) - ) - ), - exprStmt(ctx.setTop(id(ctx.local("spreadArr")))), - breakStmt(), - ]; -} - -// --- Optional calls --- - -/** - * CALL_OPTIONAL: like CALL but returns undefined if function is nullish. - * - * ``` - * ...spread preamble... - * var fn=X();W(fn==null?void 0:fn(...callArgs));break; - * ``` - */ -function CALL_OPTIONAL(ctx: HandlerCtx): JsNode[] { - return [ - ...spreadPreamble(ctx), - varDecl(ctx.local("func"), ctx.pop()), - exprStmt( - ctx.push( - ternary( - bin(BOp.Eq, id(ctx.local("func")), lit(null)), - un(UOp.Void, lit(0)), - call(id(ctx.local("func")), [ - spread(id(ctx.local("callArgs"))), - ]) - ) - ) - ), - breakStmt(), - ]; -} - -/** - * CALL_METHOD_OPTIONAL: like CALL_METHOD but returns undefined if function is nullish. - * - * ``` - * ...spread preamble... - * var recv=X();var fn=X();W(fn==null?void 0:fn.call(recv,...callArgs));break; - * ``` - */ -function CALL_METHOD_OPTIONAL(ctx: HandlerCtx): JsNode[] { - return [ - ...spreadPreamble(ctx), - varDecl(ctx.local("receiver"), ctx.pop()), - varDecl(ctx.local("func"), ctx.pop()), - exprStmt( - ctx.push( - ternary( - bin(BOp.Eq, id(ctx.local("func")), lit(null)), - un(UOp.Void, lit(0)), - call(member(id(ctx.local("func")), "call"), [ - id(ctx.local("receiver")), - spread(id(ctx.local("callArgs"))), - ]) - ) - ) - ), - breakStmt(), - ]; -} - -// --- Eval --- - -/** `{var code=X();W(eval(code));break;}` */ -function DIRECT_EVAL(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("code"), ctx.pop()), - exprStmt(ctx.push(call(id("eval"), [id(ctx.local("code"))]))), - breakStmt(), - ]; -} - -// --- Tagged template call --- - -/** - * CALL_TAGGED_TEMPLATE: pop arguments, pop tag function, call with spread. - * - * ``` - * var argc=O;var callArgs=new Array(argc); - * for(var ai=argc-1;ai>=0;ai--)callArgs[ai]=X(); - * var fn=X();W(fn(...callArgs));break; - * ``` - */ -function CALL_TAGGED_TEMPLATE(ctx: HandlerCtx): JsNode[] { - return [ - ...simplePreamble(ctx), - varDecl(ctx.local("func"), ctx.pop()), - exprStmt( - ctx.push( - call(id(ctx.local("func")), [spread(id(ctx.local("callArgs")))]) - ) - ), - breakStmt(), - ]; -} - -// --- Super method call --- - -/** - * CALL_SUPER_METHOD: call a named method on the super prototype. - * Operand packs argc in low 16 bits and name constant index in high 16 bits. - * - * ``` - * var argc=O&0xFFFF;var nameIdx=(O>>16)&0xFFFF; - * var callArgs=new Array(argc);for(var ai=argc-1;ai>=0;ai--)callArgs[ai]=X(); - * var sp2=HO?Object.getPrototypeOf(HO):Object.getPrototypeOf(Object.getPrototypeOf(TV)); - * var fn=sp2?sp2[C[nameIdx]]:void 0;W(fn?fn.apply(TV,callArgs):void 0);break; - * ``` - */ -function CALL_SUPER_METHOD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("argc"), bin(BOp.BitAnd, id(ctx.O), lit(0xffff))), - varDecl( - ctx.local("nameIdx"), - bin(BOp.BitAnd, bin(BOp.Shr, id(ctx.O), lit(16)), lit(0xffff)) - ), - varDecl( - ctx.local("callArgs"), - newExpr(id("Array"), [id(ctx.local("argc"))]) - ), - forStmt( - varDecl( - ctx.local("argIndex"), - bin(BOp.Sub, id(ctx.local("argc")), lit(1)) - ), - bin(BOp.Gte, id(ctx.local("argIndex")), lit(0)), - update(UpOp.Dec, false, id(ctx.local("argIndex"))), - [ - exprStmt( - assign( - index( - id(ctx.local("callArgs")), - id(ctx.local("argIndex")) - ), - ctx.pop() - ) - ), - ] - ), - varDecl(ctx.local("superProto"), superProto(ctx)), - varDecl( - ctx.local("func"), - ternary( - id(ctx.local("superProto")), - index( - id(ctx.local("superProto")), - index(id(ctx.C), id(ctx.local("nameIdx"))) - ), - un(UOp.Void, lit(0)) - ) - ), - exprStmt( - ctx.push( - ternary( - id(ctx.local("func")), - call(member(id(ctx.local("func")), "call"), [ - id(ctx.TV), - spread(id(ctx.local("callArgs"))), - ]), - un(UOp.Void, lit(0)) - ) - ) - ), - breakStmt(), - ]; -} - -// --- Fast-path calls (no spread, fixed arity) --- - -/** `{var fn=X();W(fn());break;}` */ -function CALL_0(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - exprStmt(ctx.push(call(id(ctx.local("func")), []))), - breakStmt(), - ]; -} - -/** `{var a1=X();var fn=X();W(fn(a1));break;}` */ -function CALL_1(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("arg1"), ctx.pop()), - varDecl(ctx.local("func"), ctx.pop()), - exprStmt( - ctx.push(call(id(ctx.local("func")), [id(ctx.local("arg1"))])) - ), - breakStmt(), - ]; -} - -/** `{var a2=X();var a1=X();var fn=X();W(fn(a1,a2));break;}` */ -function CALL_2(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("arg2"), ctx.pop()), - varDecl(ctx.local("arg1"), ctx.pop()), - varDecl(ctx.local("func"), ctx.pop()), - exprStmt( - ctx.push( - call(id(ctx.local("func")), [ - id(ctx.local("arg1")), - id(ctx.local("arg2")), - ]) - ) - ), - breakStmt(), - ]; -} - -/** `{var a3=X();var a2=X();var a1=X();var fn=X();W(fn(a1,a2,a3));break;}` */ -function CALL_3(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("arg3"), ctx.pop()), - varDecl(ctx.local("arg2"), ctx.pop()), - varDecl(ctx.local("arg1"), ctx.pop()), - varDecl(ctx.local("func"), ctx.pop()), - exprStmt( - ctx.push( - call(id(ctx.local("func")), [ - id(ctx.local("arg1")), - id(ctx.local("arg2")), - id(ctx.local("arg3")), - ]) - ) - ), - breakStmt(), - ]; -} - -// --- Registration --- - -registry.set(Op.CALL, CALL); -registry.set(Op.CALL_METHOD, CALL_METHOD); -registry.set(Op.CALL_NEW, CALL_NEW); -registry.set(Op.SUPER_CALL, SUPER_CALL); -registry.set(Op.SPREAD_ARGS, SPREAD_ARGS); -registry.set(Op.CALL_OPTIONAL, CALL_OPTIONAL); -registry.set(Op.CALL_METHOD_OPTIONAL, CALL_METHOD_OPTIONAL); -registry.set(Op.DIRECT_EVAL, DIRECT_EVAL); -registry.set(Op.CALL_TAGGED_TEMPLATE, CALL_TAGGED_TEMPLATE); -registry.set(Op.CALL_SUPER_METHOD, CALL_SUPER_METHOD); -registry.set(Op.CALL_0, CALL_0); -registry.set(Op.CALL_1, CALL_1); -registry.set(Op.CALL_2, CALL_2); -registry.set(Op.CALL_3, CALL_3); diff --git a/packages/ruam/src/ruamvm/handlers/classes.ts b/packages/ruam/src/ruamvm/handlers/classes.ts deleted file mode 100644 index 5eb8f33..0000000 --- a/packages/ruam/src/ruamvm/handlers/classes.ts +++ /dev/null @@ -1,721 +0,0 @@ -/** - * Class definition opcode handlers using pure AST nodes. - * - * Covers 22 opcodes: - * - Class creation: NEW_CLASS, NEW_DERIVED_CLASS, EXTEND_CLASS - * - Methods: DEFINE_METHOD, DEFINE_STATIC_METHOD - * - Accessors: DEFINE_GETTER, DEFINE_STATIC_GETTER, - * DEFINE_SETTER, DEFINE_STATIC_SETTER - * - Fields: DEFINE_FIELD, DEFINE_STATIC_FIELD - * - Private members: DEFINE_PRIVATE_METHOD, DEFINE_PRIVATE_GETTER, - * DEFINE_PRIVATE_SETTER, DEFINE_PRIVATE_FIELD, - * DEFINE_STATIC_PRIVATE_FIELD, DEFINE_STATIC_PRIVATE_METHOD - * - Static blocks: CLASS_STATIC_BLOCK - * - Finalization: FINALIZE_CLASS - * - Private env: INIT_PRIVATE_ENV, ADD_PRIVATE_BRAND, CHECK_PRIVATE_BRAND - * - * All handlers use pure AST nodes — no raw() escape hatch. - * - * @module ruamvm/handlers/classes - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - bin, - un, - assign, - call, - member, - index, - varDecl, - exprStmt, - ifStmt, - returnStmt, - fnExpr, - ternary, - breakStmt, - obj, - BOp, - UOp, -} from "../nodes.js"; -import { registry, type HandlerCtx } from "./registry.js"; -import { debugTrace } from "./helpers.js"; - -// --- Helpers --- - -/** - * Build the IIFE-wrapped class constructor pattern. - * - * Creates a constructor proxy via an immediately-invoked function expression - * to isolate `var _ctor` and prevent var-hoisting from sharing across classes. - * - * ```js - * (function(){ - * var c=null; - * var f=function(){if(c)return c.apply(this,arguments);}; - * f.__setCtor=function(x){c=x;}; - * return f; - * })() - * ``` - * - * @returns JsNode — IIFE call expression producing the constructor proxy - */ -function buildCtorIIFE(ctx: HandlerCtx): JsNode { - return call( - fnExpr( - undefined, - [], - [ - varDecl(ctx.local("ctor"), lit(null)), - varDecl( - ctx.local("ctorProxy"), - fnExpr( - undefined, - [], - [ - ifStmt(id(ctx.local("ctor")), [ - returnStmt( - call( - member(id(ctx.local("ctor")), "apply"), - [id("this"), id("arguments")] - ) - ), - ]), - ] - ) - ), - exprStmt( - assign( - member(id(ctx.local("ctorProxy")), "__setCtor"), - fnExpr( - undefined, - ["x"], - [exprStmt(assign(id(ctx.local("ctor")), id("x")))] - ) - ) - ), - returnStmt(id(ctx.local("ctorProxy"))), - ] - ), - [] - ); -} - -/** - * Build prototype chain setup nodes for class inheritance. - * - * ```js - * cls.prototype = Object.create(SuperClass.prototype); - * cls.prototype.constructor = cls; - * Object.setPrototypeOf(cls, SuperClass); - * ``` - * - * @param clsName - Local variable name for the class - * @param superName - Local variable name for the superclass - * @returns JsNode[] — prototype chain setup statements - */ -function buildPrototypeChain(clsName: string, superName: string): JsNode[] { - return [ - exprStmt( - assign( - member(id(clsName), "prototype"), - call(member(id("Object"), "create"), [ - member(id(superName), "prototype"), - ]) - ) - ), - exprStmt( - assign( - member(member(id(clsName), "prototype"), "constructor"), - id(clsName) - ) - ), - exprStmt( - call(member(id("Object"), "setPrototypeOf"), [ - id(clsName), - id(superName), - ]) - ), - ]; -} - -// --- Class creation --- - -/** - * NEW_CLASS: create a new class with optional superclass. - * Uses IIFE-wrapped `_ctor` to prevent var-hoisting sharing across classes. - * Includes debug trace when debug mode is enabled. - * - * ``` - * var hasSuperClass=O;var SuperClass=hasSuperClass?X():null; - * var cls=(function(){var c=null;var f=function(){if(c)return c.apply(this,arguments);}; - * f.__setCtor=function(x){c=x;};return f;})(); - * if(SuperClass){cls.prototype=Object.create(SuperClass.prototype); - * cls.prototype.constructor=cls;Object.setPrototypeOf(cls,SuperClass);} - * W(cls);break; - * ``` - */ -function NEW_CLASS(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("hasSuperClass"), id(ctx.O)), - varDecl( - ctx.local("SuperClass"), - ternary(id(ctx.local("hasSuperClass")), ctx.pop(), lit(null)) - ), - ...debugTrace( - ctx, - "NEW_CLASS", - bin( - BOp.Add, - lit("hasSuper="), - un(UOp.Not, un(UOp.Not, id(ctx.local("hasSuperClass")))) - ) - ), - varDecl(ctx.local("cls"), buildCtorIIFE(ctx)), - ifStmt( - id(ctx.local("SuperClass")), - buildPrototypeChain( - ctx.local("cls") as string, - ctx.local("SuperClass") as string - ) - ), - exprStmt(ctx.push(id(ctx.local("cls")))), - breakStmt(), - ]; -} - -/** - * NEW_DERIVED_CLASS: create a derived class (always has superclass). - * Includes debug trace when debug mode is enabled. - * - * ``` - * var SuperClass=X(); - * var cls=(function(){var c=null;var f=function(){if(c)return c.apply(this,arguments);}; - * f.__setCtor=function(x){c=x;};return f;})(); - * cls.prototype=Object.create(SuperClass.prototype);cls.prototype.constructor=cls; - * Object.setPrototypeOf(cls,SuperClass);W(cls);break; - * ``` - */ -function NEW_DERIVED_CLASS(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("SuperClass"), ctx.pop()), - ...debugTrace(ctx, "NEW_DERIVED_CLASS"), - varDecl(ctx.local("cls"), buildCtorIIFE(ctx)), - ...buildPrototypeChain( - ctx.local("cls") as string, - ctx.local("SuperClass") as string - ), - exprStmt(ctx.push(id(ctx.local("cls")))), - breakStmt(), - ]; -} - -/** - * EXTEND_CLASS: set up prototype chain for class inheritance. - * - * ``` - * var superCls=X();var cls=Y(); - * cls.prototype=Object.create(superCls.prototype); - * cls.prototype.constructor=cls;Object.setPrototypeOf(cls,superCls);break; - * ``` - */ -function EXTEND_CLASS(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("superCls"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - ...buildPrototypeChain( - ctx.local("cls") as string, - ctx.local("superCls") as string - ), - breakStmt(), - ]; -} - -// --- Method definition --- - -/** - * DEFINE_METHOD: define an instance or static method with home object stamping. - * Operand packs name constant index in low 16 bits and isStatic flag in bit 16. - * Constructor detection routes through `__setCtor` for IIFE-wrapped class constructors. - * Includes debug trace when debug mode is enabled. - */ -function DEFINE_METHOD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - varDecl( - ctx.local("methodName"), - index(id(ctx.C), bin(BOp.BitAnd, id(ctx.O), lit(0xffff))) - ), - varDecl( - ctx.local("isStatic"), - bin(BOp.BitAnd, bin(BOp.Shr, id(ctx.O), lit(16)), lit(1)) - ), - ...debugTrace( - ctx, - "DEFINE_METHOD", - bin(BOp.Add, lit("name="), id(ctx.local("methodName"))), - bin( - BOp.Add, - lit("static="), - un(UOp.Not, un(UOp.Not, id(ctx.local("isStatic")))) - ), - bin( - BOp.Add, - lit("isCtor="), - bin(BOp.Seq, id(ctx.local("methodName")), lit("constructor")) - ) - ), - ifStmt( - bin(BOp.Seq, id(ctx.local("methodName")), lit("constructor")), - [ - ifStmt(member(id(ctx.local("cls")), "__setCtor"), [ - exprStmt( - call(member(id(ctx.local("cls")), "__setCtor"), [ - id(ctx.local("func")), - ]) - ), - ]), - exprStmt( - assign( - member(id(ctx.local("func")), ctx.t("_ho")), - member(id(ctx.local("cls")), "prototype") - ) - ), - exprStmt( - assign( - member( - member(id(ctx.local("cls")), "prototype"), - "constructor" - ), - id(ctx.local("func")) - ) - ), - ], - [ - ifStmt( - id(ctx.local("isStatic")), - [ - exprStmt( - assign( - member(id(ctx.local("func")), ctx.t("_ho")), - id(ctx.local("cls")) - ) - ), - exprStmt( - assign( - index( - id(ctx.local("cls")), - id(ctx.local("methodName")) - ), - id(ctx.local("func")) - ) - ), - ], - [ - varDecl( - ctx.t("_tgt"), - bin( - BOp.Or, - member(id(ctx.local("cls")), "prototype"), - id(ctx.local("cls")) - ) - ), - exprStmt( - assign( - member(id(ctx.local("func")), ctx.t("_ho")), - id(ctx.t("_tgt")) - ) - ), - exprStmt( - assign( - index( - id(ctx.t("_tgt")), - id(ctx.local("methodName")) - ), - id(ctx.local("func")) - ) - ), - ] - ), - ] - ), - breakStmt(), - ]; -} - -/** `{var fn=X();var cls=Y();fn._ho=cls;cls[C[O]]=fn;break;}` */ -function DEFINE_STATIC_METHOD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - assign( - member(id(ctx.local("func")), ctx.t("_ho")), - id(ctx.local("cls")) - ) - ), - exprStmt( - assign( - index(id(ctx.local("cls")), index(id(ctx.C), id(ctx.O))), - id(ctx.local("func")) - ) - ), - breakStmt(), - ]; -} - -// --- Accessor definition --- - -/** - * DEFINE_GETTER: define a getter with home object stamping and `enumerable: false`. - * Operand packs name constant index in low 16 bits and isStatic flag in bit 16. - */ -function DEFINE_GETTER(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - varDecl( - ctx.local("accessorName"), - index(id(ctx.C), bin(BOp.BitAnd, id(ctx.O), lit(0xffff))) - ), - varDecl( - ctx.local("isStatic"), - bin(BOp.BitAnd, bin(BOp.Shr, id(ctx.O), lit(16)), lit(1)) - ), - varDecl( - ctx.local("target"), - ternary( - id(ctx.local("isStatic")), - id(ctx.local("cls")), - bin( - BOp.Or, - member(id(ctx.local("cls")), "prototype"), - id(ctx.local("cls")) - ) - ) - ), - exprStmt( - assign( - member(id(ctx.local("func")), ctx.t("_ho")), - id(ctx.local("target")) - ) - ), - exprStmt( - call(member(id("Object"), "defineProperty"), [ - id(ctx.local("target")), - id(ctx.local("accessorName")), - obj( - ["get", id(ctx.local("func"))], - ["configurable", lit(true)], - ["enumerable", lit(false)] - ), - ]) - ), - breakStmt(), - ]; -} - -/** `{var fn=X();var cls=Y();fn._ho=cls;Object.defineProperty(cls,C[O],{get:fn,configurable:true,enumerable:false});break;}` */ -function DEFINE_STATIC_GETTER(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - assign( - member(id(ctx.local("func")), ctx.t("_ho")), - id(ctx.local("cls")) - ) - ), - exprStmt( - call(member(id("Object"), "defineProperty"), [ - id(ctx.local("cls")), - index(id(ctx.C), id(ctx.O)), - obj( - ["get", id(ctx.local("func"))], - ["configurable", lit(true)], - ["enumerable", lit(false)] - ), - ]) - ), - breakStmt(), - ]; -} - -/** - * DEFINE_SETTER: define a setter with home object stamping and `enumerable: false`. - * Operand packs name constant index in low 16 bits and isStatic flag in bit 16. - */ -function DEFINE_SETTER(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - varDecl( - ctx.local("accessorName"), - index(id(ctx.C), bin(BOp.BitAnd, id(ctx.O), lit(0xffff))) - ), - varDecl( - ctx.local("isStatic"), - bin(BOp.BitAnd, bin(BOp.Shr, id(ctx.O), lit(16)), lit(1)) - ), - varDecl( - ctx.local("target"), - ternary( - id(ctx.local("isStatic")), - id(ctx.local("cls")), - bin( - BOp.Or, - member(id(ctx.local("cls")), "prototype"), - id(ctx.local("cls")) - ) - ) - ), - exprStmt( - assign( - member(id(ctx.local("func")), ctx.t("_ho")), - id(ctx.local("target")) - ) - ), - exprStmt( - call(member(id("Object"), "defineProperty"), [ - id(ctx.local("target")), - id(ctx.local("accessorName")), - obj( - ["set", id(ctx.local("func"))], - ["configurable", lit(true)], - ["enumerable", lit(false)] - ), - ]) - ), - breakStmt(), - ]; -} - -/** `{var fn=X();var cls=Y();fn._ho=cls;Object.defineProperty(cls,C[O],{set:fn,configurable:true,enumerable:false});break;}` */ -function DEFINE_STATIC_SETTER(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - assign( - member(id(ctx.local("func")), ctx.t("_ho")), - id(ctx.local("cls")) - ) - ), - exprStmt( - call(member(id("Object"), "defineProperty"), [ - id(ctx.local("cls")), - index(id(ctx.C), id(ctx.O)), - obj( - ["set", id(ctx.local("func"))], - ["configurable", lit(true)], - ["enumerable", lit(false)] - ), - ]) - ), - breakStmt(), - ]; -} - -// --- Field definition --- - -/** `{var val=X();var name=C[O];var obj=Y();obj[name]=val;break;}` */ -function DEFINE_FIELD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("fieldName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("object"), ctx.peek()), - exprStmt( - assign( - index(id(ctx.local("object")), id(ctx.local("fieldName"))), - id(ctx.local("value")) - ) - ), - breakStmt(), - ]; -} - -/** `{var val=X();var cls=Y();cls[C[O]]=val;break;}` */ -function DEFINE_STATIC_FIELD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - assign( - index(id(ctx.local("cls")), index(id(ctx.C), id(ctx.O))), - id(ctx.local("value")) - ) - ), - breakStmt(), - ]; -} - -// --- Private member definition --- - -/** `{var fn=X();var cls=Y();var _tgt=(cls.prototype||cls);_tgt[C[O]]=fn;break;}` */ -function DEFINE_PRIVATE_METHOD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - varDecl( - ctx.t("_tgt"), - bin( - BOp.Or, - member(id(ctx.local("cls")), "prototype"), - id(ctx.local("cls")) - ) - ), - exprStmt( - assign( - index(id(ctx.t("_tgt")), index(id(ctx.C), id(ctx.O))), - id(ctx.local("func")) - ) - ), - breakStmt(), - ]; -} - -/** `{var fn=X();var cls=Y();Object.defineProperty(cls.prototype||cls,C[O],{get:fn,configurable:true});break;}` */ -function DEFINE_PRIVATE_GETTER(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - call(member(id("Object"), "defineProperty"), [ - bin( - BOp.Or, - member(id(ctx.local("cls")), "prototype"), - id(ctx.local("cls")) - ), - index(id(ctx.C), id(ctx.O)), - obj( - ["get", id(ctx.local("func"))], - ["configurable", lit(true)] - ), - ]) - ), - breakStmt(), - ]; -} - -/** `{var fn=X();var cls=Y();Object.defineProperty(cls.prototype||cls,C[O],{set:fn,configurable:true});break;}` */ -function DEFINE_PRIVATE_SETTER(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - call(member(id("Object"), "defineProperty"), [ - bin( - BOp.Or, - member(id(ctx.local("cls")), "prototype"), - id(ctx.local("cls")) - ), - index(id(ctx.C), id(ctx.O)), - obj( - ["set", id(ctx.local("func"))], - ["configurable", lit(true)] - ), - ]) - ), - breakStmt(), - ]; -} - -/** `{var val=X();var obj=Y();obj[C[O]]=val;break;}` */ -function DEFINE_PRIVATE_FIELD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("object"), ctx.peek()), - exprStmt( - assign( - index(id(ctx.local("object")), index(id(ctx.C), id(ctx.O))), - id(ctx.local("value")) - ) - ), - breakStmt(), - ]; -} - -/** `{var val=X();var cls=Y();cls[C[O]]=val;break;}` */ -function DEFINE_STATIC_PRIVATE_FIELD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - assign( - index(id(ctx.local("cls")), index(id(ctx.C), id(ctx.O))), - id(ctx.local("value")) - ) - ), - breakStmt(), - ]; -} - -/** `{var fn=X();var cls=Y();cls[C[O]]=fn;break;}` */ -function DEFINE_STATIC_PRIVATE_METHOD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - assign( - index(id(ctx.local("cls")), index(id(ctx.C), id(ctx.O))), - id(ctx.local("func")) - ) - ), - breakStmt(), - ]; -} - -// --- Static block --- - -/** `{var fn=X();var cls=Y();fn.call(cls);break;}` */ -function CLASS_STATIC_BLOCK(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.pop()), - varDecl(ctx.local("cls"), ctx.peek()), - exprStmt( - call(member(id(ctx.local("func")), "call"), [id(ctx.local("cls"))]) - ), - breakStmt(), - ]; -} - -// --- No-op class handlers --- - -/** FINALIZE_CLASS: no-op, just break. */ -function FINALIZE_CLASS(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -/** INIT_PRIVATE_ENV / ADD_PRIVATE_BRAND / CHECK_PRIVATE_BRAND: no-op stubs. */ -function PRIVATE_ENV_NOP(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -// --- Registration --- - -registry.set(Op.NEW_CLASS, NEW_CLASS); -registry.set(Op.NEW_DERIVED_CLASS, NEW_DERIVED_CLASS); -registry.set(Op.EXTEND_CLASS, EXTEND_CLASS); -registry.set(Op.DEFINE_METHOD, DEFINE_METHOD); -registry.set(Op.DEFINE_STATIC_METHOD, DEFINE_STATIC_METHOD); -registry.set(Op.DEFINE_GETTER, DEFINE_GETTER); -registry.set(Op.DEFINE_STATIC_GETTER, DEFINE_STATIC_GETTER); -registry.set(Op.DEFINE_SETTER, DEFINE_SETTER); -registry.set(Op.DEFINE_STATIC_SETTER, DEFINE_STATIC_SETTER); -registry.set(Op.DEFINE_FIELD, DEFINE_FIELD); -registry.set(Op.DEFINE_STATIC_FIELD, DEFINE_STATIC_FIELD); -registry.set(Op.DEFINE_PRIVATE_METHOD, DEFINE_PRIVATE_METHOD); -registry.set(Op.DEFINE_PRIVATE_GETTER, DEFINE_PRIVATE_GETTER); -registry.set(Op.DEFINE_PRIVATE_SETTER, DEFINE_PRIVATE_SETTER); -registry.set(Op.DEFINE_PRIVATE_FIELD, DEFINE_PRIVATE_FIELD); -registry.set(Op.DEFINE_STATIC_PRIVATE_FIELD, DEFINE_STATIC_PRIVATE_FIELD); -registry.set(Op.DEFINE_STATIC_PRIVATE_METHOD, DEFINE_STATIC_PRIVATE_METHOD); -registry.set(Op.CLASS_STATIC_BLOCK, CLASS_STATIC_BLOCK); -registry.set(Op.FINALIZE_CLASS, FINALIZE_CLASS); -registry.set(Op.INIT_PRIVATE_ENV, PRIVATE_ENV_NOP); -registry.set(Op.ADD_PRIVATE_BRAND, PRIVATE_ENV_NOP); -registry.set(Op.CHECK_PRIVATE_BRAND, PRIVATE_ENV_NOP); diff --git a/packages/ruam/src/ruamvm/handlers/comparison.ts b/packages/ruam/src/ruamvm/handlers/comparison.ts deleted file mode 100644 index 1a8ee44..0000000 --- a/packages/ruam/src/ruamvm/handlers/comparison.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** @module ruamvm/handlers/comparison */ - -import { Op } from "../../compiler/opcodes.js"; -import { - id, - bin, - assign, - varDecl, - exprStmt, - breakStmt, - BOp, - type BOpKind, -} from "../nodes.js"; -import type { HandlerCtx, HandlerFn } from "./registry.js"; -import { registry } from "./registry.js"; - -// --- Helpers --- - -/** - * Build a binary comparison handler. - * - * Pattern: `{var b=S[P--];S[P]=S[P] b;break;}` - * - * @param op - JS comparison operator (`==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, `>=`) - * @returns Handler function producing the case body AST nodes - */ -function cmpHandler(op: BOpKind): HandlerFn { - return (ctx: HandlerCtx) => [ - varDecl(ctx.local("rhs"), ctx.pop()), - exprStmt(ctx.setTop(bin(op, ctx.peek(), id(ctx.local("rhs"))))), - breakStmt(), - ]; -} - -// --- Registration --- - -registry.set(Op.EQ, cmpHandler(BOp.Eq)); -registry.set(Op.NEQ, cmpHandler(BOp.Neq)); -registry.set(Op.SEQ, cmpHandler(BOp.Seq)); -registry.set(Op.SNEQ, cmpHandler(BOp.Sneq)); -registry.set(Op.LT, cmpHandler(BOp.Lt)); -registry.set(Op.LTE, cmpHandler(BOp.Lte)); -registry.set(Op.GT, cmpHandler(BOp.Gt)); -registry.set(Op.GTE, cmpHandler(BOp.Gte)); diff --git a/packages/ruam/src/ruamvm/handlers/compound-scoped.ts b/packages/ruam/src/ruamvm/handlers/compound-scoped.ts deleted file mode 100644 index d6d39e3..0000000 --- a/packages/ruam/src/ruamvm/handlers/compound-scoped.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Compound scoped assignment opcode handlers in AST node form. - * - * Covers 20 opcodes for in-place scope chain modifications: - * - Increment/decrement: INC_SCOPED, DEC_SCOPED, POST_INC_SCOPED, POST_DEC_SCOPED - * - Arithmetic assign: ADD_ASSIGN_SCOPED, SUB_ASSIGN_SCOPED, MUL_ASSIGN_SCOPED, - * DIV_ASSIGN_SCOPED, MOD_ASSIGN_SCOPED, POW_ASSIGN_SCOPED - * - Bitwise assign: BIT_AND_ASSIGN_SCOPED, BIT_OR_ASSIGN_SCOPED, - * BIT_XOR_ASSIGN_SCOPED, SHL_ASSIGN_SCOPED, - * SHR_ASSIGN_SCOPED, USHR_ASSIGN_SCOPED - * - Logical assign: AND_ASSIGN_SCOPED, OR_ASSIGN_SCOPED, NULLISH_ASSIGN_SCOPED - * - No-op: ASSIGN_OP - * - * All handlers use ctx.scopeWalk() for structured AST scope chain walking. - * - * @module ruamvm/handlers/compound-scoped - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - index, - assign, - bin, - lit, - varDecl, - exprStmt, - ifStmt, - breakStmt, - BOp, - AOp, - type AOpKind, - type BOpKind, -} from "../nodes.js"; -import type { HandlerCtx, HandlerFn } from "./registry.js"; -import { registry } from "./registry.js"; - -// --- Helpers --- - -/** - * Build a compound assignment handler that pops a value from the stack, - * walks the scope chain, and applies the given compound operator. - * - * @param assignOp - The operator prefix (e.g. `'+'`, `'-'`, `'**'`) - * @returns Handler function producing the case body - */ -function compoundScopedAssign(assignOp: AOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk([ - exprStmt(assign(ctx.sv(), id(ctx.local("value")), assignOp)), - exprStmt(ctx.push(ctx.sv())), - ]), - ]; -} - -/** - * Build a logical assignment handler that pops a value from the stack, - * walks the scope chain, and applies the given logical operator as a - * full assignment (not compound, since `&&=` is not a simple operator). - * - * @param logicalOp - The logical operator (`'&&'` or `'||'`) - * @returns Handler function producing the case body - */ -function logicalScopedAssign(logicalOp: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk([ - exprStmt( - assign( - ctx.sv(), - bin(logicalOp, ctx.sv(), id(ctx.local("value"))) - ) - ), - exprStmt(ctx.push(ctx.sv())), - ]), - ]; -} - -// --- Increment / decrement --- - -/** INC_SCOPED: pre-increment a scoped variable, push new value. */ -function INC_SCOPED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk([ - exprStmt(assign(ctx.sv(), bin(BOp.Add, ctx.sv(), lit(1)))), - exprStmt(ctx.push(ctx.sv())), - ]), - ]; -} - -/** DEC_SCOPED: pre-decrement a scoped variable, push new value. */ -function DEC_SCOPED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk([ - exprStmt(assign(ctx.sv(), bin(BOp.Sub, ctx.sv(), lit(1)))), - exprStmt(ctx.push(ctx.sv())), - ]), - ]; -} - -/** POST_INC_SCOPED: post-increment a scoped variable, push old value. */ -function POST_INC_SCOPED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk([ - varDecl(ctx.local("oldVal"), ctx.sv()), - exprStmt( - assign(ctx.sv(), bin(BOp.Add, id(ctx.local("oldVal")), lit(1))) - ), - exprStmt(ctx.push(id(ctx.local("oldVal")))), - ]), - ]; -} - -/** POST_DEC_SCOPED: post-decrement a scoped variable, push old value. */ -function POST_DEC_SCOPED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk([ - varDecl(ctx.local("oldVal"), ctx.sv()), - exprStmt( - assign(ctx.sv(), bin(BOp.Sub, id(ctx.local("oldVal")), lit(1))) - ), - exprStmt(ctx.push(id(ctx.local("oldVal")))), - ]), - ]; -} - -// --- Nullish assign (special: conditional assignment) --- - -/** NULLISH_ASSIGN_SCOPED: `??=` — only assign if current value is null/undefined. */ -function NULLISH_ASSIGN_SCOPED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk([ - ifStmt(bin(BOp.Eq, ctx.sv(), lit(null)), [ - exprStmt(assign(ctx.sv(), id(ctx.local("value")))), - ]), - exprStmt(ctx.push(ctx.sv())), - ]), - ]; -} - -// --- ASSIGN_OP (no-op marker) --- - -/** ASSIGN_OP: no-op marker opcode, just break. */ -function ASSIGN_OP(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -// --- Registration --- - -registry.set(Op.INC_SCOPED, INC_SCOPED); -registry.set(Op.DEC_SCOPED, DEC_SCOPED); -registry.set(Op.POST_INC_SCOPED, POST_INC_SCOPED); -registry.set(Op.POST_DEC_SCOPED, POST_DEC_SCOPED); -registry.set(Op.ADD_ASSIGN_SCOPED, compoundScopedAssign(AOp.Add)); -registry.set(Op.SUB_ASSIGN_SCOPED, compoundScopedAssign(AOp.Sub)); -registry.set(Op.MUL_ASSIGN_SCOPED, compoundScopedAssign(AOp.Mul)); -registry.set(Op.DIV_ASSIGN_SCOPED, compoundScopedAssign(AOp.Div)); -registry.set(Op.MOD_ASSIGN_SCOPED, compoundScopedAssign(AOp.Mod)); -registry.set(Op.POW_ASSIGN_SCOPED, compoundScopedAssign(AOp.Pow)); -registry.set(Op.BIT_AND_ASSIGN_SCOPED, compoundScopedAssign(AOp.BitAnd)); -registry.set(Op.BIT_OR_ASSIGN_SCOPED, compoundScopedAssign(AOp.BitOr)); -registry.set(Op.BIT_XOR_ASSIGN_SCOPED, compoundScopedAssign(AOp.BitXor)); -registry.set(Op.SHL_ASSIGN_SCOPED, compoundScopedAssign(AOp.Shl)); -registry.set(Op.SHR_ASSIGN_SCOPED, compoundScopedAssign(AOp.Shr)); -registry.set(Op.USHR_ASSIGN_SCOPED, compoundScopedAssign(AOp.Ushr)); -registry.set(Op.AND_ASSIGN_SCOPED, logicalScopedAssign(BOp.And)); -registry.set(Op.OR_ASSIGN_SCOPED, logicalScopedAssign(BOp.Or)); -registry.set(Op.NULLISH_ASSIGN_SCOPED, NULLISH_ASSIGN_SCOPED); -registry.set(Op.ASSIGN_OP, ASSIGN_OP); diff --git a/packages/ruam/src/ruamvm/handlers/control-flow.ts b/packages/ruam/src/ruamvm/handlers/control-flow.ts deleted file mode 100644 index 59ca49a..0000000 --- a/packages/ruam/src/ruamvm/handlers/control-flow.ts +++ /dev/null @@ -1,291 +0,0 @@ -/** - * Control flow opcode handlers in AST node form. - * - * Covers 15 opcodes: - * - Jumps: JMP, JMP_TRUE, JMP_FALSE, JMP_NULLISH, JMP_UNDEFINED, - * JMP_TRUE_KEEP, JMP_FALSE_KEEP, JMP_NULLISH_KEEP - * - Returns: RETURN, RETURN_VOID - * - Throws: THROW, RETHROW - * - Misc: NOP, TABLE_SWITCH, LOOKUP_SWITCH - * - * All handlers use pure AST nodes. - * - * @module ruamvm/handlers/control-flow - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - bin, - un, - exprStmt, - assign, - ifStmt, - varDecl, - breakStmt, - returnStmt, - throwStmt, - whileStmt, - call, - member, - BOp, - UOp, -} from "../nodes.js"; -import { registry, type HandlerCtx } from "./registry.js"; -import { debugTrace } from "./helpers.js"; - -// --- Shorthand helpers --- - -/** `IP=O*2;` — standard jump target assignment */ -function ipAssign(ctx: HandlerCtx): JsNode { - return exprStmt(assign(id(ctx.IP), bin(BOp.Mul, id(ctx.O), lit(2)))); -} - -// --- Jump handlers --- - -/** `IP=O*2;break;` */ -function JMP(ctx: HandlerCtx): JsNode[] { - return [ipAssign(ctx), breakStmt()]; -} - -/** `if(S.pop())IP=O*2;break;` */ -function JMP_TRUE(ctx: HandlerCtx): JsNode[] { - return [ifStmt(ctx.pop(), [ipAssign(ctx)]), breakStmt()]; -} - -/** `if(!S.pop())IP=O*2;break;` */ -function JMP_FALSE(ctx: HandlerCtx): JsNode[] { - return [ifStmt(un(UOp.Not, ctx.pop()), [ipAssign(ctx)]), breakStmt()]; -} - -/** `{var v=S.pop();if(v===null||v===void 0)IP=O*2;break;}` */ -function JMP_NULLISH(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - ifStmt( - bin( - BOp.Or, - bin(BOp.Seq, id(ctx.local("value")), lit(null)), - bin(BOp.Seq, id(ctx.local("value")), un(UOp.Void, lit(0))) - ), - [ipAssign(ctx)] - ), - breakStmt(), - ]; -} - -/** `{var v=S.pop();if(v===void 0)IP=O*2;break;}` */ -function JMP_UNDEFINED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - ifStmt(bin(BOp.Seq, id(ctx.local("value")), un(UOp.Void, lit(0))), [ - ipAssign(ctx), - ]), - breakStmt(), - ]; -} - -/** `if(S[S.length-1])IP=O*2;break;` — keeps value on stack */ -function JMP_TRUE_KEEP(ctx: HandlerCtx): JsNode[] { - return [ifStmt(ctx.peek(), [ipAssign(ctx)]), breakStmt()]; -} - -/** `if(!S[S.length-1])IP=O*2;break;` — keeps value on stack */ -function JMP_FALSE_KEEP(ctx: HandlerCtx): JsNode[] { - return [ifStmt(un(UOp.Not, ctx.peek()), [ipAssign(ctx)]), breakStmt()]; -} - -/** `{var v=S[S.length-1];if(v===null||v===void 0)IP=O*2;break;}` — keeps value on stack */ -function JMP_NULLISH_KEEP(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.peek()), - ifStmt( - bin( - BOp.Or, - bin(BOp.Seq, id(ctx.local("value")), lit(null)), - bin(BOp.Seq, id(ctx.local("value")), un(UOp.Void, lit(0))) - ), - [ipAssign(ctx)] - ), - breakStmt(), - ]; -} - -// --- Return / throw handlers --- - -/** - * Unwind the exception-handler stack toward the nearest enclosing `finally` - * for a deferred completion (return). - * - * Pops handler frames (restoring the stack depth saved on each) until either a - * frame carrying a `finally` is found — in which case `IP` is set to that - * finally and the deferral flag is cleared so control transfers there — or the - * stack is exhausted. Catch-only frames in between are popped without running - * (a `catch` does not execute during an abrupt return). This makes - * `return`-through-`finally` work for arbitrarily nested `try`/`finally`, not - * just a single level. - * - * `completionValue` is the value assigned to `CV` when a finally is found; the - * finally's `END_FINALLY` later resumes the deferral. The loop is driven purely - * by its condition (no inner `break`/`continue`/`return`) so it survives the - * handler-body break/return transforms applied by every dispatch style. - */ -function unwindToFinally(ctx: HandlerCtx, completionValue: JsNode): JsNode { - const flag = ctx.local("retDefer"); - return whileStmt( - bin( - BOp.And, - id(flag), - bin( - BOp.And, - id(ctx.EX), - bin(BOp.Gt, member(id(ctx.EX), "length"), lit(0)) - ) - ), - [ - varDecl(ctx.t("_h"), call(member(id(ctx.EX), "pop"), [])), - exprStmt( - assign( - member(id(ctx.S), "length"), - member(id(ctx.t("_h")), ctx.t("_sp")) - ) - ), - ifStmt( - bin(BOp.Gte, member(id(ctx.t("_h")), ctx.t("_fi")), lit(0)), - [ - exprStmt(assign(id(ctx.CT), lit(1))), - exprStmt(assign(id(ctx.CV), completionValue)), - exprStmt( - assign( - id(ctx.IP), - bin( - BOp.Mul, - member(id(ctx.t("_h")), ctx.t("_fi")), - lit(2) - ) - ) - ), - exprStmt(assign(id(flag), lit(0))), - ] - ), - ] - ); -} - -/** - * RETURN: pop return value, run any enclosing `finally` blocks first - * (deferring via completion tracking), then return. - * - * ``` - * var _rv=S.pop(); - * - * var _df=1; - * while(_df&&EX&&EX.length>0){var _h=EX.pop();S.length=_h._sp;if(_h._fi>=0){CT=1;CV=_rv;IP=_h._fi*2;_df=0;}} - * if(_df)return _rv; - * break; - * ``` - */ -function RETURN(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.t("_rv"), ctx.pop()), - ...debugTrace(ctx, "RETURN", lit("value="), id(ctx.t("_rv"))), - varDecl(ctx.local("retDefer"), lit(1)), - unwindToFinally(ctx, id(ctx.t("_rv"))), - ifStmt(id(ctx.local("retDefer")), [returnStmt(id(ctx.t("_rv")))]), - breakStmt(), - ]; -} - -/** - * RETURN_VOID: return undefined, running any enclosing `finally` blocks first. - * - * ``` - * - * var _df=1; - * while(_df&&EX&&EX.length>0){var _h=EX.pop();S.length=_h._sp;if(_h._fi>=0){CT=1;CV=void 0;IP=_h._fi*2;_df=0;}} - * if(_df)return void 0; - * break; - * ``` - */ -function RETURN_VOID(ctx: HandlerCtx): JsNode[] { - return [ - ...debugTrace(ctx, "RETURN_VOID"), - varDecl(ctx.local("retDefer"), lit(1)), - unwindToFinally(ctx, un(UOp.Void, lit(0))), - ifStmt(id(ctx.local("retDefer")), [ - returnStmt(un(UOp.Void, lit(0))), - ]), - breakStmt(), - ]; -} - -/** - * THROW: pop value and throw it. - * - * ``` - * var _te=S[P--]; - * - * throw _te; - * ``` - */ -function THROW(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.t("_te"), ctx.pop()), - ...debugTrace(ctx, "THROW", lit("value="), id(ctx.t("_te"))), - throwStmt(id(ctx.t("_te"))), - ]; -} - -/** - * RETHROW: re-throw pending exception if one exists. - * - * ``` - * if(HPE){var ex=PE;PE=null;HPE=false;throw ex;}break; - * ``` - */ -function RETHROW(ctx: HandlerCtx): JsNode[] { - return [ - ifStmt(id(ctx.HPE), [ - varDecl(ctx.local("exception"), id(ctx.PE)), - exprStmt(assign(id(ctx.PE), lit(null))), - exprStmt(assign(id(ctx.HPE), lit(false))), - throwStmt(id(ctx.local("exception"))), - ]), - breakStmt(), - ]; -} - -// --- Misc handlers --- - -/** NOP: no-op, just break. */ -function NOP(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -/** - * TABLE_SWITCH / LOOKUP_SWITCH: at runtime these are already resolved to - * a JMP target. The handler just sets IP=O*2. - */ -function SWITCH_JMP(ctx: HandlerCtx): JsNode[] { - return [ipAssign(ctx), breakStmt()]; -} - -// --- Registration --- - -registry.set(Op.JMP, JMP); -registry.set(Op.JMP_TRUE, JMP_TRUE); -registry.set(Op.JMP_FALSE, JMP_FALSE); -registry.set(Op.JMP_NULLISH, JMP_NULLISH); -registry.set(Op.JMP_UNDEFINED, JMP_UNDEFINED); -registry.set(Op.JMP_TRUE_KEEP, JMP_TRUE_KEEP); -registry.set(Op.JMP_FALSE_KEEP, JMP_FALSE_KEEP); -registry.set(Op.JMP_NULLISH_KEEP, JMP_NULLISH_KEEP); -registry.set(Op.RETURN, RETURN); -registry.set(Op.RETURN_VOID, RETURN_VOID); -registry.set(Op.THROW, THROW); -registry.set(Op.RETHROW, RETHROW); -registry.set(Op.NOP, NOP); -registry.set(Op.TABLE_SWITCH, SWITCH_JMP); -registry.set(Op.LOOKUP_SWITCH, SWITCH_JMP); diff --git a/packages/ruam/src/ruamvm/handlers/destructuring.ts b/packages/ruam/src/ruamvm/handlers/destructuring.ts deleted file mode 100644 index aabbc63..0000000 --- a/packages/ruam/src/ruamvm/handlers/destructuring.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Destructuring opcode handlers in AST node form. - * - * Covers 6 opcodes: - * - DESTRUCTURE_BIND, DESTRUCTURE_DEFAULT - * - DESTRUCTURE_REST_ARRAY, DESTRUCTURE_REST_OBJECT - * - ARRAY_PATTERN_INIT, OBJECT_PATTERN_GET - * - * DESTRUCTURE_BIND is a simple no-op (break only). All other handlers use - * pure AST nodes for structured code generation. - * - * @module ruamvm/handlers/destructuring - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - breakStmt, - varDecl, - id, - lit, - bin, - un, - assign, - call, - member, - index, - exprStmt, - ifStmt, - whileStmt, - forStmt, - obj, - arr, - update, - BOp, - UOp, - UpOp, -} from "../nodes.js"; -import { registry, type HandlerCtx } from "./registry.js"; - -// --- Simple handler --- - -/** DESTRUCTURE_BIND: no-op marker, just break. */ -function DESTRUCTURE_BIND(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -// --- Converted handlers (pure AST) --- - -/** - * DESTRUCTURE_DEFAULT: apply default value if top-of-stack is undefined. - * - * ``` - * var v=S[S.length-1];if(v===void 0){S.pop();var def=C[O];S.push(def);}break; - * ``` - */ -function DESTRUCTURE_DEFAULT(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.peek()), - ifStmt(bin(BOp.Seq, id(ctx.local("value")), un(UOp.Void, lit(0))), [ - exprStmt(ctx.pop()), - varDecl(ctx.local("defaultVal"), index(id(ctx.C), id(ctx.O))), - exprStmt(ctx.push(id(ctx.local("defaultVal")))), - ]), - breakStmt(), - ]; -} - -/** - * DESTRUCTURE_REST_ARRAY: collect remaining iterator values into a rest array. - * - * ``` - * var iterObj=S[P--];var rest=[]; - * while(!iterObj._done){rest.push(iterObj._value); - * var nxt=iterObj._iter.next();iterObj._done=!!nxt.done;iterObj._value=nxt.value;} - * S[++P]=rest;break; - * ``` - */ -function DESTRUCTURE_REST_ARRAY(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.pop()), - varDecl(ctx.local("restVal"), arr()), - whileStmt( - un(UOp.Not, member(id(ctx.local("iterObj")), ctx.t("_done"))), - [ - exprStmt( - call(member(id(ctx.local("restVal")), "push"), [ - member(id(ctx.local("iterObj")), ctx.t("_value")), - ]) - ), - varDecl( - ctx.local("next"), - call( - member( - member(id(ctx.local("iterObj")), ctx.t("_iter")), - "next" - ), - [] - ) - ), - exprStmt( - assign( - member(id(ctx.local("iterObj")), ctx.t("_done")), - un( - UOp.Not, - un(UOp.Not, member(id(ctx.local("next")), "done")) - ) - ) - ), - exprStmt( - assign( - member(id(ctx.local("iterObj")), ctx.t("_value")), - member(id(ctx.local("next")), "value") - ) - ), - ] - ), - exprStmt(ctx.push(id(ctx.local("restVal")))), - breakStmt(), - ]; -} - -/** - * DESTRUCTURE_REST_OBJECT: collect remaining object keys into a rest object. - * - * ``` - * var excludeKeys=S[P--];var src=S[P--];var rest={}; - * var keys=Object.keys(src); - * for(var ki=0;ki>16)&0xFFFF;var _fi=O&0xFFFF; - * if(_ci===0xFFFF)_ci=-1;if(_fi===0xFFFF)_fi=-1; - * if(!EX)EX=[];EX.push({_ci:_ci,_fi:_fi,_sp:S.length});break; - * ``` - */ -function TRY_PUSH(ctx: HandlerCtx): JsNode[] { - return [ - varDecl( - ctx.t("_ci"), - bin(BOp.BitAnd, bin(BOp.Shr, id(ctx.O), lit(16)), lit(0xffff)) - ), - varDecl(ctx.t("_fi"), bin(BOp.BitAnd, id(ctx.O), lit(0xffff))), - ifStmt(bin(BOp.Seq, id(ctx.t("_ci")), lit(0xffff)), [ - exprStmt(assign(id(ctx.t("_ci")), un(UOp.Neg, lit(1)))), - ]), - ifStmt(bin(BOp.Seq, id(ctx.t("_fi")), lit(0xffff)), [ - exprStmt(assign(id(ctx.t("_fi")), un(UOp.Neg, lit(1)))), - ]), - ifStmt(un(UOp.Not, id(ctx.EX)), [exprStmt(assign(id(ctx.EX), arr()))]), - exprStmt( - call(member(id(ctx.EX), "push"), [ - obj( - [ctx.t("_ci"), id(ctx.t("_ci"))], - [ctx.t("_fi"), id(ctx.t("_fi"))], - [ctx.t("_sp"), member(id(ctx.S), "length")] - ), - ]) - ), - breakStmt(), - ]; -} - -/** TRY_POP: pop the top exception handler frame. */ -function TRY_POP(ctx: HandlerCtx): JsNode[] { - return [exprStmt(call(member(id(ctx.EX), "pop"), [])), breakStmt()]; -} - -/** - * CATCH_BIND: bind caught exception to a variable. - * - * If operand >= 0, uses constant pool for name and stores in scope vars - * or register. Otherwise pushes the error onto the stack. - * - * ``` - * var err=S[P--];if(O>=0){var cname=C[O];if(typeof cname==='string'){SC.sV[cname]=err;}else{R[O]=err;}}else{W(err);}break; - * ``` - */ -function CATCH_BIND(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("error"), ctx.pop()), - ifStmt( - bin(BOp.Gte, id(ctx.O), lit(0)), - [ - varDecl(ctx.local("catchName"), index(id(ctx.C), id(ctx.O))), - ifStmt( - bin( - BOp.Seq, - un(UOp.Typeof, id(ctx.local("catchName"))), - lit("string") - ), - [ - exprStmt( - assign( - index(id(ctx.SC), id(ctx.local("catchName"))), - id(ctx.local("error")) - ) - ), - ], - [ - exprStmt( - assign( - index(id(ctx.R), id(ctx.O)), - id(ctx.local("error")) - ) - ), - ] - ), - ], - [exprStmt(ctx.push(id(ctx.local("error"))))] - ), - breakStmt(), - ]; -} - -/** - * CATCH_BIND_PATTERN: bind caught exception for destructuring. - * - * Simply pops the exception from the internal state and pushes it onto - * the stack for subsequent destructuring opcodes. - */ -function CATCH_BIND_PATTERN(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("error"), ctx.pop()), - exprStmt(ctx.push(id(ctx.local("error")))), - breakStmt(), - ]; -} - -// --- Finally handlers --- - -/** FINALLY_MARK: no-op marker for finally block entry. */ -function FINALLY_MARK(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -/** - * END_FINALLY: complete a finally block. - * - * Re-throws a pending exception if one exists. Otherwise, if a `return` was - * deferred to run this finally (CT===1), the return must continue through any - * *outer* enclosing `finally` blocks before completing — so the handler stack - * is unwound to the next finally (preserving the deferred value in CV) and - * control transfers there; only when no further finally remains does the - * function actually return. This makes `return`-through-`finally` correct for - * arbitrarily nested `try`/`finally`, not just a single level. - * - * The unwind loop is driven purely by its condition (no inner - * `break`/`continue`/`return`) so it survives the handler-body break/return - * transforms applied by every dispatch style. - * - * ``` - * if(HPE){var ex=PE;PE=null;HPE=false;throw ex;} - * if(CT===1){ - * var _df=1; - * while(_df&&EX&&EX.length>0){var _h=EX.pop();S.length=_h._sp;if(_h._fi>=0){IP=_h._fi*2;_df=0;}} - * if(_df){var _rv2=CV;CT=0;CV=void 0;return _rv2;} - * } - * break; - * ``` - */ -function END_FINALLY(ctx: HandlerCtx): JsNode[] { - const flag = ctx.local("retDefer"); - return [ - ifStmt(id(ctx.HPE), [ - varDecl(ctx.local("error"), id(ctx.PE)), - exprStmt(assign(id(ctx.PE), lit(null))), - exprStmt(assign(id(ctx.HPE), lit(false))), - throwStmt(id(ctx.local("error"))), - ]), - ifStmt(bin(BOp.Seq, id(ctx.CT), lit(1)), [ - varDecl(flag, lit(1)), - whileStmt( - bin( - BOp.And, - id(flag), - bin( - BOp.And, - id(ctx.EX), - bin(BOp.Gt, member(id(ctx.EX), "length"), lit(0)) - ) - ), - [ - varDecl(ctx.t("_h"), call(member(id(ctx.EX), "pop"), [])), - exprStmt( - assign( - member(id(ctx.S), "length"), - member(id(ctx.t("_h")), ctx.t("_sp")) - ) - ), - ifStmt( - bin( - BOp.Gte, - member(id(ctx.t("_h")), ctx.t("_fi")), - lit(0) - ), - [ - exprStmt( - assign( - id(ctx.IP), - bin( - BOp.Mul, - member(id(ctx.t("_h")), ctx.t("_fi")), - lit(2) - ) - ) - ), - exprStmt(assign(id(flag), lit(0))), - ] - ), - ] - ), - ifStmt(id(flag), [ - varDecl(ctx.t("_rv2"), id(ctx.CV)), - exprStmt(assign(id(ctx.CT), lit(0))), - exprStmt(assign(id(ctx.CV), un(UOp.Void, lit(0)))), - returnStmt(id(ctx.t("_rv2"))), - ]), - ]), - breakStmt(), - ]; -} - -// --- Guard handlers --- - -/** - * THROW_IF_NOT_OBJECT: throw TypeError if top-of-stack is not an object. - * - * Peeks at the stack top without consuming it. - */ -function THROW_IF_NOT_OBJECT(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.peek()), - ifStmt( - bin( - BOp.Or, - bin( - BOp.Sneq, - un(UOp.Typeof, id(ctx.local("value"))), - lit("object") - ), - bin(BOp.Seq, id(ctx.local("value")), lit(null)) - ), - [ - throwStmt( - newExpr(id("TypeError"), [lit("Value is not an object")]) - ), - ] - ), - breakStmt(), - ]; -} - -// --- Error constructor handlers --- - -/** - * THROW_REF_ERROR: throw a ReferenceError with message from constant pool. - */ -function THROW_REF_ERROR(ctx: HandlerCtx): JsNode[] { - return [ - throwStmt( - newExpr(id("ReferenceError"), [ - bin(BOp.Or, index(id(ctx.C), id(ctx.O)), lit("not defined")), - ]) - ), - ]; -} - -/** - * THROW_TYPE_ERROR: throw a TypeError with message from constant pool. - */ -function THROW_TYPE_ERROR(ctx: HandlerCtx): JsNode[] { - return [ - throwStmt( - newExpr(id("TypeError"), [ - bin(BOp.Or, index(id(ctx.C), id(ctx.O)), lit("type error")), - ]) - ), - ]; -} - -/** - * THROW_SYNTAX_ERROR: throw a SyntaxError with message from constant pool. - */ -function THROW_SYNTAX_ERROR(ctx: HandlerCtx): JsNode[] { - return [ - throwStmt( - newExpr(id("SyntaxError"), [ - bin(BOp.Or, index(id(ctx.C), id(ctx.O)), lit("syntax error")), - ]) - ), - ]; -} - -// --- Registration --- - -registry.set(Op.TRY_PUSH, TRY_PUSH); -registry.set(Op.TRY_POP, TRY_POP); -registry.set(Op.CATCH_BIND, CATCH_BIND); -registry.set(Op.CATCH_BIND_PATTERN, CATCH_BIND_PATTERN); -registry.set(Op.FINALLY_MARK, FINALLY_MARK); -registry.set(Op.END_FINALLY, END_FINALLY); -registry.set(Op.THROW_IF_NOT_OBJECT, THROW_IF_NOT_OBJECT); -registry.set(Op.THROW_REF_ERROR, THROW_REF_ERROR); -registry.set(Op.THROW_TYPE_ERROR, THROW_TYPE_ERROR); -registry.set(Op.THROW_SYNTAX_ERROR, THROW_SYNTAX_ERROR); diff --git a/packages/ruam/src/ruamvm/handlers/functions.ts b/packages/ruam/src/ruamvm/handlers/functions.ts deleted file mode 100644 index 4eac7ec..0000000 --- a/packages/ruam/src/ruamvm/handlers/functions.ts +++ /dev/null @@ -1,553 +0,0 @@ -/** - * Function creation and closure opcode handlers in AST node form. - * - * Covers 12 opcodes: - * - Closures: NEW_CLOSURE, NEW_FUNCTION, NEW_ARROW, NEW_ASYNC, - * NEW_GENERATOR, NEW_ASYNC_GENERATOR - * - Metadata: SET_FUNC_NAME, SET_FUNC_LENGTH - * - Stubs: BIND_THIS, MAKE_METHOD - * - Closure vars: PUSH_CLOSURE_VAR, STORE_CLOSURE_VAR - * - * All handlers use pure AST nodes — no raw() escape hatch. - * - * @module ruamvm/handlers/functions - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - bin, - un, - assign, - call, - member, - index, - varDecl, - exprStmt, - ifStmt, - fnExpr, - returnStmt, - tryCatch, - breakStmt, - obj, - BOp, - UOp, -} from "../nodes.js"; -import type { HandlerCtx } from "./registry.js"; -import { registry } from "./registry.js"; -import { - buildArrowClosureIIFE, - buildRegularClosureIIFE, - buildThisBoxing, - debugTrace, -} from "./helpers.js"; - -// --- Debug closure IIFE builders --- - -/** - * Build a debug-mode arrow closure IIFE. - * - * Like buildArrowClosureIIFE but captures `uid` and emits debug trace calls - * inside the wrapper function body. - * - * @param ctx - Handler context with debug function name - * @returns JsNode — IIFE call expression - */ -function buildDebugArrowClosureIIFE(ctx: HandlerCtx): JsNode { - const innerBody = (isAsync: boolean): JsNode[] => [ - exprStmt( - call(id(ctx.dbg), [ - lit("CALL_CLOSURE"), - bin( - BOp.Add, - lit(isAsync ? "async arrow uid=" : "arrow uid="), - id("uid") - ), - bin(BOp.Add, lit("args="), member(id(ctx.t("_a")), "length")), - ]) - ), - returnStmt( - call(id(isAsync ? ctx.execAsync : ctx.exec), [ - id("u"), - id(ctx.t("_a")), - id("cs"), - id("ct"), - ]) - ), - ]; - return call( - fnExpr( - undefined, - ["u", "uid", "cs", "ct"], - [ - ifStmt(member(id("u"), "s"), [ - returnStmt( - fnExpr( - undefined, - ["..." + ctx.t("_a")], - innerBody(true), - { - async: true, - } - ) - ), - ]), - returnStmt( - fnExpr(undefined, ["..." + ctx.t("_a")], innerBody(false)) - ), - ] - ), - [id(ctx.t("_cu")), id(ctx.t("_cuid")), id(ctx.SC), id(ctx.TV)] - ); -} - -/** - * Build a debug-mode regular (non-arrow) closure IIFE. - * - * Like buildRegularClosureIIFE but captures `uid` and emits debug trace calls - * inside the wrapper function body. - * - * @param ctx - Handler context with debug function name - * @returns JsNode — IIFE call expression - */ -function buildDebugRegularClosureIIFE(ctx: HandlerCtx): JsNode { - const innerBody = (isAsync: boolean): JsNode[] => [ - exprStmt( - call(id(ctx.dbg), [ - lit("CALL_CLOSURE"), - bin(BOp.Add, lit(isAsync ? "async uid=" : "uid="), id("uid")), - bin(BOp.Add, lit("args="), member(id(ctx.t("_a")), "length")), - ]) - ), - ...buildThisBoxing(ctx), - returnStmt( - call(id(isAsync ? ctx.execAsync : ctx.exec), [ - id("u"), - id(ctx.t("_a")), - id("cs"), - id(ctx.t("_tv")), - un(UOp.Void, lit(0)), - member(id("fn"), ctx.t("_ho")), - ]) - ), - ]; - return call( - fnExpr( - undefined, - ["u", "uid", "cs"], - [ - ifStmt(member(id("u"), "s"), [ - varDecl( - "fn", - fnExpr( - undefined, - ["..." + ctx.t("_a")], - innerBody(true), - { - async: true, - } - ) - ), - returnStmt(id("fn")), - ]), - varDecl( - "fn", - fnExpr(undefined, ["..." + ctx.t("_a")], innerBody(false)) - ), - returnStmt(id("fn")), - ] - ), - [id(ctx.t("_cu")), id(ctx.t("_cuid")), id(ctx.SC)] - ); -} - -/** - * Build a debug-mode function (non-arrow, no arrow branch) closure IIFE. - * - * Like buildDebugRegularClosureIIFE but uses "CALL_FUNCTION" trace label - * and captures `_fuid` as the unit ID. - * - * @param ctx - Handler context with debug function name - * @returns JsNode — IIFE call expression - */ -function buildDebugFunctionClosureIIFE(ctx: HandlerCtx): JsNode { - const innerBody = (isAsync: boolean): JsNode[] => [ - exprStmt( - call(id(ctx.dbg), [ - lit("CALL_FUNCTION"), - bin(BOp.Add, lit(isAsync ? "async uid=" : "uid="), id("uid")), - bin(BOp.Add, lit("args="), member(id(ctx.t("_a")), "length")), - ]) - ), - ...buildThisBoxing(ctx), - returnStmt( - call(id(isAsync ? ctx.execAsync : ctx.exec), [ - id("u"), - id(ctx.t("_a")), - id("cs"), - id(ctx.t("_tv")), - un(UOp.Void, lit(0)), - member(id("fn"), ctx.t("_ho")), - ]) - ), - ]; - return call( - fnExpr( - undefined, - ["u", "uid", "cs"], - [ - ifStmt(member(id("u"), "s"), [ - varDecl( - "fn", - fnExpr( - undefined, - ["..." + ctx.t("_a")], - innerBody(true), - { - async: true, - } - ) - ), - returnStmt(id("fn")), - ]), - varDecl( - "fn", - fnExpr(undefined, ["..." + ctx.t("_a")], innerBody(false)) - ), - returnStmt(id("fn")), - ] - ), - [id(ctx.t("_fu")), id(ctx.t("_fuid")), id(ctx.SC)] - ); -} - -// --- Primary closure handlers --- - -/** - * NEW_CLOSURE: create a closure wrapper for a compiled unit. - * - * Branches on arrow (captures outer this/scope) vs regular (this-boxing), - * sync vs async, with debug tracing when enabled. Home object (`fn._ho`) - * is forwarded for super call resolution. - */ -function NEW_CLOSURE(ctx: HandlerCtx): JsNode[] { - if (ctx.debug) { - return [ - varDecl(ctx.t("_cuid"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.t("_cu"), call(id(ctx.load), [id(ctx.t("_cuid"))])), - exprStmt( - assign( - member(id(ctx.t("_cu")), ctx.t("_dbgId")), - id(ctx.t("_cuid")) - ) - ), - exprStmt( - call(id(ctx.dbg), [ - lit("NEW_CLOSURE"), - bin(BOp.Add, lit("uid="), id(ctx.t("_cuid"))), - bin( - BOp.Add, - lit("async="), - un(UOp.Not, un(UOp.Not, member(id(ctx.t("_cu")), "s"))) - ), - bin(BOp.Add, lit("params="), member(id(ctx.t("_cu")), "p")), - bin( - BOp.Add, - lit("arrow="), - un(UOp.Not, un(UOp.Not, member(id(ctx.t("_cu")), "a"))) - ), - ]) - ), - ifStmt( - member(id(ctx.t("_cu")), "a"), - [exprStmt(ctx.push(buildDebugArrowClosureIIFE(ctx)))], - [exprStmt(ctx.push(buildDebugRegularClosureIIFE(ctx)))] - ), - breakStmt(), - ]; - } - return [ - varDecl( - ctx.t("_cu"), - call(id(ctx.load), [index(id(ctx.C), id(ctx.O))]) - ), - ifStmt( - member(id(ctx.t("_cu")), "a"), - [exprStmt(ctx.push(buildArrowClosureIIFE(ctx)))], - [exprStmt(ctx.push(buildRegularClosureIIFE(ctx)))] - ), - breakStmt(), - ]; -} - -/** - * NEW_FUNCTION: create a function wrapper (no arrow variant). - * - * Simpler than NEW_CLOSURE — always non-arrow, so always includes - * this-boxing and home object forwarding. - */ -function NEW_FUNCTION(ctx: HandlerCtx): JsNode[] { - if (ctx.debug) { - return [ - varDecl(ctx.t("_fuid"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.t("_fu"), call(id(ctx.load), [id(ctx.t("_fuid"))])), - exprStmt( - assign( - member(id(ctx.t("_fu")), ctx.t("_dbgId")), - id(ctx.t("_fuid")) - ) - ), - exprStmt( - call(id(ctx.dbg), [ - lit("NEW_FUNCTION"), - bin(BOp.Add, lit("uid="), id(ctx.t("_fuid"))), - bin( - BOp.Add, - lit("async="), - un(UOp.Not, un(UOp.Not, member(id(ctx.t("_fu")), "s"))) - ), - bin(BOp.Add, lit("params="), member(id(ctx.t("_fu")), "p")), - ]) - ), - exprStmt(ctx.push(buildDebugFunctionClosureIIFE(ctx))), - breakStmt(), - ]; - } - return [ - varDecl( - ctx.t("_cu"), - call(id(ctx.load), [index(id(ctx.C), id(ctx.O))]) - ), - exprStmt(ctx.push(buildRegularClosureIIFE(ctx))), - breakStmt(), - ]; -} - -// --- Specialized function creation handlers --- - -/** - * NEW_ARROW: create an arrow function (captures outer this + scope). - * - * No this-boxing — arrow functions inherit `this` from enclosing context. - */ -function NEW_ARROW(ctx: HandlerCtx): JsNode[] { - return [ - varDecl( - ctx.t("_cu"), - call(id(ctx.load), [index(id(ctx.C), id(ctx.O))]) - ), - exprStmt(ctx.push(buildArrowClosureIIFE(ctx))), - breakStmt(), - ]; -} - -/** - * NEW_ASYNC: create an async function with this-boxing. - * - * Always async, always non-arrow — uses execAsync with this-boxing. - */ -function NEW_ASYNC(ctx: HandlerCtx): JsNode[] { - const asyncBody: JsNode[] = [ - ...buildThisBoxing(ctx), - returnStmt( - call(id(ctx.execAsync), [ - id("u"), - id(ctx.t("_a")), - id("cs"), - id(ctx.t("_tv")), - ]) - ), - ]; - return [ - varDecl( - ctx.t("_cu"), - call(id(ctx.load), [index(id(ctx.C), id(ctx.O))]) - ), - exprStmt( - ctx.push( - call( - fnExpr( - undefined, - ["u", "cs"], - [ - returnStmt( - fnExpr( - undefined, - ["..." + ctx.t("_a")], - asyncBody, - { - async: true, - } - ) - ), - ] - ), - [id(ctx.t("_cu")), id(ctx.SC)] - ) - ) - ), - breakStmt(), - ]; -} - -/** - * NEW_GENERATOR / NEW_ASYNC_GENERATOR: create a generator function. - * - * Both are handled identically — generators are stub-executed (run to completion). - * Always non-arrow, always sync, uses exec with this-boxing. - */ -function NEW_GENERATOR_HANDLER(ctx: HandlerCtx): JsNode[] { - const fnBody: JsNode[] = [ - ...buildThisBoxing(ctx), - returnStmt( - call(id(ctx.exec), [ - id("u"), - id(ctx.t("_a")), - id("cs"), - id(ctx.t("_tv")), - ]) - ), - ]; - return [ - varDecl( - ctx.t("_cu"), - call(id(ctx.load), [index(id(ctx.C), id(ctx.O))]) - ), - exprStmt( - ctx.push( - call( - fnExpr( - undefined, - ["u", "cs"], - [ - returnStmt( - fnExpr(undefined, ["..." + ctx.t("_a")], fnBody) - ), - ] - ), - [id(ctx.t("_cu")), id(ctx.SC)] - ) - ) - ), - breakStmt(), - ]; -} - -// --- Metadata handlers --- - -/** - * SET_FUNC_NAME: set the `name` property on the function at stack top. - * - * Uses Object.defineProperty for configurable-only (non-writable, non-enumerable). - */ -function SET_FUNC_NAME(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.peek()), - tryCatch( - [ - exprStmt( - call(member(id("Object"), "defineProperty"), [ - id(ctx.local("func")), - lit("name"), - obj( - ["value", index(id(ctx.C), id(ctx.O))], - ["configurable", lit(true)] - ), - ]) - ), - ], - ctx.local("catchErr"), - [] - ), - breakStmt(), - ]; -} - -/** - * SET_FUNC_LENGTH: set the `length` property on the function at stack top. - */ -function SET_FUNC_LENGTH(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("func"), ctx.peek()), - tryCatch( - [ - exprStmt( - call(member(id("Object"), "defineProperty"), [ - id(ctx.local("func")), - lit("length"), - obj(["value", id(ctx.O)], ["configurable", lit(true)]), - ]) - ), - ], - ctx.local("catchErr"), - [] - ), - breakStmt(), - ]; -} - -// --- Stub handlers --- - -/** BIND_THIS / MAKE_METHOD: no-op stubs. */ -function BIND_STUB(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -// --- Closure variable handlers --- - -/** - * PUSH_CLOSURE_VAR: walk scope chain to find a captured variable, push its value. - * - * Uses ctx.scopeWalk() for structured scope chain traversal. - */ -function PUSH_CLOSURE_VAR(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk( - [exprStmt(ctx.push(ctx.sv(id(ctx.local("varName")))))], - id(ctx.local("varName")) - ), - ]; -} - -/** - * STORE_CLOSURE_VAR: walk scope chain to find a captured variable, store a value. - * - * Pops the value from the stack, then walks the scope chain to find the slot. - */ -function STORE_CLOSURE_VAR(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk( - [ - exprStmt( - assign( - ctx.sv(id(ctx.local("varName"))), - id(ctx.local("value")) - ) - ), - ], - id(ctx.local("varName")) - ), - ]; -} - -// --- Registration --- - -registry.set(Op.NEW_CLOSURE, NEW_CLOSURE); -registry.set(Op.NEW_FUNCTION, NEW_FUNCTION); -registry.set(Op.NEW_ARROW, NEW_ARROW); -registry.set(Op.NEW_ASYNC, NEW_ASYNC); -registry.set(Op.NEW_GENERATOR, NEW_GENERATOR_HANDLER); -registry.set(Op.NEW_ASYNC_GENERATOR, NEW_GENERATOR_HANDLER); -registry.set(Op.SET_FUNC_NAME, SET_FUNC_NAME); -registry.set(Op.SET_FUNC_LENGTH, SET_FUNC_LENGTH); -registry.set(Op.BIND_THIS, BIND_STUB); -registry.set(Op.MAKE_METHOD, BIND_STUB); -registry.set(Op.PUSH_CLOSURE_VAR, PUSH_CLOSURE_VAR); -registry.set(Op.STORE_CLOSURE_VAR, STORE_CLOSURE_VAR); diff --git a/packages/ruam/src/ruamvm/handlers/generators.ts b/packages/ruam/src/ruamvm/handlers/generators.ts deleted file mode 100644 index d30b1e4..0000000 --- a/packages/ruam/src/ruamvm/handlers/generators.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Generator and async opcode handlers in AST node form. - * - * Covers 12 opcodes: - * - Generator: YIELD, YIELD_DELEGATE, CREATE_GENERATOR, - * GENERATOR_RESUME, GENERATOR_RETURN, GENERATOR_THROW - * - Async: AWAIT - * - Suspend/resume: SUSPEND, RESUME - * - Async generator: ASYNC_GENERATOR_YIELD, ASYNC_GENERATOR_NEXT, - * ASYNC_GENERATOR_RETURN, ASYNC_GENERATOR_THROW - * - * AWAIT conditionally emits `await` when ctx.isAsync is true. - * YIELD/YIELD_DELEGATE push undefined (generators are stub-executed). - * - * @module ruamvm/handlers/generators - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - exprStmt, - breakStmt, - un, - lit, - bin, - assign, - awaitExpr, - ternary, - BOp, - UOp, -} from "../nodes.js"; -import type { HandlerCtx } from "./registry.js"; -import { registry } from "./registry.js"; -import { debugTrace } from "./helpers.js"; - -// --- Yield handlers --- - -/** - * YIELD / YIELD_DELEGATE: push undefined (stub — generators run to completion). - * - * `S[++P]=void 0;break;` - */ -function YIELD_HANDLER(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(un(UOp.Void, lit(0)))), breakStmt()]; -} - -// --- Await handler --- - -/** - * AWAIT: await the top-of-stack value when in async mode, otherwise replace with undefined. - * - * Async: `S[P]=await S[P];break;` - * Sync: `S[P]=void 0;break;` - */ -function AWAIT(ctx: HandlerCtx): JsNode[] { - if (ctx.isAsync) { - return [ - ...debugTrace( - ctx, - "AWAIT", - lit("awaiting:"), - ternary( - bin(BOp.Seq, un(UOp.Typeof, ctx.peek()), lit("object")), - lit("[Promise]"), - ctx.peek() - ) - ), - exprStmt(ctx.setTop(awaitExpr(ctx.peek()))), - breakStmt(), - ]; - } - return [exprStmt(ctx.setTop(un(UOp.Void, lit(0)))), breakStmt()]; -} - -// --- Stub handlers (no-op, just break) --- - -/** - * CREATE_GENERATOR / GENERATOR_RESUME / GENERATOR_RETURN / GENERATOR_THROW: - * Generator lifecycle stubs. `break;` - */ -function GENERATOR_STUB(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -/** - * SUSPEND / RESUME: coroutine suspension stubs. `break;` - */ -function SUSPEND_STUB(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -/** - * ASYNC_GENERATOR_YIELD / ASYNC_GENERATOR_NEXT / ASYNC_GENERATOR_RETURN / - * ASYNC_GENERATOR_THROW: async generator lifecycle stubs. `break;` - */ -function ASYNC_GENERATOR_STUB(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -// --- Registration --- - -registry.set(Op.YIELD, YIELD_HANDLER); -registry.set(Op.YIELD_DELEGATE, YIELD_HANDLER); -registry.set(Op.AWAIT, AWAIT); -registry.set(Op.CREATE_GENERATOR, GENERATOR_STUB); -registry.set(Op.GENERATOR_RESUME, GENERATOR_STUB); -registry.set(Op.GENERATOR_RETURN, GENERATOR_STUB); -registry.set(Op.GENERATOR_THROW, GENERATOR_STUB); -registry.set(Op.SUSPEND, SUSPEND_STUB); -registry.set(Op.RESUME, SUSPEND_STUB); -registry.set(Op.ASYNC_GENERATOR_YIELD, ASYNC_GENERATOR_STUB); -registry.set(Op.ASYNC_GENERATOR_NEXT, ASYNC_GENERATOR_STUB); -registry.set(Op.ASYNC_GENERATOR_RETURN, ASYNC_GENERATOR_STUB); -registry.set(Op.ASYNC_GENERATOR_THROW, ASYNC_GENERATOR_STUB); diff --git a/packages/ruam/src/ruamvm/handlers/helpers.ts b/packages/ruam/src/ruamvm/handlers/helpers.ts deleted file mode 100644 index cacb16e..0000000 --- a/packages/ruam/src/ruamvm/handlers/helpers.ts +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Shared AST-building helpers for opcode handlers. - * - * Provides reusable patterns for this-boxing, debug tracing, - * closure wrapping, and super property resolution. - * - * @module ruamvm/handlers/helpers - */ - -import type { JsNode } from "../nodes.js"; -import { - id, - lit, - bin, - un, - assign, - call, - member, - index, - varDecl, - exprStmt, - ifStmt, - returnStmt, - fnExpr, - ternary, - BOp, - UOp, -} from "../nodes.js"; -import type { HandlerCtx } from "./registry.js"; - -// --- This-boxing --- - -/** - * Build sloppy-mode this-boxing AST nodes. - * - * Declares `_tv` from `this`, then boxes null->globalThis, - * primitives->Object(). Used by non-arrow function handlers. - * - * @returns JsNode[] to insert in function body - */ -export function buildThisBoxing(ctx?: HandlerCtx): JsNode[] { - const tvName = ctx ? ctx.t("_tv") : "_tv"; - const ttName = ctx ? ctx.t("_tt") : "_tt"; - return [ - varDecl(tvName, id("this")), - ifStmt(un(UOp.Not, member(id("u"), "st")), [ - ifStmt( - bin(BOp.Eq, id(tvName), lit(null)), - [exprStmt(assign(id(tvName), id("globalThis")))], - [ - varDecl(ttName, un(UOp.Typeof, id(tvName))), - ifStmt( - bin( - BOp.And, - bin(BOp.Sneq, id(ttName), lit("object")), - bin(BOp.Sneq, id(ttName), lit("function")) - ), - [ - exprStmt( - assign( - id(tvName), - call(id("Object"), [id(tvName)]) - ) - ), - ] - ), - ] - ), - ]), - ]; -} - -// --- Debug tracing --- - -/** - * Emit conditional debug trace call. - * - * Returns empty array when debug is off, so it can always be spread - * into a statement list: `...debugTrace(ctx, 'NAME', args)`. - * - * @param ctx - Handler context with debug flag and dbg function name - * @param name - Opcode name for the trace log - * @param args - Additional AST nodes to pass to the debug function - * @returns JsNode[] — empty when debug is off, otherwise a single exprStmt - */ -export function debugTrace( - ctx: HandlerCtx, - name: string, - ...args: JsNode[] -): JsNode[] { - if (!ctx.debug) return []; - return [exprStmt(call(id(ctx.dbg), [lit(name), ...args]))]; -} - -// --- Super property resolution --- - -/** - * Build the super prototype resolution expression. - * - * ```js - * HO ? Object.getPrototypeOf(HO) : Object.getPrototypeOf(Object.getPrototypeOf(TV)) - * ``` - * - * @param ctx - Handler context with HO and TV names - * @returns JsNode — ternary expression resolving the super prototype - */ -export function superProto(ctx: HandlerCtx): JsNode { - const gpo = (arg: JsNode) => - call(member(id("Object"), "getPrototypeOf"), [arg]); - return ternary(id(ctx.HO), gpo(id(ctx.HO)), gpo(gpo(id(ctx.TV)))); -} - -/** - * Build the super property key resolution expression. - * - * If operand >= 0, uses constant pool; otherwise pops from stack. - * ```js - * O >= 0 ? C[O] : S[P--] - * ``` - * - * @param ctx - Handler context with O, C names and pop() factory - * @returns JsNode — ternary expression resolving the super key - */ -export function superKey(ctx: HandlerCtx): JsNode { - return ternary( - bin(BOp.Gte, id(ctx.O), lit(0)), - index(id(ctx.C), id(ctx.O)), - ctx.pop() - ); -} - -// --- Closure IIFE builders --- - -/** - * Build an arrow-function closure IIFE (captures outer this + scope). - * - * ```js - * (function(u,cs,ct){ - * if(u.s) return async function(..._a){ return execAsync(u,_a,cs,ct); }; - * return function(..._a){ return exec(u,_a,cs,ct); }; - * })(_cu, SC, TV) - * ``` - * - * @param ctx - Handler context with exec/execAsync names and SC/TV - * @returns JsNode — IIFE call expression - */ -export function buildArrowClosureIIFE(ctx: HandlerCtx): JsNode { - const execCall = (isAsync: boolean) => - returnStmt( - call(id(isAsync ? ctx.execAsync : ctx.exec), [ - id("u"), - id(ctx.t("_a")), - id("cs"), - id("ct"), - ]) - ); - return call( - fnExpr( - undefined, - ["u", "cs", "ct"], - [ - ifStmt(member(id("u"), "s"), [ - returnStmt( - fnExpr( - undefined, - ["..." + ctx.t("_a")], - [execCall(true)], - { - async: true, - } - ) - ), - ]), - returnStmt( - fnExpr(undefined, ["..." + ctx.t("_a")], [execCall(false)]) - ), - ] - ), - [id(ctx.t("_cu")), id(ctx.SC), id(ctx.TV)] - ); -} - -/** - * Build a non-arrow closure IIFE (with this-boxing + home object). - * - * ```js - * (function(u,cs){ - * if(u.s) { var fn = async function(..._a){ ; return execAsync(u,_a,cs,_tv,void 0,fn._ho); }; return fn; } - * var fn = function(..._a){ ; return exec(u,_a,cs,_tv,void 0,fn._ho); }; return fn; - * })(_cu, SC) - * ``` - * - * @param ctx - Handler context with exec/execAsync names and SC - * @returns JsNode — IIFE call expression - */ -export function buildRegularClosureIIFE(ctx: HandlerCtx): JsNode { - const fnBody = (isAsync: boolean): JsNode[] => [ - ...buildThisBoxing(ctx), - returnStmt( - call(id(isAsync ? ctx.execAsync : ctx.exec), [ - id("u"), - id(ctx.t("_a")), - id("cs"), - id(ctx.t("_tv")), - un(UOp.Void, lit(0)), - member(id("fn"), ctx.t("_ho")), - ]) - ), - ]; - return call( - fnExpr( - undefined, - ["u", "cs"], - [ - ifStmt(member(id("u"), "s"), [ - varDecl( - "fn", - fnExpr(undefined, ["..." + ctx.t("_a")], fnBody(true), { - async: true, - }) - ), - returnStmt(id("fn")), - ]), - varDecl( - "fn", - fnExpr(undefined, ["..." + ctx.t("_a")], fnBody(false)) - ), - returnStmt(id("fn")), - ] - ), - [id(ctx.t("_cu")), id(ctx.SC)] - ); -} diff --git a/packages/ruam/src/ruamvm/handlers/index.ts b/packages/ruam/src/ruamvm/handlers/index.ts deleted file mode 100644 index e1aa26d..0000000 --- a/packages/ruam/src/ruamvm/handlers/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Opcode handler registry barrel module. - * - * Re-exports the registry, types, and builder from registry.ts, - * and triggers side-effect imports to populate the registry. - * - * @module ruamvm/handlers - */ - -// Re-export types and registry from the dependency-free registry module -export { registry, makeHandlerCtx } from "./registry.js"; -export type { HandlerCtx, HandlerFn } from "./registry.js"; - -// Side-effect imports — each file registers its handlers in the registry on import. -// These must come after the re-export so that the registry is initialized -// (handler files import from ./registry.js, not ./index.js). -import "./stack.js"; -import "./arithmetic.js"; -import "./comparison.js"; -import "./logical.js"; -import "./control-flow.js"; -import "./registers.js"; -import "./type-ops.js"; -import "./special.js"; -import "./destructuring.js"; -import "./scope.js"; -import "./compound-scoped.js"; -import "./objects.js"; -import "./calls.js"; -import "./classes.js"; -import "./exceptions.js"; -import "./iterators.js"; -import "./generators.js"; -import "./functions.js"; -import "./superinstructions.js"; -import "./mutation.js"; diff --git a/packages/ruam/src/ruamvm/handlers/iterators.ts b/packages/ruam/src/ruamvm/handlers/iterators.ts deleted file mode 100644 index 77877f2..0000000 --- a/packages/ruam/src/ruamvm/handlers/iterators.ts +++ /dev/null @@ -1,524 +0,0 @@ -/** - * Iterator opcode handlers in AST node form. - * - * Covers 16 opcodes across sync and async iteration: - * - Sync iterators: GET_ITERATOR, ITER_NEXT, ITER_DONE, ITER_VALUE, - * ITER_CLOSE, ITER_RESULT_UNWRAP - * - For-in: FORIN_INIT, FORIN_NEXT, FORIN_DONE - * - Async iterators: GET_ASYNC_ITERATOR, ASYNC_ITER_NEXT, ASYNC_ITER_DONE, - * ASYNC_ITER_VALUE, ASYNC_ITER_CLOSE, FOR_AWAIT_NEXT - * - Conversion: CREATE_ASYNC_FROM_SYNC_ITER - * - * Async iterator handlers conditionally emit `await` when ctx.isAsync is true. - * All handlers use pure AST nodes — no raw() escape hatch. - * - * @module ruamvm/handlers/iterators - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - bin, - un, - assign, - call, - member, - index, - varDecl, - exprStmt, - ifStmt, - breakStmt, - forIn, - obj, - arr, - awaitExpr, - update, - UOp, - UpOp, - BOp, -} from "../nodes.js"; -import type { HandlerCtx } from "./registry.js"; -import { registry } from "./registry.js"; - -// --- Helpers --- - -/** Wrap an expression with `await` when ctx.isAsync is true. */ -function maybeAwait(ctx: HandlerCtx, expr: JsNode): JsNode { - return ctx.isAsync ? awaitExpr(expr) : expr; -} - -// --- Sync iterator handlers --- - -/** - * GET_ITERATOR: pop iterable, create iterator, advance to first result. - * - * ``` - * var iterable=X();var iter=iterable[Symbol.iterator](); - * var first=iter.next();W({_iter:iter,_done:!!first.done,_value:first.value});break; - * ``` - */ -function GET_ITERATOR(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterable"), ctx.pop()), - varDecl( - ctx.local("iterator"), - call( - index( - id(ctx.local("iterable")), - member(id("Symbol"), "iterator") - ), - [] - ) - ), - varDecl( - ctx.local("firstIter"), - call(member(id(ctx.local("iterator")), "next"), []) - ), - exprStmt( - ctx.push( - obj( - [ctx.t("_iter"), id(ctx.local("iterator"))], - [ - ctx.t("_done"), - un( - UOp.Not, - un( - UOp.Not, - member(id(ctx.local("firstIter")), "done") - ) - ), - ], - [ - ctx.t("_value"), - member(id(ctx.local("firstIter")), "value"), - ] - ) - ) - ), - breakStmt(), - ]; -} - -/** - * ITER_NEXT: pop iterator object, push current value, then advance. - * - * ``` - * var iterObj=X();W(iterObj._value); - * var nxt=iterObj._iter.next();iterObj._done=!!nxt.done;iterObj._value=nxt.value;break; - * ``` - */ -function ITER_NEXT(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.pop()), - exprStmt(ctx.push(member(id(ctx.local("iterObj")), ctx.t("_value")))), - varDecl( - ctx.local("next"), - call( - member( - member(id(ctx.local("iterObj")), ctx.t("_iter")), - "next" - ), - [] - ) - ), - exprStmt( - assign( - member(id(ctx.local("iterObj")), ctx.t("_done")), - un(UOp.Not, un(UOp.Not, member(id(ctx.local("next")), "done"))) - ) - ), - exprStmt( - assign( - member(id(ctx.local("iterObj")), ctx.t("_value")), - member(id(ctx.local("next")), "value") - ) - ), - breakStmt(), - ]; -} - -/** - * ITER_DONE: peek at iterator object, push its done flag. - */ -function ITER_DONE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.peek()), - exprStmt( - ctx.push( - un( - UOp.Not, - un( - UOp.Not, - member(id(ctx.local("iterObj")), ctx.t("_done")) - ) - ) - ) - ), - breakStmt(), - ]; -} - -/** - * ITER_VALUE: peek at iterator object, push its current value. - */ -function ITER_VALUE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.peek()), - exprStmt(ctx.push(member(id(ctx.local("iterObj")), ctx.t("_value")))), - breakStmt(), - ]; -} - -/** - * ITER_CLOSE: pop iterator object and call its return method if present. - */ -function ITER_CLOSE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.pop()), - ifStmt( - member(member(id(ctx.local("iterObj")), ctx.t("_iter")), "return"), - [ - exprStmt( - call( - member( - member(id(ctx.local("iterObj")), ctx.t("_iter")), - "return" - ), - [] - ) - ), - ] - ), - breakStmt(), - ]; -} - -/** - * ITER_RESULT_UNWRAP: peek at iterator, push value then done flag. - */ -function ITER_RESULT_UNWRAP(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.peek()), - exprStmt(ctx.push(member(id(ctx.local("iterObj")), ctx.t("_value")))), - exprStmt( - ctx.push( - un( - UOp.Not, - un( - UOp.Not, - member(id(ctx.local("iterObj")), ctx.t("_done")) - ) - ) - ) - ), - breakStmt(), - ]; -} - -// --- For-in handlers --- - -/** - * FORIN_INIT: pop object, collect all enumerable keys. - * - * ``` - * var obj=X();var keys=[];for(var k in obj)keys.push(k); - * W({_keys:keys,_idx:0});break; - * ``` - */ -function FORIN_INIT(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("object"), ctx.pop()), - varDecl(ctx.local("keys"), arr()), - forIn(ctx.local("key"), id(ctx.local("object")), [ - exprStmt( - call(member(id(ctx.local("keys")), "push"), [ - id(ctx.local("key")), - ]) - ), - ]), - exprStmt( - ctx.push( - obj( - [ctx.t("_keys"), id(ctx.local("keys"))], - [ctx.t("_idx"), lit(0)] - ) - ) - ), - breakStmt(), - ]; -} - -/** - * FORIN_NEXT: pop for-in state, push next key. - */ -function FORIN_NEXT(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("forInState"), ctx.pop()), - exprStmt( - ctx.push( - index( - member(id(ctx.local("forInState")), ctx.t("_keys")), - update( - UpOp.Inc, - false, - member(id(ctx.local("forInState")), ctx.t("_idx")) - ) - ) - ) - ), - breakStmt(), - ]; -} - -/** - * FORIN_DONE: peek at for-in state, push whether iteration is complete. - */ -function FORIN_DONE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("forInState"), ctx.peek()), - exprStmt( - ctx.push( - bin( - BOp.Gte, - member(id(ctx.local("forInState")), ctx.t("_idx")), - member( - member(id(ctx.local("forInState")), ctx.t("_keys")), - "length" - ) - ) - ) - ), - breakStmt(), - ]; -} - -// --- Async iterator handlers --- - -/** - * GET_ASYNC_ITERATOR: pop iterable, get async (or sync) iterator. - * - * Falls back to Symbol.iterator if Symbol.asyncIterator is not present. - */ -function GET_ASYNC_ITERATOR(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterable"), ctx.pop()), - varDecl( - ctx.local("method"), - bin( - BOp.Or, - index( - id(ctx.local("iterable")), - member(id("Symbol"), "asyncIterator") - ), - index( - id(ctx.local("iterable")), - member(id("Symbol"), "iterator") - ) - ) - ), - varDecl( - ctx.local("iterator"), - call(member(id(ctx.local("method")), "call"), [ - id(ctx.local("iterable")), - ]) - ), - exprStmt( - ctx.push( - obj( - [ctx.t("_iter"), id(ctx.local("iterator"))], - [ctx.t("_done"), lit(false)], - [ctx.t("_value"), un(UOp.Void, lit(0))], - [ctx.t("_async"), lit(true)] - ) - ) - ), - breakStmt(), - ]; -} - -/** - * ASYNC_ITER_NEXT: advance async iterator, optionally awaiting the result. - * - * When ctx.isAsync is true, emits `await` before `iterObj._iter.next()`. - */ -function ASYNC_ITER_NEXT(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.peek()), - varDecl( - ctx.local("result"), - maybeAwait( - ctx, - call( - member( - member(id(ctx.local("iterObj")), ctx.t("_iter")), - "next" - ), - [] - ) - ) - ), - exprStmt( - assign( - member(id(ctx.local("iterObj")), ctx.t("_done")), - un( - UOp.Not, - un(UOp.Not, member(id(ctx.local("result")), "done")) - ) - ) - ), - exprStmt( - assign( - member(id(ctx.local("iterObj")), ctx.t("_value")), - member(id(ctx.local("result")), "value") - ) - ), - breakStmt(), - ]; -} - -/** - * ASYNC_ITER_DONE: peek at async iterator, push its done flag. - */ -function ASYNC_ITER_DONE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.peek()), - exprStmt( - ctx.push( - un( - UOp.Not, - un( - UOp.Not, - member(id(ctx.local("iterObj")), ctx.t("_done")) - ) - ) - ) - ), - breakStmt(), - ]; -} - -/** - * ASYNC_ITER_VALUE: peek at async iterator, push its current value. - */ -function ASYNC_ITER_VALUE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.peek()), - exprStmt(ctx.push(member(id(ctx.local("iterObj")), ctx.t("_value")))), - breakStmt(), - ]; -} - -/** - * ASYNC_ITER_CLOSE: pop async iterator and call its return method if present. - * - * When ctx.isAsync is true, emits `await` before the return call. - */ -function ASYNC_ITER_CLOSE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.pop()), - ifStmt( - member(member(id(ctx.local("iterObj")), ctx.t("_iter")), "return"), - [ - exprStmt( - maybeAwait( - ctx, - call( - member( - member( - id(ctx.local("iterObj")), - ctx.t("_iter") - ), - "return" - ), - [] - ) - ) - ), - ] - ), - breakStmt(), - ]; -} - -/** - * FOR_AWAIT_NEXT: advance async iterator, push the value. - * - * When ctx.isAsync is true, emits `await` before `iterObj._iter.next()`. - */ -function FOR_AWAIT_NEXT(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("iterObj"), ctx.peek()), - varDecl( - ctx.local("result"), - maybeAwait( - ctx, - call( - member( - member(id(ctx.local("iterObj")), ctx.t("_iter")), - "next" - ), - [] - ) - ) - ), - exprStmt( - assign( - member(id(ctx.local("iterObj")), ctx.t("_done")), - un( - UOp.Not, - un(UOp.Not, member(id(ctx.local("result")), "done")) - ) - ) - ), - exprStmt( - assign( - member(id(ctx.local("iterObj")), ctx.t("_value")), - member(id(ctx.local("result")), "value") - ) - ), - exprStmt(ctx.push(member(id(ctx.local("result")), "value"))), - breakStmt(), - ]; -} - -// --- Conversion handler --- - -/** - * CREATE_ASYNC_FROM_SYNC_ITER: pop sync iterator, wrap as async-compatible. - */ -function CREATE_ASYNC_FROM_SYNC_ITER(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("syncIter"), ctx.pop()), - exprStmt( - ctx.push( - obj( - [ctx.t("_iter"), id(ctx.local("syncIter"))], - [ctx.t("_done"), lit(false)], - [ctx.t("_value"), un(UOp.Void, lit(0))] - ) - ) - ), - breakStmt(), - ]; -} - -// --- Registration --- - -registry.set(Op.GET_ITERATOR, GET_ITERATOR); -registry.set(Op.ITER_NEXT, ITER_NEXT); -registry.set(Op.ITER_DONE, ITER_DONE); -registry.set(Op.ITER_VALUE, ITER_VALUE); -registry.set(Op.ITER_CLOSE, ITER_CLOSE); -registry.set(Op.ITER_RESULT_UNWRAP, ITER_RESULT_UNWRAP); -registry.set(Op.FORIN_INIT, FORIN_INIT); -registry.set(Op.FORIN_NEXT, FORIN_NEXT); -registry.set(Op.FORIN_DONE, FORIN_DONE); -registry.set(Op.GET_ASYNC_ITERATOR, GET_ASYNC_ITERATOR); -registry.set(Op.ASYNC_ITER_NEXT, ASYNC_ITER_NEXT); -registry.set(Op.ASYNC_ITER_DONE, ASYNC_ITER_DONE); -registry.set(Op.ASYNC_ITER_VALUE, ASYNC_ITER_VALUE); -registry.set(Op.ASYNC_ITER_CLOSE, ASYNC_ITER_CLOSE); -registry.set(Op.FOR_AWAIT_NEXT, FOR_AWAIT_NEXT); -registry.set(Op.CREATE_ASYNC_FROM_SYNC_ITER, CREATE_ASYNC_FROM_SYNC_ITER); diff --git a/packages/ruam/src/ruamvm/handlers/logical.ts b/packages/ruam/src/ruamvm/handlers/logical.ts deleted file mode 100644 index 937848d..0000000 --- a/packages/ruam/src/ruamvm/handlers/logical.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** @module ruamvm/handlers/logical */ - -import { Op } from "../../compiler/opcodes.js"; -import { - id, - lit, - bin, - un, - assign, - update, - varDecl, - exprStmt, - ifStmt, - breakStmt, - BOp, - UOp, -} from "../nodes.js"; -import type { HandlerCtx } from "./registry.js"; -import { registry } from "./registry.js"; - -// --- NOT --- - -/** - * `S[P]=!S[P];break;` - */ -registry.set(Op.NOT, (ctx: HandlerCtx) => [ - exprStmt(ctx.setTop(un(UOp.Not, ctx.peek()))), - breakStmt(), -]); - -// --- LOGICAL_AND --- - -/** - * `{var v=S[P];if(!v){IP=O*2;}else{P--;}break;}` - * - * Short-circuit: if falsy, jump to operand target; otherwise pop TOS and continue. - */ -registry.set(Op.LOGICAL_AND, (ctx: HandlerCtx) => [ - varDecl(ctx.local("value"), ctx.peek()), - ifStmt( - un(UOp.Not, id(ctx.local("value"))), - [exprStmt(assign(id(ctx.IP), bin(BOp.Mul, id(ctx.O), lit(2))))], - [exprStmt(ctx.pop())] - ), - breakStmt(), -]); - -// --- LOGICAL_OR --- - -/** - * `{var v=S[P];if(v){IP=O*2;}else{P--;}break;}` - * - * Short-circuit: if truthy, jump to operand target; otherwise pop TOS and continue. - */ -registry.set(Op.LOGICAL_OR, (ctx: HandlerCtx) => [ - varDecl(ctx.local("value"), ctx.peek()), - ifStmt( - id(ctx.local("value")), - [exprStmt(assign(id(ctx.IP), bin(BOp.Mul, id(ctx.O), lit(2))))], - [exprStmt(ctx.pop())] - ), - breakStmt(), -]); - -// --- NULLISH_COALESCE --- - -/** - * `{var v=S[P];if(v!==null&&v!==void 0){IP=O*2;}else{P--;}break;}` - * - * Short-circuit: if non-nullish (not null and not undefined), jump; otherwise pop and continue. - */ -registry.set(Op.NULLISH_COALESCE, (ctx: HandlerCtx) => [ - varDecl(ctx.local("value"), ctx.peek()), - ifStmt( - bin( - BOp.And, - bin(BOp.Sneq, id(ctx.local("value")), lit(null)), - bin(BOp.Sneq, id(ctx.local("value")), un(UOp.Void, lit(0))) - ), - [exprStmt(assign(id(ctx.IP), bin(BOp.Mul, id(ctx.O), lit(2))))], - [exprStmt(ctx.pop())] - ), - breakStmt(), -]); diff --git a/packages/ruam/src/ruamvm/handlers/mutation.ts b/packages/ruam/src/ruamvm/handlers/mutation.ts deleted file mode 100644 index adb482a..0000000 --- a/packages/ruam/src/ruamvm/handlers/mutation.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Runtime opcode mutation handler. - * - * The MUTATE opcode permutes the handler table `_ht` at runtime, - * changing which handler index each physical opcode maps to. - * Uses the same deterministic swap algorithm as the build-time encoder. - * - * @module ruamvm/handlers/mutation - */ - -import { Op } from "../../compiler/opcodes.js"; -import { registry } from "./registry.js"; -import type { HandlerCtx } from "./registry.js"; -import { - BOp, - UpOp, - id, - lit, - bin, - un, - assign, - varDecl, - forStmt, - exprStmt, - index, - call, - member, - update, -} from "../nodes.js"; -import type { JsNode } from "../nodes.js"; - -/** Number of swaps per mutation (must match compiler/opcode-mutation.ts). */ -const SWAPS_PER_MUTATION = 4; - -registry.set(Op.MUTATE, (ctx: HandlerCtx): JsNode[] => { - // At runtime, this handler: - // 1. Reads the mutation seed from the operand - // 2. Performs SWAPS_PER_MUTATION deterministic swaps on _ht - // - // var _ms = O; - // for (var _mk = 0; _mk < 4; _mk++) { - // _ms = imul(_ms, 1664525) + 1013904223 >>> 0; - // var _mi = (_ms >>> 16) % _ht.length; - // _ms = imul(_ms, 1664525) + 1013904223 >>> 0; - // var _mj = (_ms >>> 16) % _ht.length; - // var _mt = _ht[_mi]; _ht[_mi] = _ht[_mj]; _ht[_mj] = _mt; - // } - - const ms = ctx.t("_ms"); // mutation seed state - const mk = ctx.t("_mk"); // loop counter - const mi = ctx.t("_mi"); // swap index i - const mj = ctx.t("_mj"); // swap index j - const mt = ctx.t("_mt"); // temp for swap - const ht = ctx.t("_ht"); // handler table - - // LCG step: _ms = (imul(_ms, 1664525) + 1013904223) >>> 0 - const lcgStep = bin( - BOp.Ushr, - bin( - BOp.Add, - call(member(id("Math"), "imul"), [id(ms), lit(1664525)]), - lit(1013904223) - ), - lit(0) - ); - - // (_ms >>> 16) % _ht.length - const modLen = bin( - BOp.Mod, - bin(BOp.Ushr, id(ms), lit(16)), - member(id(ht), "length") - ); - - return [ - // var _ms = O - varDecl(ms, id(ctx.O)), - - // for (var _mk = 0; _mk < SWAPS; _mk++) - forStmt( - varDecl(mk, lit(0)), - bin(BOp.Lt, id(mk), lit(SWAPS_PER_MUTATION)), - update(UpOp.Inc, true, id(mk)), - [ - // _ms = LCG(_ms) - exprStmt(assign(id(ms), lcgStep)), - // var _mi = (_ms >>> 16) % _ht.length - varDecl(mi, modLen), - - // _ms = LCG(_ms) - exprStmt(assign(id(ms), lcgStep)), - // var _mj = (_ms >>> 16) % _ht.length - varDecl(mj, modLen), - - // var _mt = _ht[_mi]; _ht[_mi] = _ht[_mj]; _ht[_mj] = _mt - varDecl(mt, index(id(ht), id(mi))), - exprStmt(assign(index(id(ht), id(mi)), index(id(ht), id(mj)))), - exprStmt(assign(index(id(ht), id(mj)), id(mt))), - ] - ), - ]; -}); diff --git a/packages/ruam/src/ruamvm/handlers/objects.ts b/packages/ruam/src/ruamvm/handlers/objects.ts deleted file mode 100644 index 6f84947..0000000 --- a/packages/ruam/src/ruamvm/handlers/objects.ts +++ /dev/null @@ -1,647 +0,0 @@ -/** - * Object and array opcode handlers using pure AST nodes. - * - * Covers 29 opcodes across four categories: - * - Property access: GET_PROP_STATIC, SET_PROP_STATIC, GET_PROP_DYNAMIC, - * SET_PROP_DYNAMIC, DELETE_PROP_STATIC, DELETE_PROP_DYNAMIC, - * OPT_CHAIN_GET, OPT_CHAIN_DYNAMIC - * - Operators: IN_OP, INSTANCEOF - * - Super: GET_SUPER_PROP, SET_SUPER_PROP - * - Private fields: GET_PRIVATE_FIELD, SET_PRIVATE_FIELD, HAS_PRIVATE_FIELD - * - Object defs: DEFINE_OWN_PROPERTY, NEW_OBJECT, NEW_ARRAY, NEW_ARRAY_WITH_SIZE, - * ARRAY_PUSH, ARRAY_HOLE, SPREAD_ARRAY, SPREAD_OBJECT, - * COPY_DATA_PROPERTIES, SET_PROTO, FREEZE_OBJECT, SEAL_OBJECT, - * DEFINE_PROPERTY_DESC, CREATE_TEMPLATE_OBJECT - * - * @module ruamvm/handlers/objects - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - bin, - un, - assign, - call, - member, - index, - varDecl, - exprStmt, - ifStmt, - forStmt, - breakStmt, - obj, - arr, - newExpr, - ternary, - update, - BOp, - UOp, - UpOp, -} from "../nodes.js"; -import { registry, type HandlerCtx } from "./registry.js"; -import { superProto, superKey } from "./helpers.js"; - -// --- Property access handlers --- - -/** `S[P]=S[P][C[O]];break;` */ -function GET_PROP_STATIC(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt( - ctx.setTop(index(ctx.peek(), index(id(ctx.C), id(ctx.O)))) - ), - breakStmt(), - ]; -} - -/** - * SET_PROP_STATIC: pop value, set on object. - * - * ``` - * var val=S.pop();var obj=S[S.length-1];var k=C[O]; - * obj[k]=val; - * break; - * ``` - * - * Previous implementation used a try/catch that fell back to - * Object.defineProperty. This masked real errors: when obj was - * null/undefined/primitive, the original TypeError was caught and - * replaced with "Object.defineProperty called on non-object". - * The fallback was intended for non-writable inherited properties, - * but that case doesn't arise in compiled bytecode (object literals - * are always fresh objects, and user assignments should propagate - * errors naturally). - */ -function SET_PROP_STATIC(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("object"), ctx.peek()), - varDecl(ctx.local("propKey"), index(id(ctx.C), id(ctx.O))), - exprStmt( - assign( - index(id(ctx.local("object")), id(ctx.local("propKey"))), - id(ctx.local("value")) - ) - ), - breakStmt(), - ]; -} - -/** `{var key=S[P--];S[P]=S[P][key];break;}` */ -function GET_PROP_DYNAMIC(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("propKey"), ctx.pop()), - exprStmt( - ctx.setTop(index(ctx.peek(), id(ctx.local("propKey")))) - ), - breakStmt(), - ]; -} - -/** `{var val=S[P--];var key=S[P--];var obj=S[P];obj[key]=val;break;}` */ -function SET_PROP_DYNAMIC(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("propKey"), ctx.pop()), - varDecl(ctx.local("object"), ctx.peek()), - exprStmt( - assign( - index(id(ctx.local("object")), id(ctx.local("propKey"))), - id(ctx.local("value")) - ) - ), - breakStmt(), - ]; -} - -/** `S[P]=delete S[P][C[O]];break;` */ -function DELETE_PROP_STATIC(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt( - ctx.setTop( - un(UOp.Delete, index(ctx.peek(), index(id(ctx.C), id(ctx.O)))) - ) - ), - breakStmt(), - ]; -} - -/** `{var key=S[P--];S[P]=delete S[P][key];break;}` */ -function DELETE_PROP_DYNAMIC(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("propKey"), ctx.pop()), - exprStmt( - ctx.setTop( - un(UOp.Delete, index(ctx.peek(), id(ctx.local("propKey")))) - ) - ), - breakStmt(), - ]; -} - -/** `{var key=C[O];var obj=S[P];S[P]=obj==null?void 0:obj[key];break;}` */ -function OPT_CHAIN_GET(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("propKey"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("object"), ctx.peek()), - exprStmt( - ctx.setTop( - ternary( - bin(BOp.Eq, id(ctx.local("object")), lit(null)), - un(UOp.Void, lit(0)), - index(id(ctx.local("object")), id(ctx.local("propKey"))) - ) - ) - ), - breakStmt(), - ]; -} - -/** `{var key=S[P--];var obj=S[P];S[P]=obj==null?void 0:obj[key];break;}` */ -function OPT_CHAIN_DYNAMIC(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("propKey"), ctx.pop()), - varDecl(ctx.local("object"), ctx.peek()), - exprStmt( - ctx.setTop( - ternary( - bin(BOp.Eq, id(ctx.local("object")), lit(null)), - un(UOp.Void, lit(0)), - index(id(ctx.local("object")), id(ctx.local("propKey"))) - ) - ) - ), - breakStmt(), - ]; -} - -// --- Operators --- - -/** `{var obj=S[P--];S[P]=S[P] in obj;break;}` */ -function IN_OP(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("object"), ctx.pop()), - exprStmt( - ctx.setTop(bin(BOp.In, ctx.peek(), id(ctx.local("object")))) - ), - breakStmt(), - ]; -} - -/** `{var ctor=S[P--];S[P]=S[P] instanceof ctor;break;}` */ -function INSTANCEOF(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("ctor"), ctx.pop()), - exprStmt( - ctx.setTop( - bin(BOp.Instanceof, ctx.peek(), id(ctx.local("ctor"))) - ) - ), - breakStmt(), - ]; -} - -// --- Super property access --- - -/** - * GET_SUPER_PROP: resolve super prototype, get property by constant or dynamic key. - * - * ``` - * var sp2=HO?Object.getPrototypeOf(HO):Object.getPrototypeOf(Object.getPrototypeOf(TV)); - * var key=O>=0?C[O]:X(); - * W(sp2?sp2[key]:void 0);break; - * ``` - */ -function GET_SUPER_PROP(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("superProto"), superProto(ctx)), - varDecl(ctx.local("propKey"), superKey(ctx)), - exprStmt( - ctx.push( - ternary( - id(ctx.local("superProto")), - index( - id(ctx.local("superProto")), - id(ctx.local("propKey")) - ), - un(UOp.Void, lit(0)) - ) - ) - ), - breakStmt(), - ]; -} - -/** - * SET_SUPER_PROP: resolve super prototype, set property value. - * - * ``` - * var val=X(); - * var sp2=HO?Object.getPrototypeOf(HO):Object.getPrototypeOf(Object.getPrototypeOf(TV)); - * var key=O>=0?C[O]:X(); - * if(sp2)sp2[key]=val;W(val);break; - * ``` - */ -function SET_SUPER_PROP(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("superProto"), superProto(ctx)), - varDecl(ctx.local("propKey"), superKey(ctx)), - ifStmt(id(ctx.local("superProto")), [ - exprStmt( - assign( - index( - id(ctx.local("superProto")), - id(ctx.local("propKey")) - ), - id(ctx.local("value")) - ) - ), - ]), - exprStmt(ctx.push(id(ctx.local("value")))), - breakStmt(), - ]; -} - -// --- Private field access --- - -/** `{var obj=X();var name=C[O];W(obj[name]);break;}` */ -function GET_PRIVATE_FIELD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("object"), ctx.pop()), - varDecl(ctx.local("fieldName"), index(id(ctx.C), id(ctx.O))), - exprStmt( - ctx.push(index(id(ctx.local("object")), id(ctx.local("fieldName")))) - ), - breakStmt(), - ]; -} - -/** `{var val=X();var obj=X();var name=C[O];obj[name]=val;W(val);break;}` */ -function SET_PRIVATE_FIELD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("object"), ctx.pop()), - varDecl(ctx.local("fieldName"), index(id(ctx.C), id(ctx.O))), - exprStmt( - assign( - index(id(ctx.local("object")), id(ctx.local("fieldName"))), - id(ctx.local("value")) - ) - ), - exprStmt(ctx.push(id(ctx.local("value")))), - breakStmt(), - ]; -} - -/** `{var obj=X();var name=C[O];W(name in obj);break;}` */ -function HAS_PRIVATE_FIELD(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("object"), ctx.pop()), - varDecl(ctx.local("fieldName"), index(id(ctx.C), id(ctx.O))), - exprStmt( - ctx.push( - bin(BOp.In, id(ctx.local("fieldName")), id(ctx.local("object"))) - ) - ), - breakStmt(), - ]; -} - -// --- Object/array construction --- - -/** `{var desc=X();var key=X();var obj=X();Object.defineProperty(obj,key,desc);W(obj);break;}` */ -function DEFINE_OWN_PROPERTY(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("descriptor"), ctx.pop()), - varDecl(ctx.local("propKey"), ctx.pop()), - varDecl(ctx.local("object"), ctx.pop()), - exprStmt( - call(member(id("Object"), "defineProperty"), [ - id(ctx.local("object")), - id(ctx.local("propKey")), - id(ctx.local("descriptor")), - ]) - ), - exprStmt(ctx.push(id(ctx.local("object")))), - breakStmt(), - ]; -} - -/** `W({});break;` */ -function NEW_OBJECT(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(obj())), breakStmt()]; -} - -/** `W([]);break;` */ -function NEW_ARRAY(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(arr())), breakStmt()]; -} - -/** `W(new Array(O));break;` */ -function NEW_ARRAY_WITH_SIZE(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(newExpr(id("Array"), [id(ctx.O)]))), breakStmt()]; -} - -/** `{var val=X();var arr=Y();arr.push(val);break;}` */ -function ARRAY_PUSH(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.pop()), - varDecl(ctx.local("array"), ctx.peek()), - exprStmt( - call(member(id(ctx.local("array")), "push"), [ - id(ctx.local("value")), - ]) - ), - breakStmt(), - ]; -} - -/** `{var arr=Y();arr.length++;break;}` */ -function ARRAY_HOLE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("array"), ctx.peek()), - exprStmt( - update(UpOp.Inc, false, member(id(ctx.local("array")), "length")) - ), - breakStmt(), - ]; -} - -/** - * SPREAD_ARRAY: spread source into target array or object. - * - * ``` - * var src=X();var target=Y(); - * if(Array.isArray(target)){var items=Array.from(src);for(var si=0;si val;S[++P]=R[O];break;}` - * - * @param op - JS binary operator string (e.g. `'+'`, `'-'`, `'*'`, `'/'`, `'%'`) - * @returns Handler function producing the case body AST nodes - */ -function regAssignHandler(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("val"), ctx.pop()), - exprStmt( - assign( - rSlot(ctx), - bin(op, index(id(ctx.R), id(ctx.O)), id(ctx.local("val"))) - ) - ), - exprStmt(ctx.push(index(id(ctx.R), id(ctx.O)))), - breakStmt(), - ]; -} - -/** - * Build a *void* compound assignment register handler — same as - * {@link regAssignHandler} but omits the result push. Fuses - * `COMPOUND_ASSIGN_REG + POP` (statement-form `s += x;`), where the pushed - * value would be immediately discarded. - * - * Pattern: `{var val=S[P--];R[O]=R[O] val;break;}` - * - * Encoding-aware: the stack read uses `ctx.pop()` (decoded under - * stackEncoding); there is no stack write, only a register store. - * - * @param op - JS binary operator string - * @returns Handler function producing the case body AST nodes - */ -function regAssignVoidHandler(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("val"), ctx.pop()), - exprStmt( - assign( - rSlot(ctx), - bin(op, index(id(ctx.R), id(ctx.O)), id(ctx.local("val"))) - ) - ), - breakStmt(), - ]; -} - -// --- Register load/store --- - -/** LOAD_REG: `S[++P]=R[O];break;` */ -function LOAD_REG(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(rSlot(ctx))), breakStmt()]; -} - -/** STORE_REG: `R[O]=S[P--];break;` */ -function STORE_REG(ctx: HandlerCtx): JsNode[] { - return [exprStmt(assign(rSlot(ctx), ctx.pop())), breakStmt()]; -} - -// --- Argument load/store --- - -/** LOAD_ARG: `S[++P]=O Name; - - /** Handler-local variable — returns a Name for a handler-local variable. */ - local: (key: string) => Name; - - // Stack operation factories. Without stackEncoding these emit plain - // `S.push(v)` / `S.pop()` / `S[S.length-1]`. With stackEncoding they route - // through the `stkEnc`/`stkDec` helpers so int32 stack values are stored - // position-XOR-masked in memory (same representation as the legacy Proxy, - // but without the per-access trap cost). - push: (value: JsNode) => JsNode; - pop: () => JsNode; - peek: () => JsNode; - - /** - * Read a stack slot by absolute index, decoded. `idx` is a *thunk* that - * produces a FRESH index AST each call (the index is emitted twice under - * encoding — once for the lookup, once for the position key — and reusing a - * single node object would alias inside the mutating MBA/structural passes). - */ - slotRead: (idx: () => JsNode) => JsNode; - - /** - * Write `value` to a stack slot by absolute index, encoded for that index. - * `idx` is a fresh-AST thunk (see {@link HandlerCtx.slotRead}). Returns the - * assignment expression (caller wraps in `exprStmt`). Re-keying is automatic: - * a value read at one index via {@link HandlerCtx.slotRead} and written to a - * different index here is decoded with the old key and re-encoded with the new. - */ - slotWrite: (idx: () => JsNode, value: JsNode) => JsNode; - - /** - * Overwrite the top of stack in place with `value` (encoded if needed). - * Use this instead of `assign(peek(), …)` — under stackEncoding `peek()` is - * a decode *call* and cannot be an assignment target. The read side stays - * `peek()` (decoded); only the write target needs this. Returns the - * assignment expression (caller wraps in `exprStmt`). - */ - setTop: (value: JsNode) => JsNode; - - // Scope chain helpers — prototypal scope (Object.create chain) - /** `s[key]` — scoped variable reference on walk variable (default key: `id("name")`) */ - sv: (key?: JsNode) => JsNode; - /** `SC[key]` — current scope variable reference (default key: `id("name")`) */ - curSv: (key?: JsNode) => JsNode; - /** Scope walk: `while(s){if(Object.prototype.hasOwnProperty.call(s,key)){break;}s=Object.getPrototypeOf(s);}break;` */ - scopeWalk: (body: JsNode[], key?: JsNode) => JsNode[]; -} - -/** A handler function returns the case body as AST nodes. */ -export type HandlerFn = (ctx: HandlerCtx) => JsNode[]; - -/** The handler registry: maps logical opcode to handler function. */ -export const registry = new Map(); - -/** - * Build a HandlerCtx from RuntimeNames, TempNames, and flags. - */ -export function makeHandlerCtx( - names: RuntimeNames, - temps: TempNames, - isAsync: boolean, - debug: boolean, - stackEncoding = false -): HandlerCtx { - // Interim pass-through: returns the key as-is (string). - // Will be wired to NameScope in Task 6. - const localFn = (key: string): Name => key; - - // --- Stack access factories (encoding-aware) --------------------------- - // When stackEncoding is on, values are stored as `[tag,payload]` entries - // with int32s position-XOR-masked, accessed through the IIFE-scope helpers - // `stkEnc(value, index, key)` / `stkDec(entry, index, key)`. The per-unit - // key `_sek` is an exec-local computed at entry (see buildStackEncodingKeyInit). - const stk = names.stk; - const sLen = (): JsNode => member(id(stk), "length"); - const sLenMinus = (k: number): JsNode => bin(BOp.Sub, sLen(), lit(k)); - - let pushFn: (value: JsNode) => JsNode; - let popFn: () => JsNode; - let peekFn: () => JsNode; - let slotReadFn: (idx: () => JsNode) => JsNode; - let slotWriteFn: (idx: () => JsNode, value: JsNode) => JsNode; - - if (stackEncoding) { - const enc = names.stkEnc; - const dec = names.stkDec; - const sek = temps["_sek"]; - if (sek === undefined) { - throw new Error("stackEncoding requires temp name _sek"); - } - const encOf = (value: JsNode, idx: JsNode): JsNode => - call(id(enc), [value, idx, id(sek)]); - const decOf = (entry: JsNode, idx: JsNode): JsNode => - call(id(dec), [entry, idx, id(sek)]); - - // push: S.push(stkEnc(v, S.length, _sek)) — S.length is read BEFORE the - // append, i.e. it equals the target index. Arg evaluated before .push runs. - pushFn = (value: JsNode) => stackPush(stk, encOf(value, sLen())); - // pop: stkDec(S.pop(), S.length, _sek) — left-to-right eval: pop() first - // (length decrements), then S.length reads the just-vacated index. - popFn = () => decOf(stackPop(stk), sLen()); - // peek: stkDec(S[S.length-1], S.length-1, _sek) - peekFn = () => decOf(index(id(stk), sLenMinus(1)), sLenMinus(1)); - // slotRead: stkDec(S[idx], idx, _sek) — idx thunk called twice (fresh AST) - slotReadFn = (idx) => decOf(index(id(stk), idx()), idx()); - // slotWrite: S[idx] = stkEnc(value, idx, _sek) - slotWriteFn = (idx, value) => - assign(index(id(stk), idx()), encOf(value, idx())); - } else { - pushFn = (value: JsNode) => stackPush(stk, value); - popFn = () => stackPop(stk); - peekFn = () => stackPeek(stk); - slotReadFn = (idx) => index(id(stk), idx()); - slotWriteFn = (idx, value) => assign(index(id(stk), idx()), value); - } - - // setTop: write to the top slot (depth 1). Encoding-aware via slotWrite. - const setTopFn = (value: JsNode): JsNode => - slotWriteFn(() => sLenMinus(1), value); - - return { - S: names.stk, - IP: names.ip, - C: names.cArr, - O: names.operand, - SC: names.scope, - R: names.regs, - EX: names.exStk, - PE: names.pEx, - HPE: names.hPEx, - CT: names.cType, - CV: names.cVal, - PH: names.phys, - U: names.unit, - A: names.args, - OS: names.outer, - TV: names.tVal, - NT: names.nTgt, - HO: names.ho, - tdzSentinel: names.tdzSentinel, - exec: names.exec, - execAsync: names.execAsync, - load: names.load, - spreadSym: names.spreadSym, - hop: names.hop, - depth: names.depth, - callStack: names.callStack, - dbg: names.dbg, - fSlots: names.fSlots, - isAsync, - debug, - t: (key: string): Name => { - const name = temps[key]; - if (name === undefined) { - throw new Error(`Unknown temp name key: ${key}`); - } - return name; - }, - local: localFn, - push: pushFn, - pop: popFn, - peek: peekFn, - slotRead: slotReadFn, - slotWrite: slotWriteFn, - setTop: setTopFn, - - // AST-returning scope helpers — prototypal scope chain - sv: (key: JsNode = id(localFn("varName"))) => - index(id(localFn("scopeWalk")), key), - curSv: (key: JsNode = id(localFn("varName"))) => - index(id(names.scope), key), - scopeWalk: ( - body: JsNode[], - key: JsNode = id(localFn("varName")) - ): JsNode[] => [ - whileStmt(id(localFn("scopeWalk")), [ - ifStmt( - call(member(id(names.hop), "call"), [ - id(localFn("scopeWalk")), - key, - ]), - [...body, breakStmt()] - ), - exprStmt( - assign( - id(localFn("scopeWalk")), - call(member(id("Object"), "getPrototypeOf"), [ - id(localFn("scopeWalk")), - ]) - ) - ), - ]), - breakStmt(), - ], - }; -} diff --git a/packages/ruam/src/ruamvm/handlers/scope.ts b/packages/ruam/src/ruamvm/handlers/scope.ts deleted file mode 100644 index f8fca43..0000000 --- a/packages/ruam/src/ruamvm/handlers/scope.ts +++ /dev/null @@ -1,403 +0,0 @@ -/** - * Scope opcode handlers in AST node form. - * - * Covers 16 opcodes across scope chain operations: - * - Load/store: LOAD_SCOPED, STORE_SCOPED - * - Declare: DECLARE_VAR, DECLARE_LET, DECLARE_CONST - * - Push/pop: PUSH_SCOPE, PUSH_BLOCK_SCOPE, PUSH_CATCH_SCOPE, POP_SCOPE - * - TDZ: TDZ_CHECK, TDZ_MARK - * - With: PUSH_WITH_SCOPE - * - Delete: DELETE_SCOPED - * - Global: LOAD_GLOBAL, STORE_GLOBAL, TYPEOF_GLOBAL - * - * All handlers use pure AST nodes — no raw() escape hatch. - * - * Scope chain is prototypal: `Object.create(parent)` for push, - * `Object.getPrototypeOf(scope)` for pop. Variables are own - * properties on the scope object. The `in` operator traverses the - * prototype chain automatically for reads; stores must walk with - * `hasOwnProperty` to find the owning scope. - * - * TDZ uses a sentinel object (per-build unique, stored at IIFE scope). - * `TDZ_CHECK` compares `SC[name] === sentinel`; `TDZ_MARK` is a no-op - * (the subsequent assignment overwrites the sentinel). - * - * @module ruamvm/handlers/scope - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - bin, - un, - assign, - call, - member, - index, - varDecl, - exprStmt, - ifStmt, - whileStmt, - throwStmt, - breakStmt, - newExpr, - BOp, - UOp, -} from "../nodes.js"; -import type { Name } from "../../naming/index.js"; -import type { HandlerCtx } from "./registry.js"; -import { registry } from "./registry.js"; - -// --- Helpers --- - -/** - * Build `_hop.call(obj, key)` AST node using cached hasOwnProperty. - * - * Root scopes created via `Object.create(null)` have no prototype, - * so `obj.hasOwnProperty(key)` would throw. The cached `_hop` - * reference (Object.prototype.hasOwnProperty) at IIFE scope avoids - * a 4-level property chain on every call. - */ -function hasOwn(hopName: Name, obj: JsNode, key: JsNode): JsNode { - return call(member(id(hopName), "call"), [obj, key]); -} - -// --- Load / store scoped --- - -/** - * LOAD_SCOPED: use `in` operator to check prototype chain, fall back to global. - * - * The `in` operator traverses the prototype chain automatically, so a - * single check replaces the old manual while-loop. - */ -function LOAD_SCOPED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - // Fast path: read once, check for sentinel/undefined - varDecl(ctx.local("storedVal"), ctx.curSv()), - // If the value is not undefined and not TDZ sentinel, push directly - // (avoids the `in` prototype chain traversal entirely) - ifStmt( - bin(BOp.Sneq, id(ctx.local("storedVal")), un(UOp.Void, lit(0))), - [ - // TDZ check: if storedVal === tdzSentinel, throw - ifStmt( - bin( - BOp.Seq, - id(ctx.local("storedVal")), - id(ctx.tdzSentinel) - ), - [ - throwStmt( - newExpr(id("ReferenceError"), [ - bin( - BOp.Add, - bin( - BOp.Add, - lit("Cannot access '"), - id(ctx.local("varName")) - ), - lit("' before initialization") - ), - ]) - ), - ] - ), - exprStmt(ctx.push(id(ctx.local("storedVal")))), - breakStmt(), - ] - ), - // Slow path: value was undefined — need `in` to distinguish - // "property exists with value undefined" from "property not found" - ifStmt(bin(BOp.In, id(ctx.local("varName")), id(ctx.SC)), [ - exprStmt(ctx.push(id(ctx.local("storedVal")))), - breakStmt(), - ]), - // Global fallback - exprStmt(ctx.push(index(id(ctx.t("_g")), id(ctx.local("varName"))))), - breakStmt(), - ]; -} - -/** - * STORE_SCOPED: walk scope chain with hasOwnProperty to find owning scope. - * - * Must find the specific scope that owns the variable (can't use `in` - * because that would always match the first scope in the chain). Falls - * back to global assignment. - */ -function STORE_SCOPED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("value"), ctx.pop()), - // Fast path: check current scope first (most stores are local) - ifStmt(hasOwn(ctx.hop, id(ctx.SC), id(ctx.local("varName"))), [ - exprStmt(assign(ctx.curSv(), id(ctx.local("value")))), - breakStmt(), - ]), - // Slow path: walk parent scopes - varDecl( - ctx.local("scopeWalk"), - call(member(id("Object"), "getPrototypeOf"), [id(ctx.SC)]) - ), - varDecl(ctx.local("found"), lit(false)), - whileStmt(id(ctx.local("scopeWalk")), [ - ifStmt( - hasOwn( - ctx.hop, - id(ctx.local("scopeWalk")), - id(ctx.local("varName")) - ), - [ - exprStmt(assign(ctx.sv(), id(ctx.local("value")))), - exprStmt(assign(id(ctx.local("found")), lit(true))), - breakStmt(), - ] - ), - exprStmt( - assign( - id(ctx.local("scopeWalk")), - call(member(id("Object"), "getPrototypeOf"), [ - id(ctx.local("scopeWalk")), - ]) - ) - ), - ]), - // Global fallback - ifStmt(un(UOp.Not, id(ctx.local("found"))), [ - exprStmt( - assign( - index(id(ctx.t("_g")), id(ctx.local("varName"))), - id(ctx.local("value")) - ) - ), - ]), - breakStmt(), - ]; -} - -// --- Declarations --- - -/** - * DECLARE_VAR: declare variable as own property on current scope. - * - * Only initializes to undefined if the name is not already an own property. - */ -function declareVarHandler(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - ifStmt( - un(UOp.Not, hasOwn(ctx.hop, id(ctx.SC), id(ctx.local("varName")))), - [exprStmt(assign(ctx.curSv(), un(UOp.Void, lit(0))))] - ), - breakStmt(), - ]; -} - -/** - * DECLARE_LET / DECLARE_CONST: declare variable with TDZ sentinel. - * - * Sets the variable to the TDZ sentinel value. TDZ_CHECK will throw - * if the variable is still the sentinel when accessed. - */ -function declareLetConstHandler(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - exprStmt(assign(ctx.curSv(), id(ctx.tdzSentinel))), - breakStmt(), - ]; -} - -// --- Push / pop scope --- - -/** - * PUSH_SCOPE / PUSH_BLOCK_SCOPE / PUSH_CATCH_SCOPE: create a new scope. - * - * Uses `Object.create(SC)` — the current scope becomes the prototype, - * so `in` operator and property lookups naturally traverse the chain. - */ -function pushScopeHandler(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt( - assign( - id(ctx.SC), - call(member(id("Object"), "create"), [id(ctx.SC)]) - ) - ), - breakStmt(), - ]; -} - -/** - * POP_SCOPE: restore parent scope via Object.getPrototypeOf. - * - * If already at root (prototype is null), stay at current scope. - */ -function POP_SCOPE(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt( - assign( - id(ctx.SC), - bin( - BOp.Or, - call(member(id("Object"), "getPrototypeOf"), [id(ctx.SC)]), - id(ctx.SC) - ) - ) - ), - breakStmt(), - ]; -} - -// --- TDZ (Temporal Dead Zone) --- - -/** - * TDZ_CHECK: throw ReferenceError if variable is still the TDZ sentinel. - */ -function TDZ_CHECK(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - ifStmt(bin(BOp.Seq, ctx.curSv(), id(ctx.tdzSentinel)), [ - throwStmt( - newExpr(id("ReferenceError"), [ - bin( - BOp.Add, - bin( - BOp.Add, - lit("Cannot access '"), - id(ctx.local("varName")) - ), - lit("' before initialization") - ), - ]) - ), - ]), - breakStmt(), - ]; -} - -/** - * TDZ_MARK: variable has been initialized — no-op. - * - * The subsequent STORE_SCOPED / assignment overwrites the sentinel, - * so TDZ_MARK doesn't need to do anything. - */ -function TDZ_MARK(_ctx: HandlerCtx): JsNode[] { - return [breakStmt()]; -} - -// --- With scope --- - -/** - * PUSH_WITH_SCOPE: pop an object from the stack and create a scope - * that delegates to it via `Object.create`. - * - * The `with` object's properties become the scope's own properties - * through prototype delegation — `in` operator finds them naturally. - */ -function PUSH_WITH_SCOPE(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("withObj"), ctx.pop()), - // Create a scope whose prototype is the current scope, - // then copy the with-object's properties as own properties. - // Object.assign(Object.create(SC), wObj) gives us both: - // with-object properties as own, parent chain as prototype. - exprStmt( - assign( - id(ctx.SC), - call(member(id("Object"), "assign"), [ - call(member(id("Object"), "create"), [id(ctx.SC)]), - id(ctx.local("withObj")), - ]) - ) - ), - breakStmt(), - ]; -} - -// --- Delete scoped --- - -/** - * DELETE_SCOPED: walk scope chain to find and delete variable. - * - * Pushes the result of `delete` onto the stack. - */ -function DELETE_SCOPED(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - varDecl(ctx.local("scopeWalk"), id(ctx.SC)), - ...ctx.scopeWalk([exprStmt(ctx.push(un(UOp.Delete, ctx.sv())))]), - ]; -} - -// --- Global access --- - -/** LOAD_GLOBAL: load a global variable by name. */ -function LOAD_GLOBAL(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("global"), id(ctx.t("_g"))), - exprStmt( - ctx.push( - index(id(ctx.local("global")), index(id(ctx.C), id(ctx.O))) - ) - ), - breakStmt(), - ]; -} - -/** STORE_GLOBAL: store a value to a global variable by name. */ -function STORE_GLOBAL(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("global"), id(ctx.t("_g"))), - exprStmt( - assign( - index(id(ctx.local("global")), index(id(ctx.C), id(ctx.O))), - ctx.pop() - ) - ), - breakStmt(), - ]; -} - -/** - * TYPEOF_GLOBAL: check prototype chain first (for closures), then fall - * back to `typeof _g[name]` for true globals. - * - * Uses `in` operator on SC (traverses prototype chain) for fast check. - */ -function TYPEOF_GLOBAL(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("varName"), index(id(ctx.C), id(ctx.O))), - ifStmt(bin(BOp.In, id(ctx.local("varName")), id(ctx.SC)), [ - exprStmt(ctx.push(un(UOp.Typeof, ctx.curSv()))), - breakStmt(), - ]), - exprStmt( - ctx.push( - un(UOp.Typeof, index(id(ctx.t("_g")), id(ctx.local("varName")))) - ) - ), - breakStmt(), - ]; -} - -// --- Registration --- - -registry.set(Op.LOAD_SCOPED, LOAD_SCOPED); -registry.set(Op.STORE_SCOPED, STORE_SCOPED); -registry.set(Op.DECLARE_VAR, declareVarHandler); -registry.set(Op.DECLARE_LET, declareLetConstHandler); -registry.set(Op.DECLARE_CONST, declareLetConstHandler); -registry.set(Op.PUSH_SCOPE, pushScopeHandler); -registry.set(Op.PUSH_BLOCK_SCOPE, pushScopeHandler); -registry.set(Op.PUSH_CATCH_SCOPE, pushScopeHandler); -registry.set(Op.POP_SCOPE, POP_SCOPE); -registry.set(Op.TDZ_CHECK, TDZ_CHECK); -registry.set(Op.TDZ_MARK, TDZ_MARK); -registry.set(Op.PUSH_WITH_SCOPE, PUSH_WITH_SCOPE); -registry.set(Op.DELETE_SCOPED, DELETE_SCOPED); -registry.set(Op.LOAD_GLOBAL, LOAD_GLOBAL); -registry.set(Op.STORE_GLOBAL, STORE_GLOBAL); -registry.set(Op.TYPEOF_GLOBAL, TYPEOF_GLOBAL); diff --git a/packages/ruam/src/ruamvm/handlers/special.ts b/packages/ruam/src/ruamvm/handlers/special.ts deleted file mode 100644 index 52005db..0000000 --- a/packages/ruam/src/ruamvm/handlers/special.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Special value push and arguments opcode handlers in AST node form. - * - * Covers 8 opcodes: - * - Value push: PUSH_THIS, PUSH_ARGUMENTS, PUSH_NEW_TARGET, PUSH_GLOBAL_THIS, - * PUSH_WELL_KNOWN_SYMBOL - * - Arguments: CREATE_UNMAPPED_ARGS, CREATE_MAPPED_ARGS, CREATE_REST_ARGS - * - * All handlers use pure AST nodes — no raw() escape hatch. - * - * @module ruamvm/handlers/special - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - bin, - member, - call, - arr, - spread, - varDecl, - exprStmt, - breakStmt, - index, - BOp, -} from "../nodes.js"; -import { registry, type HandlerCtx } from "./registry.js"; - -// --- Simple push handlers --- - -/** `S[++P]=TV;break;` — push `this` value */ -function PUSH_THIS(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(id(ctx.TV))), breakStmt()]; -} - -/** `S[++P]=A;break;` — push arguments object */ -function PUSH_ARGUMENTS(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(id(ctx.A))), breakStmt()]; -} - -/** `S[++P]=NT;break;` — push new.target */ -function PUSH_NEW_TARGET(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(id(ctx.NT))), breakStmt()]; -} - -// --- Complex push handlers --- - -/** - * PUSH_GLOBAL_THIS: `{var g=_g;W(g);break;}` - * - * Uses intermediate `var g` to match the original runtime pattern. - */ -function PUSH_GLOBAL_THIS(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("global"), id(ctx.t("_g"))), - exprStmt(ctx.push(id(ctx.local("global")))), - breakStmt(), - ]; -} - -/** - * PUSH_WELL_KNOWN_SYMBOL: push a well-known Symbol by index. - * - * ``` - * var syms=[Symbol.iterator,Symbol.asyncIterator,Symbol.hasInstance, - * Symbol.toPrimitive,Symbol.toStringTag,Symbol.species, - * Symbol.isConcatSpreadable,Symbol.match,Symbol.replace, - * Symbol.search,Symbol.split,Symbol.unscopables]; - * W(syms[O]||Symbol.iterator);break; - * ``` - */ -function PUSH_WELL_KNOWN_SYMBOL(ctx: HandlerCtx): JsNode[] { - return [ - varDecl( - ctx.local("symbols"), - arr( - member(id("Symbol"), "iterator"), - member(id("Symbol"), "asyncIterator"), - member(id("Symbol"), "hasInstance"), - member(id("Symbol"), "toPrimitive"), - member(id("Symbol"), "toStringTag"), - member(id("Symbol"), "species"), - member(id("Symbol"), "isConcatSpreadable"), - member(id("Symbol"), "match"), - member(id("Symbol"), "replace"), - member(id("Symbol"), "search"), - member(id("Symbol"), "split"), - member(id("Symbol"), "unscopables") - ) - ), - exprStmt( - ctx.push( - bin( - BOp.Or, - index(id(ctx.local("symbols")), id(ctx.O)), - member(id("Symbol"), "iterator") - ) - ) - ), - breakStmt(), - ]; -} - -// --- Arguments handlers --- - -/** - * CREATE_UNMAPPED_ARGS / CREATE_MAPPED_ARGS: `W([...A]);break;` - * - * Both opcodes produce the same handler — a spread copy of the arguments object. - */ -function CREATE_ARGS_COPY(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(arr(spread(id(ctx.A))))), breakStmt()]; -} - -/** - * CREATE_REST_ARGS: `W(A.slice(O));break;` - * - * Slices arguments from the operand index onward (rest parameter start). - */ -function CREATE_REST_ARGS(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt(ctx.push(call(member(id(ctx.A), "slice"), [id(ctx.O)]))), - breakStmt(), - ]; -} - -// --- Registration --- - -registry.set(Op.PUSH_THIS, PUSH_THIS); -registry.set(Op.PUSH_ARGUMENTS, PUSH_ARGUMENTS); -registry.set(Op.PUSH_NEW_TARGET, PUSH_NEW_TARGET); -registry.set(Op.PUSH_GLOBAL_THIS, PUSH_GLOBAL_THIS); -registry.set(Op.PUSH_WELL_KNOWN_SYMBOL, PUSH_WELL_KNOWN_SYMBOL); -registry.set(Op.CREATE_UNMAPPED_ARGS, CREATE_ARGS_COPY); -registry.set(Op.CREATE_MAPPED_ARGS, CREATE_ARGS_COPY); -registry.set(Op.CREATE_REST_ARGS, CREATE_REST_ARGS); diff --git a/packages/ruam/src/ruamvm/handlers/stack.ts b/packages/ruam/src/ruamvm/handlers/stack.ts deleted file mode 100644 index b81e238..0000000 --- a/packages/ruam/src/ruamvm/handlers/stack.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Stack manipulation opcode handlers in AST node form. - * - * Covers push/pop/dup/swap/rotate operations on the VM stack. - * Uses Array.push()/pop() and length-based indexing instead of a - * dedicated stack pointer variable — the stack looks like normal - * array manipulation rather than a VM stack machine. - * - * @module ruamvm/handlers/stack - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - exprStmt, - breakStmt, - varDecl, - id, - lit, - bin, - un, - assign, - index, - member, - call, - BOp, - UOp, - AOp, -} from "../nodes.js"; -import { type HandlerCtx, registry } from "./registry.js"; - -// --- Push handlers --- - -function PUSH_CONST(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(index(id(ctx.C), id(ctx.O)))), breakStmt()]; -} - -function PUSH_UNDEFINED(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(un(UOp.Void, lit(0)))), breakStmt()]; -} - -function PUSH_NULL(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(lit(null))), breakStmt()]; -} - -function PUSH_TRUE(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(lit(true))), breakStmt()]; -} - -function PUSH_FALSE(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(lit(false))), breakStmt()]; -} - -function PUSH_ZERO(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(lit(0))), breakStmt()]; -} - -function PUSH_ONE(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(lit(1))), breakStmt()]; -} - -function PUSH_NEG_ONE(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(un(UOp.Neg, lit(1)))), breakStmt()]; -} - -function PUSH_EMPTY_STRING(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(lit(""))), breakStmt()]; -} - -function PUSH_NAN(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(id("NaN"))), breakStmt()]; -} - -function PUSH_INFINITY(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(id("Infinity"))), breakStmt()]; -} - -function PUSH_NEG_INFINITY(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.push(un(UOp.Neg, id("Infinity")))), breakStmt()]; -} - -// --- Pop / stack pointer handlers --- - -/** POP: discard top of stack. */ -function POP(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.pop()), breakStmt()]; -} - -/** POP_N: discard top N elements from stack. */ -function POP_N(ctx: HandlerCtx): JsNode[] { - // S.length -= O - return [ - exprStmt(assign(member(id(ctx.S), "length"), id(ctx.O), AOp.Sub)), - breakStmt(), - ]; -} - -// --- Duplication handlers --- - -/** DUP: duplicate top of stack. */ -function DUP(ctx: HandlerCtx): JsNode[] { - // S.push(S[S.length-1]) - return [exprStmt(ctx.push(ctx.peek())), breakStmt()]; -} - -/** Fresh `S.length - k` AST (a new node each call — required by slotRead/slotWrite). */ -function depth(ctx: HandlerCtx, k: number): () => JsNode { - return () => bin(BOp.Sub, member(id(ctx.S), "length"), lit(k)); -} - -/** DUP2: duplicate top two elements. */ -function DUP2(ctx: HandlerCtx): JsNode[] { - return [ - // var _a=S[S.length-2], _b=S[S.length-1] (decoded reads) - varDecl(ctx.t("_a"), ctx.slotRead(depth(ctx, 2))), - varDecl(ctx.t("_b"), ctx.peek()), - // S.push(_a); S.push(_b) (re-encoded at the new positions) - exprStmt(ctx.push(id(ctx.t("_a")))), - exprStmt(ctx.push(id(ctx.t("_b")))), - breakStmt(), - ]; -} - -// --- Swap / rotate handlers --- -// Under stackEncoding, slotRead decodes at the source index and slotWrite -// re-encodes at the destination index, so every rotated value is correctly -// re-keyed to its new position (the legacy Proxy did this via get/set traps). - -/** SWAP: swap top two elements via direct index access (no pop/push). */ -function SWAP(ctx: HandlerCtx): JsNode[] { - // var _t=S[len-1]; S[len-1]=S[len-2]; S[len-2]=_t; - return [ - varDecl(ctx.local("swapTemp"), ctx.slotRead(depth(ctx, 1))), - exprStmt(ctx.slotWrite(depth(ctx, 1), ctx.slotRead(depth(ctx, 2)))), - exprStmt(ctx.slotWrite(depth(ctx, 2), id(ctx.local("swapTemp")))), - breakStmt(), - ]; -} - -/** ROT3: rotate top 3 elements (abc -> cab) via direct index access. */ -function ROT3(ctx: HandlerCtx): JsNode[] { - // abc -> cab: S[top-2]=c, S[top-1]=a, S[top]=b - return [ - varDecl(ctx.local("rotC"), ctx.slotRead(depth(ctx, 1))), // _c = top - varDecl(ctx.local("rotB"), ctx.slotRead(depth(ctx, 2))), // _b - varDecl(ctx.local("rotA"), ctx.slotRead(depth(ctx, 3))), // _a - exprStmt(ctx.slotWrite(depth(ctx, 3), id(ctx.local("rotC")))), - exprStmt(ctx.slotWrite(depth(ctx, 2), id(ctx.local("rotA")))), - exprStmt(ctx.slotWrite(depth(ctx, 1), id(ctx.local("rotB")))), - breakStmt(), - ]; -} - -/** ROT4: rotate top 4 elements (abcd -> dabc) via direct index access. */ -function ROT4(ctx: HandlerCtx): JsNode[] { - // abcd -> dabc - return [ - varDecl(ctx.local("rotD"), ctx.slotRead(depth(ctx, 1))), - varDecl(ctx.local("rotC"), ctx.slotRead(depth(ctx, 2))), - varDecl(ctx.local("rotB"), ctx.slotRead(depth(ctx, 3))), - varDecl(ctx.local("rotA"), ctx.slotRead(depth(ctx, 4))), - exprStmt(ctx.slotWrite(depth(ctx, 4), id(ctx.local("rotD")))), - exprStmt(ctx.slotWrite(depth(ctx, 3), id(ctx.local("rotA")))), - exprStmt(ctx.slotWrite(depth(ctx, 2), id(ctx.local("rotB")))), - exprStmt(ctx.slotWrite(depth(ctx, 1), id(ctx.local("rotC")))), - breakStmt(), - ]; -} - -// --- Pick handler --- - -/** PICK: copy element at depth O onto top of stack. */ -function PICK(ctx: HandlerCtx): JsNode[] { - // S.push(S[S.length-1-O]) — read at depth (1+O), re-encoded onto the top. - const pickIdx = (): JsNode => - bin( - BOp.Sub, - bin(BOp.Sub, member(id(ctx.S), "length"), lit(1)), - id(ctx.O) - ); - return [ - exprStmt(ctx.push(ctx.slotRead(pickIdx))), - breakStmt(), - ]; -} - -// --- Registration --- - -registry.set(Op.PUSH_CONST, PUSH_CONST); -registry.set(Op.PUSH_UNDEFINED, PUSH_UNDEFINED); -registry.set(Op.PUSH_NULL, PUSH_NULL); -registry.set(Op.PUSH_TRUE, PUSH_TRUE); -registry.set(Op.PUSH_FALSE, PUSH_FALSE); -registry.set(Op.PUSH_ZERO, PUSH_ZERO); -registry.set(Op.PUSH_ONE, PUSH_ONE); -registry.set(Op.PUSH_NEG_ONE, PUSH_NEG_ONE); -registry.set(Op.PUSH_EMPTY_STRING, PUSH_EMPTY_STRING); -registry.set(Op.PUSH_NAN, PUSH_NAN); -registry.set(Op.PUSH_INFINITY, PUSH_INFINITY); -registry.set(Op.PUSH_NEG_INFINITY, PUSH_NEG_INFINITY); -registry.set(Op.POP, POP); -registry.set(Op.POP_N, POP_N); -registry.set(Op.DUP, DUP); -registry.set(Op.DUP2, DUP2); -registry.set(Op.SWAP, SWAP); -registry.set(Op.ROT3, ROT3); -registry.set(Op.ROT4, ROT4); -registry.set(Op.PICK, PICK); diff --git a/packages/ruam/src/ruamvm/handlers/superinstructions.ts b/packages/ruam/src/ruamvm/handlers/superinstructions.ts deleted file mode 100644 index 419dbe8..0000000 --- a/packages/ruam/src/ruamvm/handlers/superinstructions.ts +++ /dev/null @@ -1,410 +0,0 @@ -/** - * Superinstruction opcode handlers in AST node form. - * - * Covers 28 fused opcodes that combine register access with arithmetic, - * comparison, property access, and conditional jumps: - * - Dual-register binary: REG_ADD, REG_SUB, REG_MUL, REG_DIV, REG_MOD, - * REG_LT, REG_LTE, REG_GT, REG_GTE, REG_SEQ, REG_SNEQ - * - Register+constant: REG_ADD_CONST, REG_CONST_SUB, REG_CONST_MUL, REG_CONST_MOD - * - Property access: REG_GET_PROP - * - Compare-and-branch (register vs constant, jump if false): - * REG_LT_CONST_JF, REG_LTE_CONST_JF, REG_GT_CONST_JF, - * REG_GTE_CONST_JF, REG_SEQ_CONST_JF, REG_SNEQ_CONST_JF - * - Compare-and-branch (register vs register, jump if false): - * REG_LT_REG_JF, REG_LTE_REG_JF, REG_GT_REG_JF, - * REG_GTE_REG_JF, REG_SEQ_REG_JF, REG_SNEQ_REG_JF - * - * All handlers use pure AST nodes with bit-packing extraction from the operand - * field and multi-step control flow for conditional jump variants. The - * compare-and-branch handlers are parameterized by operator so every - * comparison shares one implementation. - * - * @module ruamvm/handlers/superinstructions - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - bin, - un, - index, - assign, - varDecl, - exprStmt, - ifStmt, - breakStmt, - BOp, - UOp, - type BOpKind, -} from "../nodes.js"; -import type { HandlerCtx, HandlerFn } from "./registry.js"; -import { registry } from "./registry.js"; - -// --- Helpers --- - -/** - * Low 16 bits of the operand: `O & 0xFFFF` - * - * @param ctx - Handler context - * @returns AST expression for `O & 0xFFFF` - */ -function lo16(ctx: HandlerCtx): JsNode { - return bin(BOp.BitAnd, id(ctx.O), lit(0xffff)); -} - -/** - * High 16 bits of the operand: `(O >>> 16) & 0xFFFF` - * - * @param ctx - Handler context - * @returns AST expression for `(O >>> 16) & 0xFFFF` - */ -function hi16(ctx: HandlerCtx): JsNode { - return bin(BOp.BitAnd, bin(BOp.Ushr, id(ctx.O), lit(16)), lit(0xffff)); -} - -/** - * Low 8 bits of the operand: `O & 0xFF` - * - * @param ctx - Handler context - * @returns AST expression for `O & 0xFF` - */ -function lo8(ctx: HandlerCtx): JsNode { - return bin(BOp.BitAnd, id(ctx.O), lit(0xff)); -} - -/** - * Bits 8-15 of the operand: `(O >>> 8) & 0xFF` - * - * @param ctx - Handler context - * @returns AST expression for `(O >>> 8) & 0xFF` - */ -function mid8(ctx: HandlerCtx): JsNode { - return bin(BOp.BitAnd, bin(BOp.Ushr, id(ctx.O), lit(8)), lit(0xff)); -} - -/** - * Build a dual-register binary operation handler. - * - * Extracts two register indices from the packed operand (low 16 bits = ra, - * high 16 bits = rb) and pushes the result of `R[ra] R[rb]`. - * - * Pattern: `{var ra=O&0xFFFF;var rb=(O>>>16)&0xFFFF;W(R[ra] R[rb]);break;}` - * - * @param op - The JS binary operator string (e.g. `'+'`, `'<'`, `'==='`) - * @returns Handler function producing the case body - */ -function regBinOp(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("regA"), lo16(ctx)), - varDecl(ctx.local("regB"), hi16(ctx)), - exprStmt( - ctx.push( - bin( - op, - index(id(ctx.R), id(ctx.local("regA"))), - index(id(ctx.R), id(ctx.local("regB"))) - ) - ) - ), - breakStmt(), - ]; -} - -/** - * Build a register + constant pool operation handler. - * - * Extracts register index (low 16 bits) and constant index (high 16 bits), - * then applies the operation and stores back to the register. - * - * Pattern: `{var r=O&0xFFFF;var ci=(O>>>16)&0xFFFF;R[r]=R[r] C[ci];break;}` - * - * @param op - The JS binary operator string - * @returns Handler function producing the case body - */ -function regConstOp(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("reg"), lo16(ctx)), - varDecl(ctx.local("constIdx"), hi16(ctx)), - exprStmt( - assign( - index(id(ctx.R), id(ctx.local("reg"))), - bin( - op, - index(id(ctx.R), id(ctx.local("reg"))), - index(id(ctx.C), id(ctx.local("constIdx"))) - ) - ) - ), - breakStmt(), - ]; -} - -/** - * Build a register + constant pool operation that pushes the result. - * - * Same operand packing as regConstOp, but pushes to stack instead of - * storing back to register. - * - * Pattern: `{var r=O&0xFFFF;var ci=(O>>>16)&0xFFFF;W(R[r] C[ci]);break;}` - * - * @param op - The JS binary operator string - * @returns Handler function producing the case body - */ -function regConstPush(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("reg"), lo16(ctx)), - varDecl(ctx.local("constIdx"), hi16(ctx)), - exprStmt( - ctx.push( - bin( - op, - index(id(ctx.R), id(ctx.local("reg"))), - index(id(ctx.C), id(ctx.local("constIdx"))) - ) - ) - ), - breakStmt(), - ]; -} - -// --- Dual-register binary operations --- - -registry.set(Op.REG_ADD, regBinOp(BOp.Add)); -registry.set(Op.REG_SUB, regBinOp(BOp.Sub)); -registry.set(Op.REG_MUL, regBinOp(BOp.Mul)); -registry.set(Op.REG_DIV, regBinOp(BOp.Div)); -registry.set(Op.REG_MOD, regBinOp(BOp.Mod)); -registry.set(Op.REG_LT, regBinOp(BOp.Lt)); -registry.set(Op.REG_LTE, regBinOp(BOp.Lte)); -registry.set(Op.REG_GT, regBinOp(BOp.Gt)); -registry.set(Op.REG_GTE, regBinOp(BOp.Gte)); -registry.set(Op.REG_SEQ, regBinOp(BOp.Seq)); -registry.set(Op.REG_SNEQ, regBinOp(BOp.Sneq)); - -// --- Register + constant operations --- - -registry.set(Op.REG_ADD_CONST, regConstOp(BOp.Add)); -registry.set(Op.REG_CONST_SUB, regConstPush(BOp.Sub)); -registry.set(Op.REG_CONST_MUL, regConstPush(BOp.Mul)); -registry.set(Op.REG_CONST_MOD, regConstPush(BOp.Mod)); - -// --- Property access --- - -/** - * REG_GET_PROP: get a named property from a register value. - * - * Extracts register index (low 16 bits) and property name constant index - * (high 16 bits), then pushes `R[r][C[ni]]`. - */ -function REG_GET_PROP(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("reg"), lo16(ctx)), - varDecl(ctx.local("nameIdx"), hi16(ctx)), - exprStmt( - ctx.push( - index( - index(id(ctx.R), id(ctx.local("reg"))), - index(id(ctx.C), id(ctx.local("nameIdx"))) - ) - ) - ), - breakStmt(), - ]; -} -registry.set(Op.REG_GET_PROP, REG_GET_PROP); - -/** - * REG_GET_PROP_DYN: dynamic (computed) property get from two registers. - * - * Fuses `LOAD_REG(a) + LOAD_REG(b) + GET_PROP_DYNAMIC` — pushes `R[a][R[b]]`. - * Pure read; no receiver/`this`/write semantics. Operand: a (low 16) | b (high 16). - * Encoding-aware: the result is pushed via `ctx.push`. - */ -function REG_GET_PROP_DYN(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("regA"), lo16(ctx)), - varDecl(ctx.local("regB"), hi16(ctx)), - exprStmt( - ctx.push( - index( - index(id(ctx.R), id(ctx.local("regA"))), - index(id(ctx.R), id(ctx.local("regB"))) - ) - ) - ), - breakStmt(), - ]; -} -registry.set(Op.REG_GET_PROP_DYN, REG_GET_PROP_DYN); - -/** - * IDX_REG: index the top-of-stack object by a register value. - * - * Fuses `LOAD_REG(r) + GET_PROP_DYNAMIC` — replaces TOS `obj` with `obj[R[r]]` - * (object stays on stack, consumed in place). Operand: r. - * - * Encoding-aware: reads the top via `ctx.peek()` (decoded) and overwrites it - * via `ctx.setTop(...)` — NOT `assign(ctx.peek(), …)`, which is invalid under - * stackEncoding (there `peek()` is a `stkDec(...)` call and cannot be an lvalue). - */ -function IDX_REG(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt(ctx.setTop(index(ctx.peek(), index(id(ctx.R), id(ctx.O))))), - breakStmt(), - ]; -} -registry.set(Op.IDX_REG, IDX_REG); - -// --- Conditional jump operations --- - -/** - * Build a fused "compare register vs constant, jump if false" handler. - * - * Operand packing: r=low 8 bits, ci=bits 8-15, tgt=bits 16-31. - * - * Pattern: - * ``` - * var r=O&0xFF;var ci=(O>>>8)&0xFF;var tgt=(O>>>16)&0xFFFF; - * if(!(R[r] C[ci]))IP=tgt*2;break; - * ``` - * - * @param op - The JS comparison operator (e.g. BOp.Lt, BOp.Seq) - * @returns Handler function producing the case body - */ -function regConstCmpJf(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("reg"), lo8(ctx)), - varDecl(ctx.local("constIdx"), mid8(ctx)), - varDecl(ctx.local("target"), hi16(ctx)), - ifStmt( - un( - UOp.Not, - bin( - op, - index(id(ctx.R), id(ctx.local("reg"))), - index(id(ctx.C), id(ctx.local("constIdx"))) - ) - ), - [ - exprStmt( - assign( - id(ctx.IP), - bin(BOp.Mul, id(ctx.local("target")), lit(2)) - ) - ), - ] - ), - breakStmt(), - ]; -} - -/** - * Build a fused "compare register vs register, jump if false" handler. - * - * Operand packing: ra=low 8 bits, rb=bits 8-15, tgt=bits 16-31. - * - * Pattern: - * ``` - * var ra=O&0xFF;var rb=(O>>>8)&0xFF;var tgt=(O>>>16)&0xFFFF; - * if(!(R[ra] R[rb]))IP=tgt*2;break; - * ``` - * - * @param op - The JS comparison operator (e.g. BOp.Lt, BOp.Seq) - * @returns Handler function producing the case body - */ -function regRegCmpJf(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("regA"), lo8(ctx)), - varDecl(ctx.local("regB"), mid8(ctx)), - varDecl(ctx.local("target"), hi16(ctx)), - ifStmt( - un( - UOp.Not, - bin( - op, - index(id(ctx.R), id(ctx.local("regA"))), - index(id(ctx.R), id(ctx.local("regB"))) - ) - ), - [ - exprStmt( - assign( - id(ctx.IP), - bin(BOp.Mul, id(ctx.local("target")), lit(2)) - ) - ), - ] - ), - breakStmt(), - ]; -} - -// Register-vs-constant compare-and-branch (jump if false). -registry.set(Op.REG_LT_CONST_JF, regConstCmpJf(BOp.Lt)); -registry.set(Op.REG_LTE_CONST_JF, regConstCmpJf(BOp.Lte)); -registry.set(Op.REG_GT_CONST_JF, regConstCmpJf(BOp.Gt)); -registry.set(Op.REG_GTE_CONST_JF, regConstCmpJf(BOp.Gte)); -registry.set(Op.REG_SEQ_CONST_JF, regConstCmpJf(BOp.Seq)); -registry.set(Op.REG_SNEQ_CONST_JF, regConstCmpJf(BOp.Sneq)); - -// Register-vs-register compare-and-branch (jump if false). -registry.set(Op.REG_LT_REG_JF, regRegCmpJf(BOp.Lt)); -registry.set(Op.REG_LTE_REG_JF, regRegCmpJf(BOp.Lte)); -registry.set(Op.REG_GT_REG_JF, regRegCmpJf(BOp.Gt)); -registry.set(Op.REG_GTE_REG_JF, regRegCmpJf(BOp.Gte)); -registry.set(Op.REG_SEQ_REG_JF, regRegCmpJf(BOp.Seq)); -registry.set(Op.REG_SNEQ_REG_JF, regRegCmpJf(BOp.Sneq)); - -/** - * Build a fused "compare TOS vs constant, jump if false" handler. - * - * Fuses `PUSH_CONST(c) + + JMP_FALSE(t)`. The comparison LHS is the value - * already on top of the stack (the elided PUSH_CONST would have pushed the RHS), - * so it is popped and compared against `C[ci]` — operand order preserved (popped - * LHS on the left, constant on the right), matching the cmp handler's - * `S[top] S.pop()` semantics. - * - * Operand packing: ci=low 16 bits, tgt=bits 16-31. - * - * Pattern: - * ``` - * var ci=O&0xFFFF;var tgt=(O>>>16)&0xFFFF; - * if(!(S.pop() C[ci]))IP=tgt*2;break; - * ``` - * - * Encoding-aware: the LHS is read via `ctx.pop()` (decoded under stackEncoding). - * - * @param op - The JS comparison operator (e.g. BOp.Lt, BOp.Seq) - * @returns Handler function producing the case body - */ -function constCmpJf(op: BOpKind): HandlerFn { - return (ctx) => [ - varDecl(ctx.local("constIdx"), lo16(ctx)), - varDecl(ctx.local("target"), hi16(ctx)), - ifStmt( - un( - UOp.Not, - bin(op, ctx.pop(), index(id(ctx.C), id(ctx.local("constIdx")))) - ), - [ - exprStmt( - assign( - id(ctx.IP), - bin(BOp.Mul, id(ctx.local("target")), lit(2)) - ) - ), - ] - ), - breakStmt(), - ]; -} - -// Constant-vs-TOS compare-and-branch (jump if false). -registry.set(Op.CONST_LT_JF, constCmpJf(BOp.Lt)); -registry.set(Op.CONST_LTE_JF, constCmpJf(BOp.Lte)); -registry.set(Op.CONST_GT_JF, constCmpJf(BOp.Gt)); -registry.set(Op.CONST_GTE_JF, constCmpJf(BOp.Gte)); -registry.set(Op.CONST_SEQ_JF, constCmpJf(BOp.Seq)); -registry.set(Op.CONST_SNEQ_JF, constCmpJf(BOp.Sneq)); diff --git a/packages/ruam/src/ruamvm/handlers/type-ops.ts b/packages/ruam/src/ruamvm/handlers/type-ops.ts deleted file mode 100644 index 8f1cb4a..0000000 --- a/packages/ruam/src/ruamvm/handlers/type-ops.ts +++ /dev/null @@ -1,384 +0,0 @@ -/** - * Type operation and miscellaneous opcode handlers in AST node form. - * - * Covers 18 opcodes: - * - Type ops: TYPEOF, VOID, TO_NUMBER, TO_STRING, TO_BOOLEAN, TO_OBJECT, - * TO_PROPERTY_KEY, TO_NUMERIC - * - Templates: TEMPLATE_LITERAL, TAGGED_TEMPLATE, CREATE_RAW_STRINGS - * - No-ops: DEBUGGER_STMT, COMMA, SOURCE_MAP - * - Meta: IMPORT_META, DYNAMIC_IMPORT - * - Assertions: ASSERT_DEFINED, ASSERT_FUNCTION - * - * All handlers use pure AST nodes — no raw() escape hatch. - * - * @module ruamvm/handlers/type-ops - */ - -import { Op } from "../../compiler/opcodes.js"; -import { - type JsNode, - id, - lit, - un, - bin, - assign, - call, - member, - index, - ternary, - newExpr, - spread, - varDecl, - exprStmt, - ifStmt, - forStmt, - throwStmt, - breakStmt, - update, - importExpr, - BOp, - UOp, - UpOp, - AOp, -} from "../nodes.js"; -import { registry, type HandlerCtx } from "./registry.js"; - -// --- Simple type coercions (AST nodes) --- - -/** `S[P]=typeof S[P];break;` */ -function TYPEOF(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt(ctx.setTop(un(UOp.Typeof, ctx.peek()))), - breakStmt(), - ]; -} - -/** `S[P]=void 0;break;` */ -function VOID(ctx: HandlerCtx): JsNode[] { - return [exprStmt(ctx.setTop(un(UOp.Void, lit(0)))), breakStmt()]; -} - -/** `S[P]=Number(S[P]);break;` */ -function TO_NUMBER(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt(ctx.setTop(call(id("Number"), [ctx.peek()]))), - breakStmt(), - ]; -} - -/** `S[P]=String(S[P]);break;` */ -function TO_STRING(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt(ctx.setTop(call(id("String"), [ctx.peek()]))), - breakStmt(), - ]; -} - -/** `S[P]=Boolean(S[P]);break;` */ -function TO_BOOLEAN(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt(ctx.setTop(call(id("Boolean"), [ctx.peek()]))), - breakStmt(), - ]; -} - -/** `S[P]=Object(S[P]);break;` */ -function TO_OBJECT(ctx: HandlerCtx): JsNode[] { - return [ - exprStmt(ctx.setTop(call(id("Object"), [ctx.peek()]))), - breakStmt(), - ]; -} - -// --- Conditional type coercions --- - -/** - * TO_PROPERTY_KEY: `{var v=S[P];S[P]=typeof v==='symbol'?v:String(v);break;}` - */ -function TO_PROPERTY_KEY(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.peek()), - exprStmt( - ctx.setTop( - ternary( - bin( - BOp.Seq, - un(UOp.Typeof, id(ctx.local("value"))), - lit("symbol") - ), - id(ctx.local("value")), - call(id("String"), [id(ctx.local("value"))]) - ) - ) - ), - breakStmt(), - ]; -} - -/** - * TO_NUMERIC: `{var v=S[P];S[P]=typeof v==='bigint'?v:Number(v);break;}` - */ -function TO_NUMERIC(ctx: HandlerCtx): JsNode[] { - return [ - varDecl(ctx.local("value"), ctx.peek()), - exprStmt( - ctx.setTop( - ternary( - bin( - BOp.Seq, - un(UOp.Typeof, id(ctx.local("value"))), - lit("bigint") - ), - id(ctx.local("value")), - call(id("Number"), [id(ctx.local("value"))]) - ) - ) - ), - breakStmt(), - ]; -} - -// --- Template handlers --- - -/** - * TEMPLATE_LITERAL: assemble template parts from the stack. - * - * ``` - * var exprCount=O;var parts=[]; - * for(var ti=exprCount*2;ti>=0;ti--)parts.unshift(X()); - * var result='';for(var ti=0;ti JsNode)[] = [ - // (x ^ y) + 2 * (x & y) - (x, y) => - bin( - BOp.Add, - bin(BOp.BitXor, x, y), - bin(BOp.Mul, lit(2), bin(BOp.BitAnd, x, y)) - ), - // (x | y) + (x & y) - (x, y) => bin(BOp.Add, bin(BOp.BitOr, x, y), bin(BOp.BitAnd, x, y)), - // 2 * (x | y) - (x ^ y) - (x, y) => - bin( - BOp.Sub, - bin(BOp.Mul, lit(2), bin(BOp.BitOr, x, y)), - bin(BOp.BitXor, x, y) - ), -]; - -/** MBA replacement for `x - y` (assumes int32 operands). */ -const SUB_VARIANTS: ((x: JsNode, y: JsNode) => JsNode)[] = [ - // (x ^ y) - 2 * (~x & y) - (x, y) => - bin( - BOp.Sub, - bin(BOp.BitXor, x, y), - bin(BOp.Mul, lit(2), bin(BOp.BitAnd, un(UOp.BitNot, x), y)) - ), - // (x & ~y) - (~x & y) - (x, y) => - bin( - BOp.Sub, - bin(BOp.BitAnd, x, un(UOp.BitNot, y)), - bin(BOp.BitAnd, un(UOp.BitNot, x), y) - ), -]; - -/** MBA replacement for `x ^ y`. */ -const XOR_VARIANTS: ((x: JsNode, y: JsNode) => JsNode)[] = [ - // (x | y) & ~(x & y) - (x, y) => - bin( - BOp.BitAnd, - bin(BOp.BitOr, x, y), - un(UOp.BitNot, bin(BOp.BitAnd, x, y)) - ), - // (~x & y) | (x & ~y) - (x, y) => - bin( - BOp.BitOr, - bin(BOp.BitAnd, un(UOp.BitNot, x), y), - bin(BOp.BitAnd, x, un(UOp.BitNot, y)) - ), -]; - -/** MBA replacement for `x & y`. */ -const AND_VARIANTS: ((x: JsNode, y: JsNode) => JsNode)[] = [ - // ~(~x | ~y) (De Morgan) - (x, y) => - un(UOp.BitNot, bin(BOp.BitOr, un(UOp.BitNot, x), un(UOp.BitNot, y))), - // (x | y) ^ (x ^ y) - (x, y) => bin(BOp.BitXor, bin(BOp.BitOr, x, y), bin(BOp.BitXor, x, y)), -]; - -/** MBA replacement for `x | y`. */ -const OR_VARIANTS: ((x: JsNode, y: JsNode) => JsNode)[] = [ - // ~(~x & ~y) (De Morgan) - (x, y) => - un(UOp.BitNot, bin(BOp.BitAnd, un(UOp.BitNot, x), un(UOp.BitNot, y))), - // (x ^ y) + (x & y) - (x, y) => bin(BOp.Add, bin(BOp.BitXor, x, y), bin(BOp.BitAnd, x, y)), -]; - -/** Map from operator to variant table (bitwise). */ -const BITWISE_MBA = new Map JsNode)[]>([ - [BOp.BitXor, XOR_VARIANTS], - [BOp.BitAnd, AND_VARIANTS], - [BOp.BitOr, OR_VARIANTS], -]); - -/** Map from operator to variant table (arithmetic — needs int32 guard). */ -const ARITH_MBA = new Map JsNode)[]>([ - [BOp.Add, ADD_VARIANTS], - [BOp.Sub, SUB_VARIANTS], -]); - -// --- LCG PRNG --- - -function makeLcg(seed: number): () => number { - let s = seed >>> 0; - return () => { - s = (s * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - return s; - }; -} - -// --- Int32 guard builder --- - -/** - * Build a runtime int32 guard ternary: - * `(a|0)===a && (b|0)===b ? mbaExpr : a op b` - */ -function int32Guard( - left: JsNode, - right: JsNode, - op: BOpKind, - mbaExpr: JsNode -): JsNode { - const leftCheck = bin(BOp.Seq, bin(BOp.BitOr, left, lit(0)), left); - const rightCheck = bin(BOp.Seq, bin(BOp.BitOr, right, lit(0)), right); - const guard = bin(BOp.And, leftCheck, rightCheck); - return ternary(guard, mbaExpr, bin(op, left, right)); -} - -// --- String / non-integer detection --- - -/** - * Check if a node sub-tree may produce a non-integer value. - * - * Walks BinOps, UnaryOps, and ternaries to find string literals - * or non-numeric literals that would make MBA semantically wrong - * or produce nonsensical bitwise-on-string expressions. - */ -function mayProduceNonInteger(node: JsNode): boolean { - if (node.type === "Literal") { - return typeof node.value === "string"; - } - if (node.type === "BinOp") { - return ( - mayProduceNonInteger(node.left) || mayProduceNonInteger(node.right) - ); - } - if (node.type === "UnaryOp") { - return mayProduceNonInteger(node.expr); - } - if (node.type === "TernaryExpr") { - return ( - mayProduceNonInteger(node.then) || mayProduceNonInteger(node.else) - ); - } - return false; -} - -// --- Core transform --- - -/** - * Apply a single level of MBA replacement to a BinOp node. - * - * @param node - The BinOp to transform - * @param lcg - PRNG for variant selection - * @returns Transformed JsNode, or the original if the op isn't MBA-eligible - */ -function mbaSingle(node: BinOp, lcg: () => number): JsNode { - const { op, left, right } = node; - - // Bitwise ops — skip when operands may contain non-integer values - const bitwiseVariants = BITWISE_MBA.get(op); - if (bitwiseVariants) { - if (mayProduceNonInteger(left) || mayProduceNonInteger(right)) { - return node; - } - const variant = bitwiseVariants[lcg() % bitwiseVariants.length]!; - return variant(left, right); - } - - // Arithmetic ops — need int32 guard for user values. - // Skip when either operand may produce a non-integer (string - // concatenation, object coercion, etc.) — the guard always fails - // at runtime, producing enormous dead code for no benefit. - const arithVariants = ARITH_MBA.get(op); - if (arithVariants) { - if (mayProduceNonInteger(left) || mayProduceNonInteger(right)) { - return node; - } - const variant = arithVariants[lcg() % arithVariants.length]!; - const mbaExpr = variant(left, right); - return int32Guard(left, right, op, mbaExpr); - } - - return node; -} - -/** Operators eligible for MBA transformation. */ -const MBA_OPS = new Set([ - BOp.BitXor, - BOp.BitAnd, - BOp.BitOr, - BOp.Add, - BOp.Sub, -]); - -/** - * Apply MBA transformation to all eligible BinOp nodes in a JsNode tree. - * - * Walks bottom-up, replacing eligible operations with MBA equivalents. - * Uses depth 1 (single pass) to avoid exponential expression growth — - * deeper nesting re-transforms int32Guard's own bitwise ops, producing - * enormous output for no additional security. - * - * @param nodes - Statement list to transform - * @param seed - LCG seed for deterministic variant selection - * @returns Transformed statement list - */ -export function applyMBA(nodes: JsNode[], seed: number): JsNode[] { - const lcg = makeLcg(seed); - - function walk(node: JsNode): JsNode { - // Walk children first (bottom-up) - const walked = mapChildren(node, (child) => walk(child)); - - // Transform eligible BinOps (single pass — no nesting) - if (walked.type === "BinOp" && MBA_OPS.has(walked.op)) { - return mbaSingle(walked, lcg); - } - - return walked; - } - - return nodes.map((n) => walk(n)); -} diff --git a/packages/ruam/src/ruamvm/nodes.ts b/packages/ruam/src/ruamvm/nodes.ts deleted file mode 100644 index 07c1546..0000000 --- a/packages/ruam/src/ruamvm/nodes.ts +++ /dev/null @@ -1,831 +0,0 @@ -/** - * JS AST node types and factory functions for runtime code generation. - * - * Replaces raw template literal strings with a structured tree representation. - * Factory function names are intentionally short — they're called thousands of times. - * - * @module ruamvm/nodes - */ - -import { type Name, NameToken, RestParam, isName } from "../naming/index.js"; -export type { Name } from "../naming/index.js"; -export { RestParam } from "../naming/index.js"; - -// --- Operator enums --- - -/** Binary operator kinds. */ -export const BOp = { - Add: 0, - Sub: 1, - Mul: 2, - Div: 3, - Mod: 4, - Pow: 5, - Shl: 6, - Shr: 7, - Ushr: 8, - BitAnd: 9, - BitOr: 10, - BitXor: 11, - Eq: 12, - Neq: 13, - Seq: 14, - Sneq: 15, - Lt: 16, - Lte: 17, - Gt: 18, - Gte: 19, - In: 20, - Instanceof: 21, - And: 22, - Or: 23, - Nullish: 24, -} as const; -export type BOpKind = (typeof BOp)[keyof typeof BOp]; - -/** Unary operator kinds. */ -export const UOp = { - Not: 0, - BitNot: 1, - Pos: 2, - Neg: 3, - Typeof: 4, - Void: 5, - Delete: 6, -} as const; -export type UOpKind = (typeof UOp)[keyof typeof UOp]; - -/** Assignment compound-operator prefix kinds. */ -export const AOp = { - Add: 0, - Sub: 1, - Mul: 2, - Div: 3, - Mod: 4, - Pow: 5, - Shl: 6, - Shr: 7, - Ushr: 8, - BitAnd: 9, - BitOr: 10, - BitXor: 11, - And: 12, - Or: 13, - Nullish: 14, -} as const; -export type AOpKind = (typeof AOp)[keyof typeof AOp]; - -/** Update operator kinds. */ -export const UpOp = { Inc: 0, Dec: 1 } as const; -export type UpOpKind = (typeof UpOp)[keyof typeof UpOp]; - -export const BOP_STR: Record = { - 0: "+", - 1: "-", - 2: "*", - 3: "/", - 4: "%", - 5: "**", - 6: "<<", - 7: ">>", - 8: ">>>", - 9: "&", - 10: "|", - 11: "^", - 12: "==", - 13: "!=", - 14: "===", - 15: "!==", - 16: "<", - 17: "<=", - 18: ">", - 19: ">=", - 20: "in", - 21: "instanceof", - 22: "&&", - 23: "||", - 24: "??", -}; -export const UOP_STR: Record = { - 0: "!", - 1: "~", - 2: "+", - 3: "-", - 4: "typeof", - 5: "void", - 6: "delete", -}; -export const AOP_STR: Record = { - 0: "+", - 1: "-", - 2: "*", - 3: "/", - 4: "%", - 5: "**", - 6: "<<", - 7: ">>", - 8: ">>>", - 9: "&", - 10: "|", - 11: "^", - 12: "&&", - 13: "||", - 14: "??", -}; -export const UPOP_STR: Record = { 0: "++", 1: "--" }; - -// --- Node type discriminants --- - -export type JsNode = - | VarDecl - | ConstDecl - | FnDecl - | ExprStmt - | Block - | IfStmt - | WhileStmt - | ForStmt - | ForInStmt - | SwitchStmt - | CaseClause - | BreakStmt - | ContinueStmt - | ReturnStmt - | ThrowStmt - | TryCatchStmt - | DebuggerStmt - | Id - | Literal - | BinOp - | UnaryOp - | UpdateExpr - | AssignExpr - | CallExpr - | MemberExpr - | IndexExpr - | TernaryExpr - | ArrayExpr - | ObjectExpr - | FnExpr - | ArrowFn - | NewExpr - | SequenceExpr - | AwaitExpr - | ImportExpr - | SpreadElement - | StackPush - | StackPop - | StackPeek; - -// --- Declarations --- - -export interface VarDecl { - type: "VarDecl"; - name: Name; - init?: JsNode; -} -export interface ConstDecl { - type: "ConstDecl"; - name: Name; - init?: JsNode; -} -export interface FnDecl { - type: "FnDecl"; - name: Name; - params: (Name | RestParam)[]; - body: JsNode[]; - async: boolean; -} - -// --- Statements --- - -export interface ExprStmt { - type: "ExprStmt"; - expr: JsNode; -} -export interface Block { - type: "Block"; - body: JsNode[]; -} -export interface IfStmt { - type: "IfStmt"; - test: JsNode; - then: JsNode[]; - else?: JsNode[]; -} -export interface WhileStmt { - type: "WhileStmt"; - test: JsNode; - body: JsNode[]; -} -export interface ForStmt { - type: "ForStmt"; - init: JsNode | null; - test: JsNode | null; - update: JsNode | null; - body: JsNode[]; -} -export interface ForInStmt { - type: "ForInStmt"; - decl: Name; - obj: JsNode; - body: JsNode[]; -} -export interface SwitchStmt { - type: "SwitchStmt"; - disc: JsNode; - cases: CaseClause[]; -} -export interface CaseClause { - type: "CaseClause"; - label: JsNode | null; - body: JsNode[]; -} -export interface BreakStmt { - type: "BreakStmt"; -} -export interface ContinueStmt { - type: "ContinueStmt"; -} -export interface ReturnStmt { - type: "ReturnStmt"; - value?: JsNode; -} -export interface ThrowStmt { - type: "ThrowStmt"; - value: JsNode; -} -export interface TryCatchStmt { - type: "TryCatchStmt"; - body: JsNode[]; - param?: Name; - handler?: JsNode[]; - finalizer?: JsNode[]; -} -export interface DebuggerStmt { - type: "DebuggerStmt"; -} - -// --- Expressions --- - -export interface Id { - type: "Id"; - name: Name; -} -export interface Literal { - type: "Literal"; - value: string | number | boolean | null | RegExp; -} -export interface BinOp { - type: "BinOp"; - op: BOpKind; - left: JsNode; - right: JsNode; -} -export interface UnaryOp { - type: "UnaryOp"; - op: UOpKind; - expr: JsNode; -} -export interface UpdateExpr { - type: "UpdateExpr"; - op: UpOpKind; - prefix: boolean; - arg: JsNode; -} -export interface AssignExpr { - type: "AssignExpr"; - target: JsNode; - value: JsNode; - op?: AOpKind; -} -export interface CallExpr { - type: "CallExpr"; - callee: JsNode; - args: JsNode[]; -} -export interface MemberExpr { - type: "MemberExpr"; - obj: JsNode; - prop: Name; -} -export interface IndexExpr { - type: "IndexExpr"; - obj: JsNode; - index: JsNode; -} -export interface TernaryExpr { - type: "TernaryExpr"; - test: JsNode; - then: JsNode; - else: JsNode; -} -export interface ArrayExpr { - type: "ArrayExpr"; - elements: JsNode[]; -} -/** A getter definition inside an object literal: `{ get name() { ... } }` */ -export interface GetterEntry { - kind: "get"; - name: Name; - body: JsNode[]; -} -/** A setter definition inside an object literal: `{ set name(param) { ... } }` */ -export interface SetterEntry { - kind: "set"; - name: Name; - param: Name; - body: JsNode[]; -} -/** A shorthand method inside an object literal: `{ name(params) { ... } }` */ -export interface MethodEntry { - kind: "method"; - name: Name | JsNode; - params: (Name | RestParam)[]; - body: JsNode[]; - async: boolean; -} -/** A spread entry inside an object literal: `{ ...expr }` */ -export interface ObjSpreadEntry { - kind: "spread"; - arg: JsNode; -} -/** A key-value property: `{ key: value }` or `{ [computed]: value }` */ -export type PropEntry = [Name | JsNode, JsNode]; -/** Any entry that can appear inside an object literal. */ -export type ObjectEntry = - | PropEntry - | GetterEntry - | SetterEntry - | MethodEntry - | ObjSpreadEntry; - -export interface ObjectExpr { - type: "ObjectExpr"; - entries: ObjectEntry[]; -} -export interface FnExpr { - type: "FnExpr"; - name?: Name; - params: (Name | RestParam)[]; - body: JsNode[]; - async: boolean; -} -export interface ArrowFn { - type: "ArrowFn"; - params: (Name | RestParam)[]; - body: JsNode[]; - async: boolean; -} -export interface NewExpr { - type: "NewExpr"; - callee: JsNode; - args: JsNode[]; -} -export interface SequenceExpr { - type: "SequenceExpr"; - exprs: JsNode[]; -} -export interface AwaitExpr { - type: "AwaitExpr"; - expr: JsNode; -} -export interface ImportExpr { - type: "ImportExpr"; - specifier: JsNode; -} -export interface SpreadElement { - type: "SpreadElement"; - arg: JsNode; -} -// --- Stack operations (emit as S.push(expr), S.pop(), S[S.length-1]) --- - -export interface StackPush { - type: "StackPush"; - value: JsNode; - S: Name; -} -export interface StackPop { - type: "StackPop"; - S: Name; -} -export interface StackPeek { - type: "StackPeek"; - S: Name; -} - -// --- Factory functions --- - -export function fn( - name: Name, - params: (Name | RestParam)[], - body: JsNode[], - opts?: { async?: boolean } -): FnDecl { - return { type: "FnDecl", name, params, body, async: opts?.async ?? false }; -} - -export function varDecl(name: Name, init?: JsNode): VarDecl { - return { type: "VarDecl", name, init }; -} - -export function constDecl(name: Name, init?: JsNode): ConstDecl { - return { type: "ConstDecl", name, init }; -} - -export function exprStmt(expr: JsNode): ExprStmt { - return { type: "ExprStmt", expr }; -} - -export function block(...body: JsNode[]): Block { - return { type: "Block", body }; -} - -export function ifStmt(test: JsNode, then: JsNode[], els?: JsNode[]): IfStmt { - return { type: "IfStmt", test, then, else: els }; -} - -export function whileStmt(test: JsNode, body: JsNode[]): WhileStmt { - return { type: "WhileStmt", test, body }; -} - -export function forStmt( - init: JsNode | null, - test: JsNode | null, - update: JsNode | null, - body: JsNode[] -): ForStmt { - return { type: "ForStmt", init, test, update, body }; -} - -export function forIn(decl: Name, obj: JsNode, body: JsNode[]): ForInStmt { - return { type: "ForInStmt", decl, obj, body }; -} - -export function switchStmt(disc: JsNode, cases: CaseClause[]): SwitchStmt { - return { type: "SwitchStmt", disc, cases }; -} - -export function caseClause(label: JsNode | null, body: JsNode[]): CaseClause { - return { type: "CaseClause", label, body }; -} - -export function breakStmt(): BreakStmt { - return { type: "BreakStmt" }; -} -export function continueStmt(): ContinueStmt { - return { type: "ContinueStmt" }; -} - -export function returnStmt(value?: JsNode): ReturnStmt { - return { type: "ReturnStmt", value }; -} - -export function throwStmt(value: JsNode): ThrowStmt { - return { type: "ThrowStmt", value }; -} - -export function tryCatch( - body: JsNode[], - param?: Name, - handler?: JsNode[], - finalizer?: JsNode[] -): TryCatchStmt { - return { type: "TryCatchStmt", body, param, handler, finalizer }; -} - -export function debuggerStmt(): DebuggerStmt { - return { type: "DebuggerStmt" }; -} - -export function id(name: Name): Id { - return { type: "Id", name }; -} - -export function lit(value: string | number | boolean | null | RegExp): Literal { - return { type: "Literal", value }; -} - -export function bin(op: BOpKind, left: JsNode, right: JsNode): BinOp { - return { type: "BinOp", op, left, right }; -} - -export function un(op: UOpKind, expr: JsNode): UnaryOp { - return { type: "UnaryOp", op, expr }; -} - -export function update(op: UpOpKind, prefix: boolean, arg: JsNode): UpdateExpr { - return { type: "UpdateExpr", op, prefix, arg }; -} - -export function assign( - target: JsNode, - value: JsNode, - op?: AOpKind -): AssignExpr { - return { type: "AssignExpr", target, value, op }; -} - -export function call(callee: JsNode, args: JsNode[]): CallExpr { - return { type: "CallExpr", callee, args }; -} - -export function member(obj: JsNode, prop: Name): MemberExpr { - return { type: "MemberExpr", obj, prop }; -} - -export function index(obj: JsNode, idx: JsNode): IndexExpr { - return { type: "IndexExpr", obj, index: idx }; -} - -export function ternary(test: JsNode, then: JsNode, els: JsNode): TernaryExpr { - return { type: "TernaryExpr", test, then, else: els }; -} - -export function arr(...elements: JsNode[]): ArrayExpr { - return { type: "ArrayExpr", elements }; -} - -export function obj(...entries: ObjectEntry[]): ObjectExpr { - return { type: "ObjectExpr", entries }; -} - -export function getter(name: Name, body: JsNode[]): GetterEntry { - return { kind: "get", name, body }; -} - -export function setter(name: Name, param: Name, body: JsNode[]): SetterEntry { - return { kind: "set", name, param, body }; -} - -export function method( - name: Name | JsNode, - params: (Name | RestParam)[], - body: JsNode[], - opts?: { async?: boolean } -): MethodEntry { - return { kind: "method", name, params, body, async: opts?.async ?? false }; -} - -export function objSpread(arg: JsNode): ObjSpreadEntry { - return { kind: "spread", arg }; -} - -export function fnExpr( - name: Name | undefined, - params: (Name | RestParam)[], - body: JsNode[], - opts?: { async?: boolean } -): FnExpr { - return { type: "FnExpr", name, params, body, async: opts?.async ?? false }; -} - -export function arrowFn( - params: (Name | RestParam)[], - body: JsNode[], - opts?: { async?: boolean } -): ArrowFn { - return { type: "ArrowFn", params, body, async: opts?.async ?? false }; -} - -export function newExpr(callee: JsNode, args: JsNode[]): NewExpr { - return { type: "NewExpr", callee, args }; -} - -export function seq(...exprs: JsNode[]): SequenceExpr { - return { type: "SequenceExpr", exprs }; -} - -export function awaitExpr(expr: JsNode): AwaitExpr { - return { type: "AwaitExpr", expr }; -} - -export function importExpr(specifier: JsNode): ImportExpr { - return { type: "ImportExpr", specifier }; -} - -export function spread(arg: JsNode): SpreadElement { - return { type: "SpreadElement", arg }; -} - -export function stackPush(S: Name, value: JsNode): StackPush { - return { type: "StackPush", value, S }; -} -export function stackPop(S: Name): StackPop { - return { type: "StackPop", S }; -} -export function stackPeek(S: Name): StackPeek { - return { type: "StackPeek", S }; -} - -// --- Convenience --- - -export function iife(body: Block): CallExpr { - return call(fnExpr(undefined, [], body.body), []); -} - -export function rest(name: Name): RestParam { - return new RestParam(name); -} - -// --- Reflective child metadata --- - -/** - * Field kind for child metadata. - * 'node' = single JsNode, 'nodes' = JsNode[], - * '?' suffix = nullable/optional (may be null or undefined). - */ -type FieldKind = "node" | "node?" | "nodes" | "nodes?"; - -/** - * Metadata map declaring which fields of each node type contain child JsNode(s). - * Fields not listed here are data (strings, booleans, etc.) and are NOT traversed. - * - * This is the single source of truth for structural traversal — `mapChildren()` - * uses this table instead of a hand-written 36-case switch. - */ -export const CHILD_FIELDS: Record> = { - // Declarations - VarDecl: { init: "node?" }, - ConstDecl: { init: "node?" }, - FnDecl: { body: "nodes" }, - // Statements - ExprStmt: { expr: "node" }, - Block: { body: "nodes" }, - IfStmt: { test: "node", then: "nodes", else: "nodes?" }, - WhileStmt: { test: "node", body: "nodes" }, - ForStmt: { init: "node?", test: "node?", update: "node?", body: "nodes" }, - ForInStmt: { obj: "node", body: "nodes" }, - SwitchStmt: { disc: "node", cases: "nodes" }, - CaseClause: { label: "node?", body: "nodes" }, - BreakStmt: {}, - ContinueStmt: {}, - ReturnStmt: { value: "node?" }, - ThrowStmt: { value: "node" }, - TryCatchStmt: { body: "nodes", handler: "nodes?", finalizer: "nodes?" }, - DebuggerStmt: {}, - // Expressions - Id: {}, - Literal: {}, - BinOp: { left: "node", right: "node" }, - UnaryOp: { expr: "node" }, - UpdateExpr: { arg: "node" }, - AssignExpr: { target: "node", value: "node" }, - CallExpr: { callee: "node", args: "nodes" }, - MemberExpr: { obj: "node" }, - IndexExpr: { obj: "node", index: "node" }, - TernaryExpr: { test: "node", then: "node", else: "node" }, - ArrayExpr: { elements: "nodes" }, - ObjectExpr: {}, // special: entries are [string|JsNode, JsNode] tuples — handled in mapChildren - FnExpr: { body: "nodes" }, - ArrowFn: { body: "nodes" }, - NewExpr: { callee: "node", args: "nodes" }, - SequenceExpr: { exprs: "nodes" }, - AwaitExpr: { expr: "node" }, - ImportExpr: { specifier: "node" }, - SpreadElement: { arg: "node" }, - StackPush: { value: "node" }, - StackPop: {}, - StackPeek: {}, -}; - -/** - * Apply a mapping function to every child JsNode of the given node. - * Uses the CHILD_FIELDS metadata table instead of a hand-written switch. - * Returns a structurally-equal new node if any child changed, or the original if not. - * - * ObjectExpr entries are handled specially (tuple array with string|JsNode keys). - * - * @param node - The node whose children to map - * @param fn - The mapping function applied to each child - * @returns The original node (if unchanged) or a new node with mapped children - */ -export function mapChildren( - node: JsNode, - fn: (child: JsNode) => JsNode -): JsNode { - // Special case: ObjectExpr has union entry types (tuples + rich entries) - if (node.type === "ObjectExpr") { - let changed = false; - const mapped = node.entries.map((entry): ObjectEntry => { - if (Array.isArray(entry)) { - // PropEntry: [string|JsNode, JsNode] - const [k, v] = entry; - const nk = isName(k) ? k : fn(k); - const nv = fn(v); - if (nk !== k || nv !== v) { - changed = true; - return [nk, nv] as PropEntry; - } - return entry; - } - switch (entry.kind) { - case "get": { - const nb = entry.body.map(fn); - if (nb.some((n, i) => n !== entry.body[i])) { - changed = true; - return { ...entry, body: nb }; - } - return entry; - } - case "set": { - const nb = entry.body.map(fn); - if (nb.some((n, i) => n !== entry.body[i])) { - changed = true; - return { ...entry, body: nb }; - } - return entry; - } - case "method": { - const nk = isName(entry.name) ? entry.name : fn(entry.name); - const nb = entry.body.map(fn); - if ( - nk !== entry.name || - nb.some((n, i) => n !== entry.body[i]) - ) { - changed = true; - return { - ...entry, - name: nk, - body: nb, - } as MethodEntry; - } - return entry; - } - case "spread": { - const na = fn(entry.arg); - if (na !== entry.arg) { - changed = true; - return { ...entry, arg: na }; - } - return entry; - } - } - }); - return changed ? { ...node, entries: mapped } : node; - } - - const schema = CHILD_FIELDS[node.type]; - const entries = Object.entries(schema); - if (entries.length === 0) return node; - - const result = { ...node } as unknown as Record; - let changed = false; - for (const [field, kind] of entries) { - const val = (node as unknown as Record)[field]; - if (val == null) continue; - if (kind === "node" || kind === "node?") { - const mapped = fn(val as JsNode); - if (mapped !== val) { - result[field] = mapped; - changed = true; - } - } else { - // 'nodes' or 'nodes?' - const arr = val as JsNode[]; - const mapped = arr.map(fn); - if (mapped.some((v, i) => v !== arr[i])) { - result[field] = mapped; - changed = true; - } - } - } - return changed ? (result as unknown as JsNode) : node; -} - -// --- Exhaustive visitor types --- - -/** All discriminant type strings in the JsNode union. */ -export type NodeType = JsNode["type"]; - -/** Extract a specific node type from the union by its discriminant. */ -export type NodeOfType = Extract; - -/** - * A visitor object that must have a handler for every node type. - * TypeScript enforces exhaustiveness at compile time — adding a new node type - * to the JsNode union immediately causes errors in all ExhaustiveVisitor objects. - */ -export type ExhaustiveVisitor = { - [K in NodeType]: (node: NodeOfType) => R; -}; - -/** - * Dispatch a node to the appropriate handler in an exhaustive visitor. - * - * @param node - The node to visit - * @param v - The visitor object with a handler for every node type - * @returns The result of the matching handler - */ -export function visit(node: JsNode, v: ExhaustiveVisitor): R { - return (v[node.type] as (node: JsNode) => R)(node); -} - -/** - * Compile-time exhaustiveness assertion for switch default cases. - * Passing a value here that isn't `never` produces a TypeScript error, - * catching unhandled node types at compile time. - */ -export function assertNever(x: never): never { - throw new Error(`Unhandled node type: ${(x as { type: string }).type}`); -} diff --git a/packages/ruam/src/ruamvm/observation-resistance.ts b/packages/ruam/src/ruamvm/observation-resistance.ts deleted file mode 100644 index b13e2a0..0000000 --- a/packages/ruam/src/ruamvm/observation-resistance.ts +++ /dev/null @@ -1,484 +0,0 @@ -/** - * Observation resistance — silently corrupt computation when - * instrumentation is detected. - * - * Function identity binding saves references to critical internal - * functions at IIFE creation. The verify function checks saved === - * current and returns a corruption XOR constant (0 if clean). - * - * Monotonic witness counter verifies handler execution order by - * incrementing a counter in every handler and checking monotonicity - * in a random subset. - * - * WeakMap canary plants a WeakMap-based sentinel at IIFE scope and - * periodically verifies it has not been monkey-patched or replaced. - * - * Stack integrity probes push/pop a known sentinel object onto the - * stack to detect proxy replacement or encoding tampering. - * - * @module ruamvm/observation-resistance - */ - -import type { RuntimeNames, TempNames } from "../naming/compat-types.js"; -import type { JsNode } from "./nodes.js"; -import type { SplitFn } from "./constant-splitting.js"; -import { deriveSeed } from "../naming/scope.js"; -import { - varDecl, - fn, - returnStmt, - ifStmt, - exprStmt, - assign, - bin, - un, - id, - lit, - call, - member, - newExpr, - ternary, - BOp, - UOp, -} from "./nodes.js"; -import type { NameRegistry } from "../naming/registry.js"; -import { - LCG_MULTIPLIER, - LCG_INCREMENT, - OR_CORRUPT_IDENTITY, - OR_CORRUPT_WITNESS, - OR_CORRUPT_CANARY, - OR_CORRUPT_PROBE, -} from "../constants.js"; - -// --- Candidate Functions --- - -/** - * All candidate RuntimeNames keys for identity binding, ordered by - * priority. Each has a `requires` tag so we skip functions that - * were not actually emitted in the current build. - */ -const BINDING_CANDIDATES: { - key: keyof RuntimeNames; - /** Feature gate — the function only exists when this gate is true. - * `"always"` means the function is always emitted. */ - gate: "always" | "rollingCipher" | "encrypt" | "incrementalCipher"; -}[] = [ - { key: "exec", gate: "always" }, - { key: "load", gate: "always" }, - { key: "deser", gate: "always" }, - { key: "vm", gate: "always" }, - { key: "rcDeriveKey", gate: "rollingCipher" }, - { key: "rcMix", gate: "rollingCipher" }, - { key: "b64", gate: "always" }, - { key: "icBlockKey", gate: "incrementalCipher" }, - { key: "icMix", gate: "incrementalCipher" }, -]; - -/** Feature gates active for the current build. */ -export interface IdentityBindingGates { - rollingCipher?: boolean; - encrypt?: boolean; - incrementalCipher?: boolean; -} - -/** Result from building identity binding declarations. */ -export interface IdentityBindingResult { - /** `var _orRefN = functionName;` declarations for IIFE scope. */ - declarations: JsNode[]; - /** `function orVerify(){ ... }` — returns 0 if untampered. */ - verifyFn: JsNode; -} - -/** - * Build function identity binding declarations and a verify function. - * - * Selects up to `bindingCount` internal functions from the candidate - * pool (filtered by active feature gates), saves their references as - * IIFE-scope variables, and builds an `orVerify()` function that - * returns a corruption XOR constant when any reference has been - * replaced (0 when all are clean). - * - * @param names Runtime identifier mapping. - * @param temps Temp identifier mapping. - * @param seed Per-build seed for deterministic selection. - * @param bindingCount Number of functions to bind (from tuning). - * @param gates Which features are active (controls candidate pool). - * @param split Optional constant splitter for numeric obfuscation. - * @param registry NameRegistry for collision-safe dynamic naming. - * @returns Declarations and verify function AST nodes. - */ -export function buildIdentityBindings( - names: RuntimeNames, - temps: TempNames, - seed: number, - bindingCount: number, - gates: IdentityBindingGates, - split?: SplitFn, - registry?: NameRegistry -): IdentityBindingResult { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - - // Filter candidates by active feature gates - const available = BINDING_CANDIDATES.filter((c) => { - if (c.gate === "always") return true; - if (c.gate === "rollingCipher") return gates.rollingCipher; - if (c.gate === "encrypt") return gates.encrypt; - if (c.gate === "incrementalCipher") return gates.incrementalCipher; - return false; - }); - - // Deterministic shuffle via seeded LCG - const selSeed = deriveSeed(seed, "orSelect"); - const pool = available.map((c) => c.key); - - let s = selSeed >>> 0; - const lcgNext = (): number => { - s = (Math.imul(s, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - return s; - }; - for (let i = pool.length - 1; i > 0; i--) { - const j = lcgNext() % (i + 1); - [pool[i], pool[j]] = [pool[j]!, pool[i]!]; - } - - // Take up to bindingCount - const selected = pool.slice(0, Math.min(bindingCount, pool.length)); - - // Generate dynamic names for the reference variables - const nameGen = registry - ? registry.createDynamicGenerator("orRef") - : undefined; - - // Per-build corruption constants derived from seed - const refNames: string[] = []; - const funcNames: string[] = []; - const corruptionConstants: number[] = []; - - for (let i = 0; i < selected.length; i++) { - const key = selected[i]!; - const funcName = names[key]; - - // Generate a unique reference variable name - const refName = nameGen - ? nameGen() - : temps["_orRef"] - ? `${temps["_orRef"]}${i}` - : `_orRef${i}`; - - // Per-build corruption constant via deriveSeed - const corruptSeed = deriveSeed(seed, `orCorruption_${i}`); - // Use the full 32-bit value, ensure non-zero - const corruptConst = (corruptSeed || OR_CORRUPT_IDENTITY) >>> 0; - - refNames.push(refName); - funcNames.push(funcName); - corruptionConstants.push(corruptConst); - } - - // Build declarations: var refName = funcName; - const declarations: JsNode[] = []; - for (let i = 0; i < selected.length; i++) { - declarations.push(varDecl(refNames[i]!, id(funcNames[i]!))); - } - - // Build verify function body - // var c = 0; - // if (refName0 !== funcName0) c = (c ^ CORRUPT0) >>> 0; - // if (refName1 !== funcName1) c = (c ^ CORRUPT1) >>> 0; - // ... - // return c; - const verifyBody: JsNode[] = [varDecl("c", lit(0))]; - - for (let i = 0; i < selected.length; i++) { - verifyBody.push( - ifStmt(bin(BOp.Sneq, id(refNames[i]!), id(funcNames[i]!)), [ - exprStmt( - assign( - id("c"), - bin( - BOp.Ushr, - bin( - BOp.BitXor, - id("c"), - L(corruptionConstants[i]!) - ), - lit(0) - ) - ) - ), - ]) - ); - } - - verifyBody.push(returnStmt(id("c"))); - - // Build the verify function declaration - const verifyFn = fn(names.orVerify, [], verifyBody); - - return { declarations, verifyFn }; -} - -// --- Monotonic Witness Counter --- - -/** Result from building witness counter declarations and helpers. */ -export interface WitnessCounterResult { - /** IIFE-scope declarations: `var _orW = 0; var _orWv = 0;` */ - declarations: JsNode[]; - /** - * Build the increment statement to prepend to every handler body: - * `_orW = (_orW + 1) | 0;` - */ - incrementStmt: () => JsNode; - /** - * Build the verification check to append to selected handler bodies: - * `if (_orW < _orWv) { rcState = (rcState ^ CORRUPT) >>> 0; } _orWv = _orW;` - */ - verifyStmts: () => JsNode[]; -} - -/** - * Build monotonic witness counter declarations and helpers. - * - * Every handler increments a hidden counter. A random subset of - * handlers verify the counter is still increasing. If someone skips - * handlers, replays them, or modifies the counter, the verification - * fails and silently corrupts `rcState`. - * - * @param names Runtime identifier mapping. - * @param temps Temp identifier mapping. - * @param seed Per-build seed for corruption constant derivation. - * @param split Optional constant splitter for numeric obfuscation. - * @returns Declarations and statement builder functions. - */ -export function buildWitnessCounter( - names: RuntimeNames, - temps: TempNames, - seed: number, - split?: SplitFn -): WitnessCounterResult { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - - const orW = temps["_orW"]; - const orWv = temps["_orWv"]; - if (orW === undefined || orWv === undefined) { - throw new Error("Missing temp names: _orW/_orWv"); - } - - // Per-build corruption constant - const corruptSeed = deriveSeed(seed, "witnessCorrupt"); - const corruptConst = (corruptSeed || OR_CORRUPT_WITNESS) >>> 0; - - const declarations: JsNode[] = [ - varDecl(orW, lit(0)), - varDecl(orWv, lit(0)), - ]; - - return { - declarations, - incrementStmt: () => - // _orW = (_orW + 1) | 0; - exprStmt( - assign( - id(orW), - bin(BOp.BitOr, bin(BOp.Add, id(orW), lit(1)), lit(0)) - ) - ), - verifyStmts: () => [ - // if (_orW < _orWv) { rcState = (rcState ^ CORRUPT) >>> 0; } - ifStmt(bin(BOp.Lt, id(orW), id(orWv)), [ - exprStmt( - assign( - id(names.rcState), - bin( - BOp.Ushr, - bin(BOp.BitXor, id(names.rcState), L(corruptConst)), - lit(0) - ) - ) - ), - ]), - // _orWv = _orW; - exprStmt(assign(id(orWv), id(orW))), - ], - }; -} - -// --- WeakMap Canary --- - -/** Result from building WeakMap canary declarations and check expression. */ -export interface WeakMapCanaryResult { - /** IIFE-scope declarations: canary object, WeakMap, and initial set. */ - declarations: JsNode[]; - /** - * Build the verification expression that returns a corruption constant - * when the canary has been tampered with, or 0 when clean. - * `(!(_orExp instanceof WeakMap) || _orExp.get(_orRef) !== true) ? CORRUPT : 0` - */ - checkExpr: () => JsNode; -} - -/** - * Build WeakMap canary declarations and a verification expression. - * - * Plants a WeakMap-based canary at IIFE scope. The canary object is - * stored as a key in the WeakMap with value `true`. Periodically - * verify the WeakMap and canary value are intact. Detects - * monkey-patching of WeakMap or replacement of IIFE-scope variables. - * - * @param names Runtime identifier mapping. - * @param temps Temp identifier mapping. - * @param seed Per-build seed for corruption constant derivation. - * @param split Optional constant splitter for numeric obfuscation. - * @param registry NameRegistry for collision-safe dynamic naming. - * @returns Declarations and check expression builder. - */ -export function buildWeakMapCanary( - names: RuntimeNames, - temps: TempNames, - seed: number, - split?: SplitFn, - registry?: NameRegistry -): WeakMapCanaryResult { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - - // Use the canary variable names from temps - const canaryRef = temps["_orRef"]; - const canaryWm = temps["_orExp"]; - if (canaryRef === undefined || canaryWm === undefined) { - throw new Error("Missing temp names: _orRef/_orExp"); - } - - // Per-build corruption constant - const corruptSeed = deriveSeed(seed, "canaryCorrupt"); - const corruptConst = (corruptSeed || OR_CORRUPT_CANARY) >>> 0; - - const declarations: JsNode[] = [ - // var _orRef = {}; - varDecl(canaryRef, call(member(id("Object"), "create"), [lit(null)])), - // var _orExp = new WeakMap(); - varDecl(canaryWm, newExpr(id("WeakMap"), [])), - // _orExp.set(_orRef, true); - exprStmt(call(member(id(canaryWm), "set"), [id(canaryRef), lit(true)])), - ]; - - return { - declarations, - checkExpr: () => - // (!(_orExp instanceof WeakMap) || _orExp.get(_orRef) !== true) ? CORRUPT : 0 - ternary( - bin( - BOp.Or, - un( - UOp.Not, - bin(BOp.Instanceof, id(canaryWm), id("WeakMap")) - ), - bin( - BOp.Sneq, - call(member(id(canaryWm), "get"), [id(canaryRef)]), - lit(true) - ) - ), - L(corruptConst), - lit(0) - ), - }; -} - -// --- Stack Integrity Probes --- - -/** Result from building stack probe statements. */ -export interface StackProbeResult { - /** - * Build the probe statements to inject into a handler body. - * Pushes tdzSentinel onto the stack, pops it, and verifies identity. - * `S.push(tdzSentinel); if (S.pop() !== tdzSentinel) { rcState = (rcState ^ CORRUPT) >>> 0; }` - */ - probeStmts: () => JsNode[]; -} - -/** - * Build stack integrity probe statements. - * - * At pseudo-random intervals, pushes a known sentinel value onto the - * stack, immediately pops it, and verifies it is the same object - * (identity check). Detects stack array replacement / encoding tampering. - * - * When `stackEncoding` is on, the push/pop are routed through the - * `stkEnc`/`stkDec` helpers (the sentinel object becomes a `[3,sentinel]` - * entry and decodes back to the same object). This makes the probe verify - * the encode/decode round-trip is intact — tampering with the helpers breaks - * identity and triggers corruption — preserving the threat model the Proxy - * stack-probe used to cover. Without encoding it uses raw push/pop (detecting - * raw stack replacement), exactly as before. - * - * @param names Runtime identifier mapping (provides stkEnc/stkDec). - * @param temps Temp name mapping (provides the per-exec key `_sek`). - * @param seed Per-build seed for corruption constant derivation. - * @param split Optional constant splitter for numeric obfuscation. - * @param stackEncoding Whether stack encoding is active for this build. - * @returns Statement builder function. - */ -export function buildStackProbe( - names: RuntimeNames, - temps: TempNames, - seed: number, - split?: SplitFn, - stackEncoding = false -): StackProbeResult { - const L = (v: number): JsNode => (split ? split(v) : lit(v)); - - // Per-build corruption constant - const corruptSeed = deriveSeed(seed, "probeCorrupt"); - const corruptConst = (corruptSeed || OR_CORRUPT_PROBE) >>> 0; - - const S = names.stk; - const sek = temps["_sek"]; - const encoded = stackEncoding && sek !== undefined; - - // Fresh AST per call (probeStmts is invoked once per injection site; - // reusing node objects would alias inside the mutating MBA/structural passes). - const buildPushed = (): JsNode => - encoded - ? call(id(names.stkEnc), [ - id(names.tdzSentinel), - member(id(S), "length"), - id(sek!), - ]) - : id(names.tdzSentinel); - const buildPopped = (): JsNode => - encoded - ? call(id(names.stkDec), [ - call(member(id(S), "pop"), []), - member(id(S), "length"), - id(sek!), - ]) - : call(member(id(S), "pop"), []); - - return { - probeStmts: () => [ - // S.push(stkEnc?(tdzSentinel)); - exprStmt(call(member(id(S), "push"), [buildPushed()])), - // if (stkDec?(S.pop()) !== tdzSentinel) { rcState = (rcState ^ CORRUPT) >>> 0; } - ifStmt( - bin(BOp.Sneq, buildPopped(), id(names.tdzSentinel)), - [ - exprStmt( - assign( - id(names.rcState), - bin( - BOp.Ushr, - bin( - BOp.BitXor, - id(names.rcState), - L(corruptConst) - ), - lit(0) - ) - ) - ), - ] - ), - ], - }; -} diff --git a/packages/ruam/src/ruamvm/opaque-predicates.ts b/packages/ruam/src/ruamvm/opaque-predicates.ts deleted file mode 100644 index fd8c83e..0000000 --- a/packages/ruam/src/ruamvm/opaque-predicates.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Opaque predicate library for handler body injection. - * - * Generates always-true or always-false conditions from mathematical - * properties that are hard to prove statically. Used by semantic opacity - * to split handler bodies into "real" and "dead" paths. - * - * All predicates are valid for int32 inputs. The inputExpr should be - * bitwise-coerced (e.g., x | 0) to ensure integer semantics. - * - * Predicate families: - * 0 — Quadratic residue (always true): ((x*x+1)%4) !== 2 - * 1 — Parity product (always true): ((x|1)*(x|1))%2 !== 0 - * 2 — Bitwise identity (always true): (x^x) === 0 - * 3 — Squares mod 4 (always false): ((x*x)%4) === 3 - * 4 — Double parity (always false): ((x&1)+(x&1))%2 !== 0 - * - * @module ruamvm/opaque-predicates - */ - -import { type JsNode, BOp, bin, lit, ifStmt } from "./nodes.js"; -import { deriveSeed } from "../naming/scope.js"; -import { LCG_MULTIPLIER, LCG_INCREMENT } from "../constants.js"; - -// --- Public types --- - -/** An opaque predicate expression together with its statically-known truth value. */ -export interface OpaquePredicate { - /** The condition AST node. Evaluates to `alwaysTrue` for every integer input. */ - expr: JsNode; - /** True if `expr` is always-true; false if it is always-false. */ - alwaysTrue: boolean; -} - -// --- Predicate families --- - -/** - * Generate an opaque predicate for the given input expression. - * - * Uses `deriveSeed(seed, "opaque_" + index)` for PRNG isolation so - * successive calls with different `index` values produce independent streams. - * - * @param inputExpr - AST node for the integer input (e.g. `id("x")`) - * @param seed - Per-build master seed - * @param index - Predicate index for stream isolation - * @returns An `OpaquePredicate` with `expr` and `alwaysTrue` - */ -export function generateOpaquePredicate( - inputExpr: JsNode, - seed: number, - index: number -): OpaquePredicate { - const derived = deriveSeed(seed, "opaque_" + index); - const family = - ((Math.imul(derived, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0) % 5; - - const x = inputExpr; - - switch (family) { - case 0: { - // Always true: ((x * x) % 4) !== 2 - // Quadratic residues mod 4 are 0 and 1 — never 2 ✓ - // (The spec description mentions x²+1 but the correct always-true - // quadratic-residue predicate uses x² directly: squares mod 4 ∈ {0,1}.) - const expr = bin( - BOp.Sneq, - bin(BOp.Mod, bin(BOp.Mul, x, x), lit(4)), - lit(2) - ); - return { expr, alwaysTrue: true }; - } - - case 1: { - // Always true: ((x | 1) * (x | 1)) % 2 !== 0 - // (x|1) is always odd; odd*odd is always odd; odd%2 === 1 !== 0 ✓ - const xOr1 = bin(BOp.BitOr, x, lit(1)); - const expr = bin( - BOp.Sneq, - bin(BOp.Mod, bin(BOp.Mul, xOr1, xOr1), lit(2)), - lit(0) - ); - return { expr, alwaysTrue: true }; - } - - case 2: { - // Always true: (x ^ x) === 0 - // Any value XOR'd with itself is 0 ✓ - const expr = bin(BOp.Seq, bin(BOp.BitXor, x, x), lit(0)); - return { expr, alwaysTrue: true }; - } - - case 3: { - // Always false: ((x * x) % 4) === 3 - // Squares mod 4 ∈ {0, 1} — never 3 ✓ - const expr = bin( - BOp.Seq, - bin(BOp.Mod, bin(BOp.Mul, x, x), lit(4)), - lit(3) - ); - return { expr, alwaysTrue: false }; - } - - case 4: { - // Always false: ((x & 1) + (x & 1)) % 2 !== 0 - // x&1 ∈ {0,1}; (x&1)+(x&1) ∈ {0,2}; both even → %2===0 → !==0 is false ✓ - const xAnd1 = bin(BOp.BitAnd, x, lit(1)); - const expr = bin( - BOp.Sneq, - bin(BOp.Mod, bin(BOp.Add, xAnd1, xAnd1), lit(2)), - lit(0) - ); - return { expr, alwaysTrue: false }; - } - - default: - // Unreachable — exhaustive over 5 families - throw new Error(`Unexpected predicate family: ${family}`); - } -} - -/** - * Wrap a handler body behind an opaque predicate, routing the real body - * to the always-taken branch and the dead body to the never-taken branch. - * - * @param body - Real handler statements (executed) - * @param deadBody - Dead code statements (never executed) - * @param predicate - Opaque predicate produced by `generateOpaquePredicate` - * @returns Wrapped statement array containing a single `if` statement - */ -export function injectOpaquePredicate( - body: JsNode[], - deadBody: JsNode[], - predicate: OpaquePredicate -): JsNode[] { - if (predicate.alwaysTrue) { - // Condition is always true → real body in `then`, dead body in `else` - return [ifStmt(predicate.expr, body, deadBody)]; - } else { - // Condition is always false → dead body in `then`, real body in `else` - return [ifStmt(predicate.expr, deadBody, body)]; - } -} diff --git a/packages/ruam/src/ruamvm/polymorphic-decoder.ts b/packages/ruam/src/ruamvm/polymorphic-decoder.ts deleted file mode 100644 index 3596c6b..0000000 --- a/packages/ruam/src/ruamvm/polymorphic-decoder.ts +++ /dev/null @@ -1,502 +0,0 @@ -/** - * Polymorphic decoder chain generator. - * - * Per-build, generates a random chain of 4-8 reversible byte operations - * for encoding/decoding string constants. The decoder function's AST - * structure differs every build — different operations, different count, - * different keys — eliminating universal decoder scripts. - * - * Advancement over KrakVm's approach: - * - Variable-length chains (4-8 ops vs fixed) - * - Includes bit rotations and nibble swaps (not just byte-level ops) - * - Decoder is AST-generated (inherits MBA, structural transforms) - * - Chain key material can be scattered across IIFE scope - * - * @module ruamvm/polymorphic-decoder - */ - -import type { JsNode } from "./nodes.js"; -import { - BOp, - UOp, - id, - lit, - bin, - un, - call, - member, - assign, - varDecl, - forStmt, - exprStmt, - returnStmt, - fn, - arr, -} from "./nodes.js"; -import { deriveSeed, lcgNext } from "../naming/scope.js"; - -// --- Operation types --- - -/** A single reversible byte operation in the chain. */ -export type DecoderOp = - | { kind: "xor"; key: number } - | { kind: "add"; key: number } - | { kind: "sub"; key: number } - | { kind: "not" } - | { kind: "rol"; n: number } - | { kind: "ror"; n: number } - | { kind: "swap_nibbles" }; - -/** A complete decoder chain with all operation parameters. */ -export interface DecoderChain { - /** The ordered sequence of operations (applied forward for encoding). */ - ops: DecoderOp[]; - /** Per-build LCG seed for position-dependent key variation. */ - positionSeed: number; -} - -// --- Chain generation --- - -/** Number of available operation kinds. */ -const OP_KIND_COUNT = 7; -/** Minimum chain length. */ -const MIN_CHAIN_LEN = 4; -/** Maximum chain length. */ -const MAX_CHAIN_LEN = 8; - -/** - * Generate a random decoder chain from a build seed. - * - * @param seed - Per-build CSPRNG seed - * @returns A decoder chain with 4-8 operations - */ -export function generateDecoderChain(seed: number): DecoderChain { - let state = deriveSeed(seed, "polyDecChain"); - - const nextByte = (): number => { - state = lcgNext(state); - return (state >>> 16) & 0xff; - }; - const nextRange = (min: number, max: number): number => { - state = lcgNext(state); - return min + ((state >>> 16) % (max - min + 1)); - }; - - const chainLen = nextRange(MIN_CHAIN_LEN, MAX_CHAIN_LEN); - const ops: DecoderOp[] = []; - - for (let i = 0; i < chainLen; i++) { - const kind = nextRange(0, OP_KIND_COUNT - 1); - switch (kind) { - case 0: - ops.push({ kind: "xor", key: nextByte() | 1 }); // Ensure non-zero - break; - case 1: - ops.push({ kind: "add", key: nextRange(1, 255) }); - break; - case 2: - ops.push({ kind: "sub", key: nextRange(1, 255) }); - break; - case 3: - ops.push({ kind: "not" }); - break; - case 4: - ops.push({ kind: "rol", n: nextRange(1, 7) }); - break; - case 5: - ops.push({ kind: "ror", n: nextRange(1, 7) }); - break; - case 6: - ops.push({ kind: "swap_nibbles" }); - break; - } - } - - // Position seed for position-dependent key variation - state = lcgNext(state); - const positionSeed = state >>> 0; - - return { ops, positionSeed }; -} - -// --- Build-time encoding --- - -/** Apply a single operation forward (encode direction). */ -function applyOpForward(byte: number, op: DecoderOp, posKey: number): number { - switch (op.kind) { - case "xor": - return (byte ^ ((op.key + posKey) & 0xff)) & 0xff; - case "add": - return (byte + ((op.key + posKey) & 0xff)) & 0xff; - case "sub": - return (byte - ((op.key + posKey) & 0xff)) & 0xff; - case "not": - return ~byte & 0xff; - case "rol": - return ((byte << op.n) | (byte >>> (8 - op.n))) & 0xff; - case "ror": - return ((byte >>> op.n) | (byte << (8 - op.n))) & 0xff; - case "swap_nibbles": - return ((byte << 4) | (byte >>> 4)) & 0xff; - } -} - -/** - * Encode a string using the polymorphic chain (build-time). - * - * @param str - The string to encode - * @param chain - The decoder chain - * @param index - Position index for position-dependent key variation - * @returns Encoded byte array as number[] - */ -export function polyEncode( - str: string, - chain: DecoderChain, - index: number -): number[] { - const result: number[] = []; - for (let i = 0; i < str.length; i++) { - let byte = str.charCodeAt(i) & 0xff; - // Position-dependent key: mix index and char position - const posKey = - (Math.imul(chain.positionSeed ^ index, 0x45d9f3b) + i) & 0xff; - - // Apply chain forward - for (const op of chain.ops) { - byte = applyOpForward(byte, op, posKey); - } - result.push(byte); - } - return result; -} - -/** - * Encode a string preserving full char codes (for non-ASCII). - * Uses two-byte encoding: high byte then low byte, each through the chain. - * - * @param str - The string to encode (may contain non-ASCII) - * @param chain - The decoder chain - * @param index - Position index - * @returns Encoded byte array as number[] - */ -export function polyEncodeWide( - str: string, - chain: DecoderChain, - index: number -): number[] { - const result: number[] = []; - for (let i = 0; i < str.length; i++) { - const code = str.charCodeAt(i); - const hi = (code >>> 8) & 0xff; - const lo = code & 0xff; - const posKey = - (Math.imul(chain.positionSeed ^ index, 0x45d9f3b) + i) & 0xff; - - let bhi = hi; - let blo = lo; - for (const op of chain.ops) { - bhi = applyOpForward(bhi, op, posKey); - blo = applyOpForward(blo, op, posKey); - } - result.push(bhi, blo); - } - return result; -} - -// --- Runtime AST generation --- - -/** - * Build the AST for the polymorphic decoder function. - * - * The generated function decodes a byte array by applying the chain - * in reverse. Its structure differs every build. - * - * @param chain - The decoder chain - * @param fnName - Runtime name for the function - * @param posSeedName - Runtime name for the position seed variable - * @returns AST nodes: [positionSeed var declaration, decoder function declaration] - */ -export function buildDecoderFunctionAST( - chain: DecoderChain, - fnName: string, - posSeedName: string -): JsNode[] { - const nodes: JsNode[] = []; - - // var _ps = positionSeed - nodes.push(varDecl(posSeedName, lit(chain.positionSeed))); - - // Build the decoder function body - // function _sd(data, idx) { - // var r = '', i, b, pk; - // for (i = 0; i < data.length; i++) { - // b = data[i]; - // pk = (imul(_ps ^ idx, 0x45d9f3b) + i) & 255; - // [reverse chain operations on b] - // r += String.fromCharCode(b); - // } - // return r; - // } - const d = id("d"); // data param - const idx = id("x"); // index param - const r = id("r"); // result string - const i = id("i"); // loop var - const b = id("b"); // current byte - const pk = id("pk"); // position key - - const bodyStmts: JsNode[] = []; - bodyStmts.push(varDecl("r", lit(""))); - bodyStmts.push(varDecl("i")); - bodyStmts.push(varDecl("b")); - bodyStmts.push(varDecl("pk")); - - // Build loop body: reverse the chain - const loopBody: JsNode[] = []; - - // b = d[i] - loopBody.push( - exprStmt( - assign( - b, - call(member(id("Math"), "imul"), [ - bin(BOp.BitXor, id(posSeedName), idx), - lit(0x45d9f3b), - ]) - ) - ) - ); - // Inline: pk = (imul(_ps ^ idx, 0x45d9f3b) + i) & 255 - loopBody.push( - exprStmt( - assign( - pk, - bin( - BOp.BitAnd, - bin( - BOp.Add, - call(member(id("Math"), "imul"), [ - bin(BOp.BitXor, id(posSeedName), idx), - lit(0x45d9f3b), - ]), - i - ), - lit(255) - ) - ) - ) - ); - // b = d[i] - // Actually, let me redo this properly - loopBody.length = 0; - - // b = d[i] - loopBody.push( - exprStmt( - assign(b, { - type: "IndexExpr", - obj: d, - index: i, - }) - ) - ); - - // pk = (imul(_ps ^ idx, 0x45d9f3b) + i) & 255 - loopBody.push( - exprStmt( - assign( - pk, - bin( - BOp.BitAnd, - bin( - BOp.Add, - call(member(id("Math"), "imul"), [ - bin(BOp.BitXor, id(posSeedName), idx), - lit(0x45d9f3b), - ]), - i - ), - lit(255) - ) - ) - ) - ); - - // Apply chain in REVERSE (decode direction) - const reversedOps = [...chain.ops].reverse(); - for (const op of reversedOps) { - loopBody.push(exprStmt(assign(b, buildReverseOp(op, b, pk)))); - } - - // r += String.fromCharCode(b) - loopBody.push( - exprStmt( - assign( - r, - call(member(id("String"), "fromCharCode"), [b]), - BOp.Add as unknown as undefined // += assignment - ) - ) - ); - - // Wait, assign's third param is AOpKind, not OpKind. Let me fix this. - // Actually I need to import AOp. Let me fix the last statement. - loopBody.pop(); // Remove the wrong one - loopBody.push( - exprStmt({ - type: "AssignExpr", - target: r, - value: call(member(id("String"), "fromCharCode"), [b]), - op: 0, // AOp.Add = 0 - }) - ); - - // for (i = 0; i < d.length; i++) - const loop = forStmt( - assign(i, lit(0)), - bin(BOp.Lt, i, member(d, "length")), - { - type: "UpdateExpr", - op: 0, // UpOp.Inc = 0 - prefix: true, - arg: i, - }, - loopBody - ); - - bodyStmts.push(loop); - bodyStmts.push(returnStmt(r)); - - nodes.push(fn(fnName, ["d", "x"], bodyStmts)); - - return nodes; -} - -/** Build the reverse (decode) AST expression for a single operation. */ -function buildReverseOp(op: DecoderOp, b: JsNode, pk: JsNode): JsNode { - switch (op.kind) { - case "xor": - // Reverse of XOR is XOR: b ^ ((key + pk) & 255) - return bin( - BOp.BitXor, - b, - bin(BOp.BitAnd, bin(BOp.Add, lit(op.key), pk), lit(255)) - ); - case "add": - // Reverse of ADD is SUB: (b - ((key + pk) & 255)) & 255 - return bin( - BOp.BitAnd, - bin( - BOp.Sub, - b, - bin(BOp.BitAnd, bin(BOp.Add, lit(op.key), pk), lit(255)) - ), - lit(255) - ); - case "sub": - // Reverse of SUB is ADD: (b + ((key + pk) & 255)) & 255 - return bin( - BOp.BitAnd, - bin( - BOp.Add, - b, - bin(BOp.BitAnd, bin(BOp.Add, lit(op.key), pk), lit(255)) - ), - lit(255) - ); - case "not": - // Reverse of NOT is NOT: (~b) & 255 - return bin(BOp.BitAnd, un(UOp.BitNot, b), lit(255)); - case "rol": - // Reverse of ROL(n) is ROR(n): (b >>> n) | (b << (8 - n)) & 255 - return bin( - BOp.BitAnd, - bin( - BOp.BitOr, - bin(BOp.Ushr, b, lit(op.n)), - bin(BOp.Shl, b, lit(8 - op.n)) - ), - lit(255) - ); - case "ror": - // Reverse of ROR(n) is ROL(n): (b << n) | (b >>> (8 - n)) & 255 - return bin( - BOp.BitAnd, - bin( - BOp.BitOr, - bin(BOp.Shl, b, lit(op.n)), - bin(BOp.Ushr, b, lit(8 - op.n)) - ), - lit(255) - ); - case "swap_nibbles": - // Reverse of swap nibbles is swap nibbles: ((b << 4) | (b >>> 4)) & 255 - return bin( - BOp.BitAnd, - bin( - BOp.BitOr, - bin(BOp.Shl, b, lit(4)), - bin(BOp.Ushr, b, lit(4)) - ), - lit(255) - ); - } -} - -/** - * Build the AST for the string table and lazy accessor function. - * - * Emits: - * - var _ste = [[encoded bytes], ...] // encoded table - * - var _stc = [] // cache - * - function _sa(i) { return _stc[i] || (_stc[i] = _sd(_ste[i], i)) } - * - * @param encodedTable - Array of encoded byte arrays (one per string) - * @param tableName - Runtime name for encoded table variable - * @param cacheName - Runtime name for cache variable - * @param accessorName - Runtime name for accessor function - * @param decoderName - Runtime name for decoder function - * @returns AST nodes for the string table infrastructure - */ -export function buildStringTableAST( - encodedTable: number[][], - tableName: string, - cacheName: string, - accessorName: string, - decoderName: string -): JsNode[] { - const nodes: JsNode[] = []; - - // var _ste = [[bytes], [bytes], ...] - const tableEntries = encodedTable.map((bytes) => - arr(...bytes.map((b) => lit(b))) - ); - nodes.push(varDecl(tableName, arr(...tableEntries))); - - // var _stc = [] - nodes.push(varDecl(cacheName, arr())); - - // function _sa(i) { return _stc[i] || (_stc[i] = _sd(_ste[i], i)) } - const i = id("i"); - const cacheAccess = { - type: "IndexExpr" as const, - obj: id(cacheName), - index: i, - }; - const tableAccess = { - type: "IndexExpr" as const, - obj: id(tableName), - index: i, - }; - const decodeCall = call(id(decoderName), [tableAccess, i]); - const cacheStore = assign(cacheAccess, decodeCall); - - nodes.push( - fn( - accessorName, - ["i"], - [returnStmt(bin(BOp.Or, cacheAccess, cacheStore))] - ) - ); - - return nodes; -} diff --git a/packages/ruam/src/ruamvm/scattered-keys.ts b/packages/ruam/src/ruamvm/scattered-keys.ts deleted file mode 100644 index 8dbd87d..0000000 --- a/packages/ruam/src/ruamvm/scattered-keys.ts +++ /dev/null @@ -1,297 +0,0 @@ -/** - * Scattered key material. - * - * Splits key material (alphabet string, handler table data, decoder keys) - * into multiple fragments scattered across the IIFE scope. Forces attackers - * to trace the full closure chain to reconstruct any single key. - * - * Advancement over KrakVm: - * - Scatters 3-5 fragments of MULTIPLE key materials (not just 2 alphabet halves) - * - Fragments spread across statement ordering tiers - * - Reassembly operations vary per build (concat, spread, push) - * - Fragment variable names are randomized per build - * - * @module ruamvm/scattered-keys - */ - -import type { JsNode } from "./nodes.js"; -import { - BOp, - id, - lit, - bin, - varDecl, - exprStmt, - assign, - call, - member, - arr, -} from "./nodes.js"; -import { deriveSeed, lcgNext } from "../naming/scope.js"; - -// --- Reassembly strategies --- - -/** Strategy for reassembling string fragments. */ -const enum StrReassembly { - /** `a + b + c` */ - Concat = 0, - /** `[a, b, c].join("")` */ - ArrayJoin = 1, - /** `"".concat(a, b, c)` */ - StringConcat = 2, -} - -/** Strategy for reassembling array fragments. */ -const enum ArrReassembly { - /** `a.concat(b, c)` */ - Concat = 0, - /** `[...a, ...b, ...c]` */ - Spread = 1, -} - -const STR_REASSEMBLY_COUNT = 3; -const ARR_REASSEMBLY_COUNT = 2; - -// --- String fragmentation --- - -/** - * Fragment a string literal into 3-5 parts and generate AST. - * - * @param value - The string to fragment - * @param fragNames - Variable names for each fragment - * @param resultName - Variable name for the reassembled string - * @param seed - Per-build seed for strategy selection - * @returns Object with fragment declarations and reassembly declaration - */ -export function fragmentString( - value: string, - fragNames: string[], - resultName: string, - seed: number -): { fragments: JsNode[]; reassembly: JsNode } { - let state = deriveSeed(seed, "scatterStrFrag"); - - // Use the number of provided fragment names (caller determines count) - state = lcgNext(state); // advance LCG for strategy selection below - const numFrags = fragNames.length; - - // Split string into roughly equal parts - const partLen = Math.ceil(value.length / numFrags); - const parts: string[] = []; - for (let i = 0; i < numFrags; i++) { - const start = i * partLen; - const end = Math.min(start + partLen, value.length); - parts.push(value.slice(start, end)); - } - - // Create fragment declarations - const fragments: JsNode[] = []; - for (let i = 0; i < parts.length; i++) { - const name = fragNames[i] ?? fragNames[fragNames.length - 1]!; - fragments.push(varDecl(name, lit(parts[i]!))); - } - - // Select reassembly strategy - state = lcgNext(state); - const strategy = (state >>> 16) % STR_REASSEMBLY_COUNT; - const fragIds = parts.map((_, i) => - id(fragNames[i] ?? fragNames[fragNames.length - 1]!) - ); - - let reassemblyExpr: JsNode; - switch (strategy as StrReassembly) { - case StrReassembly.Concat: { - // a + b + c - let expr: JsNode = fragIds[0]!; - for (let i = 1; i < fragIds.length; i++) { - expr = bin(BOp.Add, expr, fragIds[i]!); - } - reassemblyExpr = expr; - break; - } - case StrReassembly.ArrayJoin: { - // [a, b, c].join("") - reassemblyExpr = call(member(arr(...fragIds), "join"), [lit("")]); - break; - } - case StrReassembly.StringConcat: { - // "".concat(a, b, c) - reassemblyExpr = call(member(lit(""), "concat"), fragIds); - break; - } - default: - reassemblyExpr = fragIds[0]!; - } - - return { - fragments, - reassembly: varDecl(resultName, reassemblyExpr), - }; -} - -/** - * Fragment a numeric array literal into 2-4 chunks and generate AST. - * - * @param values - The array to fragment - * @param fragNames - Variable names for each fragment - * @param resultName - Variable name for the reassembled array - * @param seed - Per-build seed for strategy selection - * @returns Object with fragment declarations and reassembly declaration - */ -export function fragmentArray( - values: number[], - fragNames: string[], - resultName: string, - seed: number -): { fragments: JsNode[]; reassembly: JsNode } { - let state = deriveSeed(seed, "scatterArrFrag"); - - // Use the number of provided fragment names (caller determines count) - state = lcgNext(state); // advance LCG for strategy selection below - const numFrags = fragNames.length; - - // Split array into roughly equal chunks - const chunkLen = Math.ceil(values.length / numFrags); - const chunks: number[][] = []; - for (let i = 0; i < numFrags; i++) { - const start = i * chunkLen; - const end = Math.min(start + chunkLen, values.length); - chunks.push(values.slice(start, end)); - } - - // Create fragment declarations - const fragments: JsNode[] = []; - for (let i = 0; i < chunks.length; i++) { - const name = fragNames[i] ?? fragNames[fragNames.length - 1]!; - const elements = chunks[i]!.map((v) => lit(v)); - fragments.push(varDecl(name, arr(...elements))); - } - - // Select reassembly strategy - state = lcgNext(state); - const strategy = (state >>> 16) % ARR_REASSEMBLY_COUNT; - const fragIds = chunks.map((_, i) => - id(fragNames[i] ?? fragNames[fragNames.length - 1]!) - ); - - let reassemblyExpr: JsNode; - switch (strategy as ArrReassembly) { - case ArrReassembly.Concat: { - // a.concat(b, c) - reassemblyExpr = call( - member(fragIds[0]!, "concat"), - fragIds.slice(1) - ); - break; - } - case ArrReassembly.Spread: { - // [...a, ...b, ...c] - const spreads = fragIds.map((fid) => ({ - type: "SpreadElement" as const, - arg: fid, - })); - reassemblyExpr = arr(...spreads); - break; - } - default: - reassemblyExpr = fragIds[0]!; - } - - return { - fragments, - reassembly: varDecl(resultName, reassemblyExpr), - }; -} - -/** - * Result of scattering key material. - * - * Contains fragment nodes to be inserted at various tiers - * and reassembly nodes to be inserted where the original - * declaration was. - */ -export interface ScatteredResult { - /** Fragment declarations (to scatter across tiers). */ - tier0Fragments: JsNode[]; - tier1Fragments: JsNode[]; - tier3Fragments: JsNode[]; - tier4Fragments: JsNode[]; - /** Reassembly declarations (insert where original was). */ - reassemblyNodes: JsNode[]; -} - -/** - * Scatter multiple key materials across tiers. - * - * @param materials - Array of {name, value, type} to scatter - * @param nameGen - Function to generate unique random names - * @param seed - Per-build seed - * @returns Scattered result with fragments assigned to tiers - */ -export function scatterKeyMaterials( - materials: Array<{ - name: string; - value: string | number[]; - type: "string" | "array"; - }>, - nameGen: () => string, - seed: number -): ScatteredResult { - const result: ScatteredResult = { - tier0Fragments: [], - tier1Fragments: [], - tier3Fragments: [], - tier4Fragments: [], - reassemblyNodes: [], - }; - - let state = deriveSeed(seed, "scatterTier"); - const tiers = [ - result.tier0Fragments, - result.tier1Fragments, - result.tier3Fragments, - result.tier4Fragments, - ]; - - for (const mat of materials) { - // Generate fragment names - const numFrags = - mat.type === "string" - ? 3 + ((lcgNext(state) >>> 16) % 3) - : 2 + ((lcgNext(state) >>> 16) % 3); - state = lcgNext(state); - const fragNames: string[] = []; - for (let i = 0; i < numFrags; i++) { - fragNames.push(nameGen()); - } - - let scattered: { fragments: JsNode[]; reassembly: JsNode }; - if (mat.type === "string") { - scattered = fragmentString( - mat.value as string, - fragNames, - mat.name, - state - ); - } else { - scattered = fragmentArray( - mat.value as number[], - fragNames, - mat.name, - state - ); - } - state = lcgNext(state); - - // Distribute fragments across tiers (round-robin with seed variation) - for (let i = 0; i < scattered.fragments.length; i++) { - state = lcgNext(state); - const tierIdx = (state >>> 16) % tiers.length; - tiers[tierIdx]!.push(scattered.fragments[i]!); - } - - result.reassemblyNodes.push(scattered.reassembly); - } - - return result; -} diff --git a/packages/ruam/src/ruamvm/string-atomization.ts b/packages/ruam/src/ruamvm/string-atomization.ts deleted file mode 100644 index 95c6c11..0000000 --- a/packages/ruam/src/ruamvm/string-atomization.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Interpreter string atomization. - * - * AST tree transform that collects all string literal nodes from handler - * and builder bodies, encodes them via the polymorphic decoder chain, - * and replaces them with indexed lookups into an encoded string table. - * - * Zero hardcoded strings remain in the interpreter output — even property - * names like "prototype", "length", "call" become `_sa(N)` calls, decoded - * lazily at first access and cached. - * - * @module ruamvm/string-atomization - */ - -import type { JsNode, Literal } from "./nodes.js"; -import { id, call, lit, mapChildren } from "./nodes.js"; -import type { DecoderChain } from "./polymorphic-decoder.js"; -import { - polyEncode, - buildDecoderFunctionAST, - buildStringTableAST, -} from "./polymorphic-decoder.js"; - -// --- Configuration --- - -/** Minimum string length to atomize (very short strings not worth it). */ -const MIN_ATOMIZE_LEN = 2; - -/** Strings to never atomize (JS keywords that must remain as-is in source). */ -const SKIP_STRINGS = new Set([ - // These appear as identifiers, not string values - "use strict", -]); - -// --- String collection --- - -/** - * Recursively collect all unique string literals from an AST node tree. - * Only collects strings from `Literal` nodes (not `Id`, `MemberExpr.prop`, etc.). - * - * @param nodes - AST nodes to scan - * @param collected - Set to accumulate strings into - */ -export function collectStrings(nodes: JsNode[], collected: Set): void { - for (const node of nodes) { - collectStringsFromNode(node, collected); - } -} - -function collectStringsFromNode(node: JsNode, collected: Set): void { - if ( - node.type === "Literal" && - typeof node.value === "string" && - node.value.length >= MIN_ATOMIZE_LEN && - !SKIP_STRINGS.has(node.value) - ) { - collected.add(node.value); - } - - // Recurse into children - mapChildren(node, (child) => { - collectStringsFromNode(child, collected); - return child; // Don't modify, just traverse - }); -} - -// --- String replacement --- - -/** - * Replace string literals in AST nodes with indexed accessor calls. - * - * @param nodes - AST nodes to transform - * @param stringMap - Map of string value → table index - * @param accessorName - Runtime name of the accessor function - * @returns Transformed AST nodes - */ -export function replaceStrings( - nodes: JsNode[], - stringMap: Map, - accessorName: string -): JsNode[] { - return nodes.map((node) => - replaceStringsInNode(node, stringMap, accessorName) - ); -} - -function replaceStringsInNode( - node: JsNode, - stringMap: Map, - accessorName: string -): JsNode { - // Replace string literals with _sa(index) - if ( - node.type === "Literal" && - typeof node.value === "string" && - node.value.length >= MIN_ATOMIZE_LEN && - !SKIP_STRINGS.has(node.value) - ) { - const idx = stringMap.get(node.value); - if (idx != null) { - return call(id(accessorName), [lit(idx)]); - } - } - - // Recurse into children - return mapChildren(node, (child) => - replaceStringsInNode(child, stringMap, accessorName) - ); -} - -// --- Main orchestrator --- - -/** Result of atomizing strings in a set of AST nodes. */ -export interface AtomizationResult { - /** The transformed AST nodes with string literals replaced. */ - transformedNodes: JsNode[]; - /** AST nodes for the string table infrastructure (emit at IIFE scope). */ - infrastructure: JsNode[]; -} - -/** - * Atomize all string literals in the given AST nodes. - * - * 1. Collects all unique string literals - * 2. Encodes each via the polymorphic decoder chain - * 3. Builds the encoded table + decoder + accessor infrastructure - * 4. Replaces string literals with _sa(index) calls - * - * @param nodes - AST nodes to transform (handler/builder bodies) - * @param chain - Polymorphic decoder chain for this build - * @param names - Runtime names for generated identifiers - * @returns Transformed nodes and infrastructure nodes - */ -export function atomizeStrings( - nodes: JsNode[], - chain: DecoderChain, - names: { - decoder: string; - posSeed: string; - table: string; - cache: string; - accessor: string; - } -): AtomizationResult { - // Step 1: Collect unique strings - const strings = new Set(); - collectStrings(nodes, strings); - - if (strings.size === 0) { - return { transformedNodes: nodes, infrastructure: [] }; - } - - // Step 2: Build string → index map (sorted for determinism) - const sortedStrings = [...strings].sort(); - const stringMap = new Map(); - for (let i = 0; i < sortedStrings.length; i++) { - stringMap.set(sortedStrings[i]!, i); - } - - // Step 3: Encode each string - const encodedTable: number[][] = sortedStrings.map((str, i) => - polyEncode(str, chain, i) - ); - - // Step 4: Build infrastructure AST - const decoderNodes = buildDecoderFunctionAST( - chain, - names.decoder, - names.posSeed - ); - const tableNodes = buildStringTableAST( - encodedTable, - names.table, - names.cache, - names.accessor, - names.decoder - ); - - // Step 5: Replace strings in source nodes - const transformedNodes = replaceStrings(nodes, stringMap, names.accessor); - - return { - transformedNodes, - infrastructure: [...decoderNodes, ...tableNodes], - }; -} diff --git a/packages/ruam/src/ruamvm/structural-transforms.ts b/packages/ruam/src/ruamvm/structural-transforms.ts deleted file mode 100644 index 73cb2b8..0000000 --- a/packages/ruam/src/ruamvm/structural-transforms.ts +++ /dev/null @@ -1,549 +0,0 @@ -/** - * AST-level structural transforms for per-build variation. - * - * Applied after all builders produce their AST nodes but before the - * emitter serializes to JS. Each transform is semantics-preserving - * and driven by the per-build {@link StructuralChoices} PRNG. - * - * Transforms: - * - **Declaration merging**: consecutive `var` statements → comma-chained - * - **Expression noise**: `obj.x` ↔ `obj["x"]`, `f()` → `(0,f)()`, - * `a===b` → `!(a!==b)`, numeric literal variations - * - **Member access variation**: dot notation ↔ bracket notation - * - * @module ruamvm/structural-transforms - */ - -import type { - JsNode, - CallExpr, - MemberExpr, - BinOp, - Literal, - VarDecl, - FnDecl, - IfStmt, - ForStmt, -} from "./nodes.js"; -import { BOp, UOp } from "./nodes.js"; -import { resolveName } from "../naming/index.js"; -import type { StructuralChoices } from "../structural-choices.js"; - -// --- Public API --- - -/** - * Apply all structural transforms to a flat array of top-level nodes. - * - * Returns a new array (does not mutate the input). Deep-walks each node - * to apply expression-level transforms, then applies statement-level - * transforms (declaration merging) to the sequence. - * - * @param nodes - The top-level AST nodes from the assembler. - * @param choices - Per-build structural variation choices. - * @returns A transformed copy of the node array. - */ -export function applyStructuralTransforms( - nodes: JsNode[], - choices: StructuralChoices -): JsNode[] { - // Phase 1: deep-walk each node for expression-level transforms - let result = nodes.map((n) => walkNode(n, choices)); - - // Phase 2: declaration style transform (statement-level) - result = applyDeclarationTransform(result, choices); - - return result; -} - -// --- Expression-level walk --- - -/** - * Recursively walk a node and apply expression-level transforms. - * Returns a new node (shallow copy where needed). - */ -function walkNode(node: JsNode, ch: StructuralChoices): JsNode { - switch (node.type) { - // --- Expressions that can be transformed --- - case "MemberExpr": - return transformMember(node, ch); - - case "BinOp": - return transformBinOp(node, ch); - - case "Literal": - return transformLiteral(node, ch); - - case "CallExpr": - return transformCallExpr(node, ch); - - // --- Containers: recurse into children --- - case "VarDecl": - return node.init - ? { ...node, init: walkNode(node.init, ch) } - : node; - case "ConstDecl": - return node.init - ? { ...node, init: walkNode(node.init, ch) } - : node; - case "FnDecl": - return transformFnDecl(node, ch); - case "ExprStmt": - return { ...node, expr: walkNode(node.expr, ch) }; - case "Block": - return { ...node, body: node.body.map((n) => walkNode(n, ch)) }; - case "IfStmt": - return transformIfStmt(node, ch); - case "WhileStmt": - return { - ...node, - test: walkNode(node.test, ch), - body: node.body.map((n) => walkNode(n, ch)), - }; - case "ForStmt": - return transformForStmt(node, ch); - case "ForInStmt": - return { - ...node, - obj: walkNode(node.obj, ch), - body: node.body.map((n) => walkNode(n, ch)), - }; - case "SwitchStmt": - return { - ...node, - disc: walkNode(node.disc, ch), - cases: node.cases.map((c) => ({ - ...c, - label: c.label ? walkNode(c.label, ch) : null, - body: c.body.map((n) => walkNode(n, ch)), - })), - }; - case "ReturnStmt": - return node.value - ? { ...node, value: walkNode(node.value, ch) } - : node; - case "ThrowStmt": - return { ...node, value: walkNode(node.value, ch) }; - case "TryCatchStmt": - return { - ...node, - body: node.body.map((n) => walkNode(n, ch)), - handler: node.handler?.map((n) => walkNode(n, ch)), - finalizer: node.finalizer?.map((n) => walkNode(n, ch)), - }; - case "AssignExpr": - return { - ...node, - target: walkNode(node.target, ch), - value: walkNode(node.value, ch), - }; - case "IndexExpr": - return { - ...node, - obj: walkNode(node.obj, ch), - index: walkNode(node.index, ch), - }; - case "TernaryExpr": - return { - ...node, - test: walkNode(node.test, ch), - then: walkNode(node.then, ch), - else: walkNode(node.else, ch), - }; - case "ArrayExpr": - return { - ...node, - elements: node.elements.map((e) => walkNode(e, ch)), - }; - case "NewExpr": - return { - ...node, - callee: walkNode(node.callee, ch), - args: node.args.map((a) => walkNode(a, ch)), - }; - case "SequenceExpr": - return { - ...node, - exprs: node.exprs.map((e) => walkNode(e, ch)), - }; - case "UnaryOp": - return { ...node, expr: walkNode(node.expr, ch) }; - case "UpdateExpr": - return { ...node, arg: walkNode(node.arg, ch) }; - case "AwaitExpr": - return { ...node, expr: walkNode(node.expr, ch) }; - case "SpreadElement": - return { ...node, arg: walkNode(node.arg, ch) }; - case "FnExpr": - return { ...node, body: node.body.map((n) => walkNode(n, ch)) }; - case "ArrowFn": - return { ...node, body: node.body.map((n) => walkNode(n, ch)) }; - case "StackPush": - return { ...node, value: walkNode(node.value, ch) }; - - // ObjectExpr has complex entries — skip deep-walking to avoid - // breaking getter/setter/method/spread shapes - case "ObjectExpr": - return node; - - // Leaf nodes — no children to walk - case "Id": - case "BreakStmt": - case "ContinueStmt": - case "DebuggerStmt": - case "StackPop": - case "StackPeek": - case "ImportExpr": - case "CaseClause": - return node; - - default: - return node; - } -} - -// --- Member access: obj.prop → obj["prop"] --- - -/** Safe property names that can be converted to bracket notation. */ -const SAFE_BRACKET_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; - -/** Built-in global names — don't transform member access on these - * because the alias declarations (e.g. `var _im = Math.imul`) are - * foundational and must stay in dot notation for downstream checks. */ -const GLOBAL_SKIP = new Set([ - "Math", - "Object", - "Array", - "Symbol", - "JSON", - "String", - "Number", - "Boolean", - "Function", - "RegExp", - "Date", - "Error", - "Promise", - "Map", - "Set", - "globalThis", - "window", - "global", - "self", - "console", - "parseInt", - "Uint8Array", - "Int32Array", - "DataView", - "ArrayBuffer", -]); - -function transformMember(node: MemberExpr, ch: StructuralChoices): JsNode { - const obj = walkNode(node.obj, ch); - // Skip transformation on global built-in objects to preserve - // recognizable alias patterns (Math.imul, Object.create, etc.) - if (obj.type === "Id" && GLOBAL_SKIP.has(resolveName(obj.name))) { - return { ...node, obj }; - } - // Only convert if the property name is a valid identifier - const propStr = resolveName(node.prop); - if ( - SAFE_BRACKET_RE.test(propStr) && - ch.prng() < ch.expressionNoise.dotToBracketBias - ) { - // obj.prop → obj["prop"] - return { - type: "IndexExpr", - obj, - index: { type: "Literal", value: propStr }, - }; - } - return { ...node, obj }; -} - -// --- BinOp: a === b → !(a !== b) --- - -function transformBinOp(node: BinOp, ch: StructuralChoices): JsNode { - const left = walkNode(node.left, ch); - const right = walkNode(node.right, ch); - - if ( - node.op === BOp.Seq && - ch.prng() < ch.expressionNoise.doubleNegationBias - ) { - return { - type: "UnaryOp", - op: UOp.Not, - expr: { type: "BinOp", op: BOp.Sneq, left, right }, - }; - } - if ( - node.op === BOp.Sneq && - ch.prng() < ch.expressionNoise.doubleNegationBias - ) { - return { - type: "UnaryOp", - op: UOp.Not, - expr: { type: "BinOp", op: BOp.Seq, left, right }, - }; - } - - return { ...node, left, right }; -} - -// --- Indirect call: f() → (0, f)() --- - -function transformCallExpr(node: CallExpr, ch: StructuralChoices): JsNode { - const callee = walkNode(node.callee, ch); - const args = node.args.map((a) => walkNode(a, ch)); - - // Only apply to plain identifier calls (not method calls, new, etc.) - // (0, f)() evaluates f without a `this` binding — safe for standalone calls - if ( - callee.type === "Id" && - !GLOBAL_SKIP.has(resolveName(callee.name)) && - ch.prng() < ch.expressionNoise.indirectCallBias - ) { - return { - ...node, - callee: { - type: "SequenceExpr", - exprs: [{ type: "Literal", value: 0 }, callee], - }, - args, - }; - } - - return { ...node, callee, args }; -} - -// --- Numeric literal variation: 42 → 0x2a --- - -function transformLiteral(node: Literal, ch: StructuralChoices): JsNode { - if ( - typeof node.value !== "number" || - !Number.isInteger(node.value) || - node.value < 2 || // Don't transform 0, 1 - node.value > 0xffffff || // Don't transform huge numbers - ch.prng() >= ch.expressionNoise.numericVariationBias - ) { - return node; - } - - const v = node.value; - const roll = ch.prng(); - - if (roll < 0.5) { - // Hex representation: emit as a hex string literal that gets - // parsed. We use IndexExpr trick: the number doesn't change, - // just the source representation. Since our Literal emitter - // outputs numbers as-is, we shift to a BinOp: (v|0) - return { - type: "BinOp", - op: BOp.BitOr, - left: { type: "Literal", value: v }, - right: { type: "Literal", value: 0 }, - }; - } - // Computed: (v + offset - offset) where offset is small - const offset = Math.floor(ch.prng() * 7) + 1; - return { - type: "BinOp", - op: BOp.Sub, - left: { - type: "BinOp", - op: BOp.Add, - left: { type: "Literal", value: v }, - right: { type: "Literal", value: offset }, - }, - right: { type: "Literal", value: offset }, - }; -} - -// --- If statement: simple if/else → ternary --- - -/** - * Convert simple if/else with single ExprStmt bodies to ternary. - * Only applies when both branches are single expression statements - * (safe — no control flow changes). - */ -function transformIfStmt(node: IfStmt, ch: StructuralChoices): JsNode { - const test = walkNode(node.test, ch); - const thenBody = node.then.map((n) => walkNode(n, ch)); - const elseBody = node.else?.map((n) => walkNode(n, ch)); - - // Only convert if both branches are single ExprStmts - if ( - elseBody && - thenBody.length === 1 && - elseBody.length === 1 && - thenBody[0]!.type === "ExprStmt" && - elseBody[0]!.type === "ExprStmt" && - ch.prng() < ch.controlFlow.ternaryBias - ) { - return { - type: "ExprStmt", - expr: { - type: "TernaryExpr", - test, - then: thenBody[0]!.expr, - else: elseBody[0]!.expr, - }, - } as JsNode; - } - - return { ...node, test, then: thenBody, else: elseBody }; -} - -// --- For loop: for → while --- - -/** - * Convert `for(init; test; update) { body }` to - * `init; while(test) { body; update; }`. - * Only applies when the for loop has all three parts. - */ -function transformForStmt(node: ForStmt, ch: StructuralChoices): JsNode { - const init = node.init ? walkNode(node.init, ch) : null; - const test = node.test ? walkNode(node.test, ch) : null; - const update = node.update ? walkNode(node.update, ch) : null; - const body = node.body.map((n) => walkNode(n, ch)); - - if ( - ch.controlFlow.loopStyle === "while" && - init && - test && - update && - !containsContinue(body) && - ch.prng() < 0.5 - ) { - // Wrap init + while in a Block - const whileBody = [ - ...body, - { type: "ExprStmt" as const, expr: update }, - ]; - return { - type: "Block", - body: [ - { type: "ExprStmt" as const, expr: init }, - { - type: "WhileStmt" as const, - test, - body: whileBody, - }, - ], - }; - } - - return { ...node, init, test, update, body }; -} - -// --- Function form: FnDecl → var = FnExpr --- - -/** - * Convert function declarations to function expressions assigned to - * a variable. Does not convert to arrow functions because runtime - * handlers use `this` and `arguments`. - */ -function transformFnDecl(node: FnDecl, ch: StructuralChoices): JsNode { - const body = node.body.map((n) => walkNode(n, ch)); - - if (ch.prng() < ch.functionFormBias) { - // FnDecl → var name = function name(...) { ... } - return { - type: "VarDecl", - name: node.name, - init: { - type: "FnExpr", - name: node.name, - params: node.params, - body, - async: node.async, - }, - }; - } - - return { ...node, body }; -} - -// --- Helpers --- - -/** Check if a body contains a ContinueStmt (shallow — doesn't enter nested loops). */ -function containsContinue(body: JsNode[]): boolean { - for (const n of body) { - if (n.type === "ContinueStmt") return true; - if (n.type === "IfStmt") { - if (containsContinue(n.then)) return true; - if (n.else && containsContinue(n.else)) return true; - } - if (n.type === "Block") { - if (containsContinue(n.body)) return true; - } - // Don't enter nested loops — their continue is scoped to them - } - return false; -} - -// --- Declaration style: merge/split consecutive var declarations --- - -function applyDeclarationTransform( - nodes: JsNode[], - ch: StructuralChoices -): JsNode[] { - if (ch.declarationStyle === "individual") return nodes; - - const result: JsNode[] = []; - let i = 0; - - while (i < nodes.length) { - const node = nodes[i]!; - - // Collect consecutive VarDecl nodes - if (node.type === "VarDecl") { - const group: VarDecl[] = [node]; - let j = i + 1; - while (j < nodes.length && nodes[j]!.type === "VarDecl") { - group.push(nodes[j] as VarDecl); - j++; - } - - if (group.length > 1) { - if (ch.declarationStyle === "chained") { - // Merge all into one chained VarDecl group - result.push({ - type: "VarDecl", - name: "__chain__", - init: undefined, - _chain: group, - } as VarDecl & { _chain: VarDecl[] }); - } else { - // "mixed": randomly group 1-4 consecutive declarations - let k = 0; - while (k < group.length) { - const size = Math.min( - Math.floor(ch.prng() * 4) + 1, - group.length - k - ); - if (size === 1) { - result.push(group[k]!); - } else { - result.push({ - type: "VarDecl", - name: "__chain__", - init: undefined, - _chain: group.slice(k, k + size), - } as VarDecl & { _chain: VarDecl[] }); - } - k += size; - } - } - } else { - result.push(node); - } - i = j; - } else { - result.push(node); - i++; - } - } - - return result; -} diff --git a/packages/ruam/src/ruamvm/transforms.ts b/packages/ruam/src/ruamvm/transforms.ts deleted file mode 100644 index 3b68105..0000000 --- a/packages/ruam/src/ruamvm/transforms.ts +++ /dev/null @@ -1,484 +0,0 @@ -/** - * AST tree transforms for runtime code generation. - * - * Structural replacements that operate on the JsNode tree before - * emission. Replaces the regex-based post-processing passes from - * the old template system. - * - * @module ruamvm/transforms - */ - -import type { JsNode } from "./nodes.js"; -import { id, mapChildren } from "./nodes.js"; -import { resolveName } from "../naming/index.js"; -import type { NameRegistry } from "../naming/registry.js"; -import { LCG_MULTIPLIER, LCG_INCREMENT } from "../constants.js"; - -// --- Generic tree walker --- - -/** - * Walk a JsNode tree bottom-up, applying a visitor to each node. - * The visitor returns a replacement node or null to keep the original. - * Raw nodes are opaque — their contents are not walked. - */ -function walkReplace( - node: JsNode, - visitor: (n: JsNode) => JsNode | null -): JsNode { - const walked = walkChildren(node, visitor); - return visitor(walked) ?? walked; -} - -/** - * Recursively walk all child nodes, producing a new node with walked children. - * Delegates to the generic mapChildren() from nodes.ts using the CHILD_FIELDS metadata table. - */ -function walkChildren( - node: JsNode, - visitor: (n: JsNode) => JsNode | null -): JsNode { - return mapChildren(node, (child) => walkReplace(child, visitor)); -} - -// --- obfuscateLocals --- - -/** Names that must NOT be renamed (JS built-ins, APIs, short names). */ -export const KEEP = new Set([ - // JS built-ins - "undefined", - "null", - "true", - "false", - "NaN", - "Infinity", - "void", - "typeof", - "instanceof", - "delete", - "new", - "this", - "arguments", - // Globals used in the output - "Object", - "Array", - "Symbol", - "String", - "Number", - "Boolean", - "BigInt", - "RegExp", - "Math", - "JSON", - "Date", - "Error", - "TypeError", - "RangeError", - "ReferenceError", - "SyntaxError", - "Uint8Array", - "DataView", - "Buffer", - "globalThis", - "window", - "global", - "self", - "console", - "atob", - "eval", - "setInterval", - "setTimeout", - "clearInterval", - "clearTimeout", - // Short generic names (already look minified) - "a", - "b", - "c", - "d", - "e", - "f", - "g", - "h", - "i", - "j", - "k", - "s", - "v", - "w", - "x", - "a1", - "a2", - "a3", - "ai", - "ki", - "si", - "ri", - "ti", - // Object property/method names that are part of the language API - "length", - "push", - "pop", - "call", - "apply", - "bind", - "keys", - "value", - "done", - "next", - "return", - "get", - "set", - "create", - "freeze", - "seal", - "from", - "assign", - "prototype", - "constructor", - "name", - "writable", - "configurable", - "enumerable", - "slice", - "concat", - "indexOf", - "join", - "charCodeAt", - "toString", - "getPrototypeOf", - "setPrototypeOf", - "defineProperty", - "isArray", - "getUint8", - "getUint16", - "getUint32", - "getInt32", - "getFloat64", - "getInt8", - "getInt16", - "buffer", - "byteOffset", - "byteLength", - "fromCharCode", - "reduce", - "floor", - "parse", - "stringify", - "iterator", - "asyncIterator", - "hasInstance", - "toPrimitive", - "toStringTag", - "species", - "isConcatSpreadable", - "match", - "replace", - "search", - "split", - "unscopables", - "raw", - "log", - "warn", - "message", - // Computed identifiers - "id", - "uid", - "cs", - "ct", -]); - -/** JS reserved words and keywords that can't be used as identifiers. */ -export const RESERVED = new Set([ - "do", - "if", - "in", - "of", - "as", - "is", - "for", - "let", - "new", - "try", - "var", - "int", - "case", - "else", - "enum", - "null", - "this", - "true", - "void", - "with", - "await", - "break", - "catch", - "class", - "const", - "false", - "super", - "throw", - "while", - "yield", - "delete", - "export", - "import", - "public", - "return", - "static", - "switch", - "typeof", - "default", - "extends", - "finally", - "package", - "private", - "continue", - "debugger", - "function", - "abstract", - "volatile", - "protected", - "interface", - "instanceof", - "implements", -]); - -/** - * Rename case-local variables with names >= 3 chars to short 2-char names. - * - * Collects VarDecl.name, FnDecl.params, FnExpr.params, ArrowFn.params, - * ForInStmt.decl, and TryCatchStmt.param entries with names >= 3 chars - * that aren't in the KEEP set and don't start with `_`. Generates - * short replacements via LCG and renames all matching Id references. - * - * Raw nodes are opaque — identifiers inside them are not renamed. - * - * @param nodes - The statement list to transform - * @param seed - LCG seed for deterministic name generation - * @param reserved - Optional set of names that must not be used as - * replacement targets (e.g. RuntimeNames/TempNames values already - * allocated for the same scope). - * @param registry - Optional NameRegistry for collision-safe name generation. - * When provided, replacement names are allocated via the registry's - * dynamic generator, ensuring handler-local names never collide with - * IIFE-scope identifiers. - * @returns Transformed statement list - */ -export function obfuscateLocals( - nodes: JsNode[], - seed: number, - reserved?: ReadonlySet, - registry?: NameRegistry -): JsNode[] { - // Collect names to rename - const toRename = new Set(); - collectNames(nodes, toRename); - - // Never rename names that are in the reserved set — these are - // runtime/temp identifiers shared with other AST trees (e.g. the - // exec function references handler group arrays declared in - // iifeDecls). Renaming them here without also renaming the - // free-variable references in the other tree breaks the binding. - if (reserved) { - for (const name of reserved) { - toRename.delete(name); - } - } - - if (toRename.size === 0) return nodes; - - // Generate replacement names — prefer registry dynamic generator - // for collision-safe allocation against the global used set. - // Falls back to internal LCG when no registry is available. - let genShort: () => string; - - if (registry) { - const dynGen = registry.createDynamicGenerator( - "handlerLocals", - "short" - ); - const localUsed = new Set(reserved); - genShort = () => { - for (;;) { - const name = dynGen(); - if ( - !KEEP.has(name) && - !RESERVED.has(name) && - !localUsed.has(name) - ) { - localUsed.add(name); - return name; - } - } - }; - } else { - let s = seed >>> 0; - function lcg(): number { - s = (s * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - return s; - } - const alpha = "abcdefghijklmnopqrstuvwxyz"; - const alnum = "abcdefghijklmnopqrstuvwxyz0123456789"; - const used = new Set(reserved); - - // Per-seed name length preference: some builds get 2-char handler - // locals, others get 2-3 char mix, breaking the "all handler - // locals are exactly N chars" fingerprint. - const threeCharBias = ((lcg() >>> 0) / 0x100000000) * 0.6; // 0-60% - - genShort = (): string => { - for (let attempt = 0; ; attempt++) { - const useThree = (lcg() >>> 0) / 0x100000000 < threeCharBias; - let name: string; - if (useThree) { - const c1 = alpha[lcg() % alpha.length]!; - const c2 = alnum[lcg() % alnum.length]!; - const c3 = alnum[lcg() % alnum.length]!; - name = c1 + c2 + c3; - } else { - const c1 = alpha[lcg() % alpha.length]!; - const c2 = alnum[lcg() % alnum.length]!; - name = c1 + c2; - } - if (!used.has(name) && !KEEP.has(name) && !RESERVED.has(name)) { - used.add(name); - return name; - } - if (attempt > 500) { - const c1 = alpha[lcg() % alpha.length]!; - const c2 = alnum[lcg() % alnum.length]!; - const fallback = c1 + c2; - if ( - !used.has(fallback) && - !KEEP.has(fallback) && - !RESERVED.has(fallback) - ) { - used.add(fallback); - return fallback; - } - } - } - }; - } - - const renameMap = new Map(); - for (const name of toRename) { - renameMap.set(name, genShort()); - } - - // Apply renames - return nodes.map((n) => renameNode(n, renameMap)); -} - -/** Check if a name should be renamed. */ -function shouldRename(name: string): boolean { - return name.length >= 3 && !KEEP.has(name) && !name.startsWith("_"); -} - -/** Collect variable and parameter names to rename from the tree. */ -function collectNames(nodes: JsNode[], out: Set): void { - for (const node of nodes) { - collectNamesFromNode(node, out); - } -} - -function collectNamesFromNode(node: JsNode, out: Set): void { - // Handle the 7 node types that declare names - switch (node.type) { - case "VarDecl": - case "ConstDecl": { - const name = resolveName(node.name); - if (shouldRename(name)) out.add(name); - break; - } - case "FnDecl": - case "FnExpr": - case "ArrowFn": - for (const p of node.params) { - const pStr = String(p); - const clean = pStr.replace(/^\.\.\./, ""); - if (shouldRename(clean)) out.add(clean); - } - break; - case "ForInStmt": { - const decl = resolveName(node.decl); - if (shouldRename(decl)) out.add(decl); - break; - } - case "TryCatchStmt": { - if (node.param) { - const param = resolveName(node.param); - if (shouldRename(param)) out.add(param); - } - break; - } - } - // Traverse all children generically — no 36-case switch needed - mapChildren(node, (child) => { - collectNamesFromNode(child, out); - return child; - }); -} - -/** Rename identifiers in a node tree using the rename map. */ -function renameNode(node: JsNode, map: Map): JsNode { - return walkReplace(node, (n) => { - switch (n.type) { - case "Id": { - const renamed = map.get(resolveName(n.name)); - return renamed ? id(renamed) : null; - } - case "VarDecl": { - const renamed = map.get(resolveName(n.name)); - return renamed ? { ...n, name: renamed } : null; - } - case "ConstDecl": { - const renamed = map.get(resolveName(n.name)); - return renamed ? { ...n, name: renamed } : null; - } - case "FnDecl": { - const newParams = renameParams(n.params as string[], map); - return newParams ? { ...n, params: newParams } : null; - } - case "FnExpr": { - const newParams = renameParams(n.params as string[], map); - return newParams ? { ...n, params: newParams } : null; - } - case "ArrowFn": { - const newParams = renameParams(n.params as string[], map); - return newParams ? { ...n, params: newParams } : null; - } - case "ForInStmt": { - const renamed = map.get(resolveName(n.decl)); - return renamed ? { ...n, decl: renamed } : null; - } - case "TryCatchStmt": { - if (n.param) { - const renamed = map.get(resolveName(n.param)); - return renamed ? { ...n, param: renamed } : null; - } - return null; - } - default: - return null; - } - }); -} - -/** Rename function params, returning null if no changes. */ -function renameParams( - params: string[], - map: Map -): string[] | null { - let changed = false; - const newParams = params.map((p) => { - const isRest = p.startsWith("..."); - const clean = isRest ? p.slice(3) : p; - const renamed = map.get(clean); - if (renamed) { - changed = true; - return isRest ? `...${renamed}` : renamed; - } - return p; - }); - return changed ? newParams : null; -} diff --git a/packages/ruam/src/structural-choices.ts b/packages/ruam/src/structural-choices.ts deleted file mode 100644 index 9fbb1c7..0000000 --- a/packages/ruam/src/structural-choices.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Per-build structural variation choices. - * - * Controls how the runtime code is structured — statement order, - * control flow style, declaration forms, expression noise. All choices - * are deterministically derived from the build seed so builds are - * reproducible. - * - * @module structural-choices - */ - -import { LCG_MULTIPLIER, LCG_INCREMENT } from "./constants.js"; -import { deriveSeed } from "./naming/scope.js"; - -// --- Public interfaces --- - -/** Dispatch architecture style for the interpreter. */ -export type DispatchStyle = "function-table" | "direct-array" | "object-lookup"; - -/** Return signaling mechanism for handler → dispatch loop. */ -export type ReturnMechanism = "sentinel" | "tagged" | "flag"; - -/** Per-build choices that affect runtime code structure. */ -export interface StructuralChoices { - /** Shuffled indices for runtime component ordering within each tier. */ - statementOrder: { - tier0: number[]; - tier1: number[]; - tier2: number[]; - tier3: number[]; - tier4: number[]; - }; - - /** - * Shuffled indices for the merged tier 0 + tier 1 preamble pool. - * When present, tier 0 and tier 1 components are combined into one - * pool and shuffled together — making the output beginning vary - * significantly between builds. - */ - preambleOrder: number[]; - - /** Interpreter dispatch architecture. */ - dispatchStyle: DispatchStyle; - - /** Return signaling mechanism. */ - returnMechanism: ReturnMechanism; - - /** - * Random tag value for tagged-return mechanism (1-254). - * Only meaningful when `returnMechanism === "tagged"`. - */ - returnTag: number; - - /** Control flow style preferences. */ - controlFlow: { - /** Probability (0-1) of converting simple if/else to ternary. */ - ternaryBias: number; - /** Preferred loop form for simple counted loops. */ - loopStyle: "for" | "while"; - /** Probability (0-1) of converting && to if block and vice versa. */ - shortCircuitBias: number; - }; - - /** How consecutive var declarations are grouped. */ - declarationStyle: "individual" | "chained" | "mixed"; - - /** Probability of converting FnDecl to var = FnExpr (0-1). */ - functionFormBias: number; - - /** Expression-level noise toggles. */ - expressionNoise: { - /** obj.x → obj["x"] probability. */ - dotToBracketBias: number; - /** f() → (0,f)() probability. */ - indirectCallBias: number; - /** a === b → !(a !== b) probability. */ - doubleNegationBias: number; - /** Numeric literal → hex/computed probability. */ - numericVariationBias: number; - }; - - /** PRNG for per-node coin flips during AST transforms. */ - prng: () => number; -} - -// --- Tier sizes (number of shuffleable components per tier) --- - -/** - * Tier 0: foundational declarations — must come first. - * Components: imul alias, spread symbol, hop alias, globalRef, TDZ sentinel. - */ -export const TIER_0_SIZE = 5; - -/** - * Tier 1: crypto/encoding primitives — used by loader/interpreter. - * Components: binary decoder, fingerprint, RC4, rolling cipher helpers, string decoder. - * (Actual count varies with options; max 5.) - */ -export const TIER_1_MAX = 5; - -/** - * Tier 2: interpreter machinery. - * Components: handler table init, integrity binding, interpreter functions. - * (Fixed ordering due to data dependency: table → integrity → interpreters.) - */ -export const TIER_2_SIZE = 1; // Not shuffleable — dependency chain - -/** - * Tier 3: dispatch layer. - * Components: runners, loader + cache + deserializer. - */ -export const TIER_3_SIZE = 3; - -/** - * Tier 4: wiring. - * Components: global exposure, debug protection, debug logging. - */ -export const TIER_4_MAX = 3; - -// --- Generator --- - -/** - * Generate all structural choices from a build seed. - * - * Uses a separate LCG stream (seeded via deriveSeed) so it doesn't - * perturb the existing opcode-shuffle / name-generation PRNG sequences. - * - * @param seed - The per-build CSPRNG seed (same as opcode shuffle seed). - * @returns A frozen {@link StructuralChoices} object. - */ -export function generateStructuralChoices(seed: number): StructuralChoices { - // Separate LCG stream so we don't alter existing PRNG sequences - let state = deriveSeed(seed, "structural"); - function lcg(): number { - state = (Math.imul(state, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - return state; - } - - /** Seeded Fisher-Yates shuffle. */ - function shuffle(n: number): number[] { - const indices = Array.from({ length: n }, (_, i) => i); - for (let i = n - 1; i > 0; i--) { - const j = lcg() % (i + 1); - [indices[i], indices[j]] = [indices[j]!, indices[i]!]; - } - return indices; - } - - /** Float in [0, 1) from next LCG value. */ - function float(): number { - return (lcg() >>> 0) / 0x100000000; - } - - /** Pick one of N options. */ - function pick(options: T[]): T { - return options[lcg() % options.length]!; - } - - // Create a separate PRNG for per-node coin flips (independent stream) - let prngState = lcg(); - function prng(): number { - prngState = - (Math.imul(prngState, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0; - return prngState / 0x100000000; - } - - // Preamble order: combined tier 0 + tier 1 shuffle (max 15 items) - const preambleOrder = shuffle(TIER_0_SIZE + TIER_1_MAX); - - return { - statementOrder: { - tier0: shuffle(TIER_0_SIZE), - tier1: shuffle(TIER_1_MAX), - tier2: [0], // Not shuffleable — dependency chain - tier3: shuffle(TIER_3_SIZE), - tier4: shuffle(TIER_4_MAX), - }, - preambleOrder, - - dispatchStyle: pick([ - "function-table", - "direct-array", - "object-lookup", - ]), - - returnMechanism: pick(["sentinel", "tagged", "flag"]), - returnTag: (lcg() % 254) + 1, // 1-254 - - controlFlow: { - ternaryBias: 0.15 + float() * 0.45, // 15-60% - loopStyle: pick(["for", "while"]), - shortCircuitBias: 0.1 + float() * 0.3, // 10-40% - }, - - declarationStyle: pick(["individual", "chained", "mixed"]), - functionFormBias: 0.2 + float() * 0.4, // 20-60% - - expressionNoise: { - dotToBracketBias: 0.1 + float() * 0.35, // 10-45% - indirectCallBias: 0.05 + float() * 0.15, // 5-20% - doubleNegationBias: 0.08 + float() * 0.22, // 8-30% - numericVariationBias: 0.1 + float() * 0.3, // 10-40% - }, - - prng, - }; -} diff --git a/packages/ruam/src/testing.ts b/packages/ruam/src/testing.ts new file mode 100644 index 0000000..455c25a --- /dev/null +++ b/packages/ruam/src/testing.ts @@ -0,0 +1,42 @@ +/** + * Internal deterministic test entry points. + * + * This module is intentionally not exported from the package root. + * + * @module testing + */ + +import { + obfuscateCodeWithEntropy, + protectCodeWithEntropy, +} from "./transform.js"; +import type { RuamOptions } from "./isogloss/options.js"; +import { createDeterministicEntropy } from "./random/entropy.js"; + +/** Obfuscate with reproducible build entropy for seed-stress tests. */ +export function obfuscateCodeDeterministic( + source: string, + options: RuamOptions = {}, + seed = 0 +): string { + return obfuscateCodeWithEntropy( + source, + options, + createDeterministicEntropy(seed) + ); +} + +/** Build the complete deterministic Isogloss result, including honest stats. */ +export function protectCodeDeterministic( + source: string, + options: RuamOptions = {}, + seed = 0 +) { + return protectCodeWithEntropy( + source, + options, + createDeterministicEntropy(seed) + ); +} + +export { createDeterministicEntropy } from "./random/entropy.js"; diff --git a/packages/ruam/src/transform.ts b/packages/ruam/src/transform.ts index 5208212..582ab20 100644 --- a/packages/ruam/src/transform.ts +++ b/packages/ruam/src/transform.ts @@ -1,1409 +1,89 @@ /** - * Main transformation orchestrator. + * Public Isogloss source transformation. * - * {@link obfuscateCode} is the core function that: - * 1. Resolves presets and options - * 2. Optionally preprocesses identifiers - * 3. Parses the source with Babel - * 4. Identifies target functions (root-level or comment-annotated) - * 5. Compiles each target to bytecode - * 6. Generates the VM runtime with randomized identifiers - * 7. Assembles the final output (runtime IIFE + bytecode table + modified AST) + * This is the only shipped execution transform. It contains no bytecode + * compiler, loader, dispatcher, interpreter, or compatibility route to the + * removed VM architecture. * * @module transform */ -import { parse } from "@babel/parser"; -import type { NodePath } from "@babel/traverse"; -import * as t from "@babel/types"; -import { traverse, generate } from "./babel-compat.js"; -import { compileFunction, resetUnitCounter } from "./compiler/index.js"; import { - generateShuffleMap, - OPCODE_COUNT, - Op, - ALL_JUMP_OPS, - PACKED_JUMP_OPS, -} from "./compiler/opcodes.js"; -import { encodeBytecodeUnit } from "./compiler/encode.js"; + buildLocalIsoglossSource, + type IsoglossSourceBuildResult, +} from "./isogloss/source-transform.js"; import { - generateVmRuntime, - generateShieldedVmRuntime, -} from "./ruamvm/assembler.js"; -import type { ShieldingGroup, VmRuntimeResult } from "./ruamvm/assembler.js"; -import type { RuntimeNames, TempNames } from "./naming/compat-types.js"; + resolveRuamOptions, + type RuamOptions, +} from "./isogloss/options.js"; +import { preprocessIdentifiers } from "./preprocess.js"; import { - setupRegistry, - setupShieldedRegistry, - deriveSeed, -} from "./naming/index.js"; -import type { NameRegistry } from "./naming/index.js"; -import { resolveOptions } from "./presets.js"; -import type { ResolvedOptions } from "./presets.js"; -import type { VmObfuscationOptions, BytecodeUnit } from "./types.js"; -import { preprocessIdentifiers, collectIdentifiers } from "./preprocess.js"; -import { - BABEL_PARSER_PLUGINS, - FNV_OFFSET_BASIS, - FNV_PRIME, - LCG_MULTIPLIER, - LCG_INCREMENT, -} from "./constants.js"; -import { buildInterpreterFunctions } from "./ruamvm/builders/interpreter.js"; -import { emit } from "./ruamvm/emit.js"; -// generateAlphabet no longer needed — provided by NameRegistry -import { generateStructuralChoices } from "./structural-choices.js"; -import type { StructuralChoices } from "./structural-choices.js"; -import { permuteBlocks } from "./compiler/block-permutation.js"; -import { - insertMutationOpcodes, - adjustEncodingForMutations, -} from "./compiler/opcode-mutation.js"; -import { scatterBytecodeUnit } from "./ruamvm/bytecode-scatter.js"; -import { - buildCipherBlocks, - type CipherBlock, -} from "./compiler/incremental-cipher.js"; -import { getTuningProfile, presetToIntensity } from "./tuning.js"; -import type { TuningProfile } from "./tuning.js"; - -import { randomBytes } from "node:crypto"; + createCryptoEntropy, + type BuildEntropy, +} from "./random/entropy.js"; -/** - * Generate a cryptographically strong 32-bit seed. - * - * Uses Node.js `crypto.randomBytes` for proper entropy instead of - * `Date.now() ^ Math.random()` which is predictable. - */ -function generateCryptoSeed(): number { - return randomBytes(4).readUInt32LE(0); +/** Build protected source and return its honest Isogloss metadata. */ +export function protectCode( + source: string, + options: RuamOptions = {} +): IsoglossSourceBuildResult { + return protectCodeWithEntropy(source, options, createCryptoEntropy()); } -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - /** - * Obfuscate a JavaScript source string by compiling eligible functions - * into custom bytecode and embedding a VM interpreter. + * Convenience string-only API. * - * @param source - The JavaScript source code. - * @param options - Obfuscation options (see {@link VmObfuscationOptions}). - * @returns The obfuscated JavaScript source code. + * This alias remains source-oriented; it invokes exactly the same Isogloss + * transform as {@link protectCode} and never routes through a VM. */ export function obfuscateCode( source: string, - options: VmObfuscationOptions = {} + options: RuamOptions = {} ): string { - const resolved = resolveOptions(options); - const { - targetMode = "root", - threshold = 1.0, - preprocessIdentifiers: preprocess = false, - encryptBytecode = false, - debugProtection = false, - debugLogging = false, - dynamicOpcodes = true, - decoyOpcodes = false, - deadCodeInjection = false, - stackEncoding = false, - rollingCipher = false, - integrityBinding = false, - vmShielding = false, - mixedBooleanArithmetic = false, - handlerFragmentation = false, - blockPermutation = false, - opcodeMutation = false, - polymorphicDecoder = false, - stringAtomization = false, - scatteredKeys = false, - bytecodeScattering = false, - incrementalCipher = false, - semanticOpacity = false, - observationResistance = false, - wrapOutput = false, - } = resolved; - - // -- Compute tuning profile from preset intensity ------------------------- - const tuning = getTuningProfile(presetToIntensity(resolved.preset)); - - // -- Generate per-file seed (needed for both preprocessing and opcodes) -- - const shuffleSeed = generateCryptoSeed(); - - // -- Optional identifier preprocessing ----------------------------------- - let code = source; - let preprocessUsedNames: Set | undefined; - if (preprocess) { - const ppResult = preprocessIdentifiers(code, shuffleSeed); - code = ppResult.code; - preprocessUsedNames = ppResult.usedNames; - } else { - // Preprocessing off: user identifiers stay LIVE in the output, so the - // NameRegistry must reserve them — otherwise a generated VM identifier - // can collide with a kept user identifier and silently overwrite it - // (a rare, seed-dependent miscompile). - preprocessUsedNames = collectIdentifiers(code); - } - - // -- Generate per-file opcode shuffle ------------------------------------ - resetUnitCounter(shuffleSeed); - const shuffleMap = generateShuffleMap(shuffleSeed); - - // -- Generate randomized runtime identifiers + alphabet via NameRegistry -- - const { - registry, - runtime: names, - temps, - alphabet, - } = setupRegistry(shuffleSeed, preprocessUsedNames); - - // -- Generate per-build structural variation choices -------------------- - const structuralChoices = generateStructuralChoices(shuffleSeed); - - // -- Parse --------------------------------------------------------------- - const ast = parse(code, { - sourceType: "unambiguous", - plugins: [...BABEL_PARSER_PLUGINS], - }); - - // -- Collect target functions -------------------------------------------- - const targetPaths = collectTargetFunctions(ast, targetMode, threshold); - - // -- VM Shielding path --------------------------------------------------- - if (vmShielding) { - return assembleShielded(ast, targetPaths, { - encryptBytecode, - debugProtection, - debugLogging, - decoyOpcodes, - deadCodeInjection, - stackEncoding, - integrityBinding, - mixedBooleanArithmetic, - handlerFragmentation, - blockPermutation, - opcodeMutation, - polymorphicDecoder, - stringAtomization, - scatteredKeys, - bytecodeScattering, - incrementalCipher, - semanticOpacity, - observationResistance, - wrapOutput, - preprocessUsedNames, - tuning, - }); - } - - // -- Generate per-build cipher salt (if rolling cipher is enabled) -------- - const cipherSalt = rollingCipher ? generateCryptoSeed() : undefined; - - // -- Compile each target (no encoding yet — need keyAnchor first) ------- - const compiledUnits = compileTargetsOnly( - targetPaths, - names, - deadCodeInjection, - shuffleSeed, - temps["_ps"], - blockPermutation, - opcodeMutation, - tuning - ); - - if (compiledUnits.size === 0) return code; - - // -- Detect async units (for conditional async interpreter emit) -------- - let hasAsyncUnits = false; - for (const [, { unit }] of compiledUnits) { - if (unit.isAsync) { - hasAsyncUnits = true; - break; - } - } - - // -- Collect used opcodes (for dynamicOpcodes / decoyOpcodes) ----------- - let usedOpcodes: Set | undefined; - if (dynamicOpcodes || decoyOpcodes) { - usedOpcodes = collectUsedOpcodes(compiledUnits); - } - - // -- Compute integrity hash + key anchor -------------------------------- - // The key anchor is a checksum of the packed handler table, computed - // by buildInterpreterFunctions. We need it before encoding because - // it's folded into the rolling cipher key derivation. - // - // When integrityBinding is on, we also hash the interpreter source - // and embed it as a literal — changing the interpreter without - // updating the hash breaks all decryption. - let integrityHash: number | undefined; - if (integrityBinding) { - const interpResult = buildInterpreterFunctions( - names, - temps, - shuffleMap, - debugLogging, - true, - shuffleSeed, - { - dynamicOpcodes, - decoyOpcodes, - stackEncoding, - usedOpcodes, - mixedBooleanArithmetic, - handlerFragmentation, - opcodeMutation, - incrementalCipher, - semanticOpacity, - observationResistance, - }, - undefined, - hasAsyncUnits, - structuralChoices - ); - const interpSource = interpResult.interpreters - .map((n) => emit(n)) - .join("\n"); - integrityHash = fnv1a(interpSource); - } - - // -- Generate runtime (produces key anchor value) ----------------------- - const runtimeResult = generateVmRuntime({ - opcodeShuffleMap: shuffleMap, - names, - temps, - encrypt: encryptBytecode, - debugProtection, - debugLogging, - dynamicOpcodes, - decoyOpcodes, - stackEncoding, - seed: shuffleSeed, - stringKey: shuffleSeed, - rollingCipher, - integrityBinding, - integrityHash, - usedOpcodes, - cipherSalt, - mixedBooleanArithmetic, - handlerFragmentation, - polymorphicDecoder, - stringAtomization, - scatteredKeys, - opcodeMutation, - bytecodeScattering, - incrementalCipher, - semanticOpacity, - observationResistance, - identityBindingCount: tuning.identityBindingCount, - witnessCheckProbability: tuning.witnessCheckProbability, - alphabet, - hasAsyncUnits, - structuralChoices, - registry, - }); - - // -- Encode all units (now that we have the key anchor) ----------------- - const keyAnchor = rollingCipher ? runtimeResult.keyAnchorValue : undefined; - const encodedUnits = encodeAllUnits( - compiledUnits, - shuffleMap, - encryptBytecode, - shuffleSeed, - rollingCipher, - integrityHash, - cipherSalt, - keyAnchor, - alphabet, - opcodeMutation, - incrementalCipher - ); - - // -- Assemble output ----------------------------------------------------- - return assembleOutputFromParts( - ast, - encodedUnits, - names, - temps, - runtimeResult.source, - wrapOutput, - shuffleSeed, - bytecodeScattering, - tuning, - registry - ); -} - -// --------------------------------------------------------------------------- -// FNV-1a hash (build-time, matches runtime ihashFn) -// --------------------------------------------------------------------------- - -function fnv1a(s: string): number { - let h = FNV_OFFSET_BASIS; - for (let i = 0; i < s.length; i++) { - h ^= s.charCodeAt(i); - h = Math.imul(h, FNV_PRIME); - } - return h >>> 0; -} - -// --------------------------------------------------------------------------- -// Target function collection -// --------------------------------------------------------------------------- - -/** - * Walk the AST and collect functions that should be compiled to bytecode. - */ -function collectTargetFunctions( - ast: t.File, - mode: "root" | "comment", - threshold: number -): NodePath[] { - const targets: NodePath[] = []; - - traverse(ast, { - FunctionDeclaration(path) { - if (shouldTarget(path as NodePath, mode, threshold)) { - targets.push(path as NodePath); - } - }, - FunctionExpression(path) { - if (shouldTarget(path as NodePath, mode, threshold)) { - targets.push(path as NodePath); - } - }, - ArrowFunctionExpression(path) { - if (shouldTarget(path as NodePath, mode, threshold)) { - targets.push(path as NodePath); - } - }, - }); - - return targets; -} - -/** - * Decide whether a function should be compiled to bytecode. - * - * - `"comment"` mode: only if preceded by `/* ruam:vm *​/` - * - `"root"` mode: any function not nested inside another function - */ -function shouldTarget( - path: NodePath, - mode: "root" | "comment", - threshold: number -): boolean { - if (mode === "comment") { - const leadingComments = path.node.leadingComments; - if (!leadingComments) return false; - return leadingComments.some((c) => c.value.trim() === "ruam:vm"); - } - - // "root" mode: reject anything nested inside another function - let current: NodePath | null = path.parentPath; - while (current) { - if (current.isFunction()) return false; - current = current.parentPath; - } - - if (threshold < 1.0 && Math.random() > threshold) return false; - return true; -} - -// --------------------------------------------------------------------------- -// Compilation -// --------------------------------------------------------------------------- - -/** - * Compile a list of target function paths into bytecode units (no encoding). - * - * Compilation and encoding are split into separate phases because the - * key anchor value (needed for encoding) comes from the handler table - * checksum, which requires knowing which opcodes are used — and that - * depends on compilation. - */ -function compileTargetsOnly( - targetPaths: NodePath[], - names: RuntimeNames, - deadCodeInjection: boolean = false, - seed: number, - scopeVarName?: string, - blockPermutationOpt: boolean = false, - opcodeMutationOpt: boolean = false, - tuning?: Readonly -): Map { - const compiledUnits: Map = new Map(); - - for (const fnPath of targetPaths) { - try { - const unit = compileFunction(fnPath); - - // --- Dead code injection (before block permutation) --- - if (deadCodeInjection) { - injectDeadCode(unit, seed, tuning); - for (const child of unit.childUnits) { - injectDeadCode(child, seed, tuning); - } - } - - // --- Block permutation: shuffle basic block order --- - if (blockPermutationOpt) { - permuteBlocks(unit, seed); - } - - // --- Opcode mutation: insert MUTATE instructions --- - if (opcodeMutationOpt) { - insertMutationOpcodes(unit, seed); - } - - compiledUnits.set(unit.id, { unit }); - for (const child of unit.childUnits) { - compiledUnits.set(child.id, { unit: child }); - } - - replaceFunctionBody(fnPath, unit.id, names, scopeVarName); - } catch (err) { - const loc = fnPath.node.loc?.start; - const locStr = loc ? ` at ${loc.line}:${loc.column}` : ""; - const fnName = - ("id" in fnPath.node && fnPath.node.id?.name) || ""; - const message = err instanceof Error ? err.message : String(err); - console.warn( - `[ruam] Failed to compile ${fnName}${locStr}: ${message}` - ); - } - } - - return compiledUnits; -} - -/** - * Encode all compiled units with the given key anchor and cipher parameters. - * - * When {@link opcodeMutationOpt} is true, each unit's opcodes are pre-encoded - * via {@link adjustEncodingForMutations} (which tracks cumulative MUTATE - * instructions) and an identity shuffle map is used for serialization. - */ -function encodeAllUnits( - compiledUnits: Map, - shuffleMap: number[], - encrypt: boolean, - stringKey: number, - rollingCipher: boolean, - integrityHash?: number, - cipherSalt?: number, - keyAnchor?: number, - alphabet?: string, - opcodeMutationOpt: boolean = false, - incrementalCipherOpt: boolean = false -): Map { - const result = new Map(); - - // Build identity map for mutation-encoded units (opcodes already physical) - let identityMap: number[] | undefined; - if (opcodeMutationOpt) { - identityMap = new Array(OPCODE_COUNT); - for (let i = 0; i < OPCODE_COUNT; i++) identityMap[i] = i; - } - - for (const [unitId, { unit }] of compiledUnits) { - // Compute cipher blocks BEFORE opcode mutation modifies the unit's - // instruction opcodes. identifyBasicBlocks needs logical opcodes to - // correctly identify jump instructions and block boundaries. - let cipherBlocks: CipherBlock[] | undefined; - if (incrementalCipherOpt) { - cipherBlocks = buildCipherBlocks(unit); - } - - // For mutation units, pre-encode opcodes and use identity shuffle map - const effectiveMap = opcodeMutationOpt - ? (adjustEncodingForMutations(unit, shuffleMap, OPCODE_COUNT), - identityMap!) - : shuffleMap; - - const encoded = encodeUnit( - unit, - effectiveMap, - encrypt, - stringKey, - rollingCipher, - integrityHash, - cipherSalt, - keyAnchor, - alphabet!, - incrementalCipherOpt, - cipherBlocks - ); - result.set(unitId, { unit, encoded }); - } - return result; + return protectCode(source, options).code; } -// --------------------------------------------------------------------------- -// Dead code injection -// --------------------------------------------------------------------------- - -/** - * Inject unreachable bytecode sequences into a compiled unit. - * - * Finds positions after RETURN opcodes where the next instruction is not a - * jump target, and inserts fake instruction sequences that look like real - * code but are never executed. This confuses static analysis tools and - * makes the bytecode harder to reverse-engineer. - */ -function injectDeadCode( - unit: BytecodeUnit, - seed: number, - tuning?: Readonly -): void { - const instrs = unit.instructions; - if (instrs.length < 4) return; - - // Collect all jump targets so we don't inject dead code where something jumps to - const jumpTargets = new Set(); - for (const instr of instrs) { - if (ALL_JUMP_OPS.has(instr.opcode)) { - jumpTargets.add(instr.operand); - } - if (PACKED_JUMP_OPS.has(instr.opcode)) { - if (instr.opcode === Op.TRY_PUSH) { - // TRY_PUSH packs catchIp in bits 16-31, finallyIp in bits 0-15 - // 0xFFFF is the sentinel for "not present" — skip it - const catchIp = (instr.operand >> 16) & 0xffff; - const finallyIp = instr.operand & 0xffff; - if (catchIp > 0 && catchIp !== 0xffff) jumpTargets.add(catchIp); - if (finallyIp > 0 && finallyIp !== 0xffff) - jumpTargets.add(finallyIp); - } else { - // REG_LT_CONST_JF / REG_LT_REG_JF: jump target in bits 16-31 - const target = (instr.operand >>> 16) & 0xffff; - jumpTargets.add(target); - } - } - } - // Also protect exception table targets from dead code insertion - for (const entry of unit.exceptionTable) { - jumpTargets.add(entry.startIp); - jumpTargets.add(entry.endIp); - if (entry.catchIp > 0) jumpTargets.add(entry.catchIp); - if (entry.finallyIp > 0) jumpTargets.add(entry.finallyIp); - } - - // Use seed for deterministic dead code patterns - let s = (seed ^ instrs.length) >>> 0; - function lcg(): number { - s = (s * 1664525 + 1013904223) >>> 0; - return s; - } - - // Build dead code blocks to insert — work backwards to preserve indices - const insertions: { - after: number; - block: { opcode: number; operand: number }[]; - }[] = []; - - for (let i = 0; i < instrs.length; i++) { - const instr = instrs[i]!; - if (instr.opcode !== Op.RETURN) continue; - if (i + 1 >= instrs.length) continue; - if (jumpTargets.has(i + 1)) continue; - - // Probability-gated injection at each eligible site - const prob = tuning?.deadCodeProbability ?? 40; - if (lcg() % 100 >= prob) continue; - - // Generate a fake instruction sequence - const minBlock = tuning?.deadCodeBlockMin ?? 3; - const maxBlock = tuning?.deadCodeBlockMax ?? 6; - const blockLen = minBlock + (lcg() % (maxBlock - minBlock + 1)); - const block: { opcode: number; operand: number }[] = []; - - for (let j = 0; j < blockLen; j++) { - const pattern = lcg() % 8; - switch (pattern) { - case 0: - block.push({ - opcode: Op.PUSH_CONST, - operand: lcg() % Math.max(1, unit.constants.length), - }); - break; - case 1: - block.push({ opcode: Op.ADD, operand: 0 }); - break; - case 2: - block.push({ opcode: Op.SUB, operand: 0 }); - break; - case 3: - block.push({ opcode: Op.POP, operand: 0 }); - break; - case 4: - block.push({ opcode: Op.DUP, operand: 0 }); - break; - case 5: - block.push({ opcode: Op.NOT, operand: 0 }); - break; - case 6: - block.push({ opcode: Op.PUSH_UNDEFINED, operand: 0 }); - break; - case 7: - block.push({ opcode: Op.PUSH_NULL, operand: 0 }); - break; - } - } - - insertions.push({ after: i, block }); - } - - // Apply insertions in reverse order so indices stay valid - for (let k = insertions.length - 1; k >= 0; k--) { - const { after, block } = insertions[k]!; - - // Patch all jump targets that point past the insertion site - for (const instr of instrs) { - if (ALL_JUMP_OPS.has(instr.opcode) && instr.operand > after) { - instr.operand += block.length; - } - if (PACKED_JUMP_OPS.has(instr.opcode)) { - if (instr.opcode === Op.TRY_PUSH) { - let catchIp = (instr.operand >> 16) & 0xffff; - let finallyIp = instr.operand & 0xffff; - // 0xFFFF is the sentinel for "not present" — never patch it - if (catchIp !== 0xffff && catchIp > after) - catchIp += block.length; - if (finallyIp !== 0xffff && finallyIp > after) - finallyIp += block.length; - instr.operand = - ((catchIp & 0xffff) << 16) | (finallyIp & 0xffff); - } else { - // REG_LT_CONST_JF / REG_LT_REG_JF: jump target in bits 16-31 - const low = instr.operand & 0xffff; - let target = (instr.operand >>> 16) & 0xffff; - if (target > after) target += block.length; - instr.operand = (low & 0xffff) | ((target & 0xffff) << 16); - } - } - } - - // Patch exception table IPs - for (const entry of unit.exceptionTable) { - if (entry.startIp > after) entry.startIp += block.length; - if (entry.endIp > after) entry.endIp += block.length; - if (entry.catchIp > after) entry.catchIp += block.length; - if (entry.finallyIp > after) entry.finallyIp += block.length; - } - - // Patch jump table IPs (label → target mapping) - for (const [label, target] of Object.entries(unit.jumpTable)) { - if (target > after) { - unit.jumpTable[Number(label)] = target + block.length; - } - } - - // Insert the dead code block - instrs.splice(after + 1, 0, ...block); - } -} - -// --------------------------------------------------------------------------- -// Top-level binding collection (for program scope object) -// --------------------------------------------------------------------------- - -/** - * Collect the names of all top-level bindings in the program body. - * - * These bindings may be referenced by compiled bytecode via LOAD_SCOPED / - * STORE_SCOPED. In module contexts (CJS / ESM) they are NOT on - * `globalThis`, so we register them in a program scope object that is - * threaded through the dispatch chain as the outer scope. - */ -function collectTopLevelBindings(body: t.Statement[]): string[] { - const bindings: string[] = []; - for (const node of body) { - if (t.isFunctionDeclaration(node) && node.id) { - bindings.push(node.id.name); - } else if (t.isVariableDeclaration(node)) { - for (const decl of node.declarations) { - if (t.isIdentifier(decl.id)) { - bindings.push(decl.id.name); - } - } - } else if (t.isClassDeclaration(node) && node.id) { - bindings.push(node.id.name); - } - } - return bindings; -} - -/** - * Build the program scope object setup code. - * - * Creates a prototypal scope object (Object.create(null)) with - * getter/setter bindings for every top-level declaration. Getters - * lazily read from the enclosing JS scope (handling hoisting and - * late initialisation correctly). - * - * @param psName - Variable name for the scope object. - * @param bindings - Top-level binding names to register. - * @returns JS source string for the scope setup statements. - */ -function buildScopeSetupCode(psName: string, bindings: string[]): string { - const lines: string[] = []; - lines.push(`var ${psName}=Object.create(null);`); - for (const name of bindings) { - lines.push( - `Object.defineProperty(${psName},"${name}",` + - `{get:function(){return ${name}},` + - `set:function(v){${name}=v},` + - `enumerable:!0,configurable:!0});` - ); - } - return lines.join(""); -} - -/** - * Build bytecode table declarations: an empty table init + individual - * assignment statements that can be scattered throughout the output. - * - * All units are custom-encoded binary strings, so values are always quoted. - */ -function buildBtParts( - units: Map, - btName: string -): { init: string; assignments: string[] } { - const assignments: string[] = []; - for (const [id, { encoded }] of units) { - assignments.push(`${btName}["${id}"]="${encoded}";`); - } - return { init: `var ${btName}={};`, assignments }; -} - -/** - * Build bytecode table declarations using the scatter engine. - * - * Each unit's encoded string is split into heterogeneous fragments - * (string literals, char code arrays, packed integer arrays) that are - * scattered throughout the output. Eliminates long contiguous encoded - * strings — the most obvious VM fingerprint. - */ -/** - * Build scattered bytecode table parts with heterogeneous typed fragments. - * - * Each encoded bytecode string is split into fragments of random types - * (string literals, packed ints, packed int arrays). Fragment declarations - * are individually scattered among runtime statements. Each unit gets - * a single reassembly assignment: `bt["id"] = frag1 + D(frag2) + frag3`. - * - * @returns init statement + fragment declaration strings + reassembly strings - */ -function buildScatteredBtParts( - units: Map, - btName: string, - decodeName: string, - seed: number, - nameGen: () => string, - tuning?: Readonly -): { init: string; fragmentDecls: string[]; assignments: string[] } { - const fragmentDecls: string[] = []; - const assignments: string[] = []; - - for (const [unitId, { encoded }] of units) { - const result = scatterBytecodeUnit( - encoded, - deriveSeed(seed, `btUnit:${unitId}`), - nameGen, - decodeName, - tuning?.bytecodeFragmentMin, - tuning?.bytecodeFragmentMax - ); - - // Emit each fragment as a var declaration (to be scattered individually) - for (const frag of result.fragments) { - fragmentDecls.push(emit(frag.decl) + ";"); - } - - // Emit reassembly: bt["id"] = reassembly_expr - const assignNode = { - type: "ExprStmt" as const, - expr: { - type: "AssignExpr" as const, - target: { - type: "IndexExpr" as const, - obj: { type: "Id" as const, name: btName }, - index: { type: "Literal" as const, value: unitId }, - }, - value: result.reassembly, - }, - }; - assignments.push(emit(assignNode) + ";"); - } - - return { - init: `var ${btName}={}`, - fragmentDecls, - assignments, - }; -} - -/** - * Collect all logical opcodes used across all compiled bytecode units. - */ -function collectUsedOpcodes( - compiledUnits: Map -): Set { - const used = new Set(); - for (const [, { unit }] of compiledUnits) { - for (const instr of unit.instructions) { - used.add(instr.opcode); - } - } - return used; -} - -/** Encode a single bytecode unit to custom binary format. */ -function encodeUnit( - unit: BytecodeUnit, - shuffleMap: number[], - encrypt: boolean, - stringKey: number, - rollingCipher: boolean = false, - integrityHash?: number, - cipherSalt?: number, - keyAnchor?: number, - alphabet: string = "", - incrementalCipher: boolean = false, - precomputedCipherBlocks?: CipherBlock[] -): string { - return encodeBytecodeUnit(unit, { - shuffleMap, - encrypt, - rollingCipher, - integrityHash, - cipherSalt, - keyAnchor, - stringKey, - alphabet, - incrementalCipher, - precomputedCipherBlocks, +/** Internal deterministic entry point used by qualification tests. */ +export function protectCodeWithEntropy( + source: string, + options: RuamOptions, + entropy: BuildEntropy +): IsoglossSourceBuildResult { + const resolved = resolveRuamOptions(options); + const fileSeed = entropy.nextUint32("isogloss-file-seed"); + const built = buildLocalIsoglossSource(source, resolved, fileSeed); + if (!resolved.preprocessIdentifiers || built.stats.protectedRegionCount === 0) { + return built; + } + + // Region discovery and domain matching use author-written binding names. + // Rename only the completed output so configuration can never accidentally + // describe a different binding after preprocessing. + const preprocessed = preprocessIdentifiers( + built.code, + entropy.nextUint32("isogloss-identifier-preprocess") + ); + const outputBytes = new TextEncoder().encode(preprocessed.code).byteLength; + const stats = Object.freeze({ + ...built.stats, + outputBytes, + expansionRatio: + built.stats.originalBytes === 0 + ? 1 + : outputBytes / built.stats.originalBytes, }); -} - -// --------------------------------------------------------------------------- -// VM Shielding assembly -// --------------------------------------------------------------------------- - -/** - * Compile and assemble the output using VM Shielding: each root function - * (and its children) gets a unique micro-interpreter with independent - * opcode shuffle, names, and rolling cipher key. - */ -function assembleShielded( - ast: t.File, - targetPaths: NodePath[], - opts: { - encryptBytecode: boolean; - debugProtection: boolean; - debugLogging: boolean; - decoyOpcodes: boolean; - deadCodeInjection: boolean; - stackEncoding: boolean; - integrityBinding: boolean; - mixedBooleanArithmetic: boolean; - handlerFragmentation: boolean; - blockPermutation: boolean; - opcodeMutation: boolean; - polymorphicDecoder: boolean; - stringAtomization: boolean; - scatteredKeys: boolean; - bytecodeScattering: boolean; - incrementalCipher: boolean; - semanticOpacity: boolean; - observationResistance: boolean; - wrapOutput: boolean; - preprocessUsedNames: Set | undefined; - tuning: Readonly; - } -): string { - // Generate per-group seeds (one per root function) - const groupSeeds = targetPaths.map(() => generateCryptoSeed()); - const sharedSeed = generateCryptoSeed(); - - // Generate names: shared + per-group via NameRegistry - const { - registry: shieldedRegistry, - shared: sharedNames, - sharedTemps, - groups: groupNameSets, - groupTemps: groupTempSets, - alphabet: shieldedAlphabet, - } = setupShieldedRegistry(sharedSeed, groupSeeds, opts.preprocessUsedNames); - - // --- Phase 1: Compile all targets (no encoding) --- - const groups: ShieldingGroup[] = []; - const allCompiledUnits = new Map(); - const groupMeta: { - unit: BytecodeUnit; - shuffleMap: number[]; - names: RuntimeNames; - temps: TempNames; - seed: number; - unitIds: string[]; - usedOpcodes: Set; - cipherSalt: number; - hasAsyncUnits: boolean; - }[] = []; - - for (let gi = 0; gi < targetPaths.length; gi++) { - const fnPath = targetPaths[gi]!; - const groupSeed = groupSeeds[gi]!; - const groupNames = groupNameSets[gi]!; - const groupTemps = groupTempSets[gi]!; - const groupShuffleMap = generateShuffleMap(groupSeed); - - let unit: BytecodeUnit; - try { - unit = compileFunction(fnPath); - } catch (err) { - const loc = fnPath.node.loc?.start; - const locStr = loc ? ` at ${loc.line}:${loc.column}` : ""; - const fnName = - ("id" in fnPath.node && fnPath.node.id?.name) || ""; - const message = err instanceof Error ? err.message : String(err); - console.warn( - `[ruam] Failed to compile ${fnName}${locStr}: ${message}` - ); - continue; - } - - // Dead code injection - if (opts.deadCodeInjection) { - injectDeadCode(unit, groupSeed, opts.tuning); - for (const child of unit.childUnits) { - injectDeadCode(child, groupSeed, opts.tuning); - } - } - - // Block permutation: shuffle basic block order - if (opts.blockPermutation) { - permuteBlocks(unit, groupSeed); - } - - // Opcode mutation: insert MUTATE instructions - if (opts.opcodeMutation) { - insertMutationOpcodes(unit, groupSeed); - } - - // Collect unit IDs - const unitIds = [unit.id, ...unit.childUnits.map((c) => c.id)]; - - // Collect used opcodes - const usedOpcodes = new Set(); - for (const instr of unit.instructions) usedOpcodes.add(instr.opcode); - for (const child of unit.childUnits) { - for (const instr of child.instructions) - usedOpcodes.add(instr.opcode); - } - - // Detect async units in this group - const groupHasAsync = - unit.isAsync || unit.childUnits.some((c) => c.isAsync); - - // Per-group cipher salt - const groupCipherSalt = generateCryptoSeed(); - - // Store compiled units (no encoding yet) - allCompiledUnits.set(unit.id, { unit }); - for (const child of unit.childUnits) { - allCompiledUnits.set(child.id, { unit: child }); - } - - // Replace function body to use router - replaceFunctionBody( - fnPath, - unit.id, - { - ...sharedNames, - vm: sharedNames.router, - } as RuntimeNames, - sharedTemps["_ps"] - ); - - groupMeta.push({ - unit, - shuffleMap: groupShuffleMap, - names: groupNames, - temps: groupTemps, - seed: groupSeed, - unitIds, - usedOpcodes, - cipherSalt: groupCipherSalt, - hasAsyncUnits: groupHasAsync, - }); - } - - if (allCompiledUnits.size === 0) - return generate(ast, { comments: false }).code; - - // --- Phase 2: Generate runtime (produces key anchors per group) --- - // First, build groups with integrity hashes computed from interpreter - // functions using the same options the runtime generator will use. - for (const gm of groupMeta) { - let groupIntegrityHash: number | undefined; - let groupKeyAnchor: number | undefined; - - if (opts.integrityBinding) { - const interpResult = buildInterpreterFunctions( - gm.names, - gm.temps, - gm.shuffleMap, - opts.debugLogging ?? false, - true, - gm.seed, - { - dynamicOpcodes: true, - decoyOpcodes: opts.decoyOpcodes, - stackEncoding: opts.stackEncoding, - usedOpcodes: gm.usedOpcodes, - mixedBooleanArithmetic: opts.mixedBooleanArithmetic, - handlerFragmentation: opts.handlerFragmentation, - opcodeMutation: opts.opcodeMutation, - incrementalCipher: opts.incrementalCipher, - semanticOpacity: opts.semanticOpacity, - observationResistance: opts.observationResistance, - }, - undefined, - gm.hasAsyncUnits - ); - const interpSource = interpResult.interpreters - .map((n) => emit(n)) - .join("\n"); - groupIntegrityHash = fnv1a(interpSource); - groupKeyAnchor = interpResult.keyAnchorValue; - } - - groups.push({ - shuffleMap: gm.shuffleMap, - names: gm.names, - temps: gm.temps, - seed: gm.seed, - unitIds: gm.unitIds, - usedOpcodes: gm.usedOpcodes, - integrityHash: groupIntegrityHash, - cipherSalt: gm.cipherSalt, - hasAsyncUnits: gm.hasAsyncUnits, - }); - } - - // Generate shielded runtime → also produces per-group key anchors - const runtimeResult = generateShieldedVmRuntime({ - groups, - sharedNames, - sharedTemps, - encrypt: opts.encryptBytecode, - debugProtection: opts.debugProtection, - debugLogging: opts.debugLogging, - decoyOpcodes: opts.decoyOpcodes, - stackEncoding: opts.stackEncoding, - integrityBinding: opts.integrityBinding, - mixedBooleanArithmetic: opts.mixedBooleanArithmetic, - handlerFragmentation: opts.handlerFragmentation, - polymorphicDecoder: opts.polymorphicDecoder, - stringAtomization: opts.stringAtomization, - scatteredKeys: opts.scatteredKeys, - opcodeMutation: opts.opcodeMutation, - bytecodeScattering: opts.bytecodeScattering, - incrementalCipher: opts.incrementalCipher, - semanticOpacity: opts.semanticOpacity, - observationResistance: opts.observationResistance, - identityBindingCount: opts.tuning.identityBindingCount, - alphabet: shieldedAlphabet, - registry: shieldedRegistry, + return Object.freeze({ + ...built, + code: preprocessed.code, + stats, }); - - // --- Phase 3: Encode all units (using per-group key anchors) --- - const allEncodedUnits = new Map< - string, - { unit: BytecodeUnit; encoded: string } - >(); - - // Build identity map for mutation-encoded units if needed - let shieldedIdentityMap: number[] | undefined; - if (opts.opcodeMutation) { - shieldedIdentityMap = new Array(OPCODE_COUNT); - for (let i = 0; i < OPCODE_COUNT; i++) shieldedIdentityMap[i] = i; - } - - for (let gi = 0; gi < groupMeta.length; gi++) { - const gm = groupMeta[gi]!; - const group = groups[gi]!; - const groupKeyAnchor = runtimeResult.groupKeyAnchors[gi]; - - const encodeGroupUnit = (u: BytecodeUnit) => { - // Compute cipher blocks BEFORE opcode mutation modifies the unit - let unitCipherBlocks: CipherBlock[] | undefined; - if (opts.incrementalCipher) { - unitCipherBlocks = buildCipherBlocks(u); - } - // When opcode mutation is active, pre-encode opcodes and use identity map - if (opts.opcodeMutation) { - adjustEncodingForMutations(u, gm.shuffleMap, OPCODE_COUNT); - } - return encodeUnit( - u, - opts.opcodeMutation ? shieldedIdentityMap! : gm.shuffleMap, - opts.encryptBytecode, - gm.seed, - true, // rolling cipher always on in shielding mode - group.integrityHash, - gm.cipherSalt, - groupKeyAnchor, - shieldedAlphabet, - opts.incrementalCipher, - unitCipherBlocks - ); - }; - - const rootEncoded = encodeGroupUnit(gm.unit); - allEncodedUnits.set(gm.unit.id, { - unit: gm.unit, - encoded: rootEncoded, - }); - for (const child of gm.unit.childUnits) { - const childEncoded = encodeGroupUnit(child); - allEncodedUnits.set(child.id, { - unit: child, - encoded: childEncoded, - }); - } - } - - // --- Phase 4: Assemble output --- - return assembleOutputFromParts( - ast, - allEncodedUnits, - sharedNames, - sharedTemps, - runtimeResult.source, - opts.wrapOutput, - sharedSeed, - opts.bytecodeScattering, - opts.tuning, - shieldedRegistry - ); } -// --------------------------------------------------------------------------- -// Output assembly -// --------------------------------------------------------------------------- - -/** - * Assemble the final obfuscated source from pre-encoded units, - * pre-generated runtime, and the modified AST. - */ -function assembleOutputFromParts( - ast: t.File, - encodedUnits: Map, - names: RuntimeNames, - temps: TempNames, - runtimeSource: string, - wrapOutput: boolean, - seed: number = 0, - bytecodeScattering = false, - tuning?: Readonly, - registry?: NameRegistry +/** Internal string-only deterministic compatibility for existing test tools. */ +export function obfuscateCodeWithEntropy( + source: string, + options: RuamOptions, + entropy: BuildEntropy ): string { - // Build bytecode table statements - const scatteredBt = - bytecodeScattering && registry - ? buildScatteredBtParts( - encodedUnits, - names.bt, - names.btDecode, - seed, - registry.createDynamicGenerator("btScatter"), - tuning - ) - : null; - const plainBt = scatteredBt ? null : buildBtParts(encodedUnits, names.bt); - - // Collect top-level bindings BEFORE adding runtime statements. - const topLevelBindings = collectTopLevelBindings(ast.program.body); - - // Build the program scope object. - const scopeCode = buildScopeSetupCode(temps["_ps"]!, topLevelBindings); - const scopeNodes = parse(scopeCode, { sourceType: "script" }).program.body; - - const btInit = scatteredBt ? scatteredBt.init : plainBt!.init; - const btInitNode = parse(btInit, { sourceType: "script" }).program.body[0]!; - - const runtimeNode = parse(runtimeSource, { sourceType: "script" }).program - .body[0]!; - const iifeCall = (runtimeNode as t.ExpressionStatement) - .expression as t.CallExpression; - const iifeFn = iifeCall.callee as t.FunctionExpression; - - const parseStmt = (s: string): t.Statement => - parse(s, { sourceType: "script" }).program.body[0]! as t.Statement; - - // insertBtInit: place `var bt = {}` at a randomized position in the first - // third of runtime statements. - const insertBtInit = (stmts: t.Statement[]): t.Statement[] => { - const result = [...stmts]; - let s2 = deriveSeed(seed, "btInit"); - s2 = (s2 * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - const maxPos = Math.max(1, Math.floor(result.length / 3)); - const pos = s2 % maxPos; - result.splice(pos, 0, btInitNode as t.Statement); - return result; - }; - - // scatterFragsAndAssignments: individually scatter fragment var declarations - // among runtime statements, then place reassembly assignments after all - // declarations. Fragment vars blend with other runtime var declarations. - const scatterFragsAndAssignments = (base: t.Statement[]): t.Statement[] => { - const allAssigns = scatteredBt - ? scatteredBt.assignments.map(parseStmt) - : plainBt!.assignments.map(parseStmt); - - if (allAssigns.length === 0) return base; - - let result = [...base]; - - // Phase 1: Scatter fragment declarations individually among runtime - // statements. Each fragment var is placed at a random position in - // the first 2/3 of the output — blending with other var declarations. - if (scatteredBt && scatteredBt.fragmentDecls.length > 0) { - const fragNodes = scatteredBt.fragmentDecls.map(parseStmt); - let sf = deriveSeed(seed, "scatterFragDecl"); - for (const fragNode of fragNodes) { - sf = (sf * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - const maxP = Math.max(1, Math.floor((result.length * 2) / 3)); - // Place after at least the first few statements (avoid position 0) - const minP = Math.min(2, maxP); - const pos = minP + (sf % Math.max(1, maxP - minP)); - result.splice(pos, 0, fragNode); - } - } - - // Phase 2: Place reassembly assignments after all declarations. - let safeFloor = 0; - for (let j = 0; j < result.length; j++) { - const st = result[j]!; - if ( - st.type === "VariableDeclaration" || - st.type === "FunctionDeclaration" - ) { - safeFloor = j + 1; - } - } - - const slotCount = result.length - safeFloor; - const gap = Math.max( - 1, - Math.floor(slotCount / (allAssigns.length + 1)) - ); - let s = deriveSeed(seed, "scatterAssign"); - let insertAt = safeFloor; - for (let i = 0; i < allAssigns.length; i++) { - s = (s * LCG_MULTIPLIER + LCG_INCREMENT) >>> 0; - const jitter = (s % Math.max(1, gap)) - Math.floor(gap / 4); - insertAt = Math.min( - result.length, - Math.max(insertAt, insertAt + gap + jitter) - ); - result.splice(insertAt, 0, allAssigns[i]!); - insertAt++; - } - return result; - }; - - if (wrapOutput) { - const userStatements = [...ast.program.body]; - iifeFn.body.body = insertBtInit(iifeFn.body.body); - iifeFn.body.body = scatterFragsAndAssignments(iifeFn.body.body); - iifeFn.body.body.push( - ...(scopeNodes as t.Statement[]), - ...userStatements - ); - ast.program.body = [runtimeNode as t.Statement]; - ast.program.directives = []; - } else { - const runtimeStatements = iifeFn.body.body; - const withBt = insertBtInit(runtimeStatements); - const scattered = scatterFragsAndAssignments(withBt); - ast.program.body.unshift( - ...scattered, - ...(scopeNodes as t.Statement[]) - ); - ast.program.directives = [ - t.directive(t.directiveLiteral("use strict")), - ]; - } - - return generate(ast, { comments: false }).code; -} - -// --------------------------------------------------------------------------- -// Function body replacement -// --------------------------------------------------------------------------- - -/** - * Replace a function's body with a VM dispatch call. - * - * - Arrow functions: converted to `(...__args) => names.vm(id, __args)` - * - Regular functions: `return names.vm.call(this, id, Array.prototype.slice.call(arguments))` - */ -function replaceFunctionBody( - fnPath: NodePath, - unitId: string, - names: RuntimeNames, - scopeVarName?: string -): void { - const node = fnPath.node; - const vmId = t.identifier(names.vm); - const argsId = t.identifier("__args"); - - // Both arrows and regular functions use rest params for natural output. - const restParam = t.restElement(argsId); - node.params = [restParam]; - - if (node.type === "ArrowFunctionExpression") { - const arrowArgs: t.Expression[] = [t.stringLiteral(unitId), argsId]; - if (scopeVarName) { - arrowArgs.push(t.identifier(scopeVarName)); - } - node.body = t.blockStatement([ - t.returnStatement(t.callExpression(vmId, arrowArgs)), - ]); - return; - } - - // Regular functions: call vm(id, args, scope, this) directly. - // The vm dispatcher handles this-boxing internally when TV is - // provided, so no .call() or Array.prototype.slice needed. - const vmArgs: t.Expression[] = [t.stringLiteral(unitId), argsId]; - if (scopeVarName) { - vmArgs.push(t.identifier(scopeVarName)); - } else { - vmArgs.push(t.nullLiteral()); - } - // Pass `this` as the thisVal parameter - vmArgs.push(t.thisExpression()); - - const vmCall = t.callExpression(vmId, vmArgs); - - // Decoy body: add a filler statement before the dispatch so the - // function doesn't look like a bare one-liner VM stub. The arg-length - // assignment looks like natural argument processing. - const decoy = t.variableDeclaration("var", [ - t.variableDeclarator( - t.identifier("_n"), - t.binaryExpression( - "|", - t.memberExpression(argsId, t.identifier("length")), - t.numericLiteral(0) - ) - ), - ]); - - node.body = t.blockStatement([decoy, t.returnStatement(vmCall)]); + return protectCodeWithEntropy(source, options, entropy).code; } diff --git a/packages/ruam/src/tuning.ts b/packages/ruam/src/tuning.ts deleted file mode 100644 index e87db30..0000000 --- a/packages/ruam/src/tuning.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Centralized tuning parameters for obfuscation features. - * - * Each preset maps to an intensity level (0 = low, 1 = medium, 2 = max). - * Modules import from this file instead of hardcoding magic numbers. - * - * @module tuning - */ - -// --- Intensity type --- - -/** Tuning intensity level: 0 = conservative, 1 = moderate, 2 = aggressive. */ -export type Intensity = 0 | 1 | 2; - -// --- Tuning profile --- - -/** Complete set of tunable numeric parameters. */ -export interface TuningProfile { - // -- Opcode mutation -- - /** Minimum instruction gap between MUTATE opcode insertions. */ - mutationIntervalMin: number; - /** Maximum instruction gap between MUTATE opcode insertions. */ - mutationIntervalMax: number; - /** Number of handler table swaps per MUTATE execution. */ - swapsPerMutation: number; - - // -- Handler fragmentation -- - /** Maximum fragment count per handler (for handlers with 3+ statements). */ - handlerFragmentMax: number; - - // -- Polymorphic decoder -- - /** Minimum operations in the decoder chain. */ - decoderChainMin: number; - /** Maximum operations in the decoder chain. */ - decoderChainMax: number; - - // -- Decoy opcodes -- - /** Minimum number of decoy handler closures. */ - decoyHandlerMin: number; - /** Maximum number of decoy handler closures. */ - decoyHandlerMax: number; - - // -- Scattered keys -- - /** Minimum string fragment count. */ - scatterStringFragMin: number; - /** Maximum string fragment count. */ - scatterStringFragMax: number; - /** Minimum array fragment count. */ - scatterArrayFragMin: number; - /** Maximum array fragment count. */ - scatterArrayFragMax: number; - - // -- Bytecode scattering -- - /** Minimum bytecode fragment count per unit. */ - bytecodeFragmentMin: number; - /** Maximum bytecode fragment count per unit. */ - bytecodeFragmentMax: number; - - // -- Dead code injection -- - /** Probability (0–100) of injecting dead code at each RETURN site. */ - deadCodeProbability: number; - /** Minimum dead code block size (instruction count). */ - deadCodeBlockMin: number; - /** Maximum dead code block size (instruction count). */ - deadCodeBlockMax: number; - - // -- MBA -- - /** MBA expression nesting depth. */ - mbaDepth: number; - - // -- String atomization -- - /** Minimum string length to atomize. */ - atomizeMinLength: number; - - // -- Structural variation biases (each 0.0–1.0 range) -- - /** Ternary bias range: [min, max]. */ - ternaryBiasRange: [number, number]; - /** Dot-to-bracket bias range: [min, max]. */ - dotBracketBiasRange: [number, number]; - /** Numeric variation bias range: [min, max]. */ - numericVariationBiasRange: [number, number]; - - // -- Observation resistance -- - /** Probability (0-100) of witness check per handler invocation. */ - witnessCheckProbability: number; - /** Number of function identity bindings. */ - identityBindingCount: number; -} - -// --- Profile definitions --- - -const PROFILES: Record = { - // Intensity 0: conservative (low preset) - 0: { - mutationIntervalMin: 30, - mutationIntervalMax: 60, - swapsPerMutation: 2, - handlerFragmentMax: 2, - decoderChainMin: 3, - decoderChainMax: 5, - decoyHandlerMin: 4, - decoyHandlerMax: 8, - scatterStringFragMin: 2, - scatterStringFragMax: 3, - scatterArrayFragMin: 2, - scatterArrayFragMax: 3, - bytecodeFragmentMin: 2, - bytecodeFragmentMax: 3, - deadCodeProbability: 25, - deadCodeBlockMin: 2, - deadCodeBlockMax: 4, - mbaDepth: 1, - atomizeMinLength: 3, - ternaryBiasRange: [0.1, 0.3], - dotBracketBiasRange: [0.05, 0.2], - numericVariationBiasRange: [0.05, 0.15], - witnessCheckProbability: 10, - identityBindingCount: 3, - }, - - // Intensity 1: moderate (medium preset) — current defaults - 1: { - mutationIntervalMin: 20, - mutationIntervalMax: 50, - swapsPerMutation: 4, - handlerFragmentMax: 3, - decoderChainMin: 4, - decoderChainMax: 8, - decoyHandlerMin: 8, - decoyHandlerMax: 16, - scatterStringFragMin: 3, - scatterStringFragMax: 5, - scatterArrayFragMin: 2, - scatterArrayFragMax: 4, - bytecodeFragmentMin: 2, - bytecodeFragmentMax: 6, - deadCodeProbability: 40, - deadCodeBlockMin: 3, - deadCodeBlockMax: 6, - mbaDepth: 2, - atomizeMinLength: 2, - ternaryBiasRange: [0.15, 0.6], - dotBracketBiasRange: [0.1, 0.45], - numericVariationBiasRange: [0.1, 0.4], - witnessCheckProbability: 25, - identityBindingCount: 5, - }, - - // Intensity 2: aggressive (max preset) - 2: { - mutationIntervalMin: 12, - mutationIntervalMax: 35, - swapsPerMutation: 6, - handlerFragmentMax: 3, - decoderChainMin: 6, - decoderChainMax: 10, - decoyHandlerMin: 12, - decoyHandlerMax: 24, - scatterStringFragMin: 4, - scatterStringFragMax: 6, - scatterArrayFragMin: 3, - scatterArrayFragMax: 5, - bytecodeFragmentMin: 3, - bytecodeFragmentMax: 8, - deadCodeProbability: 60, - deadCodeBlockMin: 4, - deadCodeBlockMax: 8, - mbaDepth: 3, - atomizeMinLength: 1, - ternaryBiasRange: [0.2, 0.7], - dotBracketBiasRange: [0.15, 0.55], - numericVariationBiasRange: [0.15, 0.5], - witnessCheckProbability: 40, - identityBindingCount: 8, - }, -}; - -// --- Public API --- - -/** - * Get the tuning profile for a given intensity level. - * - * @param intensity - 0 (conservative), 1 (moderate), or 2 (aggressive) - * @returns Frozen TuningProfile with all numeric parameters - */ -export function getTuningProfile( - intensity: Intensity -): Readonly { - return PROFILES[intensity]; -} - -/** - * Map a preset name to its intensity level. - */ -export function presetToIntensity( - preset: "low" | "medium" | "max" | undefined -): Intensity { - switch (preset) { - case "low": - return 0; - case "medium": - return 1; - case "max": - return 2; - default: - return 1; // default to moderate - } -} diff --git a/packages/ruam/src/types.ts b/packages/ruam/src/types.ts deleted file mode 100644 index 18f6f32..0000000 --- a/packages/ruam/src/types.ts +++ /dev/null @@ -1,331 +0,0 @@ -/** - * Core type definitions for the Ruam VM obfuscator. - * @module types - */ - -// --- Public API Options --- - -/** Preset names that group multiple options. */ -export type PresetName = "low" | "medium" | "max"; - -/** - * Target execution environment. - * - * Controls environment-specific output settings (e.g. IIFE wrapping). - * Explicit options always override target defaults. - * - * - `"node"` — Node.js (CJS or ESM modules). - * - `"browser"` — Plain browser scripts (`