parseman 0.60.4 → 0.60.5

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.
@@ -24,7 +24,7 @@ import { createUnplugin } from 'unplugin'
24
24
  import { parseSync } from 'oxc-parser'
25
25
  import { ResolverFactory } from 'oxc-resolver'
26
26
  import MagicString from 'magic-string'
27
- import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, externalAccesses, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, setBuilderImportResolver, propName, ModuleScope, type FreeNameResolution, type Scope, type ScopeEntry } from './evaluator.ts'
27
+ import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, externalAccesses, ruleMapRecipes, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, setBuilderImportResolver, propName, ModuleScope, type FreeNameResolution, type Scope, type ScopeEntry } from './evaluator.ts'
28
28
  import { classifyRuleMap } from '../analysis/commitment.ts'
29
29
  import { compile } from '../table/compile.ts'
30
30
  import { compileRuleMap, type CompiledRuleMapTable } from '../table/compile-rule-map.ts'
@@ -223,12 +223,6 @@ const DEFAULT_ASSEMBLY_BUDGET = 0.25
223
223
  /** A build failure the lowering must not soften into a warning and a fallback. */
224
224
  class LinkRefusal extends Error {}
225
225
 
226
- /** Whether a rule-map entry has a definition (a `g.X` merely read does not). */
227
- function isResolved(rule: Combinator<unknown>): boolean {
228
- if (rule._def.tag !== 'lazy') return true
229
- try { rule._def.thunk(); return true } catch { return false }
230
- }
231
-
232
226
  function reasonSuffix(runtimeOnly: readonly string[] | undefined): string {
233
227
  return runtimeOnly === undefined || runtimeOnly.length === 0
234
228
  ? ''
@@ -2061,7 +2055,7 @@ function transformMacroImpl(
2061
2055
  /**
2062
2056
  * Evaluate a carried list back to combinators, one entry per piece: its rule map
2063
2057
  * and the `rules()` recipe it was built from. `compose()` links the recipes into
2064
- * one namespace (`mergedCarriedRules`); `composeLeaf()` merges the maps by name.
2058
+ * one namespace (`mergedCarriedRules`), and `composeLeaf()` does the same.
2065
2059
  * Either way the merged grammar is encoded ONCE by `compileRuleMap` — no relocation
2066
2060
  * of code offsets and no merging of const / fn / expected pools between two
2067
2061
  * already-encoded programs.
@@ -2097,38 +2091,6 @@ function transformMacroImpl(
2097
2091
  return { pieces, trackLines }
2098
2092
  }
2099
2093
 
2100
- /**
2101
- * Fold ordered rule maps into the composed map: a later piece's name WINS, and
2102
- * `Map` keeps each name at its first-sighting position so the encoded rule order
2103
- * tracks the source lowering's rather than drifting per compose.
2104
- *
2105
- * A REFERENCE IS NOT A DEFINITION. A `rules(g => …)` cache also holds every `g.X`
2106
- * that was merely ACCESSED, so a delta that calls `g.Pair` carries a `Pair` entry
2107
- * that is an unresolved named lazy. Merged in name order that entry lands LAST and
2108
- * shadows the base piece which actually defines `Pair` — the encoder then finds a
2109
- * hole where the winner should be and refuses the whole grammar with "ref() used
2110
- * before .define()". The same filter guards `compileLinkableTable`'s `isLocal`
2111
- * (`compile-linkable-table.ts`) and `recoverComposedRules`; this site needs it
2112
- * for the same reason, and skipping it is why cross-piece composition looked blocked.
2113
- *
2114
- * The reference itself is not lost: it stays inside the delta's own rule bodies and
2115
- * `enc.winners` binds it BY NAME to whichever piece supplies the definition.
2116
- */
2117
- const mergeRuleMaps = (
2118
- maps: ReadonlyArray<ReadonlyArray<readonly [string, Combinator<unknown>]>>,
2119
- ): Array<[string, Combinator<unknown>]> => {
2120
- const winners = new Map<string, Combinator<unknown>>()
2121
- for (const map of maps) {
2122
- for (const [name, rule] of map) {
2123
- if (rule._def.tag === 'lazy') {
2124
- try { rule._def.thunk() } catch { continue }
2125
- }
2126
- winners.set(name, rule)
2127
- }
2128
- }
2129
- return [...winners]
2130
- }
2131
-
2132
2094
  /**
2133
2095
  * LINK a carried list into the composed map, exactly as runtime `compose()` does:
2134
2096
  * every piece's carried `rules()` recipe runs again against ONE namespace
@@ -2156,25 +2118,6 @@ function transformMacroImpl(
2156
2118
  return { rules: Object.entries(linked), trackLines: carriedMaps.trackLines, dangling }
2157
2119
  }
2158
2120
 
2159
- /**
2160
- * COMPOSING-WINS for `composeLeaf()`'s carried pieces, as an OVERRIDE rather than
2161
- * a gap-fill: `compileRuleMap`'s `applyAmbient` only fills a rule that carries no
2162
- * trivia of its own. (`compose()` gets the same override from `linkRules`.)
2163
- *
2164
- * Safe to mutate: every rule here came from `evalRuleMapIR`, which constructs FRESH
2165
- * combinators per piece per compose, so this cannot leak into another compose of the
2166
- * same base grammar.
2167
- */
2168
- const applyComposingTrivia = (
2169
- ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>,
2170
- composing: Combinator<unknown>,
2171
- ): void => {
2172
- for (const [, rule] of ruleMap) {
2173
- if (rule._meta.isTrivia) continue
2174
- ;(rule._meta as { grammarTrivia?: Combinator<unknown> }).grammarTrivia = composing
2175
- }
2176
- }
2177
-
2178
2121
  /** Resolve one `compose([...])` argument to its RE-LOWERABLE carried items (IR
2179
2122
  * pieces / import spreads / baked-pieces fallback). The composing grammar's trivia
2180
2123
  * is applied later, uniformly, in materializeCarried — never per-piece here. */
@@ -2340,10 +2283,17 @@ function transformMacroImpl(
2340
2283
  // does — one encode, one `tableRules(…)` expression, no linker. `carried` is
2341
2284
  // unchanged either way: it is the re-lowerable IR list, not a lowering artifact,
2342
2285
  // so a downstream re-compose behaves identically whichever engine emitted here.
2343
- const merged = mergedCarriedRules(carried, {
2344
- trivia: composing,
2345
- ...(cHostMode ? { hostMode: cHostMode as HostMode } : {}),
2346
- })
2286
+ let merged: ReturnType<typeof mergedCarriedRules>
2287
+ try {
2288
+ merged = mergedCarriedRules(carried, {
2289
+ trivia: composing,
2290
+ ...(cHostMode ? { hostMode: cHostMode as HostMode } : {}),
2291
+ })
2292
+ } catch (e) {
2293
+ // Linking refuses the composition (a `prior.X` no earlier piece defines): a
2294
+ // runtime `compose()` would refuse it the same way, so the build fails, here.
2295
+ throw new Error(`${id}:${lineOf(init.start)} — ${(e as Error).message}`, { cause: e })
2296
+ }
2347
2297
  if (merged?.dangling !== undefined) throw new Error(`${id}:${lineOf(init.start)} — compose: ${merged.dangling}`)
2348
2298
  if (merged !== null) {
2349
2299
  // Harvest the direct-builder import provenance carried on the merged graph so
@@ -2475,22 +2425,15 @@ function transformMacroImpl(
2475
2425
  return null
2476
2426
  }
2477
2427
  const localArg = elements[elements.length - 1]!
2478
- let localRules: Iterable<readonly [string, unknown]> | null = null
2479
- // The LOCAL leaf grammar's own ambient scanSkip (from its `rules({ scanSkip })`)
2480
- // governs the scanTo/balanced sites IT defines — unlike trivia (composing-wins),
2481
- // opaque units are dialect-specific, so the local declaration is threaded to the
2482
- // local piece's compile.
2483
- let localScanSkip: Combinator<unknown>[] | undefined
2428
+ let localRules: ReadonlyMap<string, Combinator<unknown>> | null = null
2484
2429
  const importedFactories: string[] = []
2485
2430
  if (isRulesCall(localArg)) {
2486
2431
  const evaluated = evaluateRulesFactory(localArg, `composeLeaf${init.start}`)
2487
2432
  localRules = evaluated?.ruleMap ?? null
2488
- localScanSkip = evaluated?.scanSkip
2489
2433
  if (evaluated?.importedFactory) importedFactories.push(evaluated.importedFactory)
2490
2434
  } else if (localArg.type === 'Identifier') {
2491
2435
  const name = (localArg as unknown as { name: string }).name
2492
2436
  localRules = localRuleMaps.get(name) ?? null
2493
- localScanSkip = localGrammarScanSkip.get(name)
2494
2437
  }
2495
2438
  if (!localRules) {
2496
2439
  // Distinguish "the argument is the wrong SHAPE" from "the argument is the right
@@ -2525,13 +2468,6 @@ function transformMacroImpl(
2525
2468
  // may place one beneath a direct node(). Re-lower their terminals with
2526
2469
  // capture enabled so that node receives the imported token values in its
2527
2470
  // normal child collector; the pieces still contain no semantic callback.
2528
- const localNs = nsFor(`composeLeaf${init.start}`)
2529
- const localEntries = [...localRules] as Array<[string, Combinator<unknown>]>
2530
-
2531
- // TABLE FIRST — same merge as `compose()`, with the local leaf map appended LAST
2532
- // so it wins every name and binds the imported shapes' holes (`enc.winners`
2533
- // resolves those by name; see `mergedCarriedRules`).
2534
- //
2535
2471
  // The recognition-only gate is preserved EXACTLY, and is the reason this path
2536
2472
  // needs no `compileLinkable`: `hasDirectBuilders` / `isRecognitionOnly` are
2537
2473
  // predicates over the combinator graph, not products of lowering it, so
@@ -2554,36 +2490,23 @@ function transformMacroImpl(
2554
2490
  warn(init.start, 'composeLeaf(): every pre-final grammar must explicitly prove recognition-only')
2555
2491
  return null
2556
2492
  }
2557
- // The local grammar's OWN ambient scanSkip is stamped onto the LOCAL entries
2558
- // only. Passing it as a merged-map option instead would let `applyAmbient`
2559
- // hand it to every imported rule that happens to carry no stamp of its own —
2560
- // opaque units are dialect-specific, and that is precisely the leak the
2561
- // per-piece threading in the source path exists to avoid.
2562
- if (localScanSkip) {
2563
- for (const [, rule] of localEntries) {
2564
- if (rule._meta.isTrivia) continue
2565
- const meta = rule._meta as { grammarScanSkip?: Combinator<unknown>[] }
2566
- if (meta.grammarScanSkip === undefined) meta.grammarScanSkip = localScanSkip
2567
- }
2493
+ // The same link as `compose()` and runtime `composeLeaf()`: the leaf is the last
2494
+ // recipe, so it wins every name, binds the imported shapes' holes and fills its
2495
+ // `prior.X`. Each recipe keeps its own scanSkip; the composing trivia governs all.
2496
+ let linked: Record<string, Combinator<unknown>>
2497
+ try {
2498
+ linked = linkRules(
2499
+ [...carriedMaps.pieces.flatMap(p => p.recipes), ...ruleMapRecipes.get(localRules)!],
2500
+ { trivia: composing },
2501
+ )
2502
+ } catch (e) {
2503
+ throw new LinkRefusal(`${id}:${lineOf(init.start)} — composeLeaf: ${(e as Error).message.replace(/^compose: /, '')}`, { cause: e })
2568
2504
  }
2569
- const leafMerged = mergeRuleMaps([...carriedMaps.pieces.map(p => p.rules), localEntries])
2570
- // The same refusal as `compose()`, over a merge by name: a name some piece only
2571
- // reads (an unresolved entry of a carried map, or a local factory's external
2572
- // access) that no piece defines.
2573
- const accessed = [
2574
- ...carriedMaps.pieces.flatMap(p => p.rules).filter(([, rule]) => !isResolved(rule)).map(([name]) => name),
2575
- ...(localRules instanceof Map ? externalAccesses.get(localRules) ?? [] : []),
2576
- ]
2577
- const dangling = danglingReference(Object.fromEntries(leafMerged), accessed)
2505
+ const dangling = danglingReference(linked)
2578
2506
  if (dangling !== undefined) throw new LinkRefusal(`${id}:${lineOf(init.start)} — composeLeaf: ${dangling}`)
2579
- // Composing-wins governs the leaf fuse too. The local entries are NOT freshly
2580
- // evaluated (they come from this module's own `rules()` factory), so they are
2581
- // excluded from the override — their declared trivia IS the composing candidate,
2582
- // and rewriting `_meta` on them would mutate the module-level grammar object.
2583
- if (composing) applyComposingTrivia(carriedMaps.pieces.flatMap(p => p.rules), composing)
2584
2507
  const refusals: string[] = []
2585
2508
  const compiled = compileRuleMap(
2586
- leafMerged,
2509
+ Object.entries(linked),
2587
2510
  {
2588
2511
  ...(composing ? { trivia: composing } : {}),
2589
2512
  ...(carriedMaps.trackLines ? { trackLines: true } : {}),
package/src/spec/model.ts CHANGED
@@ -507,7 +507,9 @@ class Builder {
507
507
 
508
508
  case 'lazy': {
509
509
  const inner = resolveLazy(self)
510
- return inner ? this.walk(inner) : { kind: 'annotation', text: '?' }
510
+ if (inner) return this.walk(inner)
511
+ const prior = (self as { _priorOf?: string })._priorOf
512
+ return { kind: 'annotation', text: prior === undefined ? '?' : `prior.${prior}` }
511
513
  }
512
514
 
513
515
  case 'unknown':
package/src/version.ts CHANGED
@@ -13,4 +13,4 @@
13
13
  * Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
14
14
  * the build if this constant drifts from the published version.
15
15
  */
16
- export const PARSEMAN_VERSION = '0.60.4'
16
+ export const PARSEMAN_VERSION = '0.60.5'