From aa69ddc52f8ab44d1cc7dd57a8fba00118cbd210 Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Fri, 22 May 2026 12:18:22 -0700 Subject: [PATCH] fix: replace as any casts in AST walkers with typed guards + add options to PredicateCall logic-ast.ts: add optional options?: Record 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. --- packages/imhotep-core/src/logic-ast.ts | 1 + packages/imhotep-playwright/src/extraction.ts | 98 ++++++++++--------- 2 files changed, 54 insertions(+), 45 deletions(-) diff --git a/packages/imhotep-core/src/logic-ast.ts b/packages/imhotep-core/src/logic-ast.ts index 78aed70..15b4d81 100644 --- a/packages/imhotep-core/src/logic-ast.ts +++ b/packages/imhotep-core/src/logic-ast.ts @@ -81,6 +81,7 @@ export interface PredicateCall extends AstNode { kind: 'predicate' predicate: string args: TermNode[] + options?: Record } // --------------------------------------------------------------------------- diff --git a/packages/imhotep-playwright/src/extraction.ts b/packages/imhotep-playwright/src/extraction.ts index f662b11..4487c37 100644 --- a/packages/imhotep-playwright/src/extraction.ts +++ b/packages/imhotep-playwright/src/extraction.ts @@ -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() 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) }