fix: replace as any casts in AST walkers with typed guards + add options to PredicateCall

logic-ast.ts: add optional options?: Record<string, unknown> to
  PredicateCall interface. Previously any predicate needing options
  (e.g., space, dimension, tolerance) smuggled them via (node as any)
  .options, bypassing the type system entirely.

extraction.ts: replace all 18 (node as any).body/.left/.right etc.
  casts with proper type guard narrowing (isForAllFormula, isExistsFormula,
  isAndFormula, isOrFormula, isNotFormula, isImpliesFormula, isPredicateCall).
  Affected functions: collectPredicates, formulaNeedsCssLengthMetrics,
  usesLayoutSpace, computeRequiredFacts & nestDomAncestry, getSelectorsFromFormula.

595 SDK + 57 E2E tests pass.
This commit is contained in:
John Dvorak
2026-05-22 12:18:22 -07:00
parent 07ed316135
commit aa69ddc52f
2 changed files with 54 additions and 45 deletions
+53 -45
View File
@@ -28,6 +28,15 @@ import type {
} from 'imhotep-core'
import { adaptCanonicalWorldToSolver } from 'imhotep-core'
import type { DomainRef, FormulaNode } from 'imhotep-core'
import {
isForAllFormula,
isExistsFormula,
isAndFormula,
isOrFormula,
isNotFormula,
isImpliesFormula,
isPredicateCall,
} from 'imhotep-core'
import {
computeGeometryCacheKey,
readCachedExtractionResult,
@@ -113,18 +122,18 @@ export {
export function collectPredicates(formula: FormulaNode): string[] {
const predicates: string[] = []
function walk(node: FormulaNode) {
if (node.kind === 'predicate') {
predicates.push((node as any).predicate)
} else if (node.kind === 'forall' || node.kind === 'exists') {
walk((node as any).body)
} else if (node.kind === 'and' || node.kind === 'or') {
walk((node as any).left)
walk((node as any).right)
} else if (node.kind === 'not') {
walk((node as any).operand)
} else if (node.kind === 'implies') {
walk((node as any).antecedent)
walk((node as any).consequent)
if (isPredicateCall(node)) {
predicates.push(node.predicate)
} else if (isForAllFormula(node) || isExistsFormula(node)) {
walk(node.body)
} else if (isAndFormula(node) || isOrFormula(node)) {
walk(node.left)
walk(node.right)
} else if (isNotFormula(node)) {
walk(node.operand)
} else if (isImpliesFormula(node)) {
walk(node.antecedent)
walk(node.consequent)
}
}
walk(formula)
@@ -152,26 +161,26 @@ export function formulaNeedsCssLengthMetrics(formula: FormulaNode): boolean {
}
const walk = (node: FormulaNode): void => {
if (needs) return
if (node.kind === 'predicate') {
scanValue((node as any).options)
if (isPredicateCall(node)) {
scanValue(node.options)
return
}
if (node.kind === 'forall' || node.kind === 'exists') {
walk((node as any).body)
if (isForAllFormula(node) || isExistsFormula(node)) {
walk(node.body)
return
}
if (node.kind === 'and' || node.kind === 'or') {
walk((node as any).left)
walk((node as any).right)
if (isAndFormula(node) || isOrFormula(node)) {
walk(node.left)
walk(node.right)
return
}
if (node.kind === 'not') {
walk((node as any).operand)
if (isNotFormula(node)) {
walk(node.operand)
return
}
if (node.kind === 'implies') {
walk((node as any).antecedent)
walk((node as any).consequent)
if (isImpliesFormula(node)) {
walk(node.antecedent)
walk(node.consequent)
}
}
walk(formula)
@@ -206,16 +215,16 @@ export function computeRequiredFacts(formulas: FormulaNode[]): {
let found = false
const scan = (node: FormulaNode) => {
if (found) return
if (node.kind === 'forall' || node.kind === 'exists') {
if (isForAllFormula(node) || isExistsFormula(node)) {
for (const b of node.bindings) {
if (b.domain.parentVar) { found = true; return }
}
scan(node.body)
} else if (node.kind === 'and' || node.kind === 'or') {
} else if (isAndFormula(node) || isOrFormula(node)) {
scan(node.left); scan(node.right)
} else if (node.kind === 'not') {
} else if (isNotFormula(node)) {
scan(node.operand)
} else if (node.kind === 'implies') {
} else if (isImpliesFormula(node)) {
scan(node.antecedent); scan(node.consequent)
}
}
@@ -238,21 +247,20 @@ export function usesLayoutSpace(formula: FormulaNode): boolean {
let found = false
function walk(node: FormulaNode) {
if (found) return
if (node.kind === 'predicate') {
const options = (node as any).options
if (options?.space === 'layout') {
if (isPredicateCall(node)) {
if (node.options?.space === 'layout') {
found = true
}
} else if (node.kind === 'forall' || node.kind === 'exists') {
walk((node as any).body)
} else if (node.kind === 'and' || node.kind === 'or') {
walk((node as any).left)
walk((node as any).right)
} else if (node.kind === 'not') {
walk((node as any).operand)
} else if (node.kind === 'implies') {
walk((node as any).antecedent)
walk((node as any).consequent)
} else if (isForAllFormula(node) || isExistsFormula(node)) {
walk(node.body)
} else if (isAndFormula(node) || isOrFormula(node)) {
walk(node.left)
walk(node.right)
} else if (isNotFormula(node)) {
walk(node.operand)
} else if (isImpliesFormula(node)) {
walk(node.antecedent)
walk(node.consequent)
}
}
walk(formula)
@@ -1332,7 +1340,7 @@ export function getSelectorsFromAssertion(assertion: FluentRelation | FluentAsse
export function getSelectorsFromFormula(formula: FormulaNode): string[] {
const selectors = new Set<string>()
const visit = (node: FormulaNode) => {
if (node.kind === 'forall' || node.kind === 'exists') {
if (isForAllFormula(node) || isExistsFormula(node)) {
for (const b of node.bindings) {
if (b.domain.selector && !b.domain.selector.startsWith('$')) {
selectors.add(b.domain.selector)
@@ -1341,16 +1349,16 @@ export function getSelectorsFromFormula(formula: FormulaNode): string[] {
visit(node.body)
return
}
if (node.kind === 'and' || node.kind === 'or') {
if (isAndFormula(node) || isOrFormula(node)) {
visit(node.left)
visit(node.right)
return
}
if (node.kind === 'not') {
if (isNotFormula(node)) {
visit(node.operand)
return
}
if (node.kind === 'implies') {
if (isImpliesFormula(node)) {
visit(node.antecedent)
visit(node.consequent)
}