diff --git a/packages/compile/src/validations.ts b/packages/compile/src/validations.ts index a21f77d8..ce9cf5e8 100644 --- a/packages/compile/src/validations.ts +++ b/packages/compile/src/validations.ts @@ -262,7 +262,7 @@ export function trait(input: File) { uid: String(uid).trim(), space: String(space).trim(), property: String(property).trim(), - value: Boolean(value), + value: value === 'undecidable' ? 'undecidable' : Boolean(value), counterexamples_id, refs, description: String(description).trim(), @@ -275,7 +275,7 @@ export function trait(input: File) { required(trait, 'space', error) required(trait, 'property', error) required(trait, 'value', error) - required(trait, 'description', error) + // description is optional for traits noExtras(rest, error) return trait diff --git a/packages/core/src/Formula.ts b/packages/core/src/Formula.ts index dd9c3aec..29fb8a8c 100644 --- a/packages/core/src/Formula.ts +++ b/packages/core/src/Formula.ts @@ -3,8 +3,10 @@ import { parse as _parse } from './Formula/Grammar.js' import { union } from './Util.js' +export type TruthValue = boolean | 'undecidable' + type F

= - | { kind: 'atom'; property: P; value: boolean } + | { kind: 'atom'; property: P; value: TruthValue } | { kind: 'or'; subs: F

[] } | { kind: 'and'; subs: F

[] } @@ -12,7 +14,7 @@ function atomSchema

(p: z.ZodSchema

): z.ZodSchema> { return z.object({ kind: z.literal('atom'), property: p, - value: z.boolean(), + value: z.union([z.boolean(), z.literal('undecidable')]), }) as any } @@ -34,7 +36,7 @@ export const formulaSchema =

(p: z.ZodSchema

): z.ZodSchema> => export interface Atom { kind: 'atom' property: P - value: boolean | X + value: TruthValue | X } export interface And { @@ -59,7 +61,7 @@ export function or(...subs: Formula[]): Or { export function atom( property: P, - value: boolean | X = true, + value: TruthValue | X = true, ): Atom { return { kind: 'atom', property, value } } @@ -78,6 +80,7 @@ export function render(f: Formula, term: (t: T) => string): string { case 'atom': // eslint-disable-next-line no-case-declarations const name = term(f.property) + if (f.value === 'undecidable') return '⊬' + name return f.value ? name : '¬' + name case 'and': return '(' + f.subs.map(sf => render(sf, term)).join(' ∧ ') + ')' @@ -86,9 +89,21 @@ export function render(f: Formula, term: (t: T) => string): string { } } +export function hasUndecidable(f: Formula): boolean { + switch (f.kind) { + case 'atom': + return f.value === 'undecidable' + default: + return f.subs.some(hasUndecidable) + } +} + export function negate

(formula: Formula

): Formula

{ switch (formula.kind) { case 'atom': + if (formula.value === 'undecidable') { + throw new Error('Cannot negate an undecidable atom') + } return atom(formula.property, !formula.value) case 'and': return or(...formula.subs.map(negate)) @@ -130,7 +145,7 @@ export function compact( export function evaluate( f: Formula, - traits: Map, + traits: Map, ): boolean | undefined { let result: boolean | undefined @@ -192,8 +207,8 @@ type Serialized = | { and: Serialized[] } | { or: Serialized[] } | { inner: Serialized } - | { property: string; value: boolean | X } - | Record + | { property: string; value: TruthValue | X } + | Record export function fromJSON(json: Serialized): Formula { if ('and' in json && typeof json.and === 'object') { @@ -211,7 +226,7 @@ export function fromJSON(json: Serialized): Formula { throw `cannot cast object with ${entries.length} keys to atom` } - if (typeof entries[0][1] !== 'boolean') { + if (typeof entries[0][1] !== 'boolean' && entries[0][1] !== 'undecidable') { throw `cannot cast object with non-boolean value` } diff --git a/packages/core/src/Logic/Derivations.ts b/packages/core/src/Logic/Derivations.ts index ef072910..893802c6 100644 --- a/packages/core/src/Logic/Derivations.ts +++ b/packages/core/src/Logic/Derivations.ts @@ -1,10 +1,11 @@ +import { type TruthValue } from '../Formula.js' import { Id } from './Types.js' type Evidence = [TheoremId, PropertyId[]] export type Derivation = { property: PropertyId - value: boolean + value: TruthValue proof: Proof } @@ -16,17 +17,17 @@ export type Proof = { export class Derivations { private evidence: Map> private given: Set - private traits: Map + private traits: Map constructor(assumptions: PropertyId[] = []) { this.given = new Set(assumptions) this.evidence = new Map>() - this.traits = new Map() + this.traits = new Map() } addEvidence( property: PropertyId, - value: boolean, + value: TruthValue, theorem: TheoremId, support: PropertyId[], ): void { @@ -34,10 +35,14 @@ export class Derivations { this.traits.set(property, value) } + overrideGiven(property: PropertyId): void { + this.given.delete(property) + } + all(): Derivation[] { const result: Derivation[] = [] - this.traits.forEach((value: boolean, property: PropertyId) => { + this.traits.forEach((value: TruthValue, property: PropertyId) => { const proof = this.proof(property) if (!proof || proof === 'given') { return diff --git a/packages/core/src/Logic/Prover.ts b/packages/core/src/Logic/Prover.ts index b09e38d4..c90e6f98 100644 --- a/packages/core/src/Logic/Prover.ts +++ b/packages/core/src/Logic/Prover.ts @@ -1,9 +1,11 @@ import { + type TruthValue, And, Atom, Formula, Or, evaluate, + hasUndecidable, negate, properties, } from '../Formula.js' @@ -35,24 +37,28 @@ export default class Prover< PropertyId >, > { - private traits: Map + private traits: Map private derivations: Derivations private queue: Queue constructor( implications: ImplicationIndex, - traits = new Map(), + traits = new Map(), ) { this.traits = traits this.derivations = new Derivations([...traits.keys()]) this.queue = new Queue(implications) - traits.forEach((_: boolean, id: PropertyId) => { + traits.forEach((_: TruthValue, id: PropertyId) => { this.queue.mark(id) }) } + enqueue(theorems: Theorem[]): void { + this.queue.addAll(theorems) + } + run(): Result { let theorem while ((theorem = this.queue.shift())) { @@ -88,14 +94,14 @@ export default class Prover< const av = evaluate(a, this.traits) const cv = evaluate(c, this.traits) - if (av === true && cv === false) { + if (av === true && cv === false && !hasUndecidable(c)) { return this.contradiction(implication.id, [ ...properties(a), ...properties(c), ]) } else if (av === true) { return this.force(implication.id, c, [...properties(a)]) - } else if (cv === false) { + } else if (cv === false && !hasUndecidable(a) && !hasUndecidable(c)) { return this.force(implication.id, negate(a), [...properties(c)]) } } @@ -114,16 +120,39 @@ export default class Prover< ): Contradiction | undefined { const property = formula.property - if (this.traits.has(property)) { - if (this.traits.get(property) !== formula.value) { - return this.contradiction(theorem, [...support, property]) - } else { + const existing = this.traits.get(property) + + if (existing !== undefined) { + if (existing === formula.value) { + return + } + // undecidable has lower priority: true/false overrides it + if (existing === 'undecidable' && formula.value !== 'undecidable') { + this.traits.set(property, formula.value as TruthValue) + this.derivations.overrideGiven(property) + this.derivations.addEvidence( + property, + formula.value as TruthValue, + theorem, + support, + ) + this.queue.mark(property) + return + } + // don't let undecidable override a definite value + if (formula.value === 'undecidable') { return } + return this.contradiction(theorem, [...support, property]) } - this.traits.set(property, formula.value) - this.derivations.addEvidence(property, formula.value, theorem, support) + this.traits.set(property, formula.value as TruthValue) + this.derivations.addEvidence( + property, + formula.value as TruthValue, + theorem, + support, + ) this.queue.mark(property) } diff --git a/packages/core/src/Logic/Queue.ts b/packages/core/src/Logic/Queue.ts index 40b8885c..28cc84dd 100644 --- a/packages/core/src/Logic/Queue.ts +++ b/packages/core/src/Logic/Queue.ts @@ -18,6 +18,12 @@ export default class Queue< this.index.withProperty(property).forEach(i => this.queue.add(i)) } + addAll(theorems: Theorem[]): void { + for (const t of theorems) { + this.queue.add(t) + } + } + shift(): Theorem | undefined { const result = this.queue.values().next() if (result.done) { diff --git a/packages/core/src/Logic/index.ts b/packages/core/src/Logic/index.ts index 88d2d015..a69a405c 100644 --- a/packages/core/src/Logic/index.ts +++ b/packages/core/src/Logic/index.ts @@ -2,18 +2,35 @@ export { default as ImplicationIndex } from './ImplicationIndex.js' export { Derivations } from './Derivations.js' export { Contradiction, Proof } from './Prover.js' -import { Formula, negate } from '../Formula.js' +import { type TruthValue, Formula, hasUndecidable, negate } from '../Formula.js' import ImplicationIndex from './ImplicationIndex.js' import Prover, { Contradiction, Proof, Result } from './Prover.js' import { Id } from './Types.js' // Given a collection of implications and a collection of traits for an object, -// find either the collection of derivable traits, or a contradiction +// find either the collection of derivable traits, or a contradiction. +// Undecidable theorems are deferred to a second phase so they don't interfere +// with normal deduction performance. export function deduceTraits( implications: ImplicationIndex, - traits: Map, + traits: Map, ): Result { - return new Prover(implications, traits).run() + const normal = implications.all.filter( + t => !hasUndecidable(t.when) && !hasUndecidable(t.then), + ) + const undecidable = implications.all.filter( + t => hasUndecidable(t.when) || hasUndecidable(t.then), + ) + + const normalIndex = new ImplicationIndex(normal) + const prover = new Prover(normalIndex, traits) + const result = prover.run() + if (result.kind === 'contradiction' || undecidable.length === 0) { + return result + } + + prover.enqueue(undecidable) + return prover.run() } // Given a collection of implications and a candidate formula, @@ -71,14 +88,16 @@ export function proveTheorem( return formatProof(result.contradiction) } - contradiction = prover.force('given', negate(then)) - if (contradiction) { - return formatProof(contradiction) - } + if (!hasUndecidable(then)) { + contradiction = prover.force('given', negate(then)) + if (contradiction) { + return formatProof(contradiction) + } - result = prover.run() - if (result.kind === 'contradiction') { - return formatProof(result.contradiction) + result = prover.run() + if (result.kind === 'contradiction') { + return formatProof(result.contradiction) + } } } diff --git a/packages/core/src/Trait.ts b/packages/core/src/Trait.ts index a9188ed9..12a68f28 100644 --- a/packages/core/src/Trait.ts +++ b/packages/core/src/Trait.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { Id as BaseId } from './Id.js' +import { type TruthValue } from './Formula.js' import { Record, recordSchema } from './Record.js' export const proofSchema = (id: z.ZodSchema): z.ZodSchema> => @@ -18,7 +19,7 @@ export const traitSchema = (id: z.ZodSchema): z.ZodSchema> => z.object({ space: id, property: id, - value: z.boolean(), + value: z.union([z.boolean(), z.literal('undecidable')]), proof: proofSchema(id).optional(), }), recordSchema, @@ -27,6 +28,6 @@ export const traitSchema = (id: z.ZodSchema): z.ZodSchema> => export type Trait = Record & { space: Id property: Id - value: boolean + value: TruthValue proof?: Proof } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bb346fae..7be8f716 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,7 +5,14 @@ import { theoremSchema } from './Theorem.js' import { traitSchema } from './Trait.js' export { type Bundle } from './Bundle.js' -export { type Formula, type And, type Atom, type Or } from './Formula.js' +export { + type Formula, + type And, + type Atom, + type Or, + type TruthValue, + hasUndecidable, +} from './Formula.js' export { parser } from './Parser.js' export { type Property, PropertyPage } from './Property.js' export { diff --git a/packages/viewer/src/components/Shared/Formula/Atom.svelte b/packages/viewer/src/components/Shared/Formula/Atom.svelte index ff37fe10..e9fd64de 100644 --- a/packages/viewer/src/components/Shared/Formula/Atom.svelte +++ b/packages/viewer/src/components/Shared/Formula/Atom.svelte @@ -8,6 +8,12 @@ export let link: boolean = true -{value.value === null ? '?' : value.value ? '' : '¬'}{#if link}{:else}{/if} +{value.value === null + ? '?' + : value.value === 'undecidable' + ? '⊬' + : value.value + ? '' + : '¬'}{#if link}{:else}{/if} diff --git a/packages/viewer/src/components/Theorems/Show.svelte b/packages/viewer/src/components/Theorems/Show.svelte index 8f469ca8..6e6d6c9b 100644 --- a/packages/viewer/src/components/Theorems/Show.svelte +++ b/packages/viewer/src/components/Theorems/Show.svelte @@ -3,12 +3,13 @@ import { Link, References, Tabs, Source, Typeset } from '../Shared' import Name from './Name.svelte' import Converse from './Converse.svelte' + import hasUndecidable from '@pibase/core' // FIXME not sure where to import this from but I doubt core is best export let theorem: Theorem export let tab: 'converse' | 'references' export let rel: string | undefined = undefined - const tabs = ['converse', 'references'] as const + const tabs: readonly ['converse', 'references'] | readonly ['references'] = (hasUndecidable(theorem.when) || hasUndecidable(theorem.then)) ? ['references'] : ['converse', 'references']

Theorem

diff --git a/packages/viewer/src/components/Traits/FilterButtons.svelte b/packages/viewer/src/components/Traits/FilterButtons.svelte index a3531b1e..c571ca80 100644 --- a/packages/viewer/src/components/Traits/FilterButtons.svelte +++ b/packages/viewer/src/components/Traits/FilterButtons.svelte @@ -46,6 +46,17 @@ > +