refactor: eliminate remaining hardcoded predicate-name dispatch

Extraction.ts (3 fixes):
  - Replace 2 'inStackingContext' string checks with isVariableArityPredicate()
  - Replace 7-name diagnostic formatting if/else with spec-driven
    getPredicateSpec() checks (isDirectional → gap message,
    isSize → threshold hint, else generic)

Grammar.ts: Replace 8 hardcoded parser routing checks
  (atLeast/atMost/aspectRatio/between/clippedBy/attachedToScrollContainer/
  escapeClippingChainOf/inStackingContext) with SIZE_PREDICATE_NAMES and
  TOPOLOGY_PREDICATE_NAMES Sets derived from spec table.

Pipeline.ts: Replace 15-entry CODE_TO_CLAUSE_KIND map with runtime
  generation from PREDICATE_SPECS. Prefix derived from spec.isSize
  ('size.*') / validOptions.includes('axis') ('alignment.*') /
  else ('relation.*'). Manual override for aspectRatio code 15.

Proofs.ts: Replace 11-case switch(kind) with 5 spec-driven if/else
  branches categorized by validOptions presence (hasGap→directional,
  hasAxis→alignment) + 2 specific name checks (inside overflow,
  aspectRatio ratio). 11 predicate names → 0 hardcoded.

Lexer.ts: Export KEYWORDS map for conformance testing.

Conformance tests:
  - Solver: every BUILTIN_PREDICATES entry matches its PREDICATE_SPECS
    counterpart; every spec name (incl. aliases) has a registered
    evaluator with matching descriptor (2 tests)
  - DSL: every predicate name from collectAllPredicateNames() appears
    in the lexer KEYWORDS table (1 test)

598 SDK + 3 conformance + 57 E2E = 658 tests pass.
This commit is contained in:
John Dvorak
2026-05-22 13:15:35 -07:00
parent 283ab1b39f
commit 9df295b915
7 changed files with 238 additions and 165 deletions
+19
View File
@@ -6,6 +6,8 @@ import assert from 'node:assert'
import { parseSpec } from './parser.js'
import { lowerToCanonical } from './lower-to-canonical.js'
import { KEYWORDS } from './lexer.js'
import { collectAllPredicateNames } from 'imhotep-core'
function parse(source: string) {
return parseSpec(source)
@@ -449,3 +451,20 @@ describe('inline state attachments', () => {
assert.strictEqual(top.state.kind, 'expanded')
})
})
// ---------------------------------------------------------------------------
// Conformance: PREDICATE_SPECS ↔ Lexer KEYWORDS sync
// ---------------------------------------------------------------------------
describe('PredicateSpec ↔ Lexer keyword conformance', () => {
it('every predicate name from PREDICATE_SPECS has a lexer keyword entry', () => {
const allNames = collectAllPredicateNames()
const missing: string[] = []
for (const name of allNames) {
if (!(name in KEYWORDS)) {
missing.push(name)
}
}
assert.deepStrictEqual(missing, [], `Predicate names missing from lexer KEYWORDS: ${missing.join(', ')}`)
})
})
+9 -6
View File
@@ -27,10 +27,14 @@ import type {
ToleranceLiteralNode,
} from 'imhotep-core'
import { isPredicateName, collectSpatialPredicateNames } from 'imhotep-core'
import { isPredicateName, collectSpatialPredicateNames, collectSizePredicateNames, collectTopologyPredicateNames } from 'imhotep-core'
import type { Token } from './lexer.js'
// Cached predicate name sets derived from the spec table.
const SIZE_PREDICATE_NAMES = new Set(collectSizePredicateNames())
const TOPOLOGY_PREDICATE_NAMES = new Set(collectTopologyPredicateNames())
// ---------------------------------------------------------------------------
// Extended TokenKind for grammar checks
// ---------------------------------------------------------------------------
@@ -1002,14 +1006,13 @@ export class GrammarParser {
private parseClause(subject: SelectorRef, start: Point, negated: boolean): any | null {
let assertion: any
// Size assertion
if (this.check('atLeast') || this.check('atMost') || this.check('aspectRatio') || this.check('between')) {
// Dispatch assertion type based on predicate category from spec table.
const kind = this.currentToken().kind as string
if (SIZE_PREDICATE_NAMES.has(kind)) {
assertion = this.parseSizeAssertion(subject, start)
} else if (this.check('clippedBy') || this.check('attachedToScrollContainer') || this.check('escapeClippingChainOf') || this.check('inStackingContext')) {
// Topology assertion
} else if (TOPOLOGY_PREDICATE_NAMES.has(kind)) {
assertion = this.parseTopologyAssertion(subject, start)
} else {
// Relation assertion
assertion = this.parseRelationAssertion(subject, start)
}
+1 -1
View File
@@ -143,7 +143,7 @@ export interface Token {
// Keywords Map
// ---------------------------------------------------------------------------
const KEYWORDS: Record<string, TokenKind> = {
export const KEYWORDS: Record<string, TokenKind> = {
in: 'in',
across: 'across',
always: 'always',