294 lines
9.5 KiB
TypeScript
294 lines
9.5 KiB
TypeScript
|
|
/**
|
||
|
|
* Tests for the FOL compiler's cross-package compatibility.
|
||
|
|
*
|
||
|
|
* Verifies that duck-type checks replace instanceof so that assertions
|
||
|
|
* compile correctly when packages are symlinked or duplicated.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it } from 'node:test'
|
||
|
|
import assert from 'node:assert'
|
||
|
|
import { compileAssertionsToFOL } from './fol-compiler.js'
|
||
|
|
|
||
|
|
describe('fol-compiler duck-type compilation', () => {
|
||
|
|
it('compiles a plain object resembling FluentRelation', () => {
|
||
|
|
const plainRelation = {
|
||
|
|
relation: 'leftOf',
|
||
|
|
assertion: {
|
||
|
|
getSubject: () => '.a',
|
||
|
|
getQuantifier: () => 'all',
|
||
|
|
},
|
||
|
|
referenceSelector: '.b',
|
||
|
|
options: { minGap: 8 },
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([plainRelation as any])
|
||
|
|
assert.ok(formula !== null, 'Plain object FluentRelation should compile to a formula')
|
||
|
|
assert.strictEqual(formula?.kind, 'forall')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('compiles a plain object resembling FluentQuantifier', () => {
|
||
|
|
const plainQuantifier = {
|
||
|
|
bindings: [{ selector: '.item' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'FormulaNode',
|
||
|
|
kind: 'forall',
|
||
|
|
bindings: [],
|
||
|
|
body: { type: 'FormulaNode', kind: 'predicate', predicate: 'true', args: [] },
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([plainQuantifier as any])
|
||
|
|
assert.ok(formula !== null, 'Plain object FluentQuantifier should compile to a formula')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('ignores objects that do not duck-type as FluentRelation or FluentQuantifier', () => {
|
||
|
|
const plainObj = { foo: 'bar' }
|
||
|
|
const formula = compileAssertionsToFOL([plainObj as any])
|
||
|
|
assert.strictEqual(formula, null)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('rejects null and primitive values', () => {
|
||
|
|
assert.strictEqual(compileAssertionsToFOL([null as any]), null)
|
||
|
|
assert.strictEqual(compileAssertionsToFOL([42 as any]), null)
|
||
|
|
assert.strictEqual(compileAssertionsToFOL(['string' as any]), null)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('adapts grammar.ts ForAll shape to logic-ast.ts shape', () => {
|
||
|
|
const grammarForAll = {
|
||
|
|
bindings: [{ selector: '.item' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'ForAll',
|
||
|
|
variable: { type: 'VariableRef', name: '$x' },
|
||
|
|
domain: { type: 'DomainRef', kind: 'elements', selector: '.item' },
|
||
|
|
body: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'leftOf',
|
||
|
|
args: [{ type: 'VariableRef', name: '$x' }],
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([grammarForAll as any])
|
||
|
|
assert.ok(formula !== null)
|
||
|
|
assert.strictEqual(formula!.kind, 'forall')
|
||
|
|
assert.strictEqual((formula as any).bindings[0].variables[0], '$x')
|
||
|
|
assert.strictEqual((formula as any).bindings[0].domain.domain, 'elements')
|
||
|
|
assert.strictEqual((formula as any).body.kind, 'predicate')
|
||
|
|
assert.strictEqual((formula as any).body.predicate, 'leftOf')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('adapts grammar.ts Exists shape to logic-ast.ts shape', () => {
|
||
|
|
const grammarExists = {
|
||
|
|
bindings: [{ selector: '.item' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'Exists',
|
||
|
|
variable: { type: 'VariableRef', name: '$y' },
|
||
|
|
domain: { type: 'DomainRef', kind: 'elements', selector: '.item' },
|
||
|
|
body: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'above',
|
||
|
|
args: [{ type: 'VariableRef', name: '$y' }],
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([grammarExists as any])
|
||
|
|
assert.ok(formula !== null)
|
||
|
|
assert.strictEqual(formula!.kind, 'exists')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('adapts grammar.ts And shape to logic-ast.ts shape', () => {
|
||
|
|
const grammarAnd = {
|
||
|
|
bindings: [{ selector: '.item' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'And',
|
||
|
|
left: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'leftOf',
|
||
|
|
args: [{ type: 'VariableRef', name: '$x' }],
|
||
|
|
},
|
||
|
|
right: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'above',
|
||
|
|
args: [{ type: 'VariableRef', name: '$x' }],
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([grammarAnd as any])
|
||
|
|
assert.ok(formula !== null)
|
||
|
|
assert.strictEqual(formula!.kind, 'and')
|
||
|
|
assert.strictEqual((formula as any).left.kind, 'predicate')
|
||
|
|
assert.strictEqual((formula as any).right.kind, 'predicate')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('adapts grammar.ts Not shape to logic-ast.ts shape', () => {
|
||
|
|
const grammarNot = {
|
||
|
|
bindings: [{ selector: '.item' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'Not',
|
||
|
|
operand: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'overlaps',
|
||
|
|
args: [{ type: 'VariableRef', name: '$x' }],
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([grammarNot as any])
|
||
|
|
assert.ok(formula !== null)
|
||
|
|
assert.strictEqual(formula!.kind, 'not')
|
||
|
|
assert.strictEqual((formula as any).operand.kind, 'predicate')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('adapts grammar.ts Implies shape to logic-ast.ts shape', () => {
|
||
|
|
const grammarImplies = {
|
||
|
|
bindings: [{ selector: '.item' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'Implies',
|
||
|
|
left: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'inside',
|
||
|
|
args: [{ type: 'VariableRef', name: '$x' }],
|
||
|
|
},
|
||
|
|
right: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'alignedWith',
|
||
|
|
args: [{ type: 'VariableRef', name: '$x' }],
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([grammarImplies as any])
|
||
|
|
assert.ok(formula !== null)
|
||
|
|
assert.strictEqual(formula!.kind, 'implies')
|
||
|
|
assert.strictEqual((formula as any).antecedent.kind, 'predicate')
|
||
|
|
assert.strictEqual((formula as any).consequent.kind, 'predicate')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('adapts deeply nested grammar.ts shape', () => {
|
||
|
|
const grammarNested = {
|
||
|
|
bindings: [{ selector: '.item' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'ForAll',
|
||
|
|
variable: { type: 'VariableRef', name: '$x' },
|
||
|
|
domain: { type: 'DomainRef', kind: 'elements', selector: '.item' },
|
||
|
|
body: {
|
||
|
|
type: 'And',
|
||
|
|
left: {
|
||
|
|
type: 'Not',
|
||
|
|
operand: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'overlaps',
|
||
|
|
args: [{ type: 'VariableRef', name: '$x' }],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
right: {
|
||
|
|
type: 'Exists',
|
||
|
|
variable: { type: 'VariableRef', name: '$y' },
|
||
|
|
domain: { type: 'DomainRef', kind: 'elements', selector: '.ref' },
|
||
|
|
body: {
|
||
|
|
type: 'PredicateCall',
|
||
|
|
name: 'leftOf',
|
||
|
|
args: [
|
||
|
|
{ type: 'VariableRef', name: '$x' },
|
||
|
|
{ type: 'VariableRef', name: '$y' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([grammarNested as any])
|
||
|
|
assert.ok(formula !== null)
|
||
|
|
assert.strictEqual(formula!.kind, 'forall')
|
||
|
|
const body = (formula as any).body
|
||
|
|
assert.strictEqual(body.kind, 'and')
|
||
|
|
assert.strictEqual(body.left.kind, 'not')
|
||
|
|
assert.strictEqual(body.right.kind, 'exists')
|
||
|
|
assert.strictEqual(body.right.body.kind, 'predicate')
|
||
|
|
assert.strictEqual(body.right.body.args.length, 2)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('passes through already-correct logic-ast.ts shape', () => {
|
||
|
|
const logicAstForAll = {
|
||
|
|
bindings: [{ selector: '.item' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'FormulaNode',
|
||
|
|
kind: 'forall',
|
||
|
|
bindings: [
|
||
|
|
{
|
||
|
|
type: 'TupleBinding',
|
||
|
|
variables: ['$x'],
|
||
|
|
domain: { type: 'DomainRef', domain: 'elements', selector: '.item' },
|
||
|
|
},
|
||
|
|
],
|
||
|
|
body: { type: 'FormulaNode', kind: 'predicate', predicate: 'true', args: [] },
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([logicAstForAll as any])
|
||
|
|
assert.ok(formula !== null)
|
||
|
|
assert.strictEqual(formula!.kind, 'forall')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('compiles mixed arrays of plain objects', () => {
|
||
|
|
const plainRelation = {
|
||
|
|
relation: 'above',
|
||
|
|
assertion: {
|
||
|
|
getSubject: () => '.c',
|
||
|
|
getQuantifier: () => 'all',
|
||
|
|
},
|
||
|
|
referenceSelector: '.d',
|
||
|
|
options: { minGap: 4 },
|
||
|
|
}
|
||
|
|
const plainQuantifier = {
|
||
|
|
bindings: [{ selector: '.x' }],
|
||
|
|
toFormula: () => ({
|
||
|
|
type: 'FormulaNode',
|
||
|
|
kind: 'exists',
|
||
|
|
bindings: [],
|
||
|
|
body: { type: 'FormulaNode', kind: 'predicate', predicate: 'true', args: [] },
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
const formula = compileAssertionsToFOL([plainRelation as any, plainQuantifier as any])
|
||
|
|
assert.ok(formula !== null)
|
||
|
|
assert.strictEqual(formula?.kind, 'and')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('propagates assertion layout space to simple relations', () => {
|
||
|
|
const relation = {
|
||
|
|
relation: 'leftOf',
|
||
|
|
assertion: {
|
||
|
|
getSubject: () => '.a',
|
||
|
|
getQuantifier: () => 'all',
|
||
|
|
getSpace: () => 'layout',
|
||
|
|
},
|
||
|
|
referenceSelector: '.b',
|
||
|
|
options: {},
|
||
|
|
}
|
||
|
|
|
||
|
|
const formula = compileAssertionsToFOL([relation as any]) as any
|
||
|
|
const predicate = formula.body
|
||
|
|
assert.strictEqual(predicate.kind, 'predicate')
|
||
|
|
assert.strictEqual(predicate.options.space, 'layout')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('propagates assertion visual space to compound relations', () => {
|
||
|
|
const relation = {
|
||
|
|
relation: 'above',
|
||
|
|
assertion: {
|
||
|
|
getSubject: () => '.a',
|
||
|
|
getQuantifier: () => 'all',
|
||
|
|
getSpace: () => 'visual',
|
||
|
|
},
|
||
|
|
referenceSelector: '.c',
|
||
|
|
options: {},
|
||
|
|
_compoundOperator: 'and',
|
||
|
|
_compoundParts: [
|
||
|
|
{ relation: 'leftOf', referenceSelector: '.b', options: {} },
|
||
|
|
{ relation: 'above', referenceSelector: '.c', options: {} },
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
const formula = compileAssertionsToFOL([relation as any]) as any
|
||
|
|
assert.strictEqual(formula.kind, 'forall')
|
||
|
|
const andBody = formula.body
|
||
|
|
assert.strictEqual(andBody.kind, 'and')
|
||
|
|
assert.strictEqual(andBody.left.body.options.space, 'visual')
|
||
|
|
assert.strictEqual(andBody.right.body.options.space, 'visual')
|
||
|
|
})
|
||
|
|
})
|