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
+1
View File
@@ -81,6 +81,7 @@ export interface PredicateCall extends AstNode {
kind: 'predicate' kind: 'predicate'
predicate: string predicate: string
args: TermNode[] args: TermNode[]
options?: Record<string, unknown>
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+53 -45
View File
@@ -28,6 +28,15 @@ import type {
} from 'imhotep-core' } from 'imhotep-core'
import { adaptCanonicalWorldToSolver } from 'imhotep-core' import { adaptCanonicalWorldToSolver } from 'imhotep-core'
import type { DomainRef, FormulaNode } from 'imhotep-core' import type { DomainRef, FormulaNode } from 'imhotep-core'
import {
isForAllFormula,
isExistsFormula,
isAndFormula,
isOrFormula,
isNotFormula,
isImpliesFormula,
isPredicateCall,
} from 'imhotep-core'
import { import {
computeGeometryCacheKey, computeGeometryCacheKey,
readCachedExtractionResult, readCachedExtractionResult,
@@ -113,18 +122,18 @@ export {
export function collectPredicates(formula: FormulaNode): string[] { export function collectPredicates(formula: FormulaNode): string[] {
const predicates: string[] = [] const predicates: string[] = []
function walk(node: FormulaNode) { function walk(node: FormulaNode) {
if (node.kind === 'predicate') { if (isPredicateCall(node)) {
predicates.push((node as any).predicate) predicates.push(node.predicate)
} else if (node.kind === 'forall' || node.kind === 'exists') { } else if (isForAllFormula(node) || isExistsFormula(node)) {
walk((node as any).body) walk(node.body)
} else if (node.kind === 'and' || node.kind === 'or') { } else if (isAndFormula(node) || isOrFormula(node)) {
walk((node as any).left) walk(node.left)
walk((node as any).right) walk(node.right)
} else if (node.kind === 'not') { } else if (isNotFormula(node)) {
walk((node as any).operand) walk(node.operand)
} else if (node.kind === 'implies') { } else if (isImpliesFormula(node)) {
walk((node as any).antecedent) walk(node.antecedent)
walk((node as any).consequent) walk(node.consequent)
} }
} }
walk(formula) walk(formula)
@@ -152,26 +161,26 @@ export function formulaNeedsCssLengthMetrics(formula: FormulaNode): boolean {
} }
const walk = (node: FormulaNode): void => { const walk = (node: FormulaNode): void => {
if (needs) return if (needs) return
if (node.kind === 'predicate') { if (isPredicateCall(node)) {
scanValue((node as any).options) scanValue(node.options)
return return
} }
if (node.kind === 'forall' || node.kind === 'exists') { if (isForAllFormula(node) || isExistsFormula(node)) {
walk((node as any).body) walk(node.body)
return return
} }
if (node.kind === 'and' || node.kind === 'or') { if (isAndFormula(node) || isOrFormula(node)) {
walk((node as any).left) walk(node.left)
walk((node as any).right) walk(node.right)
return return
} }
if (node.kind === 'not') { if (isNotFormula(node)) {
walk((node as any).operand) walk(node.operand)
return return
} }
if (node.kind === 'implies') { if (isImpliesFormula(node)) {
walk((node as any).antecedent) walk(node.antecedent)
walk((node as any).consequent) walk(node.consequent)
} }
} }
walk(formula) walk(formula)
@@ -206,16 +215,16 @@ export function computeRequiredFacts(formulas: FormulaNode[]): {
let found = false let found = false
const scan = (node: FormulaNode) => { const scan = (node: FormulaNode) => {
if (found) return if (found) return
if (node.kind === 'forall' || node.kind === 'exists') { if (isForAllFormula(node) || isExistsFormula(node)) {
for (const b of node.bindings) { for (const b of node.bindings) {
if (b.domain.parentVar) { found = true; return } if (b.domain.parentVar) { found = true; return }
} }
scan(node.body) scan(node.body)
} else if (node.kind === 'and' || node.kind === 'or') { } else if (isAndFormula(node) || isOrFormula(node)) {
scan(node.left); scan(node.right) scan(node.left); scan(node.right)
} else if (node.kind === 'not') { } else if (isNotFormula(node)) {
scan(node.operand) scan(node.operand)
} else if (node.kind === 'implies') { } else if (isImpliesFormula(node)) {
scan(node.antecedent); scan(node.consequent) scan(node.antecedent); scan(node.consequent)
} }
} }
@@ -238,21 +247,20 @@ export function usesLayoutSpace(formula: FormulaNode): boolean {
let found = false let found = false
function walk(node: FormulaNode) { function walk(node: FormulaNode) {
if (found) return if (found) return
if (node.kind === 'predicate') { if (isPredicateCall(node)) {
const options = (node as any).options if (node.options?.space === 'layout') {
if (options?.space === 'layout') {
found = true found = true
} }
} else if (node.kind === 'forall' || node.kind === 'exists') { } else if (isForAllFormula(node) || isExistsFormula(node)) {
walk((node as any).body) walk(node.body)
} else if (node.kind === 'and' || node.kind === 'or') { } else if (isAndFormula(node) || isOrFormula(node)) {
walk((node as any).left) walk(node.left)
walk((node as any).right) walk(node.right)
} else if (node.kind === 'not') { } else if (isNotFormula(node)) {
walk((node as any).operand) walk(node.operand)
} else if (node.kind === 'implies') { } else if (isImpliesFormula(node)) {
walk((node as any).antecedent) walk(node.antecedent)
walk((node as any).consequent) walk(node.consequent)
} }
} }
walk(formula) walk(formula)
@@ -1332,7 +1340,7 @@ export function getSelectorsFromAssertion(assertion: FluentRelation | FluentAsse
export function getSelectorsFromFormula(formula: FormulaNode): string[] { export function getSelectorsFromFormula(formula: FormulaNode): string[] {
const selectors = new Set<string>() const selectors = new Set<string>()
const visit = (node: FormulaNode) => { const visit = (node: FormulaNode) => {
if (node.kind === 'forall' || node.kind === 'exists') { if (isForAllFormula(node) || isExistsFormula(node)) {
for (const b of node.bindings) { for (const b of node.bindings) {
if (b.domain.selector && !b.domain.selector.startsWith('$')) { if (b.domain.selector && !b.domain.selector.startsWith('$')) {
selectors.add(b.domain.selector) selectors.add(b.domain.selector)
@@ -1341,16 +1349,16 @@ export function getSelectorsFromFormula(formula: FormulaNode): string[] {
visit(node.body) visit(node.body)
return return
} }
if (node.kind === 'and' || node.kind === 'or') { if (isAndFormula(node) || isOrFormula(node)) {
visit(node.left) visit(node.left)
visit(node.right) visit(node.right)
return return
} }
if (node.kind === 'not') { if (isNotFormula(node)) {
visit(node.operand) visit(node.operand)
return return
} }
if (node.kind === 'implies') { if (isImpliesFormula(node)) {
visit(node.antecedent) visit(node.antecedent)
visit(node.consequent) visit(node.consequent)
} }