solve-engine 1.0.0-beta.1 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/{Lexer-BJdhlnej.d.cts → Lexer-Dkt7KGaU.d.ts} +18 -2
  2. package/dist/{Lexer-D9l4Yrk2.d.ts → Lexer-y8uY6n6t.d.cts} +18 -2
  3. package/dist/{PackageRegistry-D-Tv_7ca.d.cts → PackageRegistry-CblRmrHc.d.cts} +60 -4
  4. package/dist/{PackageRegistry-DaH4aIKP.d.ts → PackageRegistry-wUQyVEyw.d.ts} +60 -4
  5. package/dist/{Parselet-CK8cQu2a.d.cts → Parselet-C974vhZG.d.cts} +1 -1
  6. package/dist/{Parselet-ConOIdRO.d.ts → Parselet-CsmtM3It.d.ts} +1 -1
  7. package/dist/{Token-0jpvUdPY.d.cts → Token-DnU2hwFY.d.cts} +14 -0
  8. package/dist/{Token-0jpvUdPY.d.ts → Token-DnU2hwFY.d.ts} +14 -0
  9. package/dist/TokenNormalizer-B8zmhO5X.d.ts +377 -0
  10. package/dist/TokenNormalizer-CqBCrYkR.d.cts +377 -0
  11. package/dist/{chunk-HO2CDZRC.js → chunk-23MFG6GG.js} +1976 -12
  12. package/dist/chunk-23MFG6GG.js.map +1 -0
  13. package/dist/{chunk-SDGRK7EP.js → chunk-2A3VOGVG.js} +4 -4
  14. package/dist/{chunk-SDGRK7EP.js.map → chunk-2A3VOGVG.js.map} +1 -1
  15. package/dist/{chunk-NMD5VRN4.cjs → chunk-2R7SVDDB.cjs} +68 -67
  16. package/dist/chunk-2R7SVDDB.cjs.map +1 -0
  17. package/dist/{chunk-RIN643A3.js → chunk-626UW5RU.js} +2 -2
  18. package/dist/chunk-626UW5RU.js.map +1 -0
  19. package/dist/{chunk-CLL7RUQV.cjs → chunk-7J2Q77OH.cjs} +34 -13
  20. package/dist/chunk-7J2Q77OH.cjs.map +1 -0
  21. package/dist/{chunk-HWSZJQCI.js → chunk-AF5AE52C.js} +33 -12
  22. package/dist/chunk-AF5AE52C.js.map +1 -0
  23. package/dist/chunk-B7LXJ6LC.cjs +36 -0
  24. package/dist/chunk-B7LXJ6LC.cjs.map +1 -0
  25. package/dist/{chunk-RZCWSXTA.cjs → chunk-BOAVE4FC.cjs} +129 -129
  26. package/dist/{chunk-RZCWSXTA.cjs.map → chunk-BOAVE4FC.cjs.map} +1 -1
  27. package/dist/{chunk-EBSPLUW4.cjs → chunk-CGIDYRAX.cjs} +19 -9
  28. package/dist/chunk-CGIDYRAX.cjs.map +1 -0
  29. package/dist/{chunk-C4XZV6E7.cjs → chunk-EEJSQEHC.cjs} +20 -20
  30. package/dist/{chunk-C4XZV6E7.cjs.map → chunk-EEJSQEHC.cjs.map} +1 -1
  31. package/dist/{chunk-W44QGY5J.cjs → chunk-F7G7ZFIZ.cjs} +3 -3
  32. package/dist/chunk-F7G7ZFIZ.cjs.map +1 -0
  33. package/dist/{chunk-GW32KPCU.cjs → chunk-FI4WDYSL.cjs} +2 -2
  34. package/dist/chunk-FI4WDYSL.cjs.map +1 -0
  35. package/dist/{chunk-ZP3UY6ZX.js → chunk-KFRBAXKW.js} +3 -3
  36. package/dist/chunk-KFRBAXKW.js.map +1 -0
  37. package/dist/{chunk-64W6GLLZ.js → chunk-OJ6LW7VQ.js} +15 -5
  38. package/dist/chunk-OJ6LW7VQ.js.map +1 -0
  39. package/dist/{chunk-3PPFLFH4.js → chunk-OWXIOHQB.js} +6 -6
  40. package/dist/{chunk-3PPFLFH4.js.map → chunk-OWXIOHQB.js.map} +1 -1
  41. package/dist/{chunk-PEWZMGH7.cjs → chunk-PG2K5BL6.cjs} +1996 -32
  42. package/dist/chunk-PG2K5BL6.cjs.map +1 -0
  43. package/dist/{chunk-XVWCOTR6.js → chunk-VM5ZKDLH.js} +5 -4
  44. package/dist/chunk-VM5ZKDLH.js.map +1 -0
  45. package/dist/chunk-YLJ4XMA6.js +33 -0
  46. package/dist/chunk-YLJ4XMA6.js.map +1 -0
  47. package/dist/{chunk-NMCRQP3Z.cjs → chunk-ZH2PCJ4V.cjs} +68 -68
  48. package/dist/{chunk-NMCRQP3Z.cjs.map → chunk-ZH2PCJ4V.cjs.map} +1 -1
  49. package/dist/{chunk-5YEMOYSE.js → chunk-ZS6CRJZP.js} +3 -3
  50. package/dist/{chunk-5YEMOYSE.js.map → chunk-ZS6CRJZP.js.map} +1 -1
  51. package/dist/constants.cjs +5 -4
  52. package/dist/constants.js +2 -1
  53. package/dist/engine.cjs +20 -19
  54. package/dist/engine.d.cts +6 -6
  55. package/dist/engine.d.ts +6 -6
  56. package/dist/engine.js +10 -9
  57. package/dist/errors.cjs +1 -0
  58. package/dist/errors.js +1 -0
  59. package/dist/format.cjs +1 -0
  60. package/dist/format.cjs.map +1 -1
  61. package/dist/format.js +1 -0
  62. package/dist/format.js.map +1 -1
  63. package/dist/index.cjs +19 -18
  64. package/dist/index.cjs.map +1 -1
  65. package/dist/index.d.cts +6 -6
  66. package/dist/index.d.ts +6 -6
  67. package/dist/index.js +11 -10
  68. package/dist/index.js.map +1 -1
  69. package/dist/language.cjs +64 -1
  70. package/dist/language.cjs.map +1 -1
  71. package/dist/language.d.cts +6 -6
  72. package/dist/language.d.ts +6 -6
  73. package/dist/language.js +64 -1
  74. package/dist/language.js.map +1 -1
  75. package/dist/lexer.cjs +15 -14
  76. package/dist/lexer.d.cts +3 -3
  77. package/dist/lexer.d.ts +3 -3
  78. package/dist/lexer.js +4 -3
  79. package/dist/normalizer.cjs +10 -9
  80. package/dist/normalizer.d.cts +4 -210
  81. package/dist/normalizer.d.ts +4 -210
  82. package/dist/normalizer.js +4 -3
  83. package/dist/packages.cjs +29 -28
  84. package/dist/packages.d.cts +5 -5
  85. package/dist/packages.d.ts +5 -5
  86. package/dist/packages.js +6 -5
  87. package/dist/parser.cjs +12 -11
  88. package/dist/parser.d.cts +2 -2
  89. package/dist/parser.d.ts +2 -2
  90. package/dist/parser.js +4 -3
  91. package/dist/resolvers.cjs +1 -0
  92. package/dist/resolvers.d.cts +1 -1
  93. package/dist/resolvers.d.ts +1 -1
  94. package/dist/resolvers.js +1 -0
  95. package/dist/services.cjs +1 -0
  96. package/dist/services.js +1 -0
  97. package/dist/uom.cjs +1 -0
  98. package/dist/uom.d.cts +1 -1
  99. package/dist/uom.d.ts +1 -1
  100. package/dist/uom.js +1 -0
  101. package/dist/utilities.cjs +1 -0
  102. package/dist/utilities.js +1 -0
  103. package/dist/variables.cjs +1 -0
  104. package/dist/variables.js +1 -0
  105. package/dist/vm.cjs +1 -0
  106. package/dist/vm.cjs.map +1 -1
  107. package/dist/vm.js +1 -0
  108. package/dist/vm.js.map +1 -1
  109. package/package.json +4 -4
  110. package/dist/NormalizerRule-BrVoVjmP.d.cts +0 -163
  111. package/dist/NormalizerRule-CEjf1FyD.d.ts +0 -163
  112. package/dist/chunk-64W6GLLZ.js.map +0 -1
  113. package/dist/chunk-CLL7RUQV.cjs.map +0 -1
  114. package/dist/chunk-EBSPLUW4.cjs.map +0 -1
  115. package/dist/chunk-GW32KPCU.cjs.map +0 -1
  116. package/dist/chunk-HO2CDZRC.js.map +0 -1
  117. package/dist/chunk-HWSZJQCI.js.map +0 -1
  118. package/dist/chunk-NMD5VRN4.cjs.map +0 -1
  119. package/dist/chunk-PEWZMGH7.cjs.map +0 -1
  120. package/dist/chunk-RIN643A3.js.map +0 -1
  121. package/dist/chunk-W44QGY5J.cjs.map +0 -1
  122. package/dist/chunk-XVWCOTR6.js.map +0 -1
  123. package/dist/chunk-ZP3UY6ZX.js.map +0 -1
package/dist/variables.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { VariableResolver, sharedVariableResolver } from './chunk-GCM5JHMX.js';
2
+ import './chunk-YLJ4XMA6.js';
2
3
  //# sourceMappingURL=variables.js.map
3
4
  //# sourceMappingURL=variables.js.map
package/dist/vm.cjs CHANGED
@@ -12,6 +12,7 @@ var chunkOC6LTXYS_cjs = require('./chunk-OC6LTXYS.cjs');
12
12
  require('./chunk-QNJ4ACRT.cjs');
13
13
  require('./chunk-53B6KDDJ.cjs');
14
14
  require('./chunk-M4N5GA5Z.cjs');
15
+ require('./chunk-B7LXJ6LC.cjs');
15
16
 
16
17
  // src/vm/VMCheckpoints.ts
17
18
  var VMCheckpointer = class {
package/dist/vm.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vm/VMCheckpoints.ts"],"names":[],"mappings":"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","file":"vm.cjs","sourcesContent":["import { Value } from \"@solve-js/vm/Value\";\nimport type { VM } from \"@solve-js/vm/OpRegistry\";\nimport type { UserFunctionDef } from \"@solve-js/parser/BytecodeBuilder\";\n\n// ── VMCheckpoint ────────────────────────────────────────────────────────\n\n/**\n * A point-in-time snapshot of VM variable state.\n *\n * Uses **prototypal inheritance** for memory efficiency: each checkpoint's\n * `variables` object has its parent checkpoint's `variables` as its\n * `__proto__`. This means a `getVar(\"x\")` lookup walks the prototype chain\n * until it finds `x`, and only variables that CHANGED at this checkpoint\n * consume heap space. Unchanged variables are inherited from the parent.\n *\n * ```text\n * Checkpoint 0 (root): {} // empty scope\n * Checkpoint 1 (:x=5): { x: 5 } __proto__ → 0\n * Checkpoint 2 (:y=8): { y: 8 } __proto__ → 1\n * Checkpoint 3 (:x=3): { x: 3 } __proto__ → 2 // shadows x=5\n * ```\n *\n * To look up `x` at checkpoint 3: find own `x=3` → done.\n * To look up `y` at checkpoint 3: not own → walk proto to checkpoint 2 → `y=8`.\n * To look up `z` at checkpoint 3: not found anywhere → undefined.\n *\n * **Memory:** O(number of variable definitions) heap, independent of\n * document length. Typical Obsidian documents have < 100 variable defs,\n * so total checkpoint heap is < 10 KB.\n */\nexport interface VMCheckpoint {\n\t/** 1-based line number where this checkpoint was created. */\n\tlineNumber: number;\n\t/** Persistent line ID from DocumentModel. */\n\tlineId: number;\n\t/**\n\t * Variable name → Value at this checkpoint.\n\t * Own properties are variables set/updated at this line.\n\t * The prototype chain provides inherited variables from parent checkpoints.\n\t */\n\tvariables: Record<string, Value>;\n\t/**\n\t * User-defined-function name → definition at this checkpoint. SEPARATE\n\t * from `variables` above (not prototypally chained the same way\n\t * `restoreTo()` replays every checkpoint in the chain in order, so a\n\t * later redefinition of the same function name naturally overwrites an\n\t * earlier one during replay, without needing its own prototype walk).\n\t *\n\t * Without this field, a function definition's checkpoint entry would be\n\t * SILENTLY LOST: `snapshot()` used to call `vm.getVar(name)` for every\n\t * written name, which returns `undefined` for a function name (function\n\t * defs live in `vm.userFunctions`, not the flat variable store), and a\n\t * `val !== undefined` guard silently skipped it. A scroll-triggered\n\t * `restoreTo()` would then reset the VM and replay only `variables`,\n\t * making a function defined above the new viewport vanish (calling it\n\t * would throw `UNDEFINED_FUNCTION`) even though the document still\n\t * shows its definition line as clean/cached.\n\t */\n\tfunctions: Record<string, UserFunctionDef>;\n\t/** Parent checkpoint (closer to document start), or null for root. */\n\tparent: VMCheckpoint | null;\n}\n\n// ── VMCheckpointer ──────────────────────────────────────────────────────\n\n/**\n * Manages VM state checkpoints for the three-tier evaluation strategy.\n *\n * **Checkpoint creation:** After a variable-definition line executes\n * (Tier 1 or Tier 3), `snapshot()` records the current values of the\n * written variables. The checkpoint is linked via prototypal inheritance\n * to the previous checkpoint, so only changed variables consume memory.\n *\n * **Checkpoint restoration:** Before evaluating a viewport whose start line\n * is not line 1, `restoreTo(lineNumber)` resets the VM and replays all\n * variable definitions up to and including that line. This avoids\n * re-evaluating the entire document from line 1 on every scroll.\n *\n * **Thread safety:** Checkpoints are created synchronously on the main\n * thread during evaluation. They are immutable after creation (Value is\n * an immutable type), so no synchronization is needed.\n *\n * **Integration with Phase 5.2e:** `setViewport()` will use `getNearestCheckpoint()`\n * to find the checkpoint just before the new viewport start, then call\n * `restoreTo()` to set up the VM before evaluating only the visible lines.\n * This is the key to O(visible lines) scrolling instead of O(document).\n */\nexport class VMCheckpointer {\n\t/** Ordered array of checkpoints (ascending lineNumber). */\n\tprivate checkpoints: VMCheckpoint[] = [];\n\t/** The VM instance whose variables are snapshotted/restored. */\n\tprivate vm: VM;\n\n\tconstructor(vm: VM) {\n\t\tthis.vm = vm;\n\t}\n\n\t// ── Snapshot ─────────────────────────────────────────────────────\n\n\t/**\n\t * Create a checkpoint at the current line, recording the VM values of\n\t * the specified variables.\n\t *\n\t * Uses prototypal inheritance: `Object.create(parent.variables)` so\n\t * that inherited variable lookups fall through to previous checkpoints\n\t * without copying all variables into each checkpoint.\n\t *\n\t * @param lineNumber 1-based line position.\n\t * @param lineId Persistent line ID from DocumentModel.\n\t * @param variableNames Names of variables that were written at this line.\n\t * @returns The new checkpoint, or null if no variable names provided.\n\t */\n\tsnapshot(\n\t\tlineNumber: number,\n\t\tlineId: number,\n\t\tvariableNames: string[]\n\t): VMCheckpoint | null {\n\t\tif (variableNames.length === 0) return null;\n\n\t\tconst parent =\n\t\t\tthis.checkpoints.length > 0\n\t\t\t\t? this.checkpoints[this.checkpoints.length - 1]\n\t\t\t\t: null;\n\n\t\t// Create prototypal chain: new checkpoint inherits from parent\n\t\tconst variables: Record<string, Value> = Object.create(\n\t\t\tparent?.variables ?? null\n\t\t) as Record<string, Value>;\n\t\tconst functions: Record<string, UserFunctionDef> = Object.create(\n\t\t\tparent?.functions ?? null\n\t\t) as Record<string, UserFunctionDef>;\n\n\t\t// Record current VM values for the written names, routing each into\n\t\t// the right bag (a name is either a variable or a user-defined\n\t\t// function, never both; see VMCheckpoint.functions's doc comment for\n\t\t// why this dispatch is required, not optional).\n\t\tfor (const name of variableNames) {\n\t\t\tif (this.vm.hasUserFunction(name)) {\n\t\t\t\tconst fn = this.vm.getUserFunction(name);\n\t\t\t\tif (fn) functions[name] = fn;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst val = this.vm.getVar(name);\n\t\t\tif (val !== undefined) {\n\t\t\t\tvariables[name] = val;\n\t\t\t}\n\t\t}\n\n\t\tconst checkpoint: VMCheckpoint = {\n\t\t\tlineNumber,\n\t\t\tlineId,\n\t\t\tvariables,\n\t\t\tfunctions,\n\t\t\tparent,\n\t\t};\n\t\tthis.checkpoints.push(checkpoint);\n\t\treturn checkpoint;\n\t}\n\n\t// ── Restore ──────────────────────────────────────────────────────\n\n\t/**\n\t * Restore the VM to the state at or just after the given line number.\n\t *\n\t * Finds the nearest checkpoint whose `lineNumber <= targetLineNumber`,\n\t * then replays all variable definitions from root → that checkpoint\n\t * into the VM via `setVar()`. The VM's stack is also reset.\n\t *\n\t * If no checkpoint exists at or before the target line, the VM is\n\t * fully reset (empty scope, empty stack).\n\t *\n\t * **Performance:** O(number of checkpoints × variables per checkpoint).\n\t * With prototypal inheritance, `Object.keys()` on each checkpoint\n\t * returns only the variables that were set at that checkpoint (not\n\t * inherited ones), so the total work is O(total variable definitions\n\t * in the document), which is < 100 for typical Obsidian documents.\n\t *\n\t * @param lineNumber Target 1-based line number. The VM will have the\n\t * state that existed AFTER evaluating lines up to `lineNumber`.\n\t */\n\trestoreTo(lineNumber: number): void {\n\t\tconst target = this.getNearestCheckpoint(lineNumber);\n\t\tif (!target) {\n\t\t\tthis.vm.reset();\n\t\t\treturn;\n\t\t}\n\n\t\t// Collect the checkpoint chain from root to target.\n\t\t// Walk parent links and reverse so root is first.\n\t\tconst chain: VMCheckpoint[] = [];\n\t\tlet current: VMCheckpoint | null = target;\n\t\twhile (current) {\n\t\t\tchain.unshift(current);\n\t\t\tcurrent = current.parent;\n\t\t}\n\n\t\tthis.vm.reset();\n\t\tfor (const cp of chain) {\n\t\t\t// Object.keys() returns only OWN enumerable properties\n\t\t\t// it does NOT include inherited properties from the prototype chain.\n\t\t\t// This means we only set variables that were defined/updated at this\n\t\t\t// specific checkpoint, not all variables from parent checkpoints.\n\t\t\tfor (const key of Object.keys(cp.variables)) {\n\t\t\t\tthis.vm.setVar(key, cp.variables[key]);\n\t\t\t}\n\t\t\t// Replay function definitions the same way, a later checkpoint's\n\t\t\t// redefinition of the same name naturally overwrites an earlier\n\t\t\t// one since the chain replays in root-to-target order.\n\t\t\tfor (const key of Object.keys(cp.functions)) {\n\t\t\t\tconst fn = cp.functions[key];\n\t\t\t\tthis.vm.defineUserFunction(fn.name, fn.params, fn.program);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ── Queries ──────────────────────────────────────────────────────\n\n\t/**\n\t * Find the nearest checkpoint at or before the given line number.\n\t *\n\t * Uses linear scan (checkpoints are sorted by lineNumber and the list\n\t * is short, typically < 20 for Obsidian documents). Can be upgraded\n\t * to binary search if needed for documents with 1000+ variable defs.\n\t *\n\t * @returns The nearest checkpoint, or null if none exists before the line.\n\t */\n\tgetNearestCheckpoint(lineNumber: number): VMCheckpoint | null {\n\t\tlet result: VMCheckpoint | null = null;\n\t\tfor (const cp of this.checkpoints) {\n\t\t\tif (cp.lineNumber <= lineNumber) {\n\t\t\t\tresult = cp;\n\t\t\t} else {\n\t\t\t\tbreak; // checkpoints are sorted ascending\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get a specific checkpoint by its line number.\n\t * @returns The checkpoint, or undefined if not found.\n\t */\n\tgetCheckpointAt(lineNumber: number): VMCheckpoint | undefined {\n\t\treturn this.checkpoints.find((cp) => cp.lineNumber === lineNumber);\n\t}\n\n\t/**\n\t * Get the entire checkpoint chain from root to the last checkpoint.\n\t * Useful for debugging and serialization.\n\t */\n\tgetAllCheckpoints(): readonly VMCheckpoint[] {\n\t\treturn this.checkpoints;\n\t}\n\n\t/**\n\t * Look up a variable's value through the checkpoint chain.\n\t *\n\t * Walks the prototype chain starting from the most recent checkpoint,\n\t * looking for the variable name as an own property. This is O(depth)\n\t * where depth is the number of checkpoints since the variable was\n\t * last set.\n\t *\n\t * **Note:** This queries the checkpointer's snapshot, not the VM.\n\t * The VM may have been modified since the last snapshot (e.g., by\n\t * Tier 2 execution of non-variable-def lines that don't create checkpoints).\n\t *\n\t * @returns The Value, or undefined if the variable was never set.\n\t */\n\tlookupVariable(name: string): Value | undefined {\n\t\tif (this.checkpoints.length === 0) return undefined;\n\n\t\tconst latest = this.checkpoints[this.checkpoints.length - 1];\n\t\tlet scope: Record<string, Value> | null = latest.variables;\n\t\twhile (scope) {\n\t\t\tif (Object.prototype.hasOwnProperty.call(scope, name)) {\n\t\t\t\treturn scope[name];\n\t\t\t}\n\t\t\tscope = Object.getPrototypeOf(scope) as Record<string, Value> | null;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// ── Lifecycle ────────────────────────────────────────────────────\n\n\t/**\n\t * Clear all checkpoints. The underlying VM is NOT reset, call\n\t * `vm.reset()` separately if needed.\n\t */\n\tclear(): void {\n\t\tthis.checkpoints = [];\n\t}\n\n\t/** Number of checkpoints stored. */\n\tget count(): number {\n\t\treturn this.checkpoints.length;\n\t}\n\n\t/** Returns true if no checkpoints have been created. */\n\tget isEmpty(): boolean {\n\t\treturn this.checkpoints.length === 0;\n\t}\n\n\t/** The associated VM instance. */\n\tget vmInstance(): VM {\n\t\treturn this.vm;\n\t}\n}\n"]}
1
+ {"version":3,"sources":["../src/vm/VMCheckpoints.ts"],"names":[],"mappings":"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","file":"vm.cjs","sourcesContent":["import { Value } from \"@solve-js/vm/Value\";\nimport type { VM } from \"@solve-js/vm/OpRegistry\";\nimport type { UserFunctionDef } from \"@solve-js/parser/BytecodeBuilder\";\n\n// ── VMCheckpoint ────────────────────────────────────────────────────────\n\n/**\n * A point-in-time snapshot of VM variable state.\n *\n * Uses **prototypal inheritance** for memory efficiency: each checkpoint's\n * `variables` object has its parent checkpoint's `variables` as its\n * `__proto__`. This means a `getVar(\"x\")` lookup walks the prototype chain\n * until it finds `x`, and only variables that CHANGED at this checkpoint\n * consume heap space. Unchanged variables are inherited from the parent.\n *\n * ```text\n * Checkpoint 0 (root): {} // empty scope\n * Checkpoint 1 (:x=5): { x: 5 } __proto__ → 0\n * Checkpoint 2 (:y=8): { y: 8 } __proto__ → 1\n * Checkpoint 3 (:x=3): { x: 3 } __proto__ → 2 // shadows x=5\n * ```\n *\n * To look up `x` at checkpoint 3: find own `x=3` → done.\n * To look up `y` at checkpoint 3: not own → walk proto to checkpoint 2 → `y=8`.\n * To look up `z` at checkpoint 3: not found anywhere → undefined.\n *\n * **Memory:** O(number of variable definitions) heap, independent of\n * document length. Typical Obsidian documents have < 100 variable defs,\n * so total checkpoint heap is < 10 KB.\n */\nexport interface VMCheckpoint {\n\t/** 1-based line number where this checkpoint was created. */\n\tlineNumber: number;\n\t/** Persistent line ID from DocumentModel. */\n\tlineId: number;\n\t/**\n\t * Variable name → Value at this checkpoint.\n\t * Own properties are variables set/updated at this line.\n\t * The prototype chain provides inherited variables from parent checkpoints.\n\t */\n\tvariables: Record<string, Value>;\n\t/**\n\t * User-defined-function name → definition at this checkpoint. SEPARATE\n\t * from `variables` above (not prototypally chained the same way\n\t * `restoreTo()` replays every checkpoint in the chain in order, so a\n\t * later redefinition of the same function name naturally overwrites an\n\t * earlier one during replay, without needing its own prototype walk).\n\t *\n\t * Without this field, a function definition's checkpoint entry would be\n\t * SILENTLY LOST: `snapshot()` used to call `vm.getVar(name)` for every\n\t * written name, which returns `undefined` for a function name (function\n\t * defs live in `vm.userFunctions`, not the flat variable store), and a\n\t * `val !== undefined` guard silently skipped it. A scroll-triggered\n\t * `restoreTo()` would then reset the VM and replay only `variables`,\n\t * making a function defined above the new viewport vanish (calling it\n\t * would throw `UNDEFINED_FUNCTION`) even though the document still\n\t * shows its definition line as clean/cached.\n\t */\n\tfunctions: Record<string, UserFunctionDef>;\n\t/** Parent checkpoint (closer to document start), or null for root. */\n\tparent: VMCheckpoint | null;\n}\n\n// ── VMCheckpointer ──────────────────────────────────────────────────────\n\n/**\n * Manages VM state checkpoints for the three-tier evaluation strategy.\n *\n * **Checkpoint creation:** After a variable-definition line executes\n * (Tier 1 or Tier 3), `snapshot()` records the current values of the\n * written variables. The checkpoint is linked via prototypal inheritance\n * to the previous checkpoint, so only changed variables consume memory.\n *\n * **Checkpoint restoration:** Before evaluating a viewport whose start line\n * is not line 1, `restoreTo(lineNumber)` resets the VM and replays all\n * variable definitions up to and including that line. This avoids\n * re-evaluating the entire document from line 1 on every scroll.\n *\n * **Thread safety:** Checkpoints are created synchronously on the main\n * thread during evaluation. They are immutable after creation (Value is\n * an immutable type), so no synchronization is needed.\n *\n * **Integration with Phase 5.2e:** `setViewport()` will use `getNearestCheckpoint()`\n * to find the checkpoint just before the new viewport start, then call\n * `restoreTo()` to set up the VM before evaluating only the visible lines.\n * This is the key to O(visible lines) scrolling instead of O(document).\n */\nexport class VMCheckpointer {\n\t/** Ordered array of checkpoints (ascending lineNumber). */\n\tprivate checkpoints: VMCheckpoint[] = [];\n\t/** The VM instance whose variables are snapshotted/restored. */\n\tprivate vm: VM;\n\n\tconstructor(vm: VM) {\n\t\tthis.vm = vm;\n\t}\n\n\t// ── Snapshot ─────────────────────────────────────────────────────\n\n\t/**\n\t * Create a checkpoint at the current line, recording the VM values of\n\t * the specified variables.\n\t *\n\t * Uses prototypal inheritance: `Object.create(parent.variables)` so\n\t * that inherited variable lookups fall through to previous checkpoints\n\t * without copying all variables into each checkpoint.\n\t *\n\t * @param lineNumber 1-based line position.\n\t * @param lineId Persistent line ID from DocumentModel.\n\t * @param variableNames Names of variables that were written at this line.\n\t * @returns The new checkpoint, or null if no variable names provided.\n\t */\n\tsnapshot(\n\t\tlineNumber: number,\n\t\tlineId: number,\n\t\tvariableNames: string[]\n\t): VMCheckpoint | null {\n\t\tif (variableNames.length === 0) return null;\n\n\t\tconst parent =\n\t\t\tthis.checkpoints.length > 0\n\t\t\t\t? this.checkpoints[this.checkpoints.length - 1]\n\t\t\t\t: null;\n\n\t\t// Create prototypal chain: new checkpoint inherits from parent\n\t\tconst variables: Record<string, Value> = Object.create(\n\t\t\tparent?.variables ?? null\n\t\t) as Record<string, Value>;\n\t\tconst functions: Record<string, UserFunctionDef> = Object.create(\n\t\t\tparent?.functions ?? null\n\t\t) as Record<string, UserFunctionDef>;\n\n\t\t// Record current VM values for the written names, routing each into\n\t\t// the right bag (a name is either a variable or a user-defined\n\t\t// function, never both; see VMCheckpoint.functions's doc comment for\n\t\t// why this dispatch is required, not optional).\n\t\tfor (const name of variableNames) {\n\t\t\tif (this.vm.hasUserFunction(name)) {\n\t\t\t\tconst fn = this.vm.getUserFunction(name);\n\t\t\t\tif (fn) functions[name] = fn;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst val = this.vm.getVar(name);\n\t\t\tif (val !== undefined) {\n\t\t\t\tvariables[name] = val;\n\t\t\t}\n\t\t}\n\n\t\tconst checkpoint: VMCheckpoint = {\n\t\t\tlineNumber,\n\t\t\tlineId,\n\t\t\tvariables,\n\t\t\tfunctions,\n\t\t\tparent,\n\t\t};\n\t\tthis.checkpoints.push(checkpoint);\n\t\treturn checkpoint;\n\t}\n\n\t// ── Restore ──────────────────────────────────────────────────────\n\n\t/**\n\t * Restore the VM to the state at or just after the given line number.\n\t *\n\t * Finds the nearest checkpoint whose `lineNumber <= targetLineNumber`,\n\t * then replays all variable definitions from root → that checkpoint\n\t * into the VM via `setVar()`. The VM's stack is also reset.\n\t *\n\t * If no checkpoint exists at or before the target line, the VM is\n\t * fully reset (empty scope, empty stack).\n\t *\n\t * **Performance:** O(number of checkpoints × variables per checkpoint).\n\t * With prototypal inheritance, `Object.keys()` on each checkpoint\n\t * returns only the variables that were set at that checkpoint (not\n\t * inherited ones), so the total work is O(total variable definitions\n\t * in the document), which is < 100 for typical Obsidian documents.\n\t *\n\t * @param lineNumber Target 1-based line number. The VM will have the\n\t * state that existed AFTER evaluating lines up to `lineNumber`.\n\t */\n\trestoreTo(lineNumber: number): void {\n\t\tconst target = this.getNearestCheckpoint(lineNumber);\n\t\tif (!target) {\n\t\t\tthis.vm.reset();\n\t\t\treturn;\n\t\t}\n\n\t\t// Collect the checkpoint chain from root to target.\n\t\t// Walk parent links and reverse so root is first.\n\t\tconst chain: VMCheckpoint[] = [];\n\t\tlet current: VMCheckpoint | null = target;\n\t\twhile (current) {\n\t\t\tchain.unshift(current);\n\t\t\tcurrent = current.parent;\n\t\t}\n\n\t\tthis.vm.reset();\n\t\tfor (const cp of chain) {\n\t\t\t// Object.keys() returns only OWN enumerable properties\n\t\t\t// it does NOT include inherited properties from the prototype chain.\n\t\t\t// This means we only set variables that were defined/updated at this\n\t\t\t// specific checkpoint, not all variables from parent checkpoints.\n\t\t\tfor (const key of Object.keys(cp.variables)) {\n\t\t\t\tthis.vm.setVar(key, cp.variables[key]);\n\t\t\t}\n\t\t\t// Replay function definitions the same way, a later checkpoint's\n\t\t\t// redefinition of the same name naturally overwrites an earlier\n\t\t\t// one since the chain replays in root-to-target order.\n\t\t\tfor (const key of Object.keys(cp.functions)) {\n\t\t\t\tconst fn = cp.functions[key];\n\t\t\t\tthis.vm.defineUserFunction(fn.name, fn.params, fn.program);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ── Queries ──────────────────────────────────────────────────────\n\n\t/**\n\t * Find the nearest checkpoint at or before the given line number.\n\t *\n\t * Uses linear scan (checkpoints are sorted by lineNumber and the list\n\t * is short, typically < 20 for Obsidian documents). Can be upgraded\n\t * to binary search if needed for documents with 1000+ variable defs.\n\t *\n\t * @returns The nearest checkpoint, or null if none exists before the line.\n\t */\n\tgetNearestCheckpoint(lineNumber: number): VMCheckpoint | null {\n\t\tlet result: VMCheckpoint | null = null;\n\t\tfor (const cp of this.checkpoints) {\n\t\t\tif (cp.lineNumber <= lineNumber) {\n\t\t\t\tresult = cp;\n\t\t\t} else {\n\t\t\t\tbreak; // checkpoints are sorted ascending\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get a specific checkpoint by its line number.\n\t * @returns The checkpoint, or undefined if not found.\n\t */\n\tgetCheckpointAt(lineNumber: number): VMCheckpoint | undefined {\n\t\treturn this.checkpoints.find((cp) => cp.lineNumber === lineNumber);\n\t}\n\n\t/**\n\t * Get the entire checkpoint chain from root to the last checkpoint.\n\t * Useful for debugging and serialization.\n\t */\n\tgetAllCheckpoints(): readonly VMCheckpoint[] {\n\t\treturn this.checkpoints;\n\t}\n\n\t/**\n\t * Look up a variable's value through the checkpoint chain.\n\t *\n\t * Walks the prototype chain starting from the most recent checkpoint,\n\t * looking for the variable name as an own property. This is O(depth)\n\t * where depth is the number of checkpoints since the variable was\n\t * last set.\n\t *\n\t * **Note:** This queries the checkpointer's snapshot, not the VM.\n\t * The VM may have been modified since the last snapshot (e.g., by\n\t * Tier 2 execution of non-variable-def lines that don't create checkpoints).\n\t *\n\t * @returns The Value, or undefined if the variable was never set.\n\t */\n\tlookupVariable(name: string): Value | undefined {\n\t\tif (this.checkpoints.length === 0) return undefined;\n\n\t\tconst latest = this.checkpoints[this.checkpoints.length - 1];\n\t\tlet scope: Record<string, Value> | null = latest.variables;\n\t\twhile (scope) {\n\t\t\tif (Object.prototype.hasOwnProperty.call(scope, name)) {\n\t\t\t\treturn scope[name];\n\t\t\t}\n\t\t\tscope = Object.getPrototypeOf(scope) as Record<string, Value> | null;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// ── Lifecycle ────────────────────────────────────────────────────\n\n\t/**\n\t * Clear all checkpoints. The underlying VM is NOT reset, call\n\t * `vm.reset()` separately if needed.\n\t */\n\tclear(): void {\n\t\tthis.checkpoints = [];\n\t}\n\n\t/** Number of checkpoints stored. */\n\tget count(): number {\n\t\treturn this.checkpoints.length;\n\t}\n\n\t/** Returns true if no checkpoints have been created. */\n\tget isEmpty(): boolean {\n\t\treturn this.checkpoints.length === 0;\n\t}\n\n\t/** The associated VM instance. */\n\tget vmInstance(): VM {\n\t\treturn this.vm;\n\t}\n}\n"]}
package/dist/vm.js CHANGED
@@ -10,6 +10,7 @@ export { Value, ValueType, bigIntValue, colVectorValue, hexValue, matrixValue, n
10
10
  import './chunk-NH2O2AUR.js';
11
11
  import './chunk-4MG4XKO2.js';
12
12
  import './chunk-6RDSQURE.js';
13
+ import './chunk-YLJ4XMA6.js';
13
14
 
14
15
  // src/vm/VMCheckpoints.ts
15
16
  var VMCheckpointer = class {
package/dist/vm.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vm/VMCheckpoints.ts"],"names":[],"mappings":"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","file":"vm.js","sourcesContent":["import { Value } from \"@solve-js/vm/Value\";\nimport type { VM } from \"@solve-js/vm/OpRegistry\";\nimport type { UserFunctionDef } from \"@solve-js/parser/BytecodeBuilder\";\n\n// ── VMCheckpoint ────────────────────────────────────────────────────────\n\n/**\n * A point-in-time snapshot of VM variable state.\n *\n * Uses **prototypal inheritance** for memory efficiency: each checkpoint's\n * `variables` object has its parent checkpoint's `variables` as its\n * `__proto__`. This means a `getVar(\"x\")` lookup walks the prototype chain\n * until it finds `x`, and only variables that CHANGED at this checkpoint\n * consume heap space. Unchanged variables are inherited from the parent.\n *\n * ```text\n * Checkpoint 0 (root): {} // empty scope\n * Checkpoint 1 (:x=5): { x: 5 } __proto__ → 0\n * Checkpoint 2 (:y=8): { y: 8 } __proto__ → 1\n * Checkpoint 3 (:x=3): { x: 3 } __proto__ → 2 // shadows x=5\n * ```\n *\n * To look up `x` at checkpoint 3: find own `x=3` → done.\n * To look up `y` at checkpoint 3: not own → walk proto to checkpoint 2 → `y=8`.\n * To look up `z` at checkpoint 3: not found anywhere → undefined.\n *\n * **Memory:** O(number of variable definitions) heap, independent of\n * document length. Typical Obsidian documents have < 100 variable defs,\n * so total checkpoint heap is < 10 KB.\n */\nexport interface VMCheckpoint {\n\t/** 1-based line number where this checkpoint was created. */\n\tlineNumber: number;\n\t/** Persistent line ID from DocumentModel. */\n\tlineId: number;\n\t/**\n\t * Variable name → Value at this checkpoint.\n\t * Own properties are variables set/updated at this line.\n\t * The prototype chain provides inherited variables from parent checkpoints.\n\t */\n\tvariables: Record<string, Value>;\n\t/**\n\t * User-defined-function name → definition at this checkpoint. SEPARATE\n\t * from `variables` above (not prototypally chained the same way\n\t * `restoreTo()` replays every checkpoint in the chain in order, so a\n\t * later redefinition of the same function name naturally overwrites an\n\t * earlier one during replay, without needing its own prototype walk).\n\t *\n\t * Without this field, a function definition's checkpoint entry would be\n\t * SILENTLY LOST: `snapshot()` used to call `vm.getVar(name)` for every\n\t * written name, which returns `undefined` for a function name (function\n\t * defs live in `vm.userFunctions`, not the flat variable store), and a\n\t * `val !== undefined` guard silently skipped it. A scroll-triggered\n\t * `restoreTo()` would then reset the VM and replay only `variables`,\n\t * making a function defined above the new viewport vanish (calling it\n\t * would throw `UNDEFINED_FUNCTION`) even though the document still\n\t * shows its definition line as clean/cached.\n\t */\n\tfunctions: Record<string, UserFunctionDef>;\n\t/** Parent checkpoint (closer to document start), or null for root. */\n\tparent: VMCheckpoint | null;\n}\n\n// ── VMCheckpointer ──────────────────────────────────────────────────────\n\n/**\n * Manages VM state checkpoints for the three-tier evaluation strategy.\n *\n * **Checkpoint creation:** After a variable-definition line executes\n * (Tier 1 or Tier 3), `snapshot()` records the current values of the\n * written variables. The checkpoint is linked via prototypal inheritance\n * to the previous checkpoint, so only changed variables consume memory.\n *\n * **Checkpoint restoration:** Before evaluating a viewport whose start line\n * is not line 1, `restoreTo(lineNumber)` resets the VM and replays all\n * variable definitions up to and including that line. This avoids\n * re-evaluating the entire document from line 1 on every scroll.\n *\n * **Thread safety:** Checkpoints are created synchronously on the main\n * thread during evaluation. They are immutable after creation (Value is\n * an immutable type), so no synchronization is needed.\n *\n * **Integration with Phase 5.2e:** `setViewport()` will use `getNearestCheckpoint()`\n * to find the checkpoint just before the new viewport start, then call\n * `restoreTo()` to set up the VM before evaluating only the visible lines.\n * This is the key to O(visible lines) scrolling instead of O(document).\n */\nexport class VMCheckpointer {\n\t/** Ordered array of checkpoints (ascending lineNumber). */\n\tprivate checkpoints: VMCheckpoint[] = [];\n\t/** The VM instance whose variables are snapshotted/restored. */\n\tprivate vm: VM;\n\n\tconstructor(vm: VM) {\n\t\tthis.vm = vm;\n\t}\n\n\t// ── Snapshot ─────────────────────────────────────────────────────\n\n\t/**\n\t * Create a checkpoint at the current line, recording the VM values of\n\t * the specified variables.\n\t *\n\t * Uses prototypal inheritance: `Object.create(parent.variables)` so\n\t * that inherited variable lookups fall through to previous checkpoints\n\t * without copying all variables into each checkpoint.\n\t *\n\t * @param lineNumber 1-based line position.\n\t * @param lineId Persistent line ID from DocumentModel.\n\t * @param variableNames Names of variables that were written at this line.\n\t * @returns The new checkpoint, or null if no variable names provided.\n\t */\n\tsnapshot(\n\t\tlineNumber: number,\n\t\tlineId: number,\n\t\tvariableNames: string[]\n\t): VMCheckpoint | null {\n\t\tif (variableNames.length === 0) return null;\n\n\t\tconst parent =\n\t\t\tthis.checkpoints.length > 0\n\t\t\t\t? this.checkpoints[this.checkpoints.length - 1]\n\t\t\t\t: null;\n\n\t\t// Create prototypal chain: new checkpoint inherits from parent\n\t\tconst variables: Record<string, Value> = Object.create(\n\t\t\tparent?.variables ?? null\n\t\t) as Record<string, Value>;\n\t\tconst functions: Record<string, UserFunctionDef> = Object.create(\n\t\t\tparent?.functions ?? null\n\t\t) as Record<string, UserFunctionDef>;\n\n\t\t// Record current VM values for the written names, routing each into\n\t\t// the right bag (a name is either a variable or a user-defined\n\t\t// function, never both; see VMCheckpoint.functions's doc comment for\n\t\t// why this dispatch is required, not optional).\n\t\tfor (const name of variableNames) {\n\t\t\tif (this.vm.hasUserFunction(name)) {\n\t\t\t\tconst fn = this.vm.getUserFunction(name);\n\t\t\t\tif (fn) functions[name] = fn;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst val = this.vm.getVar(name);\n\t\t\tif (val !== undefined) {\n\t\t\t\tvariables[name] = val;\n\t\t\t}\n\t\t}\n\n\t\tconst checkpoint: VMCheckpoint = {\n\t\t\tlineNumber,\n\t\t\tlineId,\n\t\t\tvariables,\n\t\t\tfunctions,\n\t\t\tparent,\n\t\t};\n\t\tthis.checkpoints.push(checkpoint);\n\t\treturn checkpoint;\n\t}\n\n\t// ── Restore ──────────────────────────────────────────────────────\n\n\t/**\n\t * Restore the VM to the state at or just after the given line number.\n\t *\n\t * Finds the nearest checkpoint whose `lineNumber <= targetLineNumber`,\n\t * then replays all variable definitions from root → that checkpoint\n\t * into the VM via `setVar()`. The VM's stack is also reset.\n\t *\n\t * If no checkpoint exists at or before the target line, the VM is\n\t * fully reset (empty scope, empty stack).\n\t *\n\t * **Performance:** O(number of checkpoints × variables per checkpoint).\n\t * With prototypal inheritance, `Object.keys()` on each checkpoint\n\t * returns only the variables that were set at that checkpoint (not\n\t * inherited ones), so the total work is O(total variable definitions\n\t * in the document), which is < 100 for typical Obsidian documents.\n\t *\n\t * @param lineNumber Target 1-based line number. The VM will have the\n\t * state that existed AFTER evaluating lines up to `lineNumber`.\n\t */\n\trestoreTo(lineNumber: number): void {\n\t\tconst target = this.getNearestCheckpoint(lineNumber);\n\t\tif (!target) {\n\t\t\tthis.vm.reset();\n\t\t\treturn;\n\t\t}\n\n\t\t// Collect the checkpoint chain from root to target.\n\t\t// Walk parent links and reverse so root is first.\n\t\tconst chain: VMCheckpoint[] = [];\n\t\tlet current: VMCheckpoint | null = target;\n\t\twhile (current) {\n\t\t\tchain.unshift(current);\n\t\t\tcurrent = current.parent;\n\t\t}\n\n\t\tthis.vm.reset();\n\t\tfor (const cp of chain) {\n\t\t\t// Object.keys() returns only OWN enumerable properties\n\t\t\t// it does NOT include inherited properties from the prototype chain.\n\t\t\t// This means we only set variables that were defined/updated at this\n\t\t\t// specific checkpoint, not all variables from parent checkpoints.\n\t\t\tfor (const key of Object.keys(cp.variables)) {\n\t\t\t\tthis.vm.setVar(key, cp.variables[key]);\n\t\t\t}\n\t\t\t// Replay function definitions the same way, a later checkpoint's\n\t\t\t// redefinition of the same name naturally overwrites an earlier\n\t\t\t// one since the chain replays in root-to-target order.\n\t\t\tfor (const key of Object.keys(cp.functions)) {\n\t\t\t\tconst fn = cp.functions[key];\n\t\t\t\tthis.vm.defineUserFunction(fn.name, fn.params, fn.program);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ── Queries ──────────────────────────────────────────────────────\n\n\t/**\n\t * Find the nearest checkpoint at or before the given line number.\n\t *\n\t * Uses linear scan (checkpoints are sorted by lineNumber and the list\n\t * is short, typically < 20 for Obsidian documents). Can be upgraded\n\t * to binary search if needed for documents with 1000+ variable defs.\n\t *\n\t * @returns The nearest checkpoint, or null if none exists before the line.\n\t */\n\tgetNearestCheckpoint(lineNumber: number): VMCheckpoint | null {\n\t\tlet result: VMCheckpoint | null = null;\n\t\tfor (const cp of this.checkpoints) {\n\t\t\tif (cp.lineNumber <= lineNumber) {\n\t\t\t\tresult = cp;\n\t\t\t} else {\n\t\t\t\tbreak; // checkpoints are sorted ascending\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get a specific checkpoint by its line number.\n\t * @returns The checkpoint, or undefined if not found.\n\t */\n\tgetCheckpointAt(lineNumber: number): VMCheckpoint | undefined {\n\t\treturn this.checkpoints.find((cp) => cp.lineNumber === lineNumber);\n\t}\n\n\t/**\n\t * Get the entire checkpoint chain from root to the last checkpoint.\n\t * Useful for debugging and serialization.\n\t */\n\tgetAllCheckpoints(): readonly VMCheckpoint[] {\n\t\treturn this.checkpoints;\n\t}\n\n\t/**\n\t * Look up a variable's value through the checkpoint chain.\n\t *\n\t * Walks the prototype chain starting from the most recent checkpoint,\n\t * looking for the variable name as an own property. This is O(depth)\n\t * where depth is the number of checkpoints since the variable was\n\t * last set.\n\t *\n\t * **Note:** This queries the checkpointer's snapshot, not the VM.\n\t * The VM may have been modified since the last snapshot (e.g., by\n\t * Tier 2 execution of non-variable-def lines that don't create checkpoints).\n\t *\n\t * @returns The Value, or undefined if the variable was never set.\n\t */\n\tlookupVariable(name: string): Value | undefined {\n\t\tif (this.checkpoints.length === 0) return undefined;\n\n\t\tconst latest = this.checkpoints[this.checkpoints.length - 1];\n\t\tlet scope: Record<string, Value> | null = latest.variables;\n\t\twhile (scope) {\n\t\t\tif (Object.prototype.hasOwnProperty.call(scope, name)) {\n\t\t\t\treturn scope[name];\n\t\t\t}\n\t\t\tscope = Object.getPrototypeOf(scope) as Record<string, Value> | null;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// ── Lifecycle ────────────────────────────────────────────────────\n\n\t/**\n\t * Clear all checkpoints. The underlying VM is NOT reset, call\n\t * `vm.reset()` separately if needed.\n\t */\n\tclear(): void {\n\t\tthis.checkpoints = [];\n\t}\n\n\t/** Number of checkpoints stored. */\n\tget count(): number {\n\t\treturn this.checkpoints.length;\n\t}\n\n\t/** Returns true if no checkpoints have been created. */\n\tget isEmpty(): boolean {\n\t\treturn this.checkpoints.length === 0;\n\t}\n\n\t/** The associated VM instance. */\n\tget vmInstance(): VM {\n\t\treturn this.vm;\n\t}\n}\n"]}
1
+ {"version":3,"sources":["../src/vm/VMCheckpoints.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAuFO,IAAM,iBAAN,MAAqB;AAAA,EAM3B,YAAY,EAAA,EAAQ;AAJpB;AAAA,IAAA,IAAA,CAAQ,cAA8B,EAAC;AAKtC,IAAA,IAAA,CAAK,EAAA,GAAK,EAAA;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,QAAA,CACC,UAAA,EACA,MAAA,EACA,aAAA,EACsB;AACtB,IAAA,IAAI,aAAA,CAAc,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAEvC,IAAA,MAAM,MAAA,GACL,IAAA,CAAK,WAAA,CAAY,MAAA,GAAS,CAAA,GACvB,IAAA,CAAK,WAAA,CAAY,IAAA,CAAK,WAAA,CAAY,MAAA,GAAS,CAAC,CAAA,GAC5C,IAAA;AAGJ,IAAA,MAAM,YAAmC,MAAA,CAAO,MAAA;AAAA,MAC/C,QAAQ,SAAA,IAAa;AAAA,KACtB;AACA,IAAA,MAAM,YAA6C,MAAA,CAAO,MAAA;AAAA,MACzD,QAAQ,SAAA,IAAa;AAAA,KACtB;AAMA,IAAA,KAAA,MAAW,QAAQ,aAAA,EAAe;AACjC,MAAA,IAAI,IAAA,CAAK,EAAA,CAAG,eAAA,CAAgB,IAAI,CAAA,EAAG;AAClC,QAAA,MAAM,EAAA,GAAK,IAAA,CAAK,EAAA,CAAG,eAAA,CAAgB,IAAI,CAAA;AACvC,QAAA,IAAI,EAAA,EAAI,SAAA,CAAU,IAAI,CAAA,GAAI,EAAA;AAC1B,QAAA;AAAA,MACD;AACA,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,EAAA,CAAG,MAAA,CAAO,IAAI,CAAA;AAC/B,MAAA,IAAI,QAAQ,MAAA,EAAW;AACtB,QAAA,SAAA,CAAU,IAAI,CAAA,GAAI,GAAA;AAAA,MACnB;AAAA,IACD;AAEA,IAAA,MAAM,UAAA,GAA2B;AAAA,MAChC,UAAA;AAAA,MACA,MAAA;AAAA,MACA,SAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA,KACD;AACA,IAAA,IAAA,CAAK,WAAA,CAAY,KAAK,UAAU,CAAA;AAChC,IAAA,OAAO,UAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,UAAU,UAAA,EAA0B;AACnC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,oBAAA,CAAqB,UAAU,CAAA;AACnD,IAAA,IAAI,CAAC,MAAA,EAAQ;AACZ,MAAA,IAAA,CAAK,GAAG,KAAA,EAAM;AACd,MAAA;AAAA,IACD;AAIA,IAAA,MAAM,QAAwB,EAAC;AAC/B,IAAA,IAAI,OAAA,GAA+B,MAAA;AACnC,IAAA,OAAO,OAAA,EAAS;AACf,MAAA,KAAA,CAAM,QAAQ,OAAO,CAAA;AACrB,MAAA,OAAA,GAAU,OAAA,CAAQ,MAAA;AAAA,IACnB;AAEA,IAAA,IAAA,CAAK,GAAG,KAAA,EAAM;AACd,IAAA,KAAA,MAAW,MAAM,KAAA,EAAO;AAKvB,MAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,EAAA,CAAG,SAAS,CAAA,EAAG;AAC5C,QAAA,IAAA,CAAK,GAAG,MAAA,CAAO,GAAA,EAAK,EAAA,CAAG,SAAA,CAAU,GAAG,CAAC,CAAA;AAAA,MACtC;AAIA,MAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,EAAA,CAAG,SAAS,CAAA,EAAG;AAC5C,QAAA,MAAM,EAAA,GAAK,EAAA,CAAG,SAAA,CAAU,GAAG,CAAA;AAC3B,QAAA,IAAA,CAAK,GAAG,kBAAA,CAAmB,EAAA,CAAG,MAAM,EAAA,CAAG,MAAA,EAAQ,GAAG,OAAO,CAAA;AAAA,MAC1D;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,qBAAqB,UAAA,EAAyC;AAC7D,IAAA,IAAI,MAAA,GAA8B,IAAA;AAClC,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,WAAA,EAAa;AAClC,MAAA,IAAI,EAAA,CAAG,cAAc,UAAA,EAAY;AAChC,QAAA,MAAA,GAAS,EAAA;AAAA,MACV,CAAA,MAAO;AACN,QAAA;AAAA,MACD;AAAA,IACD;AACA,IAAA,OAAO,MAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB,UAAA,EAA8C;AAC7D,IAAA,OAAO,KAAK,WAAA,CAAY,IAAA,CAAK,CAAC,EAAA,KAAO,EAAA,CAAG,eAAe,UAAU,CAAA;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAA,GAA6C;AAC5C,IAAA,OAAO,IAAA,CAAK,WAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,eAAe,IAAA,EAAiC;AAC/C,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAE1C,IAAA,MAAM,SAAS,IAAA,CAAK,WAAA,CAAY,IAAA,CAAK,WAAA,CAAY,SAAS,CAAC,CAAA;AAC3D,IAAA,IAAI,QAAsC,MAAA,CAAO,SAAA;AACjD,IAAA,OAAO,KAAA,EAAO;AACb,MAAA,IAAI,OAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,KAAA,EAAO,IAAI,CAAA,EAAG;AACtD,QAAA,OAAO,MAAM,IAAI,CAAA;AAAA,MAClB;AACA,MAAA,KAAA,GAAQ,MAAA,CAAO,eAAe,KAAK,CAAA;AAAA,IACpC;AACA,IAAA,OAAO,MAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,KAAA,GAAc;AACb,IAAA,IAAA,CAAK,cAAc,EAAC;AAAA,EACrB;AAAA;AAAA,EAGA,IAAI,KAAA,GAAgB;AACnB,IAAA,OAAO,KAAK,WAAA,CAAY,MAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,OAAA,GAAmB;AACtB,IAAA,OAAO,IAAA,CAAK,YAAY,MAAA,KAAW,CAAA;AAAA,EACpC;AAAA;AAAA,EAGA,IAAI,UAAA,GAAiB;AACpB,IAAA,OAAO,IAAA,CAAK,EAAA;AAAA,EACb;AACD","file":"vm.js","sourcesContent":["import { Value } from \"@solve-js/vm/Value\";\nimport type { VM } from \"@solve-js/vm/OpRegistry\";\nimport type { UserFunctionDef } from \"@solve-js/parser/BytecodeBuilder\";\n\n// ── VMCheckpoint ────────────────────────────────────────────────────────\n\n/**\n * A point-in-time snapshot of VM variable state.\n *\n * Uses **prototypal inheritance** for memory efficiency: each checkpoint's\n * `variables` object has its parent checkpoint's `variables` as its\n * `__proto__`. This means a `getVar(\"x\")` lookup walks the prototype chain\n * until it finds `x`, and only variables that CHANGED at this checkpoint\n * consume heap space. Unchanged variables are inherited from the parent.\n *\n * ```text\n * Checkpoint 0 (root): {} // empty scope\n * Checkpoint 1 (:x=5): { x: 5 } __proto__ → 0\n * Checkpoint 2 (:y=8): { y: 8 } __proto__ → 1\n * Checkpoint 3 (:x=3): { x: 3 } __proto__ → 2 // shadows x=5\n * ```\n *\n * To look up `x` at checkpoint 3: find own `x=3` → done.\n * To look up `y` at checkpoint 3: not own → walk proto to checkpoint 2 → `y=8`.\n * To look up `z` at checkpoint 3: not found anywhere → undefined.\n *\n * **Memory:** O(number of variable definitions) heap, independent of\n * document length. Typical Obsidian documents have < 100 variable defs,\n * so total checkpoint heap is < 10 KB.\n */\nexport interface VMCheckpoint {\n\t/** 1-based line number where this checkpoint was created. */\n\tlineNumber: number;\n\t/** Persistent line ID from DocumentModel. */\n\tlineId: number;\n\t/**\n\t * Variable name → Value at this checkpoint.\n\t * Own properties are variables set/updated at this line.\n\t * The prototype chain provides inherited variables from parent checkpoints.\n\t */\n\tvariables: Record<string, Value>;\n\t/**\n\t * User-defined-function name → definition at this checkpoint. SEPARATE\n\t * from `variables` above (not prototypally chained the same way\n\t * `restoreTo()` replays every checkpoint in the chain in order, so a\n\t * later redefinition of the same function name naturally overwrites an\n\t * earlier one during replay, without needing its own prototype walk).\n\t *\n\t * Without this field, a function definition's checkpoint entry would be\n\t * SILENTLY LOST: `snapshot()` used to call `vm.getVar(name)` for every\n\t * written name, which returns `undefined` for a function name (function\n\t * defs live in `vm.userFunctions`, not the flat variable store), and a\n\t * `val !== undefined` guard silently skipped it. A scroll-triggered\n\t * `restoreTo()` would then reset the VM and replay only `variables`,\n\t * making a function defined above the new viewport vanish (calling it\n\t * would throw `UNDEFINED_FUNCTION`) even though the document still\n\t * shows its definition line as clean/cached.\n\t */\n\tfunctions: Record<string, UserFunctionDef>;\n\t/** Parent checkpoint (closer to document start), or null for root. */\n\tparent: VMCheckpoint | null;\n}\n\n// ── VMCheckpointer ──────────────────────────────────────────────────────\n\n/**\n * Manages VM state checkpoints for the three-tier evaluation strategy.\n *\n * **Checkpoint creation:** After a variable-definition line executes\n * (Tier 1 or Tier 3), `snapshot()` records the current values of the\n * written variables. The checkpoint is linked via prototypal inheritance\n * to the previous checkpoint, so only changed variables consume memory.\n *\n * **Checkpoint restoration:** Before evaluating a viewport whose start line\n * is not line 1, `restoreTo(lineNumber)` resets the VM and replays all\n * variable definitions up to and including that line. This avoids\n * re-evaluating the entire document from line 1 on every scroll.\n *\n * **Thread safety:** Checkpoints are created synchronously on the main\n * thread during evaluation. They are immutable after creation (Value is\n * an immutable type), so no synchronization is needed.\n *\n * **Integration with Phase 5.2e:** `setViewport()` will use `getNearestCheckpoint()`\n * to find the checkpoint just before the new viewport start, then call\n * `restoreTo()` to set up the VM before evaluating only the visible lines.\n * This is the key to O(visible lines) scrolling instead of O(document).\n */\nexport class VMCheckpointer {\n\t/** Ordered array of checkpoints (ascending lineNumber). */\n\tprivate checkpoints: VMCheckpoint[] = [];\n\t/** The VM instance whose variables are snapshotted/restored. */\n\tprivate vm: VM;\n\n\tconstructor(vm: VM) {\n\t\tthis.vm = vm;\n\t}\n\n\t// ── Snapshot ─────────────────────────────────────────────────────\n\n\t/**\n\t * Create a checkpoint at the current line, recording the VM values of\n\t * the specified variables.\n\t *\n\t * Uses prototypal inheritance: `Object.create(parent.variables)` so\n\t * that inherited variable lookups fall through to previous checkpoints\n\t * without copying all variables into each checkpoint.\n\t *\n\t * @param lineNumber 1-based line position.\n\t * @param lineId Persistent line ID from DocumentModel.\n\t * @param variableNames Names of variables that were written at this line.\n\t * @returns The new checkpoint, or null if no variable names provided.\n\t */\n\tsnapshot(\n\t\tlineNumber: number,\n\t\tlineId: number,\n\t\tvariableNames: string[]\n\t): VMCheckpoint | null {\n\t\tif (variableNames.length === 0) return null;\n\n\t\tconst parent =\n\t\t\tthis.checkpoints.length > 0\n\t\t\t\t? this.checkpoints[this.checkpoints.length - 1]\n\t\t\t\t: null;\n\n\t\t// Create prototypal chain: new checkpoint inherits from parent\n\t\tconst variables: Record<string, Value> = Object.create(\n\t\t\tparent?.variables ?? null\n\t\t) as Record<string, Value>;\n\t\tconst functions: Record<string, UserFunctionDef> = Object.create(\n\t\t\tparent?.functions ?? null\n\t\t) as Record<string, UserFunctionDef>;\n\n\t\t// Record current VM values for the written names, routing each into\n\t\t// the right bag (a name is either a variable or a user-defined\n\t\t// function, never both; see VMCheckpoint.functions's doc comment for\n\t\t// why this dispatch is required, not optional).\n\t\tfor (const name of variableNames) {\n\t\t\tif (this.vm.hasUserFunction(name)) {\n\t\t\t\tconst fn = this.vm.getUserFunction(name);\n\t\t\t\tif (fn) functions[name] = fn;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst val = this.vm.getVar(name);\n\t\t\tif (val !== undefined) {\n\t\t\t\tvariables[name] = val;\n\t\t\t}\n\t\t}\n\n\t\tconst checkpoint: VMCheckpoint = {\n\t\t\tlineNumber,\n\t\t\tlineId,\n\t\t\tvariables,\n\t\t\tfunctions,\n\t\t\tparent,\n\t\t};\n\t\tthis.checkpoints.push(checkpoint);\n\t\treturn checkpoint;\n\t}\n\n\t// ── Restore ──────────────────────────────────────────────────────\n\n\t/**\n\t * Restore the VM to the state at or just after the given line number.\n\t *\n\t * Finds the nearest checkpoint whose `lineNumber <= targetLineNumber`,\n\t * then replays all variable definitions from root → that checkpoint\n\t * into the VM via `setVar()`. The VM's stack is also reset.\n\t *\n\t * If no checkpoint exists at or before the target line, the VM is\n\t * fully reset (empty scope, empty stack).\n\t *\n\t * **Performance:** O(number of checkpoints × variables per checkpoint).\n\t * With prototypal inheritance, `Object.keys()` on each checkpoint\n\t * returns only the variables that were set at that checkpoint (not\n\t * inherited ones), so the total work is O(total variable definitions\n\t * in the document), which is < 100 for typical Obsidian documents.\n\t *\n\t * @param lineNumber Target 1-based line number. The VM will have the\n\t * state that existed AFTER evaluating lines up to `lineNumber`.\n\t */\n\trestoreTo(lineNumber: number): void {\n\t\tconst target = this.getNearestCheckpoint(lineNumber);\n\t\tif (!target) {\n\t\t\tthis.vm.reset();\n\t\t\treturn;\n\t\t}\n\n\t\t// Collect the checkpoint chain from root to target.\n\t\t// Walk parent links and reverse so root is first.\n\t\tconst chain: VMCheckpoint[] = [];\n\t\tlet current: VMCheckpoint | null = target;\n\t\twhile (current) {\n\t\t\tchain.unshift(current);\n\t\t\tcurrent = current.parent;\n\t\t}\n\n\t\tthis.vm.reset();\n\t\tfor (const cp of chain) {\n\t\t\t// Object.keys() returns only OWN enumerable properties\n\t\t\t// it does NOT include inherited properties from the prototype chain.\n\t\t\t// This means we only set variables that were defined/updated at this\n\t\t\t// specific checkpoint, not all variables from parent checkpoints.\n\t\t\tfor (const key of Object.keys(cp.variables)) {\n\t\t\t\tthis.vm.setVar(key, cp.variables[key]);\n\t\t\t}\n\t\t\t// Replay function definitions the same way, a later checkpoint's\n\t\t\t// redefinition of the same name naturally overwrites an earlier\n\t\t\t// one since the chain replays in root-to-target order.\n\t\t\tfor (const key of Object.keys(cp.functions)) {\n\t\t\t\tconst fn = cp.functions[key];\n\t\t\t\tthis.vm.defineUserFunction(fn.name, fn.params, fn.program);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ── Queries ──────────────────────────────────────────────────────\n\n\t/**\n\t * Find the nearest checkpoint at or before the given line number.\n\t *\n\t * Uses linear scan (checkpoints are sorted by lineNumber and the list\n\t * is short, typically < 20 for Obsidian documents). Can be upgraded\n\t * to binary search if needed for documents with 1000+ variable defs.\n\t *\n\t * @returns The nearest checkpoint, or null if none exists before the line.\n\t */\n\tgetNearestCheckpoint(lineNumber: number): VMCheckpoint | null {\n\t\tlet result: VMCheckpoint | null = null;\n\t\tfor (const cp of this.checkpoints) {\n\t\t\tif (cp.lineNumber <= lineNumber) {\n\t\t\t\tresult = cp;\n\t\t\t} else {\n\t\t\t\tbreak; // checkpoints are sorted ascending\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get a specific checkpoint by its line number.\n\t * @returns The checkpoint, or undefined if not found.\n\t */\n\tgetCheckpointAt(lineNumber: number): VMCheckpoint | undefined {\n\t\treturn this.checkpoints.find((cp) => cp.lineNumber === lineNumber);\n\t}\n\n\t/**\n\t * Get the entire checkpoint chain from root to the last checkpoint.\n\t * Useful for debugging and serialization.\n\t */\n\tgetAllCheckpoints(): readonly VMCheckpoint[] {\n\t\treturn this.checkpoints;\n\t}\n\n\t/**\n\t * Look up a variable's value through the checkpoint chain.\n\t *\n\t * Walks the prototype chain starting from the most recent checkpoint,\n\t * looking for the variable name as an own property. This is O(depth)\n\t * where depth is the number of checkpoints since the variable was\n\t * last set.\n\t *\n\t * **Note:** This queries the checkpointer's snapshot, not the VM.\n\t * The VM may have been modified since the last snapshot (e.g., by\n\t * Tier 2 execution of non-variable-def lines that don't create checkpoints).\n\t *\n\t * @returns The Value, or undefined if the variable was never set.\n\t */\n\tlookupVariable(name: string): Value | undefined {\n\t\tif (this.checkpoints.length === 0) return undefined;\n\n\t\tconst latest = this.checkpoints[this.checkpoints.length - 1];\n\t\tlet scope: Record<string, Value> | null = latest.variables;\n\t\twhile (scope) {\n\t\t\tif (Object.prototype.hasOwnProperty.call(scope, name)) {\n\t\t\t\treturn scope[name];\n\t\t\t}\n\t\t\tscope = Object.getPrototypeOf(scope) as Record<string, Value> | null;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// ── Lifecycle ────────────────────────────────────────────────────\n\n\t/**\n\t * Clear all checkpoints. The underlying VM is NOT reset, call\n\t * `vm.reset()` separately if needed.\n\t */\n\tclear(): void {\n\t\tthis.checkpoints = [];\n\t}\n\n\t/** Number of checkpoints stored. */\n\tget count(): number {\n\t\treturn this.checkpoints.length;\n\t}\n\n\t/** Returns true if no checkpoints have been created. */\n\tget isEmpty(): boolean {\n\t\treturn this.checkpoints.length === 0;\n\t}\n\n\t/** The associated VM instance. */\n\tget vmInstance(): VM {\n\t\treturn this.vm;\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solve-engine",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "The expression evaluation engine behind Solve — lexer, parser, bytecode VM, and an extensible package system.",
5
5
  "keywords": [
6
6
  "expression-evaluator",
@@ -208,9 +208,7 @@
208
208
  "typecheck:tsc": "tsc --noEmit --skipLibCheck"
209
209
  },
210
210
  "dependencies": {
211
- "@tanstack/query-core": "^5.100.10",
212
- "semver": "^7.6.0",
213
- "tslib": "^2.6.1"
211
+ "@tanstack/query-core": "^5.100.10"
214
212
  },
215
213
  "devDependencies": {
216
214
  "@jest/globals": "^30.4.1",
@@ -218,9 +216,11 @@
218
216
  "@types/semver": "^7.5.0",
219
217
  "convert": "^7.0.0",
220
218
  "jest": "^30.4.2",
219
+ "semver": "^7.6.0",
221
220
  "ts-jest": "^29.4.12",
222
221
  "tsup": "8.5.1",
223
222
  "typescript": "5.9.3"
224
223
  },
224
+ "//sideEffects": "Load-bearing claim, not boilerplate. tsup's splitting emits bare chunk imports at the top of dist/index.js, and this field tells Rollup, webpack and Vite they may delete them, which esbuild reports as [ignored-bare-import] once per import during `npm run size`. The engine does have module-level work to lose: registerAllTokenTypes(), _PrecedenceParser.BP_TABLE and its cached token ids, and several process-wide registries. It is safe because every chunk doing that work is also imported for its bindings somewhere, so no bundler can drop it. That is a property of how tsup currently splits the code rather than a design guarantee, so `npm run smoke:bundled` (scripts/check-tree-shaking.mjs) proves it on every run of `npm run verify` instead of assuming it, and its header explains the two fixes if it ever stops holding.",
225
225
  "sideEffects": false
226
226
  }
@@ -1,163 +0,0 @@
1
- import { T as Token } from './Token-0jpvUdPY.cjs';
2
-
3
- /**
4
- * NormalizerRule, pluggable token normalization rule for the
5
- * TokenNormalizer post-lexer pass.
6
- *
7
- * ## Purpose
8
- * After the ExpressionLexer produces raw tokens (numbers, identifiers,
9
- * operators, etc.), the TokenNormalizer applies domain-specific rules
10
- * to transform the token stream before parsing. This keeps the lexer
11
- * focused on single-token production and moves multi-token pattern
12
- * matching into a dedicated normalization layer.
13
- *
14
- * ## How rules work
15
- * Rules are applied in priority order (highest first). At each token
16
- * position, the normalizer tries every rule in priority order until one
17
- * matches. Matched tokens are consumed and replaced; unmatched tokens
18
- * pass through unchanged.
19
- *
20
- * ## What rules can do
21
- * - **Phrase fusion**: Merge consecutive words into compound tokens
22
- * (e.g., `"to" "the" "power" "of"` → `CARET`)
23
- * - **Implicit operators**: Insert missing operators between tokens
24
- * (e.g., `NUMBER IDENT` → `NUMBER STAR IDENT`)
25
- * - **Domain transformations**: Coalesce item names, currency pairs, etc.
26
- *
27
- * ## Why a separate file?
28
- * This is a duplicate-free copy of the interface defined in
29
- * TokenNormalizer.ts. Storing it in a separate file avoids circular
30
- * imports, TokenNormalizer imports NormalizerRule, and rule factories
31
- * import TokenNormalizer's `createFusedToken`.
32
- *
33
- * @module NormalizerRule
34
- */
35
-
36
- /**
37
- * Result of a successful rule match attempt against the token stream.
38
- *
39
- * When a {@link NormalizerRule.match} function finds a pattern at the
40
- * current position, it returns a NormalizerMatch describing how many
41
- * tokens to consume and what to replace them with.
42
- *
43
- * @example
44
- * ```ts
45
- * // The phrase "to the power of" (5 tokens) becomes a single CARET token
46
- * const match: NormalizerMatch = {
47
- * consumed: 5,
48
- * replacement: [caretToken],
49
- * };
50
- * ```
51
- */
52
- interface NormalizerMatch {
53
- /**
54
- * Number of tokens consumed from the stream at the match position.
55
- * Must be ≥ 1, a match always advances the cursor.
56
- */
57
- consumed: number;
58
- /**
59
- * Replacement tokens to insert at the match position.
60
- * May be empty (deletion), a single token (fusion), or multiple
61
- * tokens (expansion/splitting).
62
- */
63
- replacement: Token[];
64
- /**
65
- * Human-readable rule name for diagnostic fusion tracking.
66
- * When set, the normalizer uses this instead of the rule's `name`
67
- * in {@link TokenFusion} records. Used by {@link PhraseTrie} to
68
- * report which specific phrase matched (e.g., "phrase:to the power of").
69
- */
70
- ruleName?: string;
71
- }
72
- /**
73
- * A pluggable normalization rule registered with the TokenNormalizer.
74
- *
75
- * Each rule has a {@link name}, {@link priority}, and {@link match} function.
76
- * The match function receives the current token stream and a position,
77
- * and returns a {@link NormalizerMatch} on success or `null` on failure.
78
- *
79
- * ## Priority ordering
80
- * Higher priority rules are tried first at each position. This allows
81
- * long phrases (priority 100, e.g. "to the power of") to match before
82
- * shorter fragments (priority 80, e.g. "power of").
83
- *
84
- * ## Match contract
85
- * - Must be pure (no side effects, no mutation of input tokens)
86
- * - Must return `null` for any position that doesn't match
87
- * - Consumed tokens must be consecutive starting at `pos`
88
- * - Replacement tokens must be valid for downstream parsing
89
- *
90
- * @example
91
- * ```ts
92
- * // A phrase fusion rule that converts "to the power of" into CARET
93
- * const phraseRule: NormalizerRule = {
94
- * name: 'phrase:to the power of',
95
- * priority: 100,
96
- * match: (tokens, pos) => {
97
- * if (pos + 4 > tokens.length) return null;
98
- * const phrase = tokens.slice(pos, pos + 5)
99
- * .map(t => t.value.toLowerCase()).join(' ');
100
- * if (phrase === 'to the power of') {
101
- * return {
102
- * consumed: 5,
103
- * replacement: [createFusedToken('CARET', 'to the power of', tokens.slice(pos, pos + 5))],
104
- * };
105
- * }
106
- * return null;
107
- * },
108
- * };
109
- * ```
110
- */
111
- interface NormalizerRule {
112
- /**
113
- * Human-readable name for debugging and diagnostic display.
114
- * Convention: `"category:description"`, e.g. `"phrase:to the power of"`.
115
- */
116
- readonly name: string;
117
- /**
118
- * Priority for ordering rules. Higher values are tried first.
119
- * Recommended ranges:
120
- * - 100: Long multi-word phrase fusion (e.g., "to the power of")
121
- * - 80: Short phrase fusion (e.g., "power of", "times by")
122
- * - 50: Implicit operator insertion (e.g., implicit multiply)
123
- * - 20: Domain-specific transformations
124
- */
125
- readonly priority: number;
126
- /**
127
- * Attempt to match a pattern starting at position `pos` in the token stream.
128
- *
129
- * @param tokens - The current token stream (may be partially normalized from prior passes)
130
- * @param pos - The current position to attempt matching from
131
- * @returns A {@link NormalizerMatch} if the pattern is found, or `null` if no match
132
- */
133
- match(tokens: Token[], pos: number): NormalizerMatch | null;
134
- }
135
- /**
136
- * Record of a token fusion event performed by the normalizer.
137
- *
138
- * When a rule merges multiple source tokens into fewer replacement tokens,
139
- * the normalizer fires a {@link NormalizerOptions.onFusion | fusion callback}
140
- * with this record. The playground uses these records to render the
141
- * fusion detail table showing exactly which tokens were merged and by
142
- * which rule.
143
- *
144
- * @example
145
- * ```ts
146
- * // "to" "the" "power" "of" fused into CARET "^"
147
- * const fusion: TokenFusion = {
148
- * rule: "phrase:to the power of",
149
- * sourceTokens: [toToken, theToken, powerToken, ofToken],
150
- * fusedToken: caretToken,
151
- * };
152
- * ```
153
- */
154
- interface TokenFusion {
155
- /** The name of the rule that triggered this fusion (e.g., "phrase:to the power of") */
156
- rule: string;
157
- /** The original tokens before fusion, always ≥ 2 tokens */
158
- sourceTokens: Token[];
159
- /** The resulting fused token with its new type and combined value */
160
- fusedToken: Token;
161
- }
162
-
163
- export type { NormalizerRule as N, TokenFusion as T, NormalizerMatch as a };
@@ -1,163 +0,0 @@
1
- import { T as Token } from './Token-0jpvUdPY.js';
2
-
3
- /**
4
- * NormalizerRule, pluggable token normalization rule for the
5
- * TokenNormalizer post-lexer pass.
6
- *
7
- * ## Purpose
8
- * After the ExpressionLexer produces raw tokens (numbers, identifiers,
9
- * operators, etc.), the TokenNormalizer applies domain-specific rules
10
- * to transform the token stream before parsing. This keeps the lexer
11
- * focused on single-token production and moves multi-token pattern
12
- * matching into a dedicated normalization layer.
13
- *
14
- * ## How rules work
15
- * Rules are applied in priority order (highest first). At each token
16
- * position, the normalizer tries every rule in priority order until one
17
- * matches. Matched tokens are consumed and replaced; unmatched tokens
18
- * pass through unchanged.
19
- *
20
- * ## What rules can do
21
- * - **Phrase fusion**: Merge consecutive words into compound tokens
22
- * (e.g., `"to" "the" "power" "of"` → `CARET`)
23
- * - **Implicit operators**: Insert missing operators between tokens
24
- * (e.g., `NUMBER IDENT` → `NUMBER STAR IDENT`)
25
- * - **Domain transformations**: Coalesce item names, currency pairs, etc.
26
- *
27
- * ## Why a separate file?
28
- * This is a duplicate-free copy of the interface defined in
29
- * TokenNormalizer.ts. Storing it in a separate file avoids circular
30
- * imports, TokenNormalizer imports NormalizerRule, and rule factories
31
- * import TokenNormalizer's `createFusedToken`.
32
- *
33
- * @module NormalizerRule
34
- */
35
-
36
- /**
37
- * Result of a successful rule match attempt against the token stream.
38
- *
39
- * When a {@link NormalizerRule.match} function finds a pattern at the
40
- * current position, it returns a NormalizerMatch describing how many
41
- * tokens to consume and what to replace them with.
42
- *
43
- * @example
44
- * ```ts
45
- * // The phrase "to the power of" (5 tokens) becomes a single CARET token
46
- * const match: NormalizerMatch = {
47
- * consumed: 5,
48
- * replacement: [caretToken],
49
- * };
50
- * ```
51
- */
52
- interface NormalizerMatch {
53
- /**
54
- * Number of tokens consumed from the stream at the match position.
55
- * Must be ≥ 1, a match always advances the cursor.
56
- */
57
- consumed: number;
58
- /**
59
- * Replacement tokens to insert at the match position.
60
- * May be empty (deletion), a single token (fusion), or multiple
61
- * tokens (expansion/splitting).
62
- */
63
- replacement: Token[];
64
- /**
65
- * Human-readable rule name for diagnostic fusion tracking.
66
- * When set, the normalizer uses this instead of the rule's `name`
67
- * in {@link TokenFusion} records. Used by {@link PhraseTrie} to
68
- * report which specific phrase matched (e.g., "phrase:to the power of").
69
- */
70
- ruleName?: string;
71
- }
72
- /**
73
- * A pluggable normalization rule registered with the TokenNormalizer.
74
- *
75
- * Each rule has a {@link name}, {@link priority}, and {@link match} function.
76
- * The match function receives the current token stream and a position,
77
- * and returns a {@link NormalizerMatch} on success or `null` on failure.
78
- *
79
- * ## Priority ordering
80
- * Higher priority rules are tried first at each position. This allows
81
- * long phrases (priority 100, e.g. "to the power of") to match before
82
- * shorter fragments (priority 80, e.g. "power of").
83
- *
84
- * ## Match contract
85
- * - Must be pure (no side effects, no mutation of input tokens)
86
- * - Must return `null` for any position that doesn't match
87
- * - Consumed tokens must be consecutive starting at `pos`
88
- * - Replacement tokens must be valid for downstream parsing
89
- *
90
- * @example
91
- * ```ts
92
- * // A phrase fusion rule that converts "to the power of" into CARET
93
- * const phraseRule: NormalizerRule = {
94
- * name: 'phrase:to the power of',
95
- * priority: 100,
96
- * match: (tokens, pos) => {
97
- * if (pos + 4 > tokens.length) return null;
98
- * const phrase = tokens.slice(pos, pos + 5)
99
- * .map(t => t.value.toLowerCase()).join(' ');
100
- * if (phrase === 'to the power of') {
101
- * return {
102
- * consumed: 5,
103
- * replacement: [createFusedToken('CARET', 'to the power of', tokens.slice(pos, pos + 5))],
104
- * };
105
- * }
106
- * return null;
107
- * },
108
- * };
109
- * ```
110
- */
111
- interface NormalizerRule {
112
- /**
113
- * Human-readable name for debugging and diagnostic display.
114
- * Convention: `"category:description"`, e.g. `"phrase:to the power of"`.
115
- */
116
- readonly name: string;
117
- /**
118
- * Priority for ordering rules. Higher values are tried first.
119
- * Recommended ranges:
120
- * - 100: Long multi-word phrase fusion (e.g., "to the power of")
121
- * - 80: Short phrase fusion (e.g., "power of", "times by")
122
- * - 50: Implicit operator insertion (e.g., implicit multiply)
123
- * - 20: Domain-specific transformations
124
- */
125
- readonly priority: number;
126
- /**
127
- * Attempt to match a pattern starting at position `pos` in the token stream.
128
- *
129
- * @param tokens - The current token stream (may be partially normalized from prior passes)
130
- * @param pos - The current position to attempt matching from
131
- * @returns A {@link NormalizerMatch} if the pattern is found, or `null` if no match
132
- */
133
- match(tokens: Token[], pos: number): NormalizerMatch | null;
134
- }
135
- /**
136
- * Record of a token fusion event performed by the normalizer.
137
- *
138
- * When a rule merges multiple source tokens into fewer replacement tokens,
139
- * the normalizer fires a {@link NormalizerOptions.onFusion | fusion callback}
140
- * with this record. The playground uses these records to render the
141
- * fusion detail table showing exactly which tokens were merged and by
142
- * which rule.
143
- *
144
- * @example
145
- * ```ts
146
- * // "to" "the" "power" "of" fused into CARET "^"
147
- * const fusion: TokenFusion = {
148
- * rule: "phrase:to the power of",
149
- * sourceTokens: [toToken, theToken, powerToken, ofToken],
150
- * fusedToken: caretToken,
151
- * };
152
- * ```
153
- */
154
- interface TokenFusion {
155
- /** The name of the rule that triggered this fusion (e.g., "phrase:to the power of") */
156
- rule: string;
157
- /** The original tokens before fusion, always ≥ 2 tokens */
158
- sourceTokens: Token[];
159
- /** The resulting fused token with its new type and combined value */
160
- fusedToken: Token;
161
- }
162
-
163
- export type { NormalizerRule as N, TokenFusion as T, NormalizerMatch as a };