Refactor (2)

This commit is contained in:
2025-09-23 15:51:32 +09:00
committed by cannorin
parent c3b1bf39a4
commit ad56ae948e
19 changed files with 902 additions and 162 deletions

View File

@@ -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",

View File

@@ -1,3 +1,4 @@
export * from "./syntax";
export * from "./semantics";
export * from "./parser";
export * from "./sat";

View File

@@ -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));
}

View File

@@ -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;

View File

@@ -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());

View File

@@ -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 {

View File

@@ -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;
}