diff --git a/README.md b/README.md index dbef332..5992ea7 100644 --- a/README.md +++ b/README.md @@ -181,10 +181,11 @@ return bytes[4 .. 4+length] decoder: `createCodec({ seed, key, maxExprDepth })` returns `{ encode, decode }`. `seed` is cosmetic-only (names, strings, numbers); `key` is structural and changes candidate selection — decoder must receive the same key. - `maxExprDepth` hard-caps expression nesting depth — at the limit, all - expression children become cosmetic (non-data-carrying). This keeps the - AST shallow enough for browser parsers (which use recursive descent and - overflow on deep trees). + `maxExprDepth` hard-caps expression nesting depth (default 1). At the limit, + only leaf expressions (literals, identifiers) are available as candidates, + so the AST never exceeds the bound. Shallow depth produces many short + statements per message (realistic module structure); higher depth produces + fewer, more complex statements. - **Custom code generator.** Handles 20+ AST node types with correct parenthesization, regex adjacency, and object/block disambiguation. @@ -198,7 +199,7 @@ return bytes[4 .. 4+length] bun install bun run lint # lint (uses @antfu/eslint-config) bun run lint:fix # auto-fix lint issues -bun run test # 66 tests including fuzz, ordering quality, and randomization invariant +bun run test # 68 tests including fuzz, ordering quality, and randomization invariant bun run typecheck # typecheck all packages bun run knip # check for unused deps/exports ``` diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts index 1431ecc..5075014 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/context.ts @@ -35,8 +35,13 @@ export interface ScopeEntry { type: ScopeType } -/** Max expression nesting depth before forcing leaf-only candidates. */ -export const MAX_EXPR_DEPTH = Infinity // default — override via createCodec for browser use +/** + * Max expression nesting depth before forcing leaf-only candidates. + * Default 1 — leaf-only expressions produce many short statements, making + * output resemble a real JS module with imports, declarations, and exports. + * Override via createCodec for deeper expression trees. + */ +export const MAX_EXPR_DEPTH = 1 export type ScopeBucket = 'top-level' | 'function-body' | 'loop-body' | 'block-body' @@ -66,6 +71,8 @@ export interface EncodingContext { blockDepth: number scopeBucket: ScopeBucket prevStmtKey: string + hasExportDefault: boolean + hasLeftImportRegion: boolean } export function initialContext(): EncodingContext { @@ -81,6 +88,8 @@ export function initialContext(): EncodingContext { blockDepth: 0, scopeBucket: 'top-level', prevStmtKey: '', + hasExportDefault: false, + hasLeftImportRegion: false, } } @@ -154,14 +163,45 @@ export function mixHash(hash: number, byte: number): number { return hash >>> 0 } -/** Derive a deterministic name from hash + index. */ +/** Derive a deterministic minifier-style name from hash + index (e.g. a, b, aa, bc). */ export function nameFromHash(hash: number, index: number): string { const ALPHA = 'abcdefghijklmnopqrstuvwxyz' const h = mixHash(hash, index) - const a = ALPHA[h % 26] - const b = ALPHA[(h >>> 8) % 26] - const c = ALPHA[(h >>> 16) % 26] - return `_${a}${b}${c}` + // 702 possible names: a–z (26) + aa–zz (676), excluding 2-letter JS reserved words + const total = 26 + 26 * 26 + let slot = h % total + // Skip 2-letter JS reserved words: 'do' (slot 118), 'if' (239), 'in' (247) + if (slot >= 26) { + const s = slot - 26 + const two = ALPHA[Math.floor(s / 26)] + ALPHA[s % 26] + if (two === 'do' || two === 'if' || two === 'in') { + slot = (slot + 1) % total + } + } + if (slot < 26) { + return ALPHA[slot] + } + const s = slot - 26 + return ALPHA[Math.floor(s / 26)] + ALPHA[s % 26] +} + +/** + * Derive a per-statement expression depth cap from the post-selection hash. + * Only meaningful when globalMax > 1 (non-default maxExprDepth). + * Distribution: 50% → 1, 25% → 2, 12.5% → 3, 12.5% → globalMax. + * Encoder and decoder call this with the same post-mixHash value so they agree. + */ +export function stmtDepthFromHash(hash: number, globalMax: number): number { + if (globalMax <= 1) + return globalMax + const r = (hash >>> 8) & 0xFF + if (r < 128) + return 1 + if (r < 192) + return Math.min(2, globalMax) + if (r < 224) + return Math.min(3, globalMax) + return globalMax } /** Derive a label name from hash. */ @@ -234,7 +274,10 @@ function buildAllCandidates(): Candidate[] { // Leaves c.push({ key: 'NumericLiteral:0', nodeType: 'NumericLiteral', variant: 0, children: [], weight: lookupWeight('NumericLiteral:0'), isStatement: false }) c.push({ key: 'StringLiteral:0', nodeType: 'StringLiteral', variant: 0, children: [], weight: lookupWeight('StringLiteral:0'), isStatement: false }) - c.push({ key: 'Identifier:0', nodeType: 'Identifier', variant: 0, children: [], weight: lookupWeight('Identifier:0'), isStatement: false }) + // Identifier:corpus — picks a name from the corpus identifier list (not a scope variable). + // variant = -1 signals corpus mode. Only present in the pool when scope is empty; when + // scope is non-empty, filterCandidates replaces it with dynamic Identifier:scope:i variants. + c.push({ key: 'Identifier:corpus', nodeType: 'Identifier', variant: -1, children: [], weight: lookupWeight('Identifier:0'), isStatement: false }) c.push({ key: 'BooleanLiteral:1', nodeType: 'BooleanLiteral', variant: 1, children: [], weight: lookupWeight('BooleanLiteral:1'), isStatement: false }) c.push({ key: 'BooleanLiteral:0', nodeType: 'BooleanLiteral', variant: 0, children: [], weight: lookupWeight('BooleanLiteral:0'), isStatement: false }) c.push({ key: 'NullLiteral:0', nodeType: 'NullLiteral', variant: 0, children: [], weight: lookupWeight('NullLiteral:0'), isStatement: false }) @@ -272,18 +315,18 @@ function buildAllCandidates(): Candidate[] { // Conditional (weight 0.8, 3 children) c.push({ key: 'ConditionalExpression:0', nodeType: 'ConditionalExpression', variant: 0, children: ['expr', 'expr', 'expr'], weight: lookupWeight('ConditionalExpression:0'), isStatement: false }) - // Call/New expression — arg count as variant (type-gated: only when scope has callable/constructable) - for (let n = 0; n < 19; n++) { + // Call/New expression — arg count 0-4 (covers most real-world calls) + for (let n = 0; n <= 4; n++) { const ch: SlotKind[] = ['expr', ...Array.from({ length: n }).fill('expr')] c.push({ key: `CallExpression:${n}`, nodeType: 'CallExpression', variant: n, children: ch, weight: lookupWeight(`CallExpression:${n}`), isStatement: false }) } - for (let n = 0; n < 16; n++) { + for (let n = 0; n <= 3; n++) { const ch: SlotKind[] = ['expr', ...Array.from({ length: n }).fill('expr')] c.push({ key: `NewExpression:${n}`, nodeType: 'NewExpression', variant: n, children: ch, weight: lookupWeight(`NewExpression:${n}`), isStatement: false }) } - // OptionalCallExpression — type-gated: expr?.(args) throws if expr is non-null non-callable - for (let n = 0; n < 19; n++) { + // OptionalCallExpression — arg count 0-3 + for (let n = 0; n <= 3; n++) { const ch: SlotKind[] = ['expr', ...Array.from({ length: n }).fill('expr')] c.push({ key: `OptionalCallExpression:${n}`, nodeType: 'OptionalCallExpression', variant: n, children: ch, weight: lookupWeight(`OptionalCallExpression:${n}`), isStatement: false }) } @@ -295,11 +338,11 @@ function buildAllCandidates(): Candidate[] { c.push({ key: 'OptionalMemberExpression:0', nodeType: 'OptionalMemberExpression', variant: 0, children: ['expr'], weight: lookupWeight('OptionalMemberExpression:0'), isStatement: false }) c.push({ key: 'OptionalMemberExpression:1', nodeType: 'OptionalMemberExpression', variant: 1, children: ['expr', 'expr'], weight: lookupWeight('OptionalMemberExpression:1'), isStatement: false }) - // Array/Object — element/prop count (extended to 0-31 for more unique candidates) - for (let n = 0; n < 32; n++) { + // Array/Object — element/prop count 0-4 (covers most real-world literals) + for (let n = 0; n <= 4; n++) { c.push({ key: `ArrayExpression:${n}`, nodeType: 'ArrayExpression', variant: n, children: Array.from({ length: n }).fill('expr'), weight: lookupWeight(`ArrayExpression:${n}`), isStatement: false }) } - for (let n = 0; n < 32; n++) { + for (let n = 0; n <= 4; n++) { const ch: SlotKind[] = [] for (let j = 0; j < n; j++) { ch.push('expr', 'expr') @@ -307,25 +350,25 @@ function buildAllCandidates(): Candidate[] { c.push({ key: `ObjectExpression:${n}`, nodeType: 'ObjectExpression', variant: n, children: ch, weight: lookupWeight(`ObjectExpression:${n}`), isStatement: false }) } - // Sequence expression (count 2-29, extended range) - for (let n = 2; n <= 29; n++) { + // Sequence expression — count 2-4 (rare in real code, small variants only) + for (let n = 2; n <= 4; n++) { c.push({ key: `SequenceExpression:${n - 2}`, nodeType: 'SequenceExpression', variant: n - 2, children: Array.from({ length: n }).fill('expr'), weight: lookupWeight(`SequenceExpression:${n - 2}`), isStatement: false }) } - // Template literals (extended to 0-16) - for (let n = 0; n < 17; n++) { + // Template literals — 0-3 interpolations + for (let n = 0; n <= 3; n++) { c.push({ key: `TemplateLiteral:${n}`, nodeType: 'TemplateLiteral', variant: n, children: Array.from({ length: n }).fill('expr'), weight: lookupWeight(`TemplateLiteral:${n}`), isStatement: false }) } - // TaggedTemplateExpression (type-gated: tag must be callable) - for (let n = 0; n < 8; n++) { + // TaggedTemplateExpression — 0-2 interpolations + for (let n = 0; n <= 2; n++) { c.push({ key: `TaggedTemplateExpression:${n}`, nodeType: 'TaggedTemplateExpression', variant: n, children: ['expr', ...Array.from({ length: n }).fill('expr')], weight: lookupWeight(`TaggedTemplateExpression:${n}`), isStatement: false }) } - // Arrow/Function expression — param count (extended to 0-23) - for (let n = 0; n < 24; n++) { + // Arrow/Function expression — param count 0-3 (covers most functions) + for (let n = 0; n <= 3; n++) { c.push({ key: `ArrowFunctionExpression:${n}`, nodeType: 'ArrowFunctionExpression', variant: n, children: ['expr'], weight: lookupWeight(`ArrowFunctionExpression:${n}`), isStatement: false }) } - for (let n = 0; n < 24; n++) { + for (let n = 0; n <= 3; n++) { c.push({ key: `FunctionExpression:${n}`, nodeType: 'FunctionExpression', variant: n, children: ['expr'], weight: lookupWeight(`FunctionExpression:${n}`), isStatement: false }) } @@ -338,10 +381,11 @@ function buildAllCandidates(): Candidate[] { // ── Statement candidates (only available in statement context) ── - // ExpressionStatement is NOT a candidate — expression candidates in statement context - // are wrapped in ExpressionStatement automatically by the encoder's default case. - // Having ExpressionStatement:0 as a separate candidate creates ambiguity in the decoder - // (can't distinguish "expression selected directly" from "ExpressionStatement selected + inner expr"). + // ExpressionStatement: when selected, the encoder calls buildExpr for a separate + // expression-level table lookup. Expressions are excluded from the statement table + // so there's no ambiguity — ExpressionStatement:0 is the only way to get an expression + // in statement context. + c.push({ key: 'ExpressionStatement:0', nodeType: 'ExpressionStatement', variant: 0, children: ['expr'], weight: lookupWeight('ExpressionStatement:0'), isStatement: true }) // VariableDeclaration: var/let/const (weight 2) c.push({ key: 'VariableDeclaration:0', nodeType: 'VariableDeclaration', variant: 0, children: ['expr'], weight: lookupWeight('VariableDeclaration:0'), isStatement: true }) // var @@ -355,18 +399,13 @@ function buildAllCandidates(): Candidate[] { // WhileStatement (weight 1) c.push({ key: 'WhileStatement:0', nodeType: 'WhileStatement', variant: 0, children: ['expr', 'block'], weight: lookupWeight('WhileStatement:0'), isStatement: true }) - // ForStatement × 8 null combos (weight 0.8) - for (let v = 0; v < 8; v++) { - const ch: SlotKind[] = [] - if (v & 1) - ch.push('expr') - if (v & 2) - ch.push('expr') - if (v & 4) - ch.push('expr') - ch.push('block') - c.push({ key: `ForStatement:${v}`, nodeType: 'ForStatement', variant: v, children: ch, weight: lookupWeight(`ForStatement:${v}`), isStatement: true }) - } + // ForStatement — only the 3 most common variants to limit bit cost + // variant 7: for(init;test;update) — the standard form + c.push({ key: 'ForStatement:7', nodeType: 'ForStatement', variant: 7, children: ['expr', 'expr', 'expr', 'block'], weight: lookupWeight('ForStatement:7'), isStatement: true }) + // variant 3: for(init;test;) — no update + c.push({ key: 'ForStatement:3', nodeType: 'ForStatement', variant: 3, children: ['expr', 'expr', 'block'], weight: lookupWeight('ForStatement:3'), isStatement: true }) + // variant 6: for(;test;update) — no init + c.push({ key: 'ForStatement:6', nodeType: 'ForStatement', variant: 6, children: ['expr', 'expr', 'block'], weight: lookupWeight('ForStatement:6'), isStatement: true }) // DoWhileStatement (weight 0.8) c.push({ key: 'DoWhileStatement:0', nodeType: 'DoWhileStatement', variant: 0, children: ['expr', 'block'], weight: lookupWeight('DoWhileStatement:0'), isStatement: true }) @@ -377,8 +416,9 @@ function buildAllCandidates(): Candidate[] { // TryStatement (weight 0.5) c.push({ key: 'TryStatement:0', nodeType: 'TryStatement', variant: 0, children: ['block', 'block'], weight: lookupWeight('TryStatement:0'), isStatement: true }) - // SwitchStatement × case counts 0-15 (weight varies) - for (let n = 0; n <= 15; n++) { + // SwitchStatement × case counts 0-2 (capped — higher counts consume too many bits + // and dominate the output when selected from a bijective table) + for (let n = 0; n <= 2; n++) { const ch: SlotKind[] = ['expr'] for (let j = 0; j < n; j++) { ch.push('expr', 'block') @@ -455,29 +495,30 @@ export function filterCandidates(ctx: EncodingContext): Candidate[] { const hasMemberSafe = scopeHasType(ctx.typedScope, MEMBER_SAFE_TYPES) const hasAnyScope = ctx.typedScope.length > 0 - return ALL_CANDIDATES.filter((c) => { + const basePool = ALL_CANDIDATES.filter((c) => { // Block depth limit: filter out block-containing statements when deep if (ctx.maxExprDepth < Infinity && ctx.blockDepth >= Math.floor(ctx.maxExprDepth / 3)) { if (c.isStatement && c.children.includes('block')) return false } - // Expression depth limit: filter out non-leaf EXPRESSIONS when deep. - // Statement candidates are NOT affected (they always start a fresh expr tree at depth 0). - // Expression candidates with children are filtered → only leaves remain. - // We have ~12 leaf expressions + ~200 total → always >= 12 unique after filter. - // But we need 256! So also keep all STATEMENT candidates (non-expression-only context). - // In expression-only context, we need >= 256 leaves — we DON'T have that. - // So for expression-only at max depth: keep the non-leaf candidates but with tiny weight. - // The weight scaling already handles this (10000x leaf bias). + // Expression depth hard cap: at max depth, only leaf expressions are allowed. + // Eliminates the need for cosmetic (padLeaf) children in the encoder and ensures + // the generated AST never exceeds maxExprDepth. The decoder already stops recursing + // at depth >= maxExprDepth — this filter makes the encoder consistent with it. + if (ctx.expressionOnly && ctx.maxExprDepth < Infinity && ctx.exprDepth >= ctx.maxExprDepth && c.children.length > 0) + return false // Expression-only context: only expressions if (ctx.expressionOnly && c.isStatement) return false - // Statement context: BOTH statements and expressions are available. - // Expressions are implicitly wrapped in ExpressionStatement by the encoder. - // The decoder identifies them from the ExpressionStatement's inner expression. + // Statement context: only statements (including ExpressionStatement:0). + // Raw expression candidates are excluded — they're selected via a separate + // expression table when ExpressionStatement:0 is chosen. This gives statements + // proper probability (~1/30) instead of being drowned by ~200 expression candidates. + if (!ctx.expressionOnly && !c.isStatement) + return false // Top-level-only candidates: imports and exports are legal only at program root if ( @@ -489,6 +530,18 @@ export function filterCandidates(ctx: EncodingContext): Candidate[] { return false } + // Only one export default per module + if (c.nodeType === 'ExportDefaultDeclaration' && ctx.hasExportDefault) + return false + + // Imports only appear before any non-import statement + if (c.nodeType === 'ImportDeclaration' && ctx.hasLeftImportRegion) + return false + + // Identifier:corpus is replaced by per-scope Identifier:scope:i variants when scope is non-empty + if (c.key === 'Identifier:corpus' && ctx.typedScope.length > 0) + return false + // Context-gated entries if (c.nodeType === 'ReturnStatement' && !ctx.inFunction) return false @@ -532,12 +585,7 @@ export function filterCandidates(ctx: EncodingContext): Candidate[] { return true }).map((c) => { - let w = lookupWeight(c.key, ctx.scopeBucket) - - // Dynamic weight: Identifier gets heavier with more scope entries - if (c.nodeType === 'Identifier' && ctx.typedScope.length > 0) { - w += ctx.typedScope.length * 0.5 - } + let w = lookupWeight(c.key === 'Identifier:corpus' ? 'Identifier:0' : c.key, ctx.scopeBucket) // Bigram transition weight: adjust weight based on previous statement if (ctx.prevStmtKey && !ctx.expressionOnly) { @@ -571,6 +619,35 @@ export function filterCandidates(ctx: EncodingContext): Candidate[] { return w !== c.weight ? { ...c, weight: w } : c }) + + // Dynamic per-scope Identifier variants: one per typedScope entry. + // Each encodes a reference to a specific declared variable. These replace + // Identifier:corpus when scope is non-empty, producing scope-referencing code. + // Weight per variant = corpus Identifier weight / N so combined weight ≈ corpus baseline. + if (ctx.expressionOnly && ctx.typedScope.length > 0) { + const corpusIdentWeight = lookupWeight('Identifier:0', ctx.scopeBucket) + const perVarWeight = corpusIdentWeight / ctx.typedScope.length + + // Apply depth scaling to per-scope variants (same as other leaf expressions) + let depthScale = 1 + if (ctx.exprDepth > 0 && ctx.maxExprDepth < Infinity) { + const depthRatio = ctx.exprDepth / ctx.maxExprDepth + depthScale = 10 ** (depthRatio * 4) // leaves scale UP near max depth + } + + for (let i = 0; i < ctx.typedScope.length; i++) { + basePool.push({ + key: `Identifier:scope:${i}`, + nodeType: 'Identifier', + variant: i, + children: [], + weight: perVarWeight * depthScale, + isStatement: false, + }) + } + } + + return basePool } /** diff --git a/packages/core/src/cosmetic-data.json b/packages/core/src/cosmetic-data.json index a8c9dc8..0689e7e 100644 --- a/packages/core/src/cosmetic-data.json +++ b/packages/core/src/cosmetic-data.json @@ -23,183 +23,183 @@ "b", "Object", "p", - "index", "context", + "index", "result", - "prototype", "state", + "prototype", "module", "data", "x", "path", - "get", "f", "u", + "get", "args", "d", - "error", "props", + "error", "y", "request", - "start", "opts", + "start", "source", "m", "parent", "obj", "_", "end", - "v", "set", "input", "target", + "v", "arguments", - "err", "call", - "k", "match", - "str", + "k", "children", - "h", - "id", + "err", + "str", "slice", "map", - "next", "Math", - "Error", + "next", + "h", "token", - "split", + "Error", + "id", "schema", + "split", "j", "defineProperty", "fn", - "buffer", "object", "el", "keys", - "constructor", "test", "task", + "constructor", "code", "params", "Array", + "buffer", "instance", - "offset", "val", "message", - "values", + "offset", "add", + "values", "size", "stream", "vnode", "Symbol", "replace", + "width", "body", - "g", "self", + "g", "config", "isArray", - "width", - "envObject", - "apply", - "callback", "string", + "apply", "date", "loc", + "max", + "callback", "position", "array", "has", - "max", "join", "other", "item", "pattern", "begin", "forEach", - "out", "chunk", + "out", "pos", "number", "line", - "enumerable", "child", + "enumerable", "output", - "scope", "M", - "concat", - "status", "range", - "String", + "concat", + "scope", "count", + "String", + "status", "resolve", - "process", "errors", + "res", "url", "entry", - "res", - "toString", + "process", "w", + "toString", "flags", - "env", "prop", - "ctx", + "material", "A", - "charCodeAt", - "_a", "format", + "charCodeAt", "C", - "TypeError", - "console", - "filter", - "controller", "left", - "material", - "len", + "controller", + "filter", + "console", + "ctx", "destination", "hljs", "parser", "prev", + "TypeError", "it", + "min", + "_a", + "z", "from", "root", - "min", + "len", "one", "D", "S", - "z", - "indexOf", "content", "expression", "Set", + "indexOf", "stack", - "arg", "R", - "func", "def", + "arg", "parts", - "cb", "ch", - "init", - "ret", - "create", "L", + "init", + "builder", "B", + "cb", "includes", - "idx", "right", - "ref", - "builder", - "version", + "create", + "idx", "E", - "segment", - "tag", "className", + "segment", "s0", - "arr", + "version", "current", + "ret", + "tag", "kind", - "Number", - "mode" + "texture", + "event", + "TAG_ID", + "mode", + "_i", + "dep", + "method" ], "properties": [ "length", @@ -207,11 +207,11 @@ "type", "prototype", "value", - "exports", "name", - "get", + "exports", "call", "slice", + "get", "split", "defineProperty", "set", @@ -225,22 +225,22 @@ "apply", "join", "forEach", - "has", "parent", + "has", "isArray", "concat", "charCodeAt", "start", "children", "index", - "id", "next", "keys", "toString", + "x", + "y", "indexOf", + "id", "error", - "y", - "x", "end", "includes", "hy", @@ -251,27 +251,25 @@ "data", "props", "source", - "create", "body", - "buffer", "key", "path", + "create", "filter", - "then", "bind", "assign", "size", + "then", "hasOwnProperty", - "status", - "constructor", "position", + "constructor", "max", - "node", "width", + "status", "flags", + "node", "context", "asm", - "setLastError", "toLowerCase", "pop", "min", @@ -282,126 +280,128 @@ "hooks", "stringify", "message", - "write", "startsWith", + "z", + "write", "content", "pos", - "z", + "buffer", "module", - "values", + "mul", "trim", + "values", "log", - "init", "copy", - "offset", + "init", "opts", - "some", "delete", + "some", "for", - "mul", "left", - "line", "input", + "line", "on", "version", "code", - "env", "splice", + "env", + "object", "charAt", "kind", - "object", "parse", + "style", "schema", "cwd", "scope", "format", - "stack", + "offset", "s", + "stack", "now", "operator", "emit", "right", "_", + "height", "entries", "nodes", "tap", "shift", - "height", "floor", - "style", "iterator", "column", + "attributes", "report", "warn", - "attributes", - "tag", "exec", + "tag", + "sort", "__esModule", "output", - "sort", "sectionStart", "clone", "clear", + "toVar", "result", "valueCallback", "raise", "warned", "keyPath", - "toVar", "finishNode", "substring", + "sub", "root", "v", "abs", "done", "visit", "raw", - "getOwnPropertyDescriptor", "count", - "dispose", + "getOwnPropertyDescriptor", "update", "openElements", + "material", + "color", "config", - "sub", "target", - "array", "setAttribute", + "array", "pendingTasks", - "color", "NODE_ENV", "eat", + "render", "arguments", "client", - "render", "TAG_ID", - "byteLength", - "mode", - "signal", - "material", "r", + "mode", "unshift", "locale", - "setUint32", + "signal", "close", - "print", "vnode", + "print", "ptr", "replay", "elements", - "headers", + "dispose", + "byteLength", "origin", - "reduce", + "headers", "properties", + "reduce", "params", + "uniforms", "toUpperCase", "subscribe", + "a", "defineLocale", - "uniforms", "component", "insertionMode", "items", - "a", - "args" + "dot", + "pow", + "round" ], "strings": [ "1", @@ -410,9 +410,9 @@ "object", "2", "_", - "undefined", " ", "boolean", + "undefined", "number", "__esModule", ".", @@ -427,8 +427,8 @@ "f", "any", "4", - "symbol", "-", + "symbol", "type", ",", "\"", @@ -441,17 +441,17 @@ ":", "*", "[", - "error", "]", + "error", "Identifier", "}", "8", "L", - "production", "\\", + "production", "146", - "name", "$", + "name", "'", "3", "140", @@ -459,8 +459,8 @@ "145", ">", "144", - "A B", "?", + "A B", "HH:mm:ss", "!", "143", @@ -537,12 +537,12 @@ "#", "125", "18.0", + "float", "..", "^", "17.2", "children", "15.4", - "float", "17.3", "16.5", "./_lib/convertToFP.js", @@ -553,26 +553,26 @@ "17.0", "d", "y", + "text", "delete", "path", "130", "w", - "text", "115", "utf8", "m", "formatting", + "none", "meta", "key", "data", "x", "120", - "end", "true", + "end", "148", "16.2", "5", - "none", "15.5", "16.0", "Object", @@ -585,19 +585,19 @@ "95", "123", "P", + "div", "i", "return", "keyword", "D MMMM YYYY HH:mm", "92", - "p", "input", + "p", "S", "119", - "div", + "right", "96", "MemberExpression", - "right", "in", "116", "121", @@ -627,23 +627,22 @@ 0.5, 14, 15, - 64, 128, + 64, 39, - 20, 60, + 20, 24, 49, 48, 1000, 17, - 63, - 23, 28, 31, 30, - 18, + 63, 47, + 18, 36, 34, 256, @@ -652,9 +651,10 @@ 62, 25, 21, - 1024, + 23, 46, 40, + 1024, 22, 61, 33, @@ -671,13 +671,13 @@ 90, 35, 26, - 58, 50, - 65536, + 58, 59, 73, 91, 27, + 65536, 37, 43, 512, @@ -686,9 +686,9 @@ 93, 2048, 96, - 55296, - 127, 95, + 127, + 55296, 200, 125, 70, @@ -701,11 +701,11 @@ 55, 56320, 500, - 53, - 56, 0.01, + 53, 52, - 0.00539 + 0.00539, + 66 ], "functionNames": [ "emitWarning", @@ -794,7 +794,6 @@ "D", "range", "transform", - "__", "y", "wrap", "w", @@ -803,18 +802,19 @@ "find", "each", "verb", - "abort", "toJSON", "plural", "isFunction", - "escape" + "escape", + "resolve", + "v" ], "packageNames": [ "three", "#compiler/builders", "three/tsl", - "tslib", "three/webgpu", + "tslib", "esm-env", "minimatch", "#client/constants", @@ -933,7 +933,6 @@ "uvu/assert", "uvu", "@pkgr/core", - "@emnapi/wasi-threads", "get-stream", "http-cache-semantics", "keyv", @@ -983,6 +982,8 @@ "#supports-color", "node:tty", "robust-predicates", + "three/addons/inspector/Extension.js", + "three/addons/inspector/ui/Tab.js", "https://cdn.jsdelivr.net/npm/mp4box@0.5.3/+esm", "preact-render-to-string", "buffer", @@ -1008,8 +1009,7 @@ "@webassemblyjs/helper-numbers", "d3-ease", "acorn-jsx", - "eslint-visitor-keys", - "get-east-asian-width" + "eslint-visitor-keys" ], "importedNames": [ "fn", @@ -1019,14 +1019,14 @@ "Vector3", "dayjs", "operate", - "_curry2", "Vector2", + "_curry2", "Fn", + "Color", "createOperatorSubscriber", "toDate", - "Color", - "b", "float", + "b", "formatDistance", "formatLong", "formatRelative", @@ -1034,8 +1034,8 @@ "match", "assertString", "buildFormatLongFn", - "BufferGeometry", "Matrix4", + "BufferGeometry", "e", "Mesh", "buildLocalizeFn", @@ -1046,134 +1046,138 @@ "warn", "vec3", "vec4", + "uniform", "noop", - "Float32BufferAttribute", + "HalfFloatType", "vec2", + "Float32BufferAttribute", + "Node", "util", "Observable", "_curry1", - "HalfFloatType", - "uniform", - "Node", "NodeMaterial", + "NodeUpdateType", "error", + "uv", "Loader", "identity", "_curry3", "constructFrom", - "uv", - "NodeUpdateType", "TempNode", - "LinearFilter", "max", - "DEV", - "w", + "LinearFilter", + "Vector4", "Object3D", - "FileLoader", "ShaderMaterial", - "Vector4", - "SRGBColorSpace", + "FileLoader", + "DEV", + "w", "nodeObject", - "colors", - "nodeProxy", + "SRGBColorSpace", "mix", + "colors", "sqrt", + "nodeProxy", "sin", "get", + "FloatType", "NearestFilter", + "abs", "BufferAttribute", "cos", "Parser", "LineBasicMaterial", - "abs", "If", + "Loop", + "RGBAFormat", "Quaternion", "MeshBasicMaterial", + "texture", "Subject", - "FloatType", - "RGBAFormat", "WebGLRenderTarget", - "texture", "UniformsUtils", + "Matrix3", "ColorManagement", "walk", "DoubleSide", "map", - "Loop", - "Matrix3", - "BackSide", "MathUtils", + "Texture", + "RenderTarget", "clamp", + "BackSide", + "min", + "int", "constant", "normalizeDates", "ClampToEdgeWrapping", - "Texture", - "min", "keys", "hydrating", "ramp", - "RenderTarget", - "int", + "UnsignedByteType", + "add", "dev", + "QuadMesh", "Subscription", "utils", "_dispatchable", "slice", - "add", "epsilon", "DataTexture", "RepeatWrapping", - "UnsignedByteType", + "RendererUtils", "Pass", "parse", "mark_subtree_dynamic", "FullScreenQuad", - "QuadMesh", "Group", "__read", "_xfBase", - "__extends", "active_effect", "merge", + "DepthTexture", + "convertToTexture", "__spreadArray", "set", - "RendererUtils", + "Box3", + "renderGroup", "LineSegments", "asyncScheduler", "isPromise", - "Box3", - "DepthTexture", "PerspectiveCamera", + "__extends", "fileURLToPath", "filter", "isArray", "parseNDigits", + "reference", "dot", - "warnOnce", - "convertToTexture", "NoBlending", "addMethodChaining", "untrack", "isArrayLike", "sub", - "reference", + "UnsignedIntType", + "RedFormat", "Sphere", "Material", "curryN", "toInt", "normalize", - "LinearSRGBColorSpace", + "select", + "EventDispatcher", + "warnOnce", + "BoxGeometry", "popScheduler", "AST_Node", "constructNow", - "select", "defaultSource", + "PlaneGeometry", + "RGFormat", "LinearMipmapLinearFilter", - "RedFormat", + "smoothstep", + "passTexture", "mul", - "BoxGeometry", - "renderGroup", - "UnsignedIntType", "AST_Call", "AST_Sequence", "unescape", @@ -1189,11 +1193,10 @@ "hooks", "addFormatToken", "Plane", - "PlaneGeometry", - "smoothstep", + "Scene", + "LinearSRGBColorSpace", "normalView", "Line", - "Euler", "EMPTY", "arrRemove", "executeSchedule", @@ -1205,14 +1208,1110 @@ "radians", "getDefaultOptions", "timer", - "Scene", + "positionLocal", "SRGBTransfer", - "options", - "isTag", - "AST_Class", - "AST_Conditional", - "AST_Number" + "positionView", + "uniformArray" ], + "packageImports": { + "three": [ + "Vector3", + "Color", + "Mesh", + "Matrix4", + "Vector2", + "BufferGeometry", + "ShaderMaterial", + "Loader", + "SRGBColorSpace", + "FileLoader", + "HalfFloatType", + "Float32BufferAttribute", + "UniformsUtils", + "MathUtils", + "WebGLRenderTarget", + "BufferAttribute", + "LinearFilter", + "Quaternion", + "Object3D", + "MeshBasicMaterial" + ], + "#compiler/builders": [ + "b" + ], + "three/tsl": [ + "Fn", + "vec4", + "float", + "uv", + "uniform", + "vec2", + "vec3", + "Loop", + "max", + "texture", + "convertToTexture", + "mix", + "nodeObject", + "If", + "int", + "passTexture", + "dot", + "abs", + "NodeUpdateType", + "clamp" + ], + "three/webgpu": [ + "NodeMaterial", + "Vector2", + "TempNode", + "RendererUtils", + "QuadMesh", + "RenderTarget", + "Vector3", + "NodeUpdateType", + "Mesh", + "HalfFloatType", + "Color", + "Node", + "DoubleSide", + "DepthTexture", + "Vector4", + "PassNode", + "Matrix4", + "FloatType", + "PlaneGeometry", + "RedFormat" + ], + "tslib": [ + "__read", + "__spreadArray", + "__extends", + "__values", + "__generator", + "__assign", + "__asyncGenerator", + "__await", + "__rest", + "__asyncValues", + "__awaiter" + ], + "esm-env": [ + "DEV", + "BROWSER" + ], + "minimatch": [ + "Minimatch", + "GLOBSTAR", + "escape", + "unescape" + ], + "#client/constants": [ + "DIRTY", + "EFFECT_TRANSPARENT", + "STATE_SYMBOL", + "CLEAN", + "DESTROYED", + "MAYBE_DIRTY", + "DERIVED", + "ASYNC", + "REACTION_RAN", + "COMMENT_NODE", + "BLOCK_EFFECT", + "BRANCH_EFFECT", + "INERT", + "STALE_REACTION", + "ROOT_EFFECT", + "EFFECT_PRESERVED", + "CONNECTED", + "WAS_MARKED", + "EFFECT", + "RENDER_EFFECT" + ], + "node:path": [ + "path", + "posix", + "win32", + "resolve", + "dirname", + "basename", + "presolve", + "prelative", + "pjoin", + "psep", + "sp", + "extname", + "sep" + ], + "d3-array": [ + "Adder", + "bisect", + "ticks", + "ascending", + "tickStep", + "merge", + "extent", + "nice", + "thresholdSturges", + "blur2", + "max", + "tickIncrement", + "sequence", + "quantile", + "threshold", + "InternMap", + "range", + "bisector" + ], + "preact": [ + "options", + "Component", + "Fragment", + "n", + "e", + "createElement", + "o", + "toChildArray", + "t", + "r", + "preactRender", + "u", + "i", + "l", + "c", + "f", + "preactHydrate", + "preactCloneElement", + "createRef", + "createContext" + ], + "domhandler": [ + "isTag", + "hasChildren", + "Document", + "isDocument", + "Text", + "isText", + "isComment", + "checkIsDocument", + "cloneNode", + "DomHandler", + "Element", + "ProcessingInstruction", + "Comment", + "isDirective", + "isCDATA" + ], + "zimmerframe": [ + "walk", + "zimmerframe_walk" + ], + "node:url": [ + "fileURLToPath", + "pathToFileURL", + "urlLib", + "URL", + "formatUrl" + ], + "fs": [ + "readFileSync", + "lstatSync", + "readdirCB", + "readdirSync", + "readlinkSync", + "rps", + "statcb", + "watchFile", + "unwatchFile", + "fs_watch", + "statSync", + "writeFile", + "require$$0$2" + ], + "node:fs": [ + "actualFS", + "fs", + "readFileSync", + "promises", + "statcb", + "Stats", + "fs_watch", + "unwatchFile", + "watchFile", + "statSync", + "createReadStream" + ], + "node:stream": [ + "Writable", + "Stream", + "Readable", + "PassThroughStream", + "PassThrough", + "Transform", + "ReadableStream", + "stream", + "pump", + "TransformStream", + "Duplex", + "finished" + ], + "domutils": [ + "removeElement", + "textContent", + "DomUtils", + "innerText", + "getChildren", + "getSiblings", + "nextElementSibling", + "prevElementSibling", + "uniqueSort", + "getFeed" + ], + "d3-selection": [ + "select", + "pointer", + "selection", + "namespace", + "style", + "selector", + "selectorAll", + "matcher" + ], + "node:fs/promises": [ + "lstat", + "readdir", + "realpath", + "readlink", + "stat", + "fsrealpath", + "open", + "nativeFsp" + ], + "path": [ + "resolve", + "dirname", + "basename", + "sysPath", + "normalize", + "require$$0$1" + ], + "d3-interpolate": [ + "interpolate", + "interpolateRound", + "interpolateNumber", + "interpolateCubehelixLong", + "interpolateTransform", + "piecewise", + "interpolateValue", + "interpolateRgbBasis", + "interpolateRgbBasisClosed", + "interpolateZoom", + "interpolateRgb", + "interpolateString" + ], + "htmlparser2": [ + "ElementType", + "parseDocument", + "parseWithHtmlparser2", + "htmlparser2" + ], + "minipass": [ + "Minipass" + ], + "d3-color": [ + "cubehelix", + "color", + "rgb", + "colorHsl", + "colorHcl", + "colorCubehelix", + "colorLab", + "colorRgb" + ], + "@webassemblyjs/ast": [ + "traverse", + "t", + "shiftSection", + "getSectionMetadata", + "getSectionMetadatas", + "isFunc", + "isGlobal", + "assertHasLoc", + "orderedInsertNode", + "getEndOfSection", + "isAnonymous", + "isInstruction" + ], + "node:util": [ + "types", + "promisify", + "inspect", + "deprecate", + "isDeepStrictEqual" + ], + "util": [ + "format", + "util", + "require$$2", + "Util" + ], + "@webassemblyjs/helper-wasm-bytecode": [ + "constants", + "getSectionForNode" + ], + "node:events": [ + "EventEmitter", + "addAbortListener", + "errorMonitor", + "on" + ], + "is-reference": [ + "is_reference" + ], + "@vue/shared": [ + "NOOP", + "extend", + "isString", + "isObject", + "isArray", + "capitalize", + "EMPTY_OBJ", + "isSymbol", + "isOn", + "isFunction", + "makeMap", + "normalizeCssVarValue", + "includeBooleanAttr", + "hasOwn", + "looseEqual", + "isSVGTag", + "NO", + "camelize", + "isBuiltInDirective", + "isReservedProp" + ], + "boolbase": [ + "boolbase" + ], + "node:crypto": [ + "createHash", + "crypto", + "randomUUID", + "randomFillSync" + ], + "acorn": [ + "tokTypes", + "acorn", + "acornNamespace", + "TokenType", + "keywordTypes", + "TokContext", + "isIdentifierStart", + "isIdentifierChar" + ], + "internmap": [ + "InternSet", + "InternMap" + ], + "crypto": [ + "crypto" + ], + "@sindresorhus/is": [ + "is", + "assert", + "isBuffer" + ], + "css-what": [ + "SelectorType", + "parse", + "AttributeAction", + "isTraversalBase", + "isTraversal" + ], + "url": [ + "fileURLToPath", + "Url" + ], + "brace-expansion": [ + "expand" + ], + "path-scurry": [ + "PathScurry", + "PathScurryDarwin", + "PathScurryPosix", + "PathScurryWin32" + ], + "lru-cache": [ + "LRUCache" + ], + "os": [ + "EOL", + "osType", + "require$$2$1" + ], + "node:os": [ + "os", + "homedir", + "osType", + "platform" + ], + "node:module": [ + "createRequire", + "module" + ], + "d3-dispatch": [ + "dispatch" + ], + "node:process": [ + "process", + "process$1" + ], + "stream": [ + "stream", + "require$$1", + "Readable" + ], + "@webassemblyjs/helper-buffer": [ + "overrideBytesInBuffer" + ], + "dom-serializer": [ + "render", + "renderWithHtmlparser2", + "renderHTML" + ], + "node:http": [ + "http", + "ServerResponse" + ], + "node:buffer": [ + "Buffer" + ], + "ansi-regex": [ + "ansiRegex" + ], + "strip-ansi": [ + "stripAnsi" + ], + "domelementtype": [ + "ElementType", + "isTagRaw" + ], + "preact/hooks": [ + "useState", + "useLayoutEffect", + "useEffect", + "useCallback", + "useContext", + "useDebugValue", + "useId", + "useImperativeHandle", + "useMemo", + "useReducer", + "useRef", + "a", + "s", + "h", + "v", + "d", + "m", + "p", + "y", + "_" + ], + "node:perf_hooks": [ + "performance" + ], + "iconv-lite": [ + "iconv" + ], + "@xtuc/long": [ + "Long" + ], + "parse5": [ + "parseDocument", + "parseFragment", + "serializeOuter", + "html", + "Parser" + ], + "events": [ + "EventEmitter", + "require$$0$3" + ], + "esrap": [ + "esrap", + "print", + "Context" + ], + "entities/decode": [ + "EntityDecoder", + "DecodingMode", + "htmlDecodeTree", + "xmlDecodeTree", + "fromCodePoint" + ], + "estraverse": [ + "estraverse" + ], + "formdata-polyfill/esm.min.js": [ + "FormData", + "formDataToBlob" + ], + "@kurkle/color": [ + "Color" + ], + "d3-time": [ + "timeYear", + "timeDay", + "utcYear", + "utcDay", + "timeMonth", + "timeWeek", + "timeHour", + "timeMinute", + "timeSecond", + "timeTicks", + "timeTickInterval", + "utcMonth", + "utcWeek", + "utcHour", + "utcMinute", + "utcSecond", + "utcTicks", + "utcTickInterval", + "timeSunday", + "timeMonday" + ], + "@vue/runtime-dom": [ + "initCustomFormatter", + "warn", + "runtimeDom", + "registerRuntimeCompiler" + ], + "@webassemblyjs/helper-api-error": [ + "CompileError" + ], + "@webassemblyjs/wasm-gen": [ + "encodeNode", + "encodeU32" + ], + "d3-timer": [ + "timer", + "timeout", + "now" + ], + "rw": [ + "rw" + ], + "commander": [ + "program" + ], + "parse5-htmlparser2-tree-adapter": [ + "htmlparser2Adapter" + ], + "lowercase-keys": [ + "lowercaseKeys" + ], + "d3-path": [ + "Path", + "path" + ], + "node:worker_threads": [ + "parentPort", + "workerData", + "MessageChannel", + "Worker", + "receiveMessageOnPort" + ], + "fs/promises": [ + "stat", + "readdir", + "open", + "lstat", + "fsrealpath" + ], + "readdirp": [ + "readdirp", + "ReaddirpStream" + ], + "mimic-response": [ + "mimicResponse" + ], + "@keyv/serialize": [ + "defaultDeserialize", + "defaultSerialize" + ], + "devalue": [ + "devalue" + ], + "svelte/internal/client": [ + "render_effect", + "get", + "noop" + ], + "locate-character": [ + "getLocator" + ], + "@jridgewell/sourcemap-codec": [ + "decode_mappings", + "encode" + ], + "esrap/languages/ts": [ + "ts" + ], + "magic-string": [ + "MagicString" + ], + "aria-query": [ + "roles_map", + "elementRoles", + "aria" + ], + "axobject-query": [ + "elementAXObjects", + "AXObjects", + "AXObjectRoles" + ], + "node:string_decoder": [ + "StringDecoder" + ], + "esrecurse": [ + "esrecurse" + ], + "node:https": [ + "https" + ], + "node:zlib": [ + "zlib" + ], + "fetch-blob/from.js": [ + "File", + "Blob", + "fileFromSync", + "fileFrom", + "blobFromSync", + "blobFrom" + ], + "node:net": [ + "isIP", + "net" + ], + "d3-time-format": [ + "timeFormat", + "utcFormat" + ], + "d3-format": [ + "format", + "formatSpecifier", + "formatPrefix", + "precisionFixed", + "precisionPrefix", + "precisionRound" + ], + "string-width": [ + "stringWidth" + ], + "ansi-styles": [ + "ansiStyles" + ], + "d3-drag": [ + "dragDisable", + "dragEnable" + ], + "d3-transition": [ + "interrupt" + ], + "uint8array-extras": [ + "stringToUint8Array", + "concatUint8Arrays", + "stringToBase64" + ], + "node:diagnostics_channel": [ + "diagnosticsChannel", + "tracingChannel", + "channel" + ], + "module": [ + "createRequire" + ], + "emoji-regex": [ + "emojiRegex" + ], + "child_process": [ + "nodeSpawn", + "spawn" + ], + "@webassemblyjs/ieee754": [ + "ieee754" + ], + "@webassemblyjs/utf8": [ + "utf8" + ], + "@webassemblyjs/leb128": [ + "decodeInt32", + "decodeUInt32", + "MAX_NUMBER_OF_BYTE_U32", + "decodeInt64", + "decodeUInt64", + "MAX_NUMBER_OF_BYTE_U64", + "leb" + ], + "@webassemblyjs/floating-point-hex-parser": [ + "parseHexFloat" + ], + "@webassemblyjs/wasm-parser": [ + "decode" + ], + "@webassemblyjs/wasm-gen/lib/encoder": [ + "encodeU32" + ], + "@xtuc/ieee754": [ + "write", + "read" + ], + "punycode": [ + "punycode" + ], + "d3-quadtree": [ + "quadtree" + ], + "node:assert/strict": [ + "assert" + ], + "@hapi/hoek": [ + "assert", + "escapeRegex" + ], + "cheerio-select": [ + "select" + ], + "uvu/assert": [ + "assert" + ], + "uvu": [ + "suite" + ], + "@pkgr/core": [ + "tryExtensions", + "findUp", + "cjsRequire", + "isPkgAvailable" + ], + "get-stream": [ + "getStreamAsBuffer" + ], + "http-cache-semantics": [ + "CachePolicy" + ], + "keyv": [ + "Keyv" + ], + "normalize-url": [ + "normalizeUrl" + ], + "responselike": [ + "Response" + ], + "clsx": [ + "_clsx" + ], + "svelte/reactivity": [ + "MediaQuery" + ], + "@jridgewell/remapping": [ + "remapping" + ], + "@sveltejs/acorn-typescript": [ + "tsPlugin" + ], + "data-uri-to-buffer": [ + "dataUriToBuffer" + ], + "fetch-blob": [ + "Blob" + ], + "debug": [ + "createDebug" + ], + "@eslint/object-schema": [ + "ObjectSchema" + ], + "levn": [ + "levn" + ], + "expo-random": [ + "getRandomBytesAsync" + ], + "d3-scale": [ + "scaleSequential" + ], + "gulp": [ + "gulp" + ], + "gulp-mocha": [ + "mocha" + ], + "gulp-eslint": [ + "eslint" + ], + "minimist": [ + "minimist" + ], + "gulp-git": [ + "git" + ], + "gulp-bump": [ + "bump" + ], + "gulp-filter": [ + "filter" + ], + "gulp-tag-version": [ + "tagVersion" + ], + "@vue/compiler-dom": [ + "compile" + ], + "entities": [ + "encodeXML", + "escapeAttribute", + "escapeText" + ], + "@isaacs/cliui": [ + "cliui" + ], + "node:tls": [ + "checkServerIdentity" + ], + "cacheable-lookup": [ + "CacheableLookup" + ], + "http2-wrapper": [ + "http2wrapper" + ], + "byte-counter": [ + "byteLength" + ], + "chunk-data": [ + "chunk" + ], + "cacheable-request": [ + "CacheableRequest", + "CacheableCacheError" + ], + "decompress-response": [ + "decompressResponse" + ], + "node:timers/promises": [ + "delay" + ], + "vite": [ + "defineConfig" + ], + "nth-check": [ + "getNCheck" + ], + "rollup-plugin-node-resolve": [ + "resolve" + ], + "rollup-plugin-commonjs": [ + "commonjs" + ], + "eastasianwidth": [ + "eastAsianWidth" + ], + "@vue/runtime-core": [ + "warn", + "BaseTransitionPropsValidators", + "h", + "BaseTransition", + "assertNumber", + "getCurrentInstance", + "onBeforeUpdate", + "queuePostFlushCb", + "onMounted", + "watch", + "onUnmounted", + "Fragment", + "Static", + "camelize", + "callWithAsyncErrorHandling", + "nextTick", + "unref", + "createVNode", + "defineComponent", + "useTransitionState" + ], + "vue": [ + "createVNode", + "ssrUtils", + "ssrContextKey", + "warn$2", + "Fragment", + "Static", + "Comment", + "Text", + "mergeProps", + "createApp", + "initDirectivesForSSR" + ], + "@vue/reactivity": [ + "pauseTracking", + "resetTracking", + "isRef", + "toRaw", + "traverse", + "watch$1", + "shallowRef", + "readonly", + "isReactive", + "ref", + "isShallow", + "isReadonly", + "shallowReadArray", + "toReadonly", + "toReactive", + "shallowReadonly", + "track", + "reactive", + "customRef", + "shallowReactive" + ], + "@vue/compiler-core": [ + "registerRuntimeHelpers", + "createSimpleExpression", + "createCompilerError", + "createObjectProperty", + "getConstantType", + "createCallExpression", + "TO_DISPLAY_STRING", + "transformModel$1", + "findProp", + "hasDynamicKeyVBind", + "findDir", + "isStaticArgOf", + "transformOn$1", + "isStaticExp", + "createCompoundExpression", + "checkCompatEnabled", + "isCommentOrWhitespace", + "noopDirectiveTransform", + "baseCompile", + "baseParse" + ], + "#ansi-styles": [ + "ansiStyles" + ], + "#supports-color": [ + "supportsColor" + ], + "node:tty": [ + "tty" + ], + "robust-predicates": [ + "orient2d" + ], + "three/addons/inspector/Extension.js": [ + "Extension" + ], + "three/addons/inspector/ui/Tab.js": [ + "Tab" + ], + "https://cdn.jsdelivr.net/npm/mp4box@0.5.3/+esm": [ + "MP4Box" + ], + "preact-render-to-string": [ + "renderToString" + ], + "buffer": [ + "Buffer" + ], + "balanced-match": [ + "balanced" + ], + "node:constants": [ + "constants" + ], + "cross-spawn": [ + "crossSpawn" + ], + "signal-exit": [ + "onExit" + ], + "node:dns": [ + "V4MAPPED", + "ADDRCONFIG", + "ALL", + "dnsPromises", + "dnsLookup" + ], + "rolldown/experimental": [ + "reactRefreshWrapperPlugin" + ], + "node:readline": [ + "readline" + ], + "delaunator": [ + "Delaunator" + ], + "d3-dsv": [ + "csvParse", + "dsvFormat", + "tsvParse" + ], + "@humanfs/core": [ + "Hfs" + ], + "@humanwhocodes/retry": [ + "Retrier" + ], + "css-select": [ + "compileToken", + "prepareContext" + ], + "is-stream": [ + "isReadableStream" + ], + "@sec-ant/readable-stream/ponyfill": [ + "asyncIterator" + ], + "node:stream/promises": [ + "finished" + ], + "whatwg-encoding": [ + "labelToName" + ], + "@webassemblyjs/ast/lib/clone": [ + "cloneNode" + ], + "@webassemblyjs/wasm-opt": [ + "shrinkPaddedLEB128" + ], + "@webassemblyjs/helper-wasm-section": [ + "resizeSectionByteSize", + "resizeSectionVecSize", + "createEmptySection", + "removeSections" + ], + "@webassemblyjs/helper-numbers": [ + "parse32F", + "parse64F", + "parse32I", + "parse64I", + "parseU32", + "isNanLiteral", + "isInfLiteral" + ], + "d3-ease": [ + "easeCubicInOut" + ], + "acorn-jsx": [ + "jsx" + ], + "eslint-visitor-keys": [ + "VisitorKeys" + ] + }, "globals": { "Array": [ "isArray", diff --git a/packages/core/src/decode.ts b/packages/core/src/decode.ts index 2aa8e3c..9584eb3 100644 --- a/packages/core/src/decode.ts +++ b/packages/core/src/decode.ts @@ -1,7 +1,7 @@ import type * as t from '@babel/types' import type { EncodingContext, ScopeBucket } from './context' import { parse } from '@babel/parser' -import { ASSIGN_OPS, bigramKey, BINARY_OPS, bitWidth, BitWriter, buildReverseTable, buildTable, deriveScopeBucket, filterCandidates, inferTypeFromKey, initialContext, LOGICAL_OPS, MAX_EXPR_DEPTH, mixHash, nameFromHash, UNARY_OPS } from './context' +import { ASSIGN_OPS, bigramKey, BINARY_OPS, bitWidth, BitWriter, buildReverseTable, buildTable, deriveScopeBucket, filterCandidates, inferTypeFromKey, initialContext, LOGICAL_OPS, MAX_EXPR_DEPTH, mixHash, nameFromHash, stmtDepthFromHash, UNARY_OPS } from './context' export interface DecodeOptions { /** Structural key — must match the key used during encoding. */ @@ -12,7 +12,7 @@ export interface DecodeOptions { type WorkItem = | { kind: 'expr', node: t.Node, depth: number } | { kind: 'stmt', node: t.Node, prev: string } - | { kind: 'block', stmts: readonly t.Statement[] } + | { kind: 'block', stmts: readonly t.Node[] } | { kind: 'block-depth-dec' } | { kind: 'scope-save', scope: string[], typedScope: any[], inFunction: boolean } | { kind: 'scope-restore', scope: string[], typedScope: any[], inFunction: boolean } @@ -23,6 +23,7 @@ type WorkItem | { kind: 'scope-push', name: string, type: string } | { kind: 'bucket-enter', bucket: ScopeBucket } | { kind: 'bucket-exit', prev: ScopeBucket } + | { kind: 'max-depth-restore', saved: number } export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { const ast = parse(jsSource, { @@ -42,7 +43,20 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { switch (node.type) { case 'NumericLiteral': return 'NumericLiteral:0' case 'StringLiteral': return 'StringLiteral:0' - case 'Identifier': return 'Identifier:0' + // DirectiveLiteral: Babel creates this when it re-parses a StringLiteral expression + // at the start of a function body as a Directive node. The encoder emitted + // StringLiteral:0 so we map back to the same key. + case 'DirectiveLiteral': return 'StringLiteral:0' + case 'Identifier': { + // Determine if this identifier references a scope variable (Identifier:scope:i) + // or a corpus ident (Identifier:corpus). Both encoder and decoder track typedScope + // in identical order, so findIndex gives the same variant index. + const name = (node as t.Identifier).name + const idx = ctx.typedScope.findIndex(e => e.name === name) + if (idx >= 0) + return `Identifier:scope:${idx}` + return 'Identifier:corpus' + } case 'BooleanLiteral': return `BooleanLiteral:${node.value ? 1 : 0}` case 'NullLiteral': return 'NullLiteral:0' case 'ThisExpression': return 'ThisExpression:0' @@ -196,7 +210,7 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { for (let i = (node as t.ObjectExpression).properties.length - 1; i >= 0; i--) { const p = (node as t.ObjectExpression).properties[i] as t.ObjectProperty work.push({ kind: 'expr', node: p.value, depth: d }) - work.push({ kind: 'expr', node: p.key, depth: d }) + // key is now a cosmetic short identifier — not structural } break case 'SequenceExpression': @@ -216,7 +230,8 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { } case 'ArrowFunctionExpression': { const n = node as t.ArrowFunctionExpression - const params = n.params.map((_, i) => nameFromHash(hash, 900 + i)) + const genParams = n.params.map((_, i) => nameFromHash(hash, 900 + i)) + const actualParams = n.params.map(p => (p as t.Identifier).name) work.push({ kind: 'scope-restore', scope: [...ctx.scope], typedScope: [...ctx.typedScope], inFunction: ctx.inFunction }) work.push({ kind: 'bucket-exit', prev: ctx.scopeBucket }) const bodyNode = n.body.type === 'BlockStatement' @@ -226,19 +241,22 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { work.push({ kind: 'expr', node: bodyNode, depth: d }) work.push({ kind: 'bucket-enter', bucket: 'function-body' }) // Push scope-save AFTER body (LIFO: save executes first) - work.push({ kind: 'scope-save', scope: params, typedScope: params.map(p => ({ name: p, type: 'any' })), inFunction: true }) + // Use genParams for scope dedup tracking, actualParams for typedScope name lookup + work.push({ kind: 'scope-save', scope: genParams, typedScope: actualParams.map(p => ({ name: p, type: 'any' })), inFunction: true }) break } case 'FunctionExpression': { const n = node as t.FunctionExpression - const params = n.params.map((_, i) => nameFromHash(hash, 900 + i)) + const genParams = n.params.map((_, i) => nameFromHash(hash, 900 + i)) + const actualParams = n.params.map(p => (p as t.Identifier).name) work.push({ kind: 'scope-restore', scope: [...ctx.scope], typedScope: [...ctx.typedScope], inFunction: ctx.inFunction }) work.push({ kind: 'bucket-exit', prev: ctx.scopeBucket }) const ret = n.body.body[0] if (ret?.type === 'ReturnStatement') work.push({ kind: 'expr', node: (ret as t.ReturnStatement).argument!, depth: d }) work.push({ kind: 'bucket-enter', bucket: 'function-body' }) - work.push({ kind: 'scope-save', scope: params, typedScope: params.map(p => ({ name: p, type: 'any' })), inFunction: true }) + // Use genParams for scope dedup tracking, actualParams for typedScope name lookup + work.push({ kind: 'scope-save', scope: genParams, typedScope: actualParams.map(p => ({ name: p, type: 'any' })), inFunction: true }) break } case 'ClassExpression': @@ -257,17 +275,26 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { function pushStmtChildren(node: t.Node): void { switch (node.type) { case 'Directive': - // Directive is a leaf — no children to push (treated as StringLiteral:0, a leaf expression) + // Babel parses leading string literals in function bodies as Directives. + // The encoder produced ExpressionStatement(StringLiteral), so push the + // DirectiveLiteral value as an expression to recover the StringLiteral:0 bits. + work.push({ kind: 'expr', node: (node as t.Directive).value, depth: 0 }) break case 'ExpressionStatement': - // Expression was directly selected as a candidate in statement context - pushExprChildren((node as t.ExpressionStatement).expression, 0) + // ExpressionStatement:0 was selected from statement table; the inner + // expression is processed via the expression table (separate lookup) + work.push({ kind: 'expr', node: (node as t.ExpressionStatement).expression, depth: 0 }) break case 'VariableDeclaration': { const n = node as t.VariableDeclaration - const name = nameFromHash(hash, ctx.scope.length) - ctx.scope.push(name) - work.push({ kind: 'var-decl', name, initNode: n.declarations[0].init!, depth: 0 }) + let genName = nameFromHash(hash, ctx.scope.length) + while (ctx.scope.includes(genName)) + genName = `${genName}${ctx.scope.length}` + ctx.scope.push(genName) + // Use the actual AST name for typedScope so Identifier:scope:i lookup works + // regardless of cosmetic renaming. genName is used only for dedup tracking. + const actualName = (n.declarations[0].id as t.Identifier).name + work.push({ kind: 'var-decl', name: actualName, initNode: n.declarations[0].init!, depth: 0 }) break } case 'IfStatement': { @@ -344,8 +371,7 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { work.push({ kind: 'bucket-exit', prev: ctx.scopeBucket }) work.push({ kind: 'block', stmts: n.cases[i].consequent }) work.push({ kind: 'bucket-enter', bucket: deriveScopeBucket('SwitchCase', 'consequent') }) - if (n.cases[i].test) - work.push({ kind: 'expr', node: n.cases[i].test!, depth: 0 }) + // case test is now a cosmetic small integer — not structural } work.push({ kind: 'expr', node: n.discriminant, depth: 0 }) break @@ -373,6 +399,7 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { break } case 'ExportDefaultDeclaration': { + ctx.hasExportDefault = true const n = node as t.ExportDefaultDeclaration work.push({ kind: 'expr', node: n.declaration as t.Node, depth: 0 }) break @@ -381,15 +408,22 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { const n = node as t.ExportNamedDeclaration if (n.declaration?.type === 'VariableDeclaration') { const vd = n.declaration as t.VariableDeclaration - const name = nameFromHash(hash, ctx.scope.length) - ctx.scope.push(name) - work.push({ kind: 'var-decl', name, initNode: vd.declarations[0].init!, depth: 0 }) + let genName = nameFromHash(hash, ctx.scope.length) + while (ctx.scope.includes(genName)) + genName = `${genName}${ctx.scope.length}` + ctx.scope.push(genName) + const actualVdName = (vd.declarations[0].id as t.Identifier).name + work.push({ kind: 'var-decl', name: actualVdName, initNode: vd.declarations[0].init!, depth: 0 }) } else if (n.declaration?.type === 'FunctionDeclaration') { const fd = n.declaration as t.FunctionDeclaration const fnName = (fd.id as t.Identifier).name - const params = fd.params.map((_, i) => nameFromHash(hash, 900 + i)) - const bodyStmts = fd.body.body + const genParams = fd.params.map((_, i) => nameFromHash(hash, 900 + i)) + const actualParams = fd.params.map(p => (p as t.Identifier).name) + // Include directives: a leading string literal in the function body is parsed by + // Babel as a Directive (in fd.body.directives), not an ExpressionStatement (in + // fd.body.body). The encoder counted it as a regular statement, so we must too. + const bodyStmts: readonly t.Node[] = [...fd.body.directives, ...fd.body.body] // LIFO: push in reverse order of desired execution // Execution order: scope-save → bucket-enter → block → bucket-exit → scope-restore → scope-push work.push({ kind: 'scope-push', name: fnName, type: 'function' }) @@ -397,7 +431,8 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { work.push({ kind: 'bucket-exit', prev: ctx.scopeBucket }) work.push({ kind: 'block', stmts: bodyStmts }) work.push({ kind: 'bucket-enter', bucket: 'function-body' }) - work.push({ kind: 'scope-save', scope: params, typedScope: params.map(p => ({ name: p, type: 'any' })), inFunction: true }) + // Use genParams for scope dedup tracking, actualParams for typedScope name lookup + work.push({ kind: 'scope-save', scope: genParams, typedScope: actualParams.map(p => ({ name: p, type: 'any' })), inFunction: true }) } break } @@ -450,9 +485,11 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { const table = buildTable(candidates, hash) const bits = bitWidth(table.length) const rev = buildReverseTable(table) - // ExpressionStatement: always use the inner expression's key - const key = item.node.type === 'ExpressionStatement' - ? exprKey((item.node as t.ExpressionStatement).expression) + // ExpressionStatement and Directive both map to 'ExpressionStatement:0' for the + // statement table lookup. Babel re-parses a leading string-literal ExpressionStatement + // inside a function body as a Directive — the encoder still selected ExpressionStatement:0. + const key = (item.node.type === 'ExpressionStatement' || item.node.type === 'Directive') + ? 'ExpressionStatement:0' : stmtKey(item.node) const value = rev.get(key) if (value !== undefined) { @@ -463,6 +500,13 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { out.write(0, bits) hash = mixHash(hash, 0) } + if (ctx.blockDepth === 0 && item.node.type !== 'ImportDeclaration') + ctx.hasLeftImportRegion = true + if (ctx.blockDepth === 0 && ctx.maxExprDepth > 1) { + const cap = stmtDepthFromHash(hash, ctx.maxExprDepth) + work.push({ kind: 'max-depth-restore', saved: ctx.maxExprDepth }) + ctx.maxExprDepth = cap + } pushStmtChildren(item.node) break } @@ -520,6 +564,10 @@ export function decode(jsSource: string, options?: DecodeOptions): Uint8Array { ctx.scopeBucket = item.prev break + case 'max-depth-restore': + ctx.maxExprDepth = item.saved + break + case 'var-decl': { // Process the init expression and infer type const exprCtx = { ...ctx, expressionOnly: true, exprDepth: item.depth } diff --git a/packages/core/src/encode.ts b/packages/core/src/encode.ts index 74e7345..815ebc8 100644 --- a/packages/core/src/encode.ts +++ b/packages/core/src/encode.ts @@ -17,6 +17,7 @@ import { MAX_EXPR_DEPTH, mixHash, nameFromHash, + stmtDepthFromHash, UNARY_OPS, UPDATE_OPS, } from './context' @@ -47,6 +48,9 @@ const CORPUS_FUNC_NAMES = cosmeticData.functionNames const CORPUS_PROPS = cosmeticData.properties const PACKAGE_NAMES: string[] = (cosmeticData.packageNames) ?? [] const IMPORTED_NAMES: string[] = (cosmeticData.importedNames) ?? [] +// Map of package name → its actual exports (from corpus). Used to pick +// realistic import specifiers that match the chosen package. +const PACKAGE_IMPORTS: Record = (cosmeticData as { packageImports?: Record }).packageImports ?? {} const VAR_KINDS = ['var', 'let', 'const'] as const export function encode(message: Uint8Array, options?: EncodeOptions): string { @@ -89,11 +93,16 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { } function cosmeticIdent(): string { - if (ctx.typedScope.length > 0 && rng() % 3 === 0) { + // For structural Identifier:corpus, pick from corpus idents only (no scope). + // padLeafExpr uses this directly; buildExprNode for scope variants uses typedScope[i].name. + if (ctx.typedScope.length > 0 && rng() % 3 !== 0) { return ctx.typedScope[rng() % ctx.typedScope.length].name } return CORPUS_IDENTS[rng() % CORPUS_IDENTS.length] } + function cosmeticCorpusIdent(): string { + return CORPUS_IDENTS[rng() % CORPUS_IDENTS.length] + } function cosmeticProp(): string { return CORPUS_PROPS[rng() % CORPUS_PROPS.length] } @@ -115,17 +124,35 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { function cosmeticFuncName(): string { return CORPUS_FUNC_NAMES[rng() % CORPUS_FUNC_NAMES.length] } - function cosmeticPackageName(h: number): string { + /** Pick a package that has at least `minExports` known imports in the corpus. */ + function cosmeticPackageWithExports(minExports: number): { pkg: string, exports: string[] } { if (PACKAGE_NAMES.length === 0) - return 'pkg' - return PACKAGE_NAMES[h % PACKAGE_NAMES.length] + return { pkg: 'pkg', exports: [] } + // Try up to 8 random packages to find one with enough exports + for (let i = 0; i < 8; i++) { + const pkg = PACKAGE_NAMES[rng() % PACKAGE_NAMES.length] + const exports = PACKAGE_IMPORTS[pkg] ?? [] + if (exports.length >= minExports) + return { pkg, exports } + } + // Fallback: any package, use global import names + const pkg = PACKAGE_NAMES[rng() % PACKAGE_NAMES.length] + return { pkg, exports: PACKAGE_IMPORTS[pkg] ?? IMPORTED_NAMES } } function cosmeticImportedName(h: number, offset: number): string { + // Uses hash so imports are deterministic from structural position + // (same structural spot → same name, allowing consistent references) if (IMPORTED_NAMES.length === 0) return nameFromHash(h, offset) const mixed = mixHash(h, offset) return IMPORTED_NAMES[mixed % IMPORTED_NAMES.length] } + /** Pick an import name from a specific package's exports, with collision dedup externally. */ + function importedFromPackage(exports: string[], idx: number): string { + if (exports.length === 0) + return cosmeticImportedName(0, idx) + return exports[(rng() + idx) % exports.length] + } function cosmeticFlags(): string { const FLAGS = 'dgimsuy' let s = '' @@ -169,13 +196,20 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { } function buildExprNode(c: Candidate, depth: number, cosmeticChildren = false): t.Expression { - // At max depth, all children become cosmetic (non-data-carrying) — hard depth cap + // cosmeticChildren is now effectively dead for data-carrying nodes (filterCandidates + // ensures only leaves reach this point at maxExprDepth). Kept for safety. const child = cosmeticChildren ? padLeafExpr : () => buildExpr(depth + 1).node switch (c.nodeType) { case 'NumericLiteral': return t.numericLiteral(cosmeticNumber()) case 'StringLiteral': return t.stringLiteral(cosmeticString()) - case 'Identifier': return t.identifier(cosmeticIdent()) + case 'Identifier': { + // Identifier:scope:i — direct reference to typedScope[i] + if (c.variant >= 0 && c.variant < ctx.typedScope.length) + return t.identifier(ctx.typedScope[c.variant].name) + // Identifier:corpus (variant = -1) — pick from corpus idents + return t.identifier(cosmeticCorpusIdent()) + } case 'BooleanLiteral': return t.booleanLiteral(c.variant === 1) case 'NullLiteral': return t.nullLiteral() case 'RegExpLiteral': return t.regExpLiteral(cosmeticTemplateRaw(), cosmeticFlags()) @@ -225,10 +259,9 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { return t.arrayExpression(Array.from({ length: c.variant }, () => child())) case 'ObjectExpression': { const pairs = Array.from({ length: c.variant }, () => { - const k = child() + const key = t.identifier(cosmeticProp()) const v = child() - const isc = !t.isIdentifier(k) && !t.isStringLiteral(k) && !t.isNumericLiteral(k) - return t.objectProperty(k, v, isc) + return t.objectProperty(key, v, false) }) return t.objectExpression(pairs) } @@ -315,7 +348,9 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { switch (c.nodeType) { case 'VariableDeclaration': { const kind = VAR_KINDS[c.variant] - const name = nameFromHash(hash, ctx.scope.length) + let name = nameFromHash(hash, ctx.scope.length) + while (ctx.scope.includes(name)) + name = `${name}${ctx.scope.length}` ctx.scope.push(name) const { node: init, candidate: initC } = buildExpr(0) const inferredType = initC ? inferTypeFromKey(initC.key) : 'any' @@ -365,8 +400,8 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { } case 'SwitchStatement': { const disc = buildExpr(0).node - const cases = Array.from({ length: c.variant }, () => { - const test = buildExpr(0).node + const cases = Array.from({ length: c.variant }, (_, i) => { + const test = t.numericLiteral(i) const body = buildBlock('SwitchCase', 'consequent') return t.switchCase(test, body) }) @@ -380,14 +415,17 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { case 'BreakStatement': return t.breakStatement() case 'ContinueStatement': return t.continueStatement() case 'ImportDeclaration': { - const pkg = cosmeticPackageName(hash) if (c.variant === 0) { - // side-effect + // side-effect — no names needed, just pick a package + const { pkg } = cosmeticPackageWithExports(0) return t.importDeclaration([], t.stringLiteral(pkg)) } if (c.variant === 1) { - // default - const local = cosmeticImportedName(hash, 1) + // default — pick a package and use one of its real exports as local name + const { pkg, exports } = cosmeticPackageWithExports(1) + let local = exports.length > 0 ? importedFromPackage(exports, 0) : cosmeticImportedName(hash, 1) + while (ctx.scope.includes(local)) + local = `${local}${ctx.scope.length}` ctx.scope.push(local) ctx.typedScope.push({ name: local, type: 'any' }) return t.importDeclaration( @@ -397,9 +435,26 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { } // named: variants 2..5 → 1..4 specifiers const count = c.variant - 1 + const { pkg, exports } = cosmeticPackageWithExports(count) + const usedFromPkg = new Set() const specifiers: t.ImportSpecifier[] = [] for (let i = 0; i < count; i++) { - const local = cosmeticImportedName(hash, 10 + i) + let local: string + if (exports.length > 0) { + // Pick an unused export from this package's real imports + local = importedFromPackage(exports, i) + let tries = 0 + while (usedFromPkg.has(local) && tries < 10) { + local = importedFromPackage(exports, i + tries + 1) + tries++ + } + } + else { + local = cosmeticImportedName(hash, 10 + i) + } + usedFromPkg.add(local) + while (ctx.scope.includes(local)) + local = `${local}${ctx.scope.length}` ctx.scope.push(local) ctx.typedScope.push({ name: local, type: 'any' }) specifiers.push(t.importSpecifier(t.identifier(local), t.identifier(local))) @@ -407,6 +462,7 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { return t.importDeclaration(specifiers, t.stringLiteral(pkg)) } case 'ExportDefaultDeclaration': { + ctx.hasExportDefault = true const { node: inner } = buildExpr(0) return t.exportDefaultDeclaration(inner) } @@ -414,7 +470,9 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { // variants 0..2: variable (var/let/const) if (c.variant >= 0 && c.variant <= 2) { const kind = VAR_KINDS[c.variant] - const name = nameFromHash(hash, ctx.scope.length) + let name = nameFromHash(hash, ctx.scope.length) + while (ctx.scope.includes(name)) + name = `${name}${ctx.scope.length}` ctx.scope.push(name) const { node: init, candidate: initC } = buildExpr(0) const inferredType = initC ? inferTypeFromKey(initC.key) : 'any' @@ -425,7 +483,9 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { // variants 10..13: function with param count 0..3 if (c.variant >= 10 && c.variant <= 13) { const paramCount = c.variant - 10 - const fnName = cosmeticFuncName() + let fnName = cosmeticFuncName() + while (ctx.scope.includes(fnName)) + fnName = `${fnName}${ctx.scope.length}` const paramNames = Array.from({ length: paramCount }, (_, i) => nameFromHash(hash, 900 + i)) // Enter function scope const savedScope = [...ctx.scope] @@ -452,7 +512,9 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { } return t.emptyStatement() } - default: return t.expressionStatement(buildExprNode(c, 0)) + case 'ExpressionStatement': + return t.expressionStatement(buildExpr(0).node) + default: return t.emptyStatement() } } @@ -487,7 +549,14 @@ export function encode(message: Uint8Array, options?: EncodeOptions): string { const value = readBits(bits) const c = table[value] hash = mixHash(hash, value) - return { stmt: buildStatement(c), candidate: c } + if (ctx.blockDepth === 0 && c.nodeType !== 'ImportDeclaration') + ctx.hasLeftImportRegion = true + const savedMaxDepth = ctx.maxExprDepth + if (ctx.blockDepth === 0 && savedMaxDepth > 1) + ctx.maxExprDepth = stmtDepthFromHash(hash, savedMaxDepth) + const stmt = buildStatement(c) + ctx.maxExprDepth = savedMaxDepth + return { stmt, candidate: c } } const body: t.Statement[] = [] diff --git a/packages/core/test/__snapshots__/roundtrip.test.ts.snap b/packages/core/test/__snapshots__/roundtrip.test.ts.snap index 0102962..d67f12d 100644 --- a/packages/core/test/__snapshots__/roundtrip.test.ts.snap +++ b/packages/core/test/__snapshots__/roundtrip.test.ts.snap @@ -1,17 +1,17 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`snapshots > all printable ASCII 1`] = `"switch((((({[((function translate(_wae,_xsa,_urm,_vii,_siu,_tzq,_qzc,_rqy,_opk,_phg,_mgs,_nyo,_qnc,_rfy,_oek,_pwg,_mvs,_nno,_kma,_lew,_idi,_jue){return (_nno<<=[(_oek&=(-(((((6,(((_nno=[((((((((--(_qnc))+((function values(_vei,_umm,_xna,_wve,_rly,_quc,_tuq,_sdu,_nto,_mbs,_pcg){return (({[(({[((function argumentCallback(_dln,_cur){return [\`ix\${((_yyy,_zqu,_aiq,_bzm,_kbc)=>((({[(((((function path(_vyb,_uhf,_xit,_wqx,_jwx,_ieb,_lfp,_knt,_noh,_mwl,_pxz,_ofd,_hbf,_gkj,_jlx,_itb){return (_rqy>>=(((function values(_ics,_jto,_gta,_hkw,_eki,_fbe,_caq,_dsm,_ary,_bju,_uww,_vos,_cpq,_dhm,_agy,_byu,_yxg,_zpc,_woo,_xfk,_ufw){return ((function ordinal(_uqo,_vil,_shw){return (((~(((({[((((((function formatRelative(_udw,_vvs){return (((function _chalk(_eni,_fee,_cdq,_dvm,_auy,_bmu,_ylg,_zdc,_wco,_xuk,_utw,_vks,_yag,_zsc,_wro,_xjk,_uiw,_vzs){return ((_wks,_xbo,_uba,_vsw,_sri,_tje,_qiq,_ram,_ozy,_pru,_mqg,_nic,_qxq,_rpm,_ooy,_pgu,_mfg,_nxc,_kwo,_lnk,_inw,_jes,_gde)=>\`u\${(_yag*=((new (((_shm,_tzi,_qyu,_rqq,_gei,_hwe,_evq,_fnm,_kxs,_loo,_ioa,_jfw,_ekq,_fcm,_cby,_dtu,_ica,_juw)=>void (((((_dqv,_cyz,_bhd,_aph,_ptz,_obd,_njh,_msl,_lap,_kjt,_jrx,_iab,_rnr,_qvv,_pez,_omd,_nuh,_mdl,_llp,_kut,_zxl,_ygp)=>[(_lap=((((_jfw||=((()=>(((new [(_gfn=>((_upt=>((((_cup,_dml,_alx,_bdt,_ycf,_zub,_wtn,_xlj,_ukv,_vbr,_sbd,_tsz,_qsl,_rjh,_oit,_pap,_mzb,_nrx,_gez,_hwv,_evh,_fnd,_cmp)=>[((\`am\${[new \`qjd\${({[(new (({[([[(((({[(((function e(_zdo,_yls,_bmg,_auk){return \`b\${((_viq,_uqu,_tyy,_shc,_rpg,_qyk,_pgo,_oos,_nxw,_mfa,_loe,_kwi,_jfm,_inq,_hvu,_gey,_fmc,_evg,_ddk,_cmo,_bus)=>(((_nuo,_mds,_pdg,_omk,_jce)=>(function o(_pdv,_olz,_nud,_mch){return false;})))?.(_tsz,"RC",_oos,_cmp,false,true,102,null,_vii,false,null,null))}bpxs\${"_"}vlj\${_msl}w\${"path"}jdkq\${127}wc\${false}tz\${null}jxdw\${null}noo\${false}reqy\${80}fg\${"f"}vqw\${null}od\`;}))(123,_byu,_dvm,true,false,102,false,_ioa,28,27,_zxl,10,false,null,_vbr,52))]:"w",[false]:null,[true]:_pwg}))!="119"))(37,_rqy),true,0,null,null,62,")",false,null,0.01,31,65536,_udw,"95",false,null,_hkw,_cup,false,"18.4",65,null,25,false,28,_nuh,_jfw,_vvs,null],_aiq,false,_ylg,true]\`smxe\${_pez}w\${"object"}zu\${_rly}in\${null}zlvr\${_tzi}xyt\${null}p\`)]:_lfp,17:null,40:"$",[false]:22,95:_sbd,"95":_upt,[null]:"object",_dln:_qxq,[false]:128,[null]:24,[null]:_dtu,"137":55296,_kwo:_zqu,_lfp:_xlj}))(_dsm,_mzb,"./_lib/convertToFP.js",_ufw,"A B",_sbd,false,true))]:26,_zdc:null,[null]:null,_oit:null,_pap:_bdt,9:null,_loo:true,[null]:18,[null]:"16.5"})}fx\${43}n\${"132"}zhzn\${127}i\${45}xt\${_mqg}oo\`(14,_loo,null,_sbd,"A B",false,_xuk,null,_lap),73,_sri,512,"8D",null,null,null,2,_qiq](22,_ycf,false,57,"a","./convert",_mzb,"module",_qzc,_wqx)}caw\${"x"}bgrb\${null}plg\`,_noh,null,false,"left",2048,true,42,_qvv,_cby,false,"end",_bju,_nic,true,"17.1",14,true,"DD/MM/YYYY","D MMMM YYYY","D MMMM YYYY",false,_dqv,20,false,_jlx,null,40,"../moment"),false,false,null,"M",_ram,false,7,_mzb,null,"keyword",false,"(",_dvm,_xit,_rjh,_ukv,_sri,false,37),true,_cby,null,null,_ptz,_lfp,"L",_nno,"|",null,_zub,_pru,true,_eki,90])).expression,_eki,63,"L",_kxs,null,_qiq,_bmu,_jlx,"pD",false,_fcm,_qxq,_kjt,25,0.01,true,9,54,"135",null,_nno,true)))(_cpq,null,_yxg,_qxq,_wco,59,_siu,123,_kma)),"8",null,_cyz,_qyu,"5",17,_pgu,true,44,73](_jto,"A",_tzi,_aiq,false,true,57,_idi,93,false))?.(null,false,_mgs,_wco,true,null,_mfg,"r",_ics,"2",_vks))?.(_jto,_fnm,"D",_jfw,23,null,false,_dqv,"i32",125,12,_dqv,true,"./convert"),_ram,"A B",52,_zdc,null)))))[null])._)),"33",true,54,_xuk,"$",_utw,_udw,4,false,_udw,21,49,_tje,73,false,61]),"D MMMM YYYY","18.3",null,true,null,25,_rqq,true,true,8,false,false,"utf8",true,_yyy,"133",42,95)))))("146",false,"$",_kma,false,_caq))(61,_wqx,"symbol","input","p",_jlx,false,14,_byu,_shw,"Identifier","140","146",true)))}hxut\${"u"}r\${_knt}yl\${_vyb}cubv\${null}qz\${"12"}nj\${null}jss\${"123"}zbzv\`);}))>>>"7D");}))<_mvs))??_vyb))]:null,123:_mvs,"end":null}))?.(false,_kma,43,false,_uww,"128","return",_cpq,null,90,false,"96"))))?.(true,_vos,null,true,null))/true);}))("8",47,_xit,_nto,200,47,null,_jlx,_yyy,21,null,false);}))?.(_vyb,36,"d",null,null,null,true,6,"boolean",_mbs,true)));}))/_opk),null,null,_nno,48,_kbc,false,_nno,"D MMMM YYYY HH:mm",73,null,null,false,_wve,61,_yyy,_siu,0.00539,0.00539,49,125,null,_aiq))]:_urm,_qnc:null,[true]:_xsa,_kma:null,47:"15.2-15.3","125":null,"79":_qnc,"production":true,[false]:_wve,25:_wae,6:47,_yyy:null,[true]:_rly,_opk:null,_jue:"145",_sdu:_xna,_rfy:_mgs,_umm:"^",_mvs:false,43:true}))+true))}vx\${null}zo\`,_wae,_lew,_xna,null,"key",15,null,_vei,0.00539,"_",false,null,_idi,29,_oek,false,_nyo,_tuq,_pcg,"_","class",1000,null,_mvs,"4.2-4.3",false];}))]:_vii,[null]:false,"J D E F A B 5C":_mbs,_tuq:_sdu,[false]:true}))]:_xna,39:"0",_opk:true,"17.4":true,[false]:null,_quc:"pD"}),_tuq,null,_rfy,20,_sdu,_tzq,_quc,_xsa,_nyo,53,52,_pcg,null,_rqy,"type","boolean",_pwg,_umm,_sdu,false,_rfy);}))))-false))?.("body","wide",true,"./placeholder",null,_xsa,_oek,null,29,16,_nyo,"33",null,"116",_xsa,null,9,true))^true))?.(null,null,"true","120",_qnc,"146",46),_wae,null,true,"array",_pwg,_mgs,_nyo,_siu,"115",_jue,"DD/MM/YYYY",null,_nno,4,"15.4","]",_nno,null,_vii]))?.("120"),null,48,null,101,"115",null,null,30,null,9,_vii,null,false,_mgs,101,_kma),_phg,_kma,null,500,null,5,true,false,_wae,true,false,"136","UC","!",null,"float","children","utf8",false,_urm,_vii,_tzq,100,_mvs,"wide"))(true,"g"))("15.4",_wae,false))))),_qzc,true,"D A",_kma,65536,null,null,"120","134",_idi,1024,_nyo,"115",true,"P",31,_qnc,_jue]);}))]:"26.2",[true]:null,63:73,0.00539:false,47:false,125:"8D",57:37,"data":false}))< all printable ASCII 1`] = `"import decompressResponse from "decompress-response";var bq=(true?\`d\`:true);let rz=41;let kx=/h/suy;export const uu=(bq).client;export default decompressResponse(false,rz,this);let xv=!(\`thmk\`);let uy=new kx("8");var xx=uy;;(uy=xx);export const dm=(xv+=[]);;const ym=xv;const gl=(function transform(om,hn){return xx;});switch((({context:ym,r:true}))){}var ts=(rz|kx);export const lw=(xx!=decompressResponse);(function _jestUtil(uv,au){return ym;});const km=rz(true,ym,this);var wt=this(null,uy);uu(kx,50);switch((uy[uu])){}var vp=((hq,ar)=>km);debugger;let zf=uy;;export var uj=(()=>uy);export var hw=(xx>>=xx);switch((wt(({}),({})))){}const wq=(decompressResponse?vp:bq);export var dq=this;let np=uj;var gi=61;export var is=(vp||((xx--)));let jr=(2!="144");var jm=decompressResponse;export var vp26=uy("16.5",false);switch(gi){}const sf=kx;export const vg=({log:wq,subscribe:xv,s:30});export var kt=!(bq);;export let tu=[decompressResponse,true,this,jm];var m=\`ltn\${uj}bi\`;let dw=(dq&=uy);let yg=(is**=null);export var dn=("delete"||null);;export var of=++(rz);const as=(ts-dq);export let yz=new uy(dw);export const pu=[uu,hw,xv,sf];let im=yg;export const qg=jm;;var hk=(qg||=false);"`; -exports[`snapshots > binary: deadbeef 1`] = `"switch((((({[((function c(_vku,_usy,_xtm,_wcq,_rsk,_qao,_tbc,_sjg,_jnq,_iwu,_pjs,_kfm,_pys){return (_pys&&=true);}))]:\`eood\${(("17.4","key",0,null,"S",200),true,true,null,null,true,50,"H",false,null,false,null,false,13,"18",92,"none",true,20,"M","p")}zei\${"17.0"}m\${null}ukk\`,[null]:true,[false]:null,[null]:"92",[null]:null,59:false,"{":false,"..":23}))<<11))){case true:case "HH:mm":case false:case false:case false:}"`; +exports[`snapshots > binary: deadbeef 1`] = `"import parseDocument from "parse5";var bq=(true?\`af\`:(({})));parseDocument;export const g=bq;export const vh=(parseDocument??=g);export default g;"`; -exports[`snapshots > hello, world! 1`] = `"switch((((({[((function clone(_vku,_usy,_xtm,_wcq,_rsk,_qao,_tbc,_sjg,_jnq,_iwu,_pjs,_kfm,_pys){return new \`bx\${(({[((function next(_nne,_mvi){return (\`xaw\${[]}vtd\${\`shij\${((_btg,_ack,_zko,_yss,_fmq,_euu,_dcy,_clc,_jea,_ime,_hvi,_gdm,_hgi,_gom,_fxq,_efu,_lys,_kgw,_jpa,_ixe)=>new ((err,(class{}),17,_zko,null,_efu,"17.3",_yss,"children",null))(36,null,_efu))}lab\${73}n\${true}eogg\${"../moment"}mpuk\${_qao}bs\${true}jy\${"style"}lz\${_usy}t\${false}ig\${null}yxm\${_vku}ttt\${false}aek\${null}u\${_rsk}jk\${26}hp\`}eh\${"7D"}yzlr\${512}f\${_pys}tbze\${"value"}qsr\${_jnq}emv\`%"=");}))]:"HH:mm"}))(27,100,"120",_iwu,_sjg)}exu\${null}v\${false}p\${_rsk}ytbc\`(_jnq,false,false,_vku,"123",true,false);}))]:500,90:null,0:"18",[null]:null,[false]:null,256:null,[null]:65536,[null]:null}))< hello, world! 1`] = `"import iconv from "iconv-lite";var bq=(true?\`w\`:(({})));const jk=new (/w/dms)(--(iconv));let ev=true;let gg=(bq||(/hspu/di));export var vm=\`jtd\`;export let ag="f";export const ve=[null,[],ag];export var cx=(gg||this);var wj=({material:bq,count:"style",pop:"18.2"});"`; -exports[`snapshots > json-like 1`] = `"switch((((({[(({[(({[[([\`uowd\${((_khn,_lzj,_mrf)=>(function f(_mra,_njw,_oas,_pso,_aow,_bgs,_cxo,_dpk,_egg,_fyc,_gqy,_hhu,_yue,_zla,_adw,_bvs,_cmo,_dek,_evg){return ((_mms,_ndo,_kca,_luw,_qec,_rwy,_ovk)=>((_icl,_jth,_gtt,_hkp,_ekb,_fbx,_caj,_dsf,_arr,_bjn,_yiz,_zav,_cpj,_dhf,_agr,_byn,_yxz,_zpv,_woh,_xfd,_ufp,_vwl)=>void (((_luw**=(((_cct,_dtp)=>(new ((function o(_crs,_djo,_aia,_baw,_yzi,_zqe,_wqq){return ({[(((_fte,_eci,_hcw,_gla,_rwj,_qfn,_tfb,_sof,_nez,_mmc,_pnr,_ovv,_zhd,_yph,_bqv,_ayz,_vpt,_uxx,_xyl,_wgp,_hrx,_gab,_jbp)=>(function s(_syy,_tqu,_qpg,_rhc,_gvu,_hnq){return [({[(-((({[((null,"79",_gqy,_dpk,"139","b",127,null,44,null,_yph,"y",null,"HH:mm:ss","138",_tfb,116,_xyl,null,_yph,null,true))]:_bjn,":":9,[null]:_tfb,[true]:false,_mra:_gla,_ufp:2,[false]:_mms,[true]:_byn,":":"in","data":"<",[null]:55296,_dhf:_gvu,_cct:"18.0",_gla:null,_pnr:false,_fbx:"7D",[null]:_mra,255:"float","18.0":_zpv,_oas:_hcw,63:null,[false]:"#",56:false,_fbx:null,_crs:",",[null]:27,96:null,[false]:true,[true]:null,[null]:"m",_djo:"5"}))))]:false,45:"D A","16.0":54,"D A":null,45:_yph,"__esModule":"w",[null]:_woh,"96":47,[null]:null,"]":null,[false]:_kca,[null]:"r",12:null,[false]:_rhc,_lzj:96,[true]:null,[null]:null,123:_byn,_caj:true,[null]:false}),13,_vpt,null,true,false,_aia,23,null,_cpj,_gab,"end","./_lib/convertToFP.js",false,null,null,"__esModule",_yxz,null,null,56320,8,_lzj,12]\`sbv\${_bvs}b\`;})))]:512,_dhf:null,[true]:true,12:"18.1",[null]:false,"b":4,_yxz:_yiz,[null]:null,_bgs:null,[null]:false});}))())\`tuv\${3}p\${40}usqo\${_fyc}n\${_dpk}fjcm\${_ovk}z\${true}tag\`)))))));}))}a\${"<"}sl\${null}jqpr\${"_"}aa\${"J D E F A B 5C"}jzqx\${true}hst\${true}fw\${"142"}ns\${12}xpa\${null}jk\${false}rizw\`],null,"125","12"),")",null,null,"null",127,"2",0.5,null,false,false,null]]:null,[false]:null,"18.3":null,[null]:null,"s":true,[false]:"y","]":false,[false]:0.00416,31:false,0.00416:null,"/":null,"^":null,[null]:"children","135":true,6:true,[null]:15,[null]:30,[null]:"key",[true]:false,96:56}))]:null,"#":null,"RC":9,"119":39,[false]:"136",[false]:true,"none":"16",[null]:"138",[false]:false,[null]:true,[false]:false,[true]:false,[true]:"125","145":"16.1",125:21,[true]:26,41:30,"svg":25,[true]:26,"16.0":"134","16.0":17,[false]:6,57:5,[false]:"18.4",20:null,65535:true,"17.0":null,[null]:"./convert",[false]:false,33:500}))]:null,38:30,[false]:false,37:2,42:false,"26.4":true,"f":"wide",[null]:41}))< json-like 1`] = `"import cliui from "@isaacs/cliui";var bq=(true?\`q\`:(({})));export var ph=((bq--))();let us=/wo/gs;export let oe=({});var tv=({constructor:ph,content:ph,call:cliui});export const xr=ph(45);(function clone(jv,qu){return ({});});let mi=/adxu/iu;export let zy=(mi||100);export var za=(function find(qo){return null;});var fq=(function r(){return tv;});export var hw=-(null);let tv12=oe;var gy=(oe).for;switch(ph){}export var tj=null[za];"`; -exports[`snapshots > sentence 1`] = `"switch((((({[(({[[((_vjw,_ura,_tze,_sii,_zbg,_yjk,_xso,_was,_dtq,_ccu,_bky,_asc,_hla,_gue,_fci,_elm)=>(function parse(_gio,_hzk,_ezw,_fqs,_sls,_tco,_qba,_rtw,_osi,_pke){return (function f(_wtm,_xli,_uku,_vcq,_iwq,_jom,_gny,_heu,_eeg,_fvc,_cvo,_dmk,_kqi,_lie,_ihq,_jzm,_gyy,_hpu,_epg,_fgc,_sbc,_tsy,_qsk){return (function format(_xgy,_woc,_zpq,_yxu,_jjc,_irg,_lsu,_kay,_fqs,_ezw){return new ((\`ae\`,\`v\${new (new ((({[[(_was^=\`hc\${((_szf,_tqb,_uix,_vat,_wrp,_xjl,_yah,_zsd,_ajz,_bbv,_ctr,_dkn,_ylh,_zdd,_avz,_bmv,_cer,_dvn,_enj)=>[(_gue,false,_xli,_kqi,"meta",null,true,null,_zbg,null,"J D E F A B 5C","wide",39,_yxu,_ccu,_osi,_fci,_ezw,null,28,15,_dvn,0,_tsy,"15.4",63,_hzk,_ezw,_gyy),_ajz,_wrp,"16.1","none",_ihq,_ezw,54,null,"#",_xgy,false,_zpq,null,"123",_was,false,_irg,null,_vat])}z\${"/"}a\${_xli}d\${null}zxa\${123}pqqe\${_tze}i\${false}bai\${null}bz\${_gio}uz\${_osi}s\${_woc}qjng\${false}aa\${"9D AE"}ebc\`),52,38,null,2,"-",0.00416,false,"body",false,null,31,true,_irg,_jzm,73,_hpu,_fci,95,false,null,_tsy,null,"<",false,_tze]]:33,62:"/",[null]:46,17:true,[null]:null,49:true,_gue:"null",256:_pke,"\`":true,[true]:40,_gny:null,_yjk:"number",[null]:_gio,[null]:false}),"wide",62,_eeg,_ezw,true,true,_fvc,"'",27,"g",_bky,24,false,_iwq,"16.2",_gny,_ezw,"MemberExpression",_gny))(_rtw,_pke,_zbg,true))(_heu,7,null,false)}xqce\${_lie}wa\${7}uuc\${false}mw\${true}h\${false}mbx\${10}zxv\${"1"}bkgi\${"142"}ahls\`(_woc,31,null,"127",_lsu,true,_woc,"]",_xli,true,null,_fgc,43,100,9,false,true),96,null,_jzm,"J D E F A B 5C","name",null,_xli,_ezw))(true,"default",_hla,36,null,_gny,_osi,null,_xgy,4,_asc,_pke);});});})),false,false,"object",true,null,null,"142",null,102,false,"16",null,false,"18.0",null,null,true,null]]:"object",1024:null,"meta":null,[false]:false,32:false,[true]:"value","130":null,[true]:true,1:false,"end":null,"26.3":22,[false]:false,37:"HH:mm",[false]:null,[true]:true,11:null,[true]:"name",[true]:55296,1024:30,"123":true,"../moment":10,[false]:"128","div":62,[null]:"class",[null]:91,"S":null,"26.1":null,"float":18,[true]:null,90:false}))]:null,"8":null,[true]:"key",[null]:512,"none":80,"#":true,[false]:58,[false]:"16.1"}))<<102))){case "#":case null:case true:case "J D E F A B 5C":case null:}"`; +exports[`snapshots > sentence 1`] = `"import nodeSpawn from "child_process";var bq=(true?\`t\`:(({})));export var ph=(--(nodeSpawn))?.(({}));let ar=new (++(bq))(62,this);const fe="127";[false,false];export default ar;const cv=+(false);export const jx=true(({}),--(bq),/nib/dm,(cv++));;export const sn=[];let di=new ((fe++))();export var wl=({offset:fe});var rm=true;"`; -exports[`snapshots > short: "hi" 1`] = `"switch((((({[((function each(_vku,_usy,_xtm,_wcq,_rsk,_qao,_tbc,_sjg,_jnq,_iwu,_pjs,_kfm,_pys){return \`qkn\${[false,6,_usy,_pjs,14,"5","d",26,125,_rsk,"null",true,_pys,_rsk,"79","26.4",127,_vku,null,_usy,_kfm,false,"null",5,"26.2",null,null,_iwu,true,_xtm]\`jmdz\${null}f\${null}cyrd\${true}l\`}c\${"131"}wpt\${false}coc\${null}mjbz\${10}tgh\${21}wqy\${"17.2"}bfd\${_qao}h\${_jnq}hw\${true}ucab\${null}yud\${_sjg}aq\`;}))]:125,"[":65535,28:true,"class":false,97:65536,"133":false,[null]:null,[null]:"S"}))<<31))){case 95:case false:case false:case "?":case 65536:}"`; +exports[`snapshots > short: "hi" 1`] = `"import cloneNode from "@webassemblyjs/ast/lib/clone";var bq=(true?\`gxbb\`:(({})));(bq|=((bq--)));export var zr=(()=>14);"`; -exports[`snapshots > single byte 0x42 1`] = `"switch((((({[((function map(_vku,_usy,_xtm,_wcq,_rsk,_qao,_tbc,_sjg,_jnq,_iwu,_pjs,_kfm,_pys){return [(_sjg<<"object"),26,125];}))]:"function",[false]:true,[true]:false,34:"26.1",[null]:"p",11:125,44:true,".":1}))< single byte 0x42 1`] = `"import utcHour from "d3-time";var bq=(true?\`hruu\`:(({})));this;export default true;"`; -exports[`snapshots > url 1`] = `"switch((((({[(({[(({[(({[((function findIndex(_umr,_ven){return [(function _chalk(_mig,_nzc,_kyo,_lqk,_qaq,_rrm,_ory,_piu,_usa,_vkw,_sji){return \`kru\${(\`ymfs\${\`ee\${this}m\${(_nzc<<=(({[[[[((((function _typeof(_wrc,_xjy,_ybu,_zsq,_akm,_bbi){return (function l(_qhk,_rzg,_kmi,_pqo,_idq,_jum,_guy,_hlu,_elg,_fcc,_cbo){return ({[((_rrm=(new [true,_nzc,true,null,"symbol",null,null,28](_pqo,54,"(",9,"set",_fcc,"137",34,_ory,_fcc,"default"))))]:true,[false]:"115","26.3":_bbi,_qaq:92,4:"full",_bbi:_elg,[null]:_ven,[true]:null,_sji:null,[null]:_kyo,"w":null,"#":_rzg,_kyo:0.01,_ybu:_xjy,[true]:255,[null]:true,[null]:1000,_idq:null,"boolean":42,_vkw:_ybu,"!":true,"17.2":null,[null]:_kmi,[false]:_cbo,"./placeholder":_qaq,[null]:64});});}),30,true,"Object",_sji,18,50,43,_rrm,_lqk,null,true,true,true,"left",false,"in",false,_nzc,null,_qaq,_ven,19,_sji,null,_rrm,_sji,53),"[",_ory),false,_umr,true,_umr,_ory,_vkw,_vkw,"KB",_qaq,_vkw,_umr,null,500,null,false,true,_qaq,null,null),null,false]],10,"9D AE",null,null,16,_usa,9,null,"16.0",36,_sji,true,64,_piu,_ven,15,null]]:_rrm,[false]:_umr,[null]:"137"})))}kge\${true}uud\${18}tsx\${"134"}plcc\`}lwz\${_qaq}bckq\${_rrm}tbny\${null}cvqe\${125}i\${33}t\${_nzc}gyi\${_ory}eniw\${10000}uhp\${"129"}olfz\${"26.3"}k\${null}lam\${null}tjuh\${true}wpp\`-_vkw)}gb\${"MemberExpression"}jf\${19}i\${_lqk}qy\${_ory}ib\${_usa}f\${_rrm}lc\${_umr}bcmu\${10000}qihl\${_sji}rldp\`;}),null,_ven,null,_umr,true,_ven,true,"146",null,_ven,null,_umr,null,_umr,"16.1",_ven,_umr,_umr,_ven,_umr,_umr,_ven,null,0];}))]:20,"float":", ","18.5-18.7":true,"error":false}))]:0,[null]:"body",[null]:true,73:"f",19:"26.4",[true]:false,[null]:null,[false]:18,"26.4":33,"4.2-4.3":"\\\\","D":"value",[null]:65,26:80,[true]:null,[null]:"?"}))]:true,"any":null,"\\\\":null,[null]:null,[false]:true,[true]:20,[null]:true,93:"return",127:true,"|":"error",[null]:31,1:255,[true]:3,"function":93,[true]:1,55:"?","D A":"object",[null]:"134","L":"33",48:91,"right":false,[null]:false,[null]:95,"140":false,42:":",[null]:"18.0",12:null,[true]:"18.4","D MMMM YYYY":"'",32:null}))]:"95",[true]:0.00416,"18.2":null,"HH:mm:ss":", ",[null]:null,[false]:null,[null]:43,[true]:"data"}))< url 1`] = `"import lstatSync from "fs";var bq=(true?\`yat\`:(({})));export var ph="__esModule";let fx=(bq??=((lstatSync++)));([]*3);export var jl=void ((/qzb/du));switch(\`r\${lstatSync}aq\`){}const fj=9;(lstatSync,"H");;(59*52);export var wt=new [](lstatSync);export let uv=(ph^[]);true;var tm=(()=>bq);let jc=uv;const rq=jc;switch((({}))){}"`; diff --git a/packages/core/test/roundtrip.test.ts b/packages/core/test/roundtrip.test.ts index f0b5944..6c91e42 100644 --- a/packages/core/test/roundtrip.test.ts +++ b/packages/core/test/roundtrip.test.ts @@ -205,47 +205,41 @@ describe('data lives in AST structure, not literal values', () => { }) it('randomize all names, literals, and labels — decode still works', () => { - // This is the definitive test: encode a message, parse the output, - // walk the AST and randomize EVERY cosmetic value (identifier names, - // string literal values, numeric literal values, regex patterns, - // bigint values, template strings, labels, var names, catch params), - // regenerate JS from the mutated AST, and verify decode still works. - - function randomizeName(): string { - // _ prefix guarantees it's never a JS keyword - const chars = 'abcdefghijklmnopqrstuvwxyz' - const len = 1 + Math.floor(Math.random() * 5) - let s = '_' - for (let i = 0; i < len; i++) s += chars[Math.floor(Math.random() * chars.length)] - return s + // Definitive test: encode a message, parse the output, walk the AST and + // randomize EVERY cosmetic value (identifier names — consistently renamed so + // all occurrences of the same name get the same replacement, string/numeric/ + // bigint literal values, regex patterns, template strings), regenerate JS, + // and verify decode still returns the same bytes. + // + // Identifier names are consistently renamed because scope-referencing + // identifiers (Identifier:scope:i) are structural: the decoder uses the name + // to look up which scope entry was referenced. Consistent renaming preserves + // this: if var 'bq' → '_r5' everywhere, the decoder builds typedScope with + // '_r5' and resolves references to '_r5' at the same index. Inconsistent + // per-node renaming would corrupt the scope lookup. + + let nameIdx = 0 + function freshName(): string { + return `_r${nameIdx++}` } - let nameCounter = 0 - const paramNodes = new Set() - function walk(node: any): void { + function walk(node: any, nameMap: Map): void { if (!node || typeof node !== 'object') return - // For function/arrow params: assign unique names to avoid clash - if ((node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') && node.params) { - for (const p of node.params) { - if (p.type === 'Identifier') { - p.name = `_r${nameCounter++}` - paramNodes.add(p) - } - } - } - - // Randomize cosmetic values (skip param identifiers — already handled) - if (node.type === 'Identifier' && typeof node.name === 'string' && !paramNodes.has(node)) { - node.name = randomizeName() + // Consistently remap identifier names: declaration sites and reference sites + // both get the same new name, preserving scope identity. + if (node.type === 'Identifier' && typeof node.name === 'string') { + if (!nameMap.has(node.name)) + nameMap.set(node.name, freshName()) + node.name = nameMap.get(node.name)! } if (node.type === 'NumericLiteral' && typeof node.value === 'number') { node.value = Math.floor(Math.random() * 99999) delete node.extra } if (node.type === 'StringLiteral' && typeof node.value === 'string') { - node.value = randomizeName() + node.value = freshName() delete node.extra } if (node.type === 'BigIntLiteral' && typeof node.value === 'string') { @@ -253,11 +247,11 @@ describe('data lives in AST structure, not literal values', () => { delete node.extra } if (node.type === 'RegExpLiteral') { - node.pattern = randomizeName() + node.pattern = freshName() delete node.extra } if (node.type === 'TemplateElement' && node.value) { - const raw = randomizeName() + const raw = freshName() node.value = { raw, cooked: raw } } // Recurse @@ -266,9 +260,9 @@ describe('data lives in AST structure, not literal values', () => { continue const val = node[key] if (Array.isArray(val)) - val.forEach(walk) + val.forEach(child => walk(child, nameMap)) else if (val && typeof val === 'object' && val.type) - walk(val) + walk(val, nameMap) } } @@ -279,17 +273,17 @@ describe('data lives in AST structure, not literal values', () => { const js = encode(data) - // Parse → randomize → regenerate + // Parse → consistently rename → regenerate const ast = parse(js, { sourceType: 'module', allowReturnOutsideFunction: true, errorRecovery: true, plugins: [['optionalChainingAssign', { version: '2023-07' }]], }) - walk(ast.program) + walk(ast.program, new Map()) const randomized = generateCompact(ast.program) - // Decode the randomized JS — must still produce the same bytes + // Decode the consistently-renamed JS — must still produce the same bytes const out = decode(randomized) expect(Array.from(out)).toEqual(Array.from(data)) } @@ -305,7 +299,7 @@ describe('encode output validity', () => { ] for (const msg of msgs) { const js = encode(msg) - expect(() => parse(js)).not.toThrow() + expect(() => parse(js, { sourceType: 'module' })).not.toThrow() } }) @@ -331,7 +325,178 @@ describe('import candidates', () => { }) }) +/** + * Walk a JS string and return the maximum expression nesting depth (0-based). + * Mirrors the encoder: depth 0 = direct expression child of a statement, + * +1 for each expression child recursion. Cosmetic (non-structural) children + * like object keys, switch-case tests, and label names are not counted. + */ +function measureMaxExprDepth(js: string): number { + const ast = parse(js, { sourceType: 'module', plugins: [['optionalChainingAssign', { version: '2023-07' }]] }) + let max = -1 + + function e(node: any, d: number): void { + if (!node || typeof node !== 'object') + return + if (d > max) + max = d + const c = d + 1 + switch (node.type) { + case 'BinaryExpression': + case 'LogicalExpression': + e(node.left, c) + e(node.right, c) + break + case 'AssignmentExpression': + e(node.right, c) + break + case 'UnaryExpression': + e(node.argument, c) + break + case 'ConditionalExpression': + e(node.test, c) + e(node.consequent, c) + e(node.alternate, c) + break + case 'CallExpression': + case 'OptionalCallExpression': + e(node.callee, c) + node.arguments.forEach((a: any) => e(a, c)) + break + case 'NewExpression': + e(node.callee, c) + node.arguments.forEach((a: any) => e(a, c)) + break + case 'MemberExpression': + case 'OptionalMemberExpression': + e(node.object, c) + if (node.computed) + e(node.property, c) + break + case 'ArrayExpression': + node.elements.forEach((el: any) => el && e(el, c)) + break + case 'ObjectExpression': + node.properties.forEach((p: any) => e(p.value, c)) + break + case 'SequenceExpression': + node.expressions.forEach((ex: any) => e(ex, c)) + break + case 'TemplateLiteral': + node.expressions.forEach((ex: any) => e(ex, c)) + break + case 'TaggedTemplateExpression': + e(node.tag, c) + node.quasi.expressions.forEach((ex: any) => e(ex, c)) + break + case 'ArrowFunctionExpression': { + const body = node.body.type === 'BlockStatement' ? node.body.body[0]?.argument : node.body + if (body) + e(body, c) + break + } + case 'FunctionExpression': { + const ret = node.body?.body?.[0] + if (ret?.type === 'ReturnStatement' && ret.argument) + e(ret.argument, c) + break + } + case 'AwaitExpression': + e(node.argument, c) + break + case 'ClassExpression': + if (node.superClass) + e(node.superClass, c) + break + } + } + + function s(node: any): void { + if (!node) + return + switch (node.type) { + case 'ExpressionStatement': + e(node.expression, 0) + break + case 'VariableDeclaration': + node.declarations.forEach((d: any) => d.init && e(d.init, 0)) + break + case 'ExportDefaultDeclaration': + e(node.declaration, 0) + break + case 'ExportNamedDeclaration': + if (node.declaration?.type === 'VariableDeclaration') + node.declaration.declarations.forEach((d: any) => d.init && e(d.init, 0)) + else if (node.declaration?.type === 'FunctionDeclaration') + node.declaration.body?.body.forEach(s) + break + case 'IfStatement': + e(node.test, 0) + s(node.consequent) + s(node.alternate) + break + case 'WhileStatement': + case 'DoWhileStatement': + e(node.test, 0) + s(node.body) + break + case 'ForStatement': + if (node.init) + e(node.init.type === 'VariableDeclaration' ? node.init.declarations[0]?.init : node.init, 0) + if (node.test) + e(node.test, 0) + if (node.update) + e(node.update, 0) + s(node.body) + break + case 'BlockStatement': + node.body.forEach(s) + break + case 'ReturnStatement': + if (node.argument) + e(node.argument, 0) + break + case 'ThrowStatement': + e(node.argument, 0) + break + case 'SwitchStatement': + e(node.discriminant, 0) + node.cases.forEach((c: any) => c.consequent.forEach(s)) + break + case 'LabeledStatement': + s(node.body) + break + case 'TryStatement': + s(node.block) + if (node.handler) + s(node.handler.body) + if (node.finalizer) + s(node.finalizer) + break + } + } + + ast.program.body.forEach(s) + return max +} + describe('maxExprDepth', () => { + it('output expression depth does not exceed maxExprDepth', () => { + const cases: Array<[Uint8Array, number]> = [ + [new TextEncoder().encode('hello world'), 1], + [new TextEncoder().encode('the quick brown fox'), 3], + [new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF, 0x12, 0x34]), 5], + [new Uint8Array(Array.from({ length: 20 }, (_, i) => i * 13)), 7], + [new TextEncoder().encode('https://example.com/api/v2'), 10], + [new Uint8Array(Array.from({ length: 30 }, (_, i) => (i * 37) & 0xFF)), 20], + ] + for (const [data, maxDepth] of cases) { + const js = encode(data, { maxExprDepth: maxDepth }) + expect(measureMaxExprDepth(js)).toBeLessThanOrEqual(maxDepth) + expect(Array.from(decode(js, { maxExprDepth: maxDepth }))).toEqual(Array.from(data)) + } + }) + it('round-trips with depth 10', () => { for (let i = 0; i < 50; i++) { const len = Math.floor(Math.random() * 20) + 1 @@ -377,7 +542,7 @@ describe('maxExprDepth', () => { it('different depths produce different output for same input', () => { const msg = new TextEncoder().encode('hello') const outputs = new Set() - for (const d of [5, 10, 15, 20, 50]) { + for (const d of [1, 5, 10, 20, 50]) { outputs.add(encode(msg, { maxExprDepth: d })) } expect(outputs.size).toBeGreaterThanOrEqual(2) @@ -443,5 +608,6 @@ describe('maxExprDepth', () => { const js = encode(data, { maxExprDepth: 64 }) const out = decode(js, { maxExprDepth: 64 }) expect(Array.from(out)).toEqual(Array.from(data)) + expect(measureMaxExprDepth(js)).toBeLessThanOrEqual(64) }) }) diff --git a/packages/core/test/tables.test.ts b/packages/core/test/tables.test.ts index abb0fbf..1daa4ef 100644 --- a/packages/core/test/tables.test.ts +++ b/packages/core/test/tables.test.ts @@ -12,10 +12,10 @@ describe('dynamic table generation', () => { const ctx = initialContext() const candidates = filterCandidates(ctx) const table = buildTable(candidates, 0) - // Table size is 2^bitWidth(uniqueCount) const bits = bitWidth(table.length) expect(table.length).toBe(1 << bits) - expect(table.length).toBeGreaterThanOrEqual(128) + // Statement-only table: ~30-50 candidates → 16 or 32 entries + expect(table.length).toBeGreaterThanOrEqual(16) }) it('reverse table maps candidate keys back to indices', () => { @@ -41,7 +41,7 @@ describe('dynamic table generation', () => { if (t1[i].key !== t2[i].key) diffs++ } - expect(diffs).toBeGreaterThan(50) + expect(diffs).toBeGreaterThan(5) }) it('expression-only context excludes statements', () => { @@ -51,11 +51,19 @@ describe('dynamic table generation', () => { expect(hasStatement).toBe(false) }) + it('statement context excludes raw expressions', () => { + const ctx = initialContext() + const candidates = filterCandidates(ctx) + const hasRawExpr = candidates.some(c => !c.isStatement) + expect(hasRawExpr).toBe(false) + // But ExpressionStatement:0 IS available as a statement candidate + expect(candidates.some(c => c.key === 'ExpressionStatement:0')).toBe(true) + }) + it('context-gated entries only appear in correct context', () => { const base = filterCandidates(initialContext()) expect(base.some(c => c.nodeType === 'ReturnStatement')).toBe(false) expect(base.some(c => c.nodeType === 'BreakStatement')).toBe(false) - expect(base.some(c => c.nodeType === 'AwaitExpression')).toBe(false) const inFn = filterCandidates({ ...initialContext(), inFunction: true }) expect(inFn.some(c => c.nodeType === 'ReturnStatement')).toBe(true) @@ -64,7 +72,8 @@ describe('dynamic table generation', () => { expect(inLoop.some(c => c.nodeType === 'BreakStatement')).toBe(true) expect(inLoop.some(c => c.nodeType === 'ContinueStatement')).toBe(true) - const inAsync = filterCandidates({ ...initialContext(), inAsync: true }) + // AwaitExpression is expression-only, not available in statement context + const inAsync = filterCandidates({ ...initialContext(), inAsync: true, expressionOnly: true }) expect(inAsync.some(c => c.nodeType === 'AwaitExpression')).toBe(true) }) }) diff --git a/playground/src/Playground.tsx b/playground/src/Playground.tsx index 1cda4e4..7ce5f0a 100644 --- a/playground/src/Playground.tsx +++ b/playground/src/Playground.tsx @@ -222,7 +222,7 @@ export function Playground() { dirRef.current = 'encode' } }} - placeholder="∞" + placeholder="1" />