Refactor (2)
This commit is contained in:
@@ -3,11 +3,11 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./syntax": "./syntax.ts",
|
||||
"./semantics": "./semantics.ts",
|
||||
"./parser": "./parser.ts",
|
||||
"./sat": "./sat.ts"
|
||||
".": "./src/index.ts",
|
||||
"./syntax": "./src/syntax.ts",
|
||||
"./semantics": "./src/semantics.ts",
|
||||
"./parser": "./src/parser.ts",
|
||||
"./sat": "./src/sat.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./syntax";
|
||||
export * from "./semantics";
|
||||
export * from "./parser";
|
||||
export * from "./sat";
|
||||
@@ -1,19 +1,37 @@
|
||||
import type {} from "@cannorin/utils/headless";
|
||||
import { type Frame, type World, left, right, worlds } from "./semantics";
|
||||
import { BitSet, maximal } from "@cannorin/utils";
|
||||
import {
|
||||
type Frame,
|
||||
type Model,
|
||||
type World,
|
||||
left,
|
||||
right,
|
||||
worlds,
|
||||
} from "./semantics";
|
||||
import {
|
||||
type Formula,
|
||||
type NNFFormula,
|
||||
type PropVar,
|
||||
not,
|
||||
propVars,
|
||||
simplify,
|
||||
tagNNF,
|
||||
toNNF,
|
||||
} from "./syntax";
|
||||
import { maximal } from "./utils";
|
||||
|
||||
export type Constraints =
|
||||
| true
|
||||
| ReadonlySet<`${World}${PropVar}` | `!${World}${PropVar}`>;
|
||||
const allValuations = worlds.flatMap((w) =>
|
||||
propVars.map((p) => `${w}${p}` as const),
|
||||
);
|
||||
|
||||
const valuationSet = new BitSet(allValuations);
|
||||
|
||||
export type Constraints = {
|
||||
positive: number;
|
||||
negative: number;
|
||||
};
|
||||
|
||||
const empty: Constraints = {
|
||||
positive: valuationSet.empty,
|
||||
negative: valuationSet.empty,
|
||||
};
|
||||
|
||||
export type Context = {
|
||||
notFml: NNFFormula<number>;
|
||||
@@ -103,30 +121,25 @@ export function buildContext(frame: Frame, fml: Formula): Context {
|
||||
return { notFml, next, memo: new Map(), constant };
|
||||
}
|
||||
|
||||
const subsumes = (c1: Constraints, c2: Constraints): boolean => {
|
||||
if (c1 === true) return c2 === true;
|
||||
if (c2 === true) return true;
|
||||
for (const v of c2.values()) {
|
||||
if (!c1.has(v)) return false;
|
||||
}
|
||||
return true;
|
||||
const isWeaker = (c1: Constraints, c2: Constraints): boolean => {
|
||||
return (
|
||||
valuationSet.isSuperset(c1.positive, c2.positive) &&
|
||||
valuationSet.isSuperset(c1.negative, c2.negative)
|
||||
);
|
||||
};
|
||||
|
||||
const union = (c1: Constraints, c2: Constraints): Constraints => {
|
||||
if (c1 === true) return c2;
|
||||
if (c2 === true) return c1;
|
||||
return new Set([...c1.values(), ...c2.values()]);
|
||||
return {
|
||||
positive: valuationSet.union(c1.positive, c2.positive),
|
||||
negative: valuationSet.union(c1.negative, c2.negative),
|
||||
};
|
||||
};
|
||||
|
||||
const consistent = (c: Constraints) => {
|
||||
if (c === true) return true;
|
||||
for (const v of c.values()) {
|
||||
if (!v.startsWith("!") && c.has(`!${v}`)) return false;
|
||||
}
|
||||
return true;
|
||||
return valuationSet.isDisjoint(c.positive, c.negative);
|
||||
};
|
||||
|
||||
export function sat(
|
||||
function sat(
|
||||
frame: Frame,
|
||||
fml: NNFFormula<number>,
|
||||
world: World,
|
||||
@@ -134,7 +147,7 @@ export function sat(
|
||||
): Constraints[] {
|
||||
const key = `${world}:${fml.tag}` as const;
|
||||
const c = ctx.constant.get(key);
|
||||
if (c === true) return [true];
|
||||
if (c === true) return [empty];
|
||||
if (c === false) return [];
|
||||
|
||||
let result = ctx.memo.get(key);
|
||||
@@ -142,16 +155,26 @@ export function sat(
|
||||
|
||||
switch (fml.type) {
|
||||
case "top":
|
||||
result = [true];
|
||||
result = [empty];
|
||||
break;
|
||||
case "bot":
|
||||
result = [];
|
||||
break;
|
||||
case "propvar":
|
||||
result = [new Set([`${world}${fml.name}` as const])];
|
||||
result = [
|
||||
{
|
||||
positive: valuationSet.create(`${world}${fml.name}`),
|
||||
negative: valuationSet.empty,
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "not":
|
||||
result = [new Set([`!${world}${fml.fml.name}` as const])];
|
||||
result = [
|
||||
{
|
||||
positive: valuationSet.empty,
|
||||
negative: valuationSet.create(`${world}${fml.fml.name}`),
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "box": {
|
||||
result = ctx.next[world]
|
||||
@@ -162,16 +185,16 @@ export function sat(
|
||||
current.flatMap((c1) =>
|
||||
prev.map((c2) => union(c1, c2)).filter(consistent),
|
||||
),
|
||||
subsumes,
|
||||
isWeaker,
|
||||
),
|
||||
[true],
|
||||
[empty],
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "diamond": {
|
||||
result = maximal(
|
||||
ctx.next[world].flatMap((w) => sat(frame, fml.fml, w, ctx)),
|
||||
subsumes,
|
||||
isWeaker,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -185,9 +208,9 @@ export function sat(
|
||||
current.flatMap((c1) =>
|
||||
prev.map((c2) => union(c1, c2)).filter(consistent),
|
||||
),
|
||||
subsumes,
|
||||
isWeaker,
|
||||
),
|
||||
[true],
|
||||
[empty],
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -197,7 +220,7 @@ export function sat(
|
||||
sat(frame, fml.left, world, ctx),
|
||||
sat(frame, fml.right, world, ctx),
|
||||
].flat(),
|
||||
subsumes,
|
||||
isWeaker,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -207,14 +230,53 @@ export function sat(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function validInWorld(frame: Frame, fml: Formula, world: World) {
|
||||
const ctx = buildContext(frame, fml);
|
||||
export function findCountermodelsAt(
|
||||
frame: Frame,
|
||||
fml: Formula,
|
||||
world: World,
|
||||
ctx = buildContext(frame, fml),
|
||||
): Model[] {
|
||||
return sat(frame, ctx.notFml, world, ctx).map(({ positive }) => ({
|
||||
...frame,
|
||||
valuations: valuationSet.decode(positive),
|
||||
}));
|
||||
}
|
||||
|
||||
export function findCountermodels(
|
||||
frame: Frame,
|
||||
fml: Formula,
|
||||
ctx = buildContext(frame, fml),
|
||||
): Model[] {
|
||||
return maximal(
|
||||
worlds.flatMap((world) => sat(frame, ctx.notFml, world, ctx)),
|
||||
isWeaker,
|
||||
).map(({ positive }) => ({
|
||||
...frame,
|
||||
valuations: valuationSet.decode(positive),
|
||||
}));
|
||||
}
|
||||
|
||||
export function validInWorld(
|
||||
frame: Frame,
|
||||
fml: Formula,
|
||||
world: World,
|
||||
ctx = buildContext(frame, fml),
|
||||
) {
|
||||
return sat(frame, ctx.notFml, world, ctx).length === 0;
|
||||
}
|
||||
|
||||
export function validWorlds(frame: Frame, fml: Formula) {
|
||||
const ctx = buildContext(frame, fml);
|
||||
return worlds.filter(
|
||||
(world) => sat(frame, ctx.notFml, world, ctx).length === 0,
|
||||
);
|
||||
export function validWorlds(
|
||||
frame: Frame,
|
||||
fml: Formula,
|
||||
ctx = buildContext(frame, fml),
|
||||
) {
|
||||
return worlds.filter((world) => validInWorld(frame, ctx.notFml, world, ctx));
|
||||
}
|
||||
|
||||
export function validInFrame(
|
||||
frame: Frame,
|
||||
fml: Formula,
|
||||
ctx = buildContext(frame, fml),
|
||||
) {
|
||||
return worlds.every((world) => validInWorld(frame, ctx.notFml, world, ctx));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Formula, type PropVar, propVars, vars } from "./syntax";
|
||||
import { decode, encode, permutations, power } from "./utils";
|
||||
import { BitSet, permutate } from "@cannorin/utils";
|
||||
import type { Formula, PropVar } from "./syntax";
|
||||
|
||||
export const worlds = ["a", "b", "c", "d"] as const;
|
||||
|
||||
@@ -15,6 +15,8 @@ export const relation: Relation[] = worlds.flatMap((w) =>
|
||||
worlds.map((x) => `${w}${x}` as const),
|
||||
);
|
||||
|
||||
const relationSet = new BitSet(relation);
|
||||
|
||||
export interface Frame {
|
||||
relations: Set<Relation>;
|
||||
}
|
||||
@@ -23,11 +25,6 @@ export interface Model extends Frame {
|
||||
valuations: Set<`${World}${PropVar}`>;
|
||||
}
|
||||
|
||||
export function valuation(fml?: Formula): `${World}${PropVar}`[] {
|
||||
const vs = fml ? Array.from(vars(fml)) : propVars;
|
||||
return worlds.flatMap((w) => vs.map((p) => `${w}${p}` as const));
|
||||
}
|
||||
|
||||
export function satisfy(m: Model, w: World, fml: Formula): boolean {
|
||||
switch (fml.type) {
|
||||
case "top":
|
||||
@@ -73,37 +70,15 @@ export function satisfy(m: Model, w: World, fml: Formula): boolean {
|
||||
export const validInModel = (m: Model, fml: Formula) =>
|
||||
worlds.every((w) => satisfy(m, w, fml));
|
||||
|
||||
export function validInFrame(f: Frame, fml: Formula) {
|
||||
for (const valuations of power(valuation(fml))) {
|
||||
if (!validInModel({ ...f, valuations }, fml)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validWorlds(f: Frame, fml: Formula) {
|
||||
const result: World[] = [];
|
||||
for (const w of worlds) {
|
||||
let valid = true;
|
||||
for (const valuations of power(valuation(fml))) {
|
||||
if (!satisfy({ ...f, valuations }, w, fml)) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid) result.push(w);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getFrame(id: number): Frame {
|
||||
return { relations: decode(relation, id) };
|
||||
return { relations: relationSet.decode(id) };
|
||||
}
|
||||
|
||||
export function getId(frame: Frame) {
|
||||
return encode(relation, frame.relations);
|
||||
return relationSet.encode(frame.relations);
|
||||
}
|
||||
|
||||
const worldPermutations = permutations(worlds).map(
|
||||
const worldPermutations = permutate(worlds).map(
|
||||
(perm) => new Map(worlds.map((k, i) => [k, perm[i] as World])),
|
||||
);
|
||||
|
||||
@@ -131,7 +106,7 @@ export function generateAllFrames() {
|
||||
for (let id = 0; id < total; id++) {
|
||||
if (map.has(id)) continue;
|
||||
|
||||
const relations = decode(relation, id);
|
||||
const relations = relationSet.decode(id);
|
||||
|
||||
const frame = { relations };
|
||||
const equivalentIds: number[] = [];
|
||||
@@ -139,7 +114,7 @@ export function generateAllFrames() {
|
||||
let canonicalId = id;
|
||||
for (const perm of worldPermutations) {
|
||||
const permuted = applyPermutation(frame, perm);
|
||||
const permutedId = encode(relation, permuted.relations);
|
||||
const permutedId = relationSet.encode(permuted.relations);
|
||||
equivalentIds.push(permutedId);
|
||||
if (canonicalId === null || permutedId < canonicalId) {
|
||||
canonicalId = permutedId;
|
||||
@@ -1,7 +1,14 @@
|
||||
import { power } from "@cannorin/utils";
|
||||
import { expect, test } from "vitest";
|
||||
import { validWorlds } from "../sat";
|
||||
import { validWorlds as validWorldsNaive } from "../semantics";
|
||||
import { prettyPrint } from "../syntax";
|
||||
import { validWorlds } from "../src/sat";
|
||||
import { type Frame, type World, satisfy, worlds } from "../src/semantics";
|
||||
import {
|
||||
type Formula,
|
||||
type PropVar,
|
||||
prettyPrint,
|
||||
propVars,
|
||||
vars,
|
||||
} from "../src/syntax";
|
||||
import { randomFormula, randomFrame, testFormulas } from "./utils";
|
||||
|
||||
const elapsed = <T>(f: () => T) => {
|
||||
@@ -11,15 +18,43 @@ const elapsed = <T>(f: () => T) => {
|
||||
return { value, elapsed: end - start };
|
||||
};
|
||||
|
||||
export function validWorldsNaive(
|
||||
f: Frame,
|
||||
fml: Formula,
|
||||
allValuations: `${World}${PropVar}`[][],
|
||||
) {
|
||||
const result: World[] = [];
|
||||
for (const w of worlds) {
|
||||
let valid = true;
|
||||
for (const v of allValuations) {
|
||||
if (!satisfy({ ...f, valuations: new Set(v) }, w, fml)) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid) result.push(w);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const fml of testFormulas.concat(
|
||||
[...Array(50)].map(() => randomFormula()),
|
||||
[...Array(100)].map(() => randomFormula()),
|
||||
)) {
|
||||
test(`SAT works for ${prettyPrint(fml)}`, () => {
|
||||
const count = 100;
|
||||
let diff = 0;
|
||||
const allValuations = power(
|
||||
worlds.flatMap((w) =>
|
||||
(fml ? Array.from(vars(fml)) : propVars).map(
|
||||
(p) => `${w}${p}` as const,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const frame = randomFrame();
|
||||
const expected = elapsed(() => validWorldsNaive(frame, fml));
|
||||
const expected = elapsed(() =>
|
||||
validWorldsNaive(frame, fml, allValuations),
|
||||
);
|
||||
const actual = elapsed(() => validWorlds(frame, fml));
|
||||
|
||||
expect(actual.value.sort()).toStrictEqual(expected.value.sort());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sample } from "@cannorin/utils/array";
|
||||
import { parse } from "../parser";
|
||||
import { getFrame, nontrivials } from "../semantics";
|
||||
import { sample } from "@cannorin/utils";
|
||||
import { parse } from "../src/parser";
|
||||
import { getFrame, nontrivials } from "../src/semantics";
|
||||
import {
|
||||
type Formula,
|
||||
and,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
propvar,
|
||||
to,
|
||||
top,
|
||||
} from "../syntax";
|
||||
} from "../src/syntax";
|
||||
|
||||
const formulaTypes = [
|
||||
"top",
|
||||
@@ -52,9 +52,24 @@ export const testFormulas: Formula[] = [
|
||||
parse("LLp -> p"),
|
||||
parse("p -> LLp"),
|
||||
|
||||
parse("M(p -> Mp)"),
|
||||
parse("r -> M((p -> Mp) & Mr)"),
|
||||
parse("p -> MMp"),
|
||||
parse("M(p -> Lp)"),
|
||||
parse("L0"),
|
||||
parse("ML0"),
|
||||
parse("L(p -> Mp)"),
|
||||
parse("p -> MMMp"),
|
||||
parse("p -> MMMMp"),
|
||||
parse("M1"),
|
||||
parse("ML(p -> Mp)"),
|
||||
parse("Lp -> LMp"),
|
||||
parse("M(p -> LLp)"),
|
||||
|
||||
// pathological
|
||||
parse("L(M(1 & p) v LM1) -> (s <-> 1)"),
|
||||
parse("L(M((q v ~q) & p) v LM(s v ~s)) -> (s <-> (p v ~p))"),
|
||||
parse("~(Lr & ¬Mp) & M~L(q v q)"),
|
||||
];
|
||||
|
||||
export function randomFormula(depth = 5): Formula {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
const bitIsSet = (num: number, pos: number) => (num & (1 << pos)) !== 0;
|
||||
|
||||
export function encode<T>(all: readonly T[], set: Set<T>): number {
|
||||
let flags = 0;
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
if (set.has(all[i] as T)) {
|
||||
flags |= 1 << i;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
export function decode<T>(all: readonly T[], flags: number): Set<T> {
|
||||
const total = 2 ** all.length;
|
||||
if (flags < 0 || flags >= total) throw Error("invalid flags");
|
||||
const decoded = new Set<T>();
|
||||
for (let j = 0; j < all.length; j++) {
|
||||
if (bitIsSet(flags, j)) decoded.add(all[j] as T);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
export function* power<T>(xs: readonly T[]) {
|
||||
const total = 2 ** xs.length;
|
||||
for (let i = 0; i < total; i++) {
|
||||
yield decode(xs, i);
|
||||
}
|
||||
}
|
||||
|
||||
export function permutations<T>(arr: readonly T[]): T[][] {
|
||||
if (arr.length === 0) return [[]];
|
||||
const result: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const current = arr[i];
|
||||
const remaining = [...arr.slice(0, i), ...arr.slice(i + 1)];
|
||||
for (const perm of permutations(remaining)) {
|
||||
result.push([current as T, ...perm]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function maximal<T>(
|
||||
xs: readonly T[],
|
||||
preorder: (x: T, y: T) => boolean,
|
||||
): T[] {
|
||||
const res: T[] = [];
|
||||
outer: for (const x of xs) {
|
||||
for (let i = 0; i < res.length; ) {
|
||||
const y = res[i] as T;
|
||||
const xLeY = preorder(x, y);
|
||||
const yLeX = preorder(y, x);
|
||||
|
||||
if (xLeY && !yLeX) {
|
||||
continue outer;
|
||||
}
|
||||
if (yLeX && !xLeY) {
|
||||
res.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
res.push(x);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
Reference in New Issue
Block a user