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:
@@ -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(', ')}`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user