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

282
packages/kripke/src/sat.ts Normal file
View File

@@ -0,0 +1,282 @@
import { BitSet, maximal } from "@cannorin/utils";
import {
type Frame,
type Model,
type World,
left,
right,
worlds,
} from "./semantics";
import {
type Formula,
type NNFFormula,
not,
propVars,
simplify,
tagNNF,
toNNF,
} from "./syntax";
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>;
next: Record<World, World[]>;
memo: Map<`${World}:${number}`, Constraints[]>;
constant: Map<`${World}:${number}`, boolean>;
};
export function buildContext(frame: Frame, fml: Formula): Context {
let index = 0;
const notFml = tagNNF(toNNF(simplify(not(fml))), () => {
index++;
return index;
});
const next: Record<World, World[]> = { a: [], b: [], c: [], d: [] };
for (const rel of frame.relations.values()) {
const u = left(rel);
const v = right(rel);
next[u].push(v);
}
const constant: Map<`${World}:${number}`, boolean> = new Map();
function addConstants(f: NNFFormula<number>) {
switch (f.type) {
case "top": {
for (const w of worlds) constant.set(`${w}:${f.tag}`, true);
return;
}
case "bot": {
for (const w of worlds) constant.set(`${w}:${f.tag}`, false);
return;
}
case "propvar":
case "not":
return;
case "box": {
addConstants(f.fml);
for (const w1 of worlds) {
const xs = next[w1].map((w2) => constant.get(`${w2}:${f.fml.tag}`));
if (xs.some((x) => x === undefined)) continue;
constant.set(
`${w1}:${f.tag}`,
xs.every((x) => x === true),
);
}
return;
}
case "diamond": {
addConstants(f.fml);
for (const w1 of worlds) {
const xs = next[w1].map((w2) => constant.get(`${w2}:${f.fml.tag}`));
if (xs.some((x) => x === undefined)) continue;
constant.set(
`${w1}:${f.tag}`,
xs.some((x) => x === true),
);
}
return;
}
case "or": {
addConstants(f.left);
addConstants(f.right);
for (const w of worlds) {
const l = constant.get(`${w}:${f.left.tag}`);
const r = constant.get(`${w}:${f.right.tag}`);
if (l === undefined || r === undefined) continue;
constant.set(`${w}:${f.tag}`, l || r);
}
return;
}
case "and": {
addConstants(f.left);
addConstants(f.right);
for (const w of worlds) {
const l = constant.get(`${w}:${f.left.tag}`);
const r = constant.get(`${w}:${f.right.tag}`);
if (l === undefined || r === undefined) continue;
constant.set(`${w}:${f.tag}`, l && r);
}
return;
}
}
}
addConstants(notFml);
return { notFml, next, memo: new Map(), constant };
}
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 => {
return {
positive: valuationSet.union(c1.positive, c2.positive),
negative: valuationSet.union(c1.negative, c2.negative),
};
};
const consistent = (c: Constraints) => {
return valuationSet.isDisjoint(c.positive, c.negative);
};
function sat(
frame: Frame,
fml: NNFFormula<number>,
world: World,
ctx: Context = buildContext(frame, fml),
): Constraints[] {
const key = `${world}:${fml.tag}` as const;
const c = ctx.constant.get(key);
if (c === true) return [empty];
if (c === false) return [];
let result = ctx.memo.get(key);
if (result) return result;
switch (fml.type) {
case "top":
result = [empty];
break;
case "bot":
result = [];
break;
case "propvar":
result = [
{
positive: valuationSet.create(`${world}${fml.name}`),
negative: valuationSet.empty,
},
];
break;
case "not":
result = [
{
positive: valuationSet.empty,
negative: valuationSet.create(`${world}${fml.fml.name}`),
},
];
break;
case "box": {
result = ctx.next[world]
.map((w) => sat(frame, fml.fml, w, ctx))
.reduce(
(prev, current) =>
maximal(
current.flatMap((c1) =>
prev.map((c2) => union(c1, c2)).filter(consistent),
),
isWeaker,
),
[empty],
);
break;
}
case "diamond": {
result = maximal(
ctx.next[world].flatMap((w) => sat(frame, fml.fml, w, ctx)),
isWeaker,
);
break;
}
case "and": {
result = [
sat(frame, fml.left, world, ctx),
sat(frame, fml.right, world, ctx),
].reduce(
(prev, current) =>
maximal(
current.flatMap((c1) =>
prev.map((c2) => union(c1, c2)).filter(consistent),
),
isWeaker,
),
[empty],
);
break;
}
case "or": {
result = maximal(
[
sat(frame, fml.left, world, ctx),
sat(frame, fml.right, world, ctx),
].flat(),
isWeaker,
);
break;
}
}
ctx.memo.set(key, result);
return result;
}
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,
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));
}