| 670 | * selection skips the already-loaded rule ids. Shell declarations go into |
| 671 | * the **current** scope — call `loadIdentities` before declaring user |
| 672 | * symbols that could shadow shell heads. |
| 673 | */ |
| 674 | export function loadIdentities( |
| 675 | ce: IComputeEngine, |
| 676 | options: FungrimLoadOptions = {} |
| 677 | ): FungrimLoadReport { |
| 678 | const data = options.data ?? FUNGRIM_CORE; |
| 679 | const topics = options.topics !== undefined ? new Set(options.topics) : null; |
| 680 | const classes = |
| 681 | options.classes !== undefined ? new Set<string>(options.classes) : null; |
| 682 | const purposes = |
| 683 | options.purposes !== undefined ? new Set<string>(options.purposes) : null; |
| 684 | const solve = options.solve === true; |
| 685 | |
| 686 | let alreadyLoaded = loadedIdsByEngine.get(ce); |
| 687 | if (alreadyLoaded === undefined) { |
| 688 | alreadyLoaded = new Set(); |
| 689 | loadedIdsByEngine.set(ce, alreadyLoaded); |
| 690 | // A checkpoint restore can roll back the declarations this loader |
| 691 | // installed. The marker below would then still claim those ids are |
| 692 | // present, so a replayed `loadIdentities()` would skip re-declaring rules |
| 693 | // the engine no longer has. Clearing it is registered as a hook ON the |
| 694 | // engine rather than being called from the checkpoint code, so the core |
| 695 | // checkpoint path does not import — and therefore does not bundle — the |
| 696 | // Fungrim payload. Registered once, on the first load for this engine. |
| 697 | (ce._checkpointResetHooks ??= []).push(() => loadedIdsByEngine.delete(ce)); |
| 698 | } |
| 699 | |
| 700 | const report: FungrimLoadReport = { |
| 701 | loaded: 0, |
| 702 | byTarget: { simplify: 0, solve: 0, harmonization: 0 }, |
| 703 | byPurpose: { simplify: 0, transform: 0, expand: 0 }, |
| 704 | declared: [], |
| 705 | skipped: [], |
| 706 | compileLedger: { ...data.manifest.ledger }, |
| 707 | }; |
| 708 | |
| 709 | // -- 1. Selection (each rule gets exactly one disposition) |
| 710 | // |
| 711 | // `referenced` accumulates the shell heads of EVERY rule that passes the |
| 712 | // class/topic/purpose/solve filters — including rules already loaded on |
| 713 | // this engine. Shell declarations are scope-local (`ce.declare`), so a |
| 714 | // rule loaded inside a since-popped scope leaves its rule object alive in |
| 715 | // the engine-global rule store while its shell heads have gone out of |
| 716 | // scope. Re-running the shell pass over the already-loaded rules too |
| 717 | // (idempotent: `declare` skips names still defined) makes those heads |
| 718 | // usable again on reload after a `popScope` (SYM P3-8). |
| 719 | const selected: CompiledFungrimRule[] = []; |
| 720 | const referenced = new Set<string>(); |
| 721 | const collectReferenced = (r: CompiledFungrimRule): void => { |
| 722 | collectSymbols(r.match, referenced); |
| 723 | collectSymbols(r.replace, referenced); |
| 724 | for (const g of r.guards) collectSymbols(guardJson(g), referenced); |
| 725 | }; |
| 726 | for (const r of data.rules) { |
| 727 | if (classes !== null && !classes.has(r.class)) { |
| 728 | report.skipped.push({ id: r.id, reason: 'filtered-class' }); |
| 729 | continue; |