From 1ac30c6e180a376ff0fbd42be4d0c8a0aa46dbe5 Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Fri, 22 May 2026 10:48:26 -0700 Subject: [PATCH] fix: support between predicate in FOL dense DSL - grammar.ts: add 'between' to isKeywordThatCanBePredicate() so the parser recognizes it as a valid predicate keyword in forall/exists formula bodies - compiler.ts: add special case in compileDenseFOLToFormula for between(, min, max, dimension?) that extracts numeric args into options ({min, max, dimension}) instead of dropping them in the generic arg loop. The existing betweenPredicate evaluator already handles these. - fol-dense-combinations.test.ts: replace GAP test with two verified-working tests for between and between with dimension --- packages/imhotep-dsl/src/compiler.ts | 29 +++++++++++++++++++ .../src/fol-dense-combinations.test.ts | 21 ++++++++++---- packages/imhotep-dsl/src/grammar.ts | 2 +- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/packages/imhotep-dsl/src/compiler.ts b/packages/imhotep-dsl/src/compiler.ts index 980eb69..04b741e 100644 --- a/packages/imhotep-dsl/src/compiler.ts +++ b/packages/imhotep-dsl/src/compiler.ts @@ -801,6 +801,35 @@ export function compileDenseFOLToFormula(dslFormula: DslFormulaNode): FormulaNod } case 'PredicateCall': { const pc = node as DslPredicateCall + + // between($x, min, max, dimension?): extract numeric args into + // options so the betweenPredicate evaluator can consume them. + // The generic arg loop drops numbers, so we handle this up-front. + if (pc.name === 'between') { + const options: Record = {} + const compiledArgs: VariableRef[] = [] + + for (let i = 0; i < pc.args.length; i++) { + const arg = pc.args[i] + if (i === 0) { + compiledArgs.push(convertTerm(arg) as VariableRef) + } else if (typeof arg === 'number') { + if (i === 1) options.min = arg + else if (i === 2) options.max = arg + } else if (typeof arg === 'string') { + options.dimension = arg + } + } + + return { + type: 'FormulaNode', + kind: 'predicate', + predicate: 'between', + args: compiledArgs, + ...(Object.keys(options).length > 0 ? { options } : {}), + } as FormulaNode + } + const implicitBindings: TupleBinding[] = [] const compiledArgs: VariableRef[] = [] diff --git a/packages/imhotep-dsl/src/fol-dense-combinations.test.ts b/packages/imhotep-dsl/src/fol-dense-combinations.test.ts index 79f2ca2..7e9ecd5 100644 --- a/packages/imhotep-dsl/src/fol-dense-combinations.test.ts +++ b/packages/imhotep-dsl/src/fol-dense-combinations.test.ts @@ -436,13 +436,24 @@ describe('FOL Dense DSL - Solver Formula Lowering', () => { describe('FOL Dense DSL - Gap Detection', () => { // These tests document known gaps in the dense DSL FOL support - it('GAP: dense DSL does not support size between assertions', () => { - // between is only in fluent API, not dense DSL grammar + it('between parses as predicate in dense DSL', () => { const source = `forall $btn in elements('.button'):\n between($btn, 44, 100)` const result = parseSpec(source) - // May parse as predicate call or fail - assert.ok(result.diagnostics.length > 0 || result.ast.children.length === 0, - 'Expected parse failure or no formula for between in dense DSL') + assert.strictEqual(result.diagnostics.length, 0, `Expected no diagnostics but got: ${result.diagnostics.map(d => d.message).join(', ')}`) + const formula = getFormula(result.ast) + assert.ok(formula, 'Expected formula for between in dense DSL') + const compiled = compileDenseFOLToFormula(formula!) + assert.ok(compiled, 'Expected compiled formula for between') + }) + + it('between with dimension parses as predicate in dense DSL', () => { + const source = `forall $btn in elements('.button'):\n between($btn, 44, 100, 'width')` + const result = parseSpec(source) + assert.strictEqual(result.diagnostics.length, 0, `Expected no diagnostics but got: ${result.diagnostics.map(d => d.message).join(', ')}`) + const formula = getFormula(result.ast) + assert.ok(formula, 'Expected formula for between with dimension in dense DSL') + const compiled = compileDenseFOLToFormula(formula!) + assert.ok(compiled, 'Expected compiled formula for between with dimension') }) it('separatedFrom parses as relation predicate in dense DSL', () => { diff --git a/packages/imhotep-dsl/src/grammar.ts b/packages/imhotep-dsl/src/grammar.ts index 98fdff6..23bd810 100644 --- a/packages/imhotep-dsl/src/grammar.ts +++ b/packages/imhotep-dsl/src/grammar.ts @@ -665,7 +665,7 @@ export class GrammarParser { // Spatial aliases 'beside', 'nextTo', 'adjacent', 'touching', 'near', 'under', 'within', // Size predicates that can appear in FOL formula bodies - 'width', 'height', 'size', + 'width', 'height', 'size', 'between', ] return predicateKinds.includes(kind) }