Refactor (1)
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cannorin/kripke": "workspace:*",
|
"@cannorin/kripke": "workspace:*",
|
||||||
|
"@cannorin/utils": "workspace:*",
|
||||||
"@fontsource/poiret-one": "5.2.6",
|
"@fontsource/poiret-one": "5.2.6",
|
||||||
"@fontsource/zen-kaku-gothic-new": "5.2.5",
|
"@fontsource/zen-kaku-gothic-new": "5.2.5",
|
||||||
"@icons-pack/svelte-simple-icons": "4.0.1",
|
"@icons-pack/svelte-simple-icons": "4.0.1",
|
||||||
|
|||||||
42
apps/web/src/lib/global.d.ts
vendored
42
apps/web/src/lib/global.d.ts
vendored
@@ -1,45 +1,3 @@
|
|||||||
interface String {
|
|
||||||
concat<S extends string>(string: S): `${this}${S}`;
|
|
||||||
concat<S1 extends string, S2 extends string>(
|
|
||||||
s1: S1,
|
|
||||||
s2: S2,
|
|
||||||
): `${this}${S1}${S2}`;
|
|
||||||
startsWith<S extends string>(searchString: S): this is `${S}${string}`;
|
|
||||||
endsWith<S extends string>(searchString: S): this is `${string}${S}`;
|
|
||||||
includes<S extends string>(
|
|
||||||
searchString: S,
|
|
||||||
position?: number,
|
|
||||||
): this is `${string}${S}${string}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
type LiteralUnionLike<T> = T extends string
|
|
||||||
? T extends ""
|
|
||||||
? T
|
|
||||||
: T extends `${T}${T}`
|
|
||||||
? never
|
|
||||||
: T
|
|
||||||
: T extends number
|
|
||||||
? `${T}0` extends `${number}`
|
|
||||||
? T
|
|
||||||
: never
|
|
||||||
: T extends null | undefined
|
|
||||||
? T
|
|
||||||
: never;
|
|
||||||
|
|
||||||
interface Array<T> {
|
|
||||||
includes(
|
|
||||||
searchElement: T extends LiteralUnionLike<T> ? unknown : never,
|
|
||||||
fromIndex?: number,
|
|
||||||
): searchElement is T extends LiteralUnionLike<T> ? T : never;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReadonlyArray<T> {
|
|
||||||
includes(
|
|
||||||
searchElement: T extends LiteralUnionLike<T> ? unknown : never,
|
|
||||||
fromIndex?: number,
|
|
||||||
): searchElement is T extends LiteralUnionLike<T> ? T : never;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "*&enhanced" {
|
declare module "*&enhanced" {
|
||||||
import type { Picture } from "vite-imagetools";
|
import type { Picture } from "vite-imagetools";
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
export function tryOneOf<const T>(
|
|
||||||
value: T extends LiteralUnionLike<T> ? unknown : never,
|
|
||||||
consts: readonly T[],
|
|
||||||
) {
|
|
||||||
if (consts.includes(value)) return value;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sample<T>(arr: T[], n: number = arr.length): T[] {
|
|
||||||
if (n > arr.length) return sample(arr, arr.length);
|
|
||||||
const copy = [...arr];
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const j = i + Math.floor(Math.random() * (copy.length - i));
|
|
||||||
[copy[i], copy[j]] = [copy[j] as T, copy[i] as T];
|
|
||||||
}
|
|
||||||
return copy.slice(0, n);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { sampleMany } from "@cannorin/utils/array";
|
||||||
import { type RequestEvent, text } from "@sveltejs/kit";
|
import { type RequestEvent, text } from "@sveltejs/kit";
|
||||||
import { RateLimiter } from "sveltekit-rate-limiter/server";
|
import { RateLimiter } from "sveltekit-rate-limiter/server";
|
||||||
|
|
||||||
import { dev } from "$app/environment";
|
import { dev } from "$app/environment";
|
||||||
import { MISSKEY_API_KEY } from "$env/static/private";
|
import { MISSKEY_API_KEY } from "$env/static/private";
|
||||||
import { sample } from "$lib";
|
|
||||||
import type { InviteListResponse } from "misskey-js/entities.js";
|
import type { InviteListResponse } from "misskey-js/entities.js";
|
||||||
|
|
||||||
const limiter = new RateLimiter({
|
const limiter = new RateLimiter({
|
||||||
@@ -31,7 +31,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
const invite = sample(json as InviteListResponse).find(
|
const invite = sampleMany(json as InviteListResponse).find(
|
||||||
(x) => !x.createdBy && !x.usedAt,
|
(x) => !x.createdBy && !x.usedAt,
|
||||||
);
|
);
|
||||||
if (invite) return text(invite.code);
|
if (invite) return text(invite.code);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
right,
|
right,
|
||||||
worlds,
|
worlds,
|
||||||
} from "@cannorin/kripke";
|
} from "@cannorin/kripke";
|
||||||
import type { SVGAttributes } from "svelte/elements";
|
|
||||||
import {
|
import {
|
||||||
type Radian,
|
type Radian,
|
||||||
type Vector,
|
type Vector,
|
||||||
@@ -17,7 +16,8 @@ import {
|
|||||||
rotate,
|
rotate,
|
||||||
sub,
|
sub,
|
||||||
theta,
|
theta,
|
||||||
} from "../lib/vector";
|
} from "@cannorin/utils/vector";
|
||||||
|
import type { SVGAttributes } from "svelte/elements";
|
||||||
|
|
||||||
export interface FrameInputProps extends SVGAttributes<SVGElement> {
|
export interface FrameInputProps extends SVGAttributes<SVGElement> {
|
||||||
frame?: Frame | undefined;
|
frame?: Frame | undefined;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { MultiSet } from "@cannorin/utils/multiset";
|
||||||
import { persisted } from "svelte-persisted-store";
|
import { persisted } from "svelte-persisted-store";
|
||||||
import type { Move } from "../components/game.svelte";
|
import type { Move } from "../components/game.svelte";
|
||||||
import { MultiSet } from "./multiset";
|
|
||||||
import { date } from "./system";
|
import { date } from "./system";
|
||||||
|
|
||||||
export type Daily = {
|
export type Daily = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
import * as Dialog from "$lib/components/ui/dialog";
|
import * as Dialog from "$lib/components/ui/dialog";
|
||||||
import { type Formula, isomorphic, validWorlds } from "@cannorin/kripke";
|
import { type Formula, isomorphic } from "@cannorin/kripke";
|
||||||
|
import { validWorlds } from "@cannorin/kripke/sat";
|
||||||
import LuRotateCw from "lucide-svelte/icons/rotate-cw";
|
import LuRotateCw from "lucide-svelte/icons/rotate-cw";
|
||||||
import LuX from "lucide-svelte/icons/x";
|
import LuX from "lucide-svelte/icons/x";
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"check": "turbo run check",
|
"check": "turbo run check",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"fix": "turbo run fix",
|
"fix": "turbo run fix",
|
||||||
|
"test": "turbo run test",
|
||||||
"gen:env": "bash scripts/generate_env_for_apps.sh"
|
"gen:env": "bash scripts/generate_env_for_apps.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
36
packages/kripke/global.d.ts
vendored
36
packages/kripke/global.d.ts
vendored
@@ -1,36 +0,0 @@
|
|||||||
interface String {
|
|
||||||
startsWith<S extends string>(searchString: S): this is `${S}${string}`;
|
|
||||||
endsWith<S extends string>(searchString: S): this is `${string}${S}`;
|
|
||||||
includes<S extends string>(
|
|
||||||
searchString: S,
|
|
||||||
position?: number,
|
|
||||||
): this is `${string}${S}${string}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
type LiteralUnionLike<T> = T extends string
|
|
||||||
? T extends ""
|
|
||||||
? T
|
|
||||||
: T extends `${T}${T}`
|
|
||||||
? never
|
|
||||||
: T
|
|
||||||
: T extends number
|
|
||||||
? `${T}0` extends `${number}`
|
|
||||||
? T
|
|
||||||
: never
|
|
||||||
: T extends null | undefined
|
|
||||||
? T
|
|
||||||
: never;
|
|
||||||
|
|
||||||
interface Array<T> {
|
|
||||||
includes(
|
|
||||||
searchElement: T extends LiteralUnionLike<T> ? unknown : never,
|
|
||||||
fromIndex?: number,
|
|
||||||
): searchElement is T extends LiteralUnionLike<T> ? T : never;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReadonlyArray<T> {
|
|
||||||
includes(
|
|
||||||
searchElement: T extends LiteralUnionLike<T> ? unknown : never,
|
|
||||||
fromIndex?: number,
|
|
||||||
): searchElement is T extends LiteralUnionLike<T> ? T : never;
|
|
||||||
}
|
|
||||||
@@ -6,17 +6,21 @@
|
|||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
"./syntax": "./syntax.ts",
|
"./syntax": "./syntax.ts",
|
||||||
"./semantics": "./semantics.ts",
|
"./semantics": "./semantics.ts",
|
||||||
"./parser": "./parser.ts"
|
"./parser": "./parser.ts",
|
||||||
|
"./sat": "./sat.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint": "biome check",
|
"lint": "biome check",
|
||||||
"fix": "biome check --fix"
|
"fix": "biome check --fix",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "5.7.3"
|
"typescript": "5.7.3",
|
||||||
|
"vitest": "3.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cannorin/utils": "workspace:*",
|
||||||
"typescript-parsec": "0.3.4"
|
"typescript-parsec": "0.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type {} from "@cannorin/utils/headless";
|
||||||
import {
|
import {
|
||||||
type Token,
|
type Token,
|
||||||
alt,
|
alt,
|
||||||
|
|||||||
220
packages/kripke/sat.ts
Normal file
220
packages/kripke/sat.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import type {} from "@cannorin/utils/headless";
|
||||||
|
import { type Frame, type World, left, right, worlds } from "./semantics";
|
||||||
|
import {
|
||||||
|
type Formula,
|
||||||
|
type NNFFormula,
|
||||||
|
type PropVar,
|
||||||
|
not,
|
||||||
|
simplify,
|
||||||
|
tagNNF,
|
||||||
|
toNNF,
|
||||||
|
} from "./syntax";
|
||||||
|
import { maximal } from "./utils";
|
||||||
|
|
||||||
|
export type Constraints =
|
||||||
|
| true
|
||||||
|
| ReadonlySet<`${World}${PropVar}` | `!${World}${PropVar}`>;
|
||||||
|
|
||||||
|
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 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 union = (c1: Constraints, c2: Constraints): Constraints => {
|
||||||
|
if (c1 === true) return c2;
|
||||||
|
if (c2 === true) return c1;
|
||||||
|
return new Set([...c1.values(), ...c2.values()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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 [true];
|
||||||
|
if (c === false) return [];
|
||||||
|
|
||||||
|
let result = ctx.memo.get(key);
|
||||||
|
if (result) return result;
|
||||||
|
|
||||||
|
switch (fml.type) {
|
||||||
|
case "top":
|
||||||
|
result = [true];
|
||||||
|
break;
|
||||||
|
case "bot":
|
||||||
|
result = [];
|
||||||
|
break;
|
||||||
|
case "propvar":
|
||||||
|
result = [new Set([`${world}${fml.name}` as const])];
|
||||||
|
break;
|
||||||
|
case "not":
|
||||||
|
result = [new Set([`!${world}${fml.fml.name}` as const])];
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
subsumes,
|
||||||
|
),
|
||||||
|
[true],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "diamond": {
|
||||||
|
result = maximal(
|
||||||
|
ctx.next[world].flatMap((w) => sat(frame, fml.fml, w, ctx)),
|
||||||
|
subsumes,
|
||||||
|
);
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
subsumes,
|
||||||
|
),
|
||||||
|
[true],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "or": {
|
||||||
|
result = maximal(
|
||||||
|
[
|
||||||
|
sat(frame, fml.left, world, ctx),
|
||||||
|
sat(frame, fml.right, world, ctx),
|
||||||
|
].flat(),
|
||||||
|
subsumes,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.memo.set(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validInWorld(frame: Frame, fml: Formula, world: World) {
|
||||||
|
const 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export type PropVar = "p" | "q" | "r" | "s";
|
export type PropVar = "p" | "q" | "r" | "s";
|
||||||
|
|
||||||
export const propVars: PropVar[] = ["p", "q", "r", "s"];
|
export const propVars = ["p", "q", "r", "s"] as const satisfies PropVar[];
|
||||||
|
|
||||||
export type Formula =
|
export type Formula =
|
||||||
| { type: "top" | "bot" }
|
| { type: "top" | "bot" }
|
||||||
@@ -149,3 +149,226 @@ export function prettyPrint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function simplify(fml: Formula): Formula {
|
||||||
|
switch (fml.type) {
|
||||||
|
case "top":
|
||||||
|
case "bot":
|
||||||
|
case "propvar":
|
||||||
|
return fml;
|
||||||
|
case "not":
|
||||||
|
case "box":
|
||||||
|
case "diamond": {
|
||||||
|
const inner = simplify(fml.fml);
|
||||||
|
if (fml.type === "not" && inner.type === "not") return inner.fml;
|
||||||
|
if (fml.type === "not" && inner.type === "top") return bot;
|
||||||
|
if (fml.type === "not" && inner.type === "bot") return top;
|
||||||
|
if (fml.type === "box" && inner.type === "top") return top;
|
||||||
|
if (fml.type === "diamond" && inner.type === "bot") return bot;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: fml.type,
|
||||||
|
fml: inner,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "or":
|
||||||
|
case "and":
|
||||||
|
case "to":
|
||||||
|
case "eq": {
|
||||||
|
const innerLeft = simplify(fml.left);
|
||||||
|
const innerRight = simplify(fml.right);
|
||||||
|
|
||||||
|
if (fml.type === "and" && innerLeft.type === "top") return innerRight;
|
||||||
|
if (fml.type === "and" && innerRight.type === "top") return innerLeft;
|
||||||
|
if (
|
||||||
|
fml.type === "and" &&
|
||||||
|
(innerLeft.type === "bot" || innerRight.type === "bot")
|
||||||
|
)
|
||||||
|
return bot;
|
||||||
|
if (fml.type === "or" && innerLeft.type === "bot") return innerRight;
|
||||||
|
if (fml.type === "or" && innerRight.type === "bot") return innerLeft;
|
||||||
|
if (
|
||||||
|
fml.type === "or" &&
|
||||||
|
(innerLeft.type === "top" || innerRight.type === "top")
|
||||||
|
)
|
||||||
|
return top;
|
||||||
|
if (fml.type === "to" && innerLeft.type === "top") return innerRight;
|
||||||
|
if (fml.type === "to" && innerLeft.type === "bot") return top;
|
||||||
|
if (fml.type === "to" && innerRight.type === "top") return top;
|
||||||
|
if (fml.type === "to" && innerRight.type === "bot")
|
||||||
|
return simplify(not(innerLeft));
|
||||||
|
if (fml.type === "eq" && innerLeft.type === "top") return innerRight;
|
||||||
|
if (fml.type === "eq" && innerRight.type === "top") return innerLeft;
|
||||||
|
if (fml.type === "eq" && innerRight.type === "bot")
|
||||||
|
return simplify(not(innerLeft));
|
||||||
|
if (fml.type === "eq" && innerLeft.type === "bot")
|
||||||
|
return simplify(not(innerRight));
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: fml.type,
|
||||||
|
left: innerLeft,
|
||||||
|
right: innerRight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NNFFormula<T = undefined> =
|
||||||
|
| { type: "top" | "bot"; tag: T }
|
||||||
|
| { type: "propvar"; name: PropVar; tag: T }
|
||||||
|
| { type: "not"; fml: { type: "propvar"; name: PropVar; tag: T }; tag: T }
|
||||||
|
| { type: "box" | "diamond"; fml: NNFFormula<T>; tag: T }
|
||||||
|
| {
|
||||||
|
type: "and" | "or";
|
||||||
|
left: NNFFormula<T>;
|
||||||
|
right: NNFFormula<T>;
|
||||||
|
tag: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toNNF(fml: Formula): NNFFormula {
|
||||||
|
switch (fml.type) {
|
||||||
|
case "top":
|
||||||
|
return { ...fml, tag: undefined };
|
||||||
|
case "bot":
|
||||||
|
return { ...fml, tag: undefined };
|
||||||
|
case "propvar":
|
||||||
|
return { ...fml, tag: undefined };
|
||||||
|
case "box": {
|
||||||
|
return {
|
||||||
|
type: "box",
|
||||||
|
fml: toNNF(fml.fml),
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "diamond": {
|
||||||
|
return {
|
||||||
|
type: "diamond",
|
||||||
|
fml: toNNF(fml.fml),
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "to": {
|
||||||
|
return toNNF(or(not(fml.left), fml.right));
|
||||||
|
}
|
||||||
|
case "eq": {
|
||||||
|
return toNNF(
|
||||||
|
and(or(not(fml.left), fml.right), or(not(fml.right), fml.left)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "or": {
|
||||||
|
return {
|
||||||
|
type: "or",
|
||||||
|
left: toNNF(fml.left),
|
||||||
|
right: toNNF(fml.right),
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "and": {
|
||||||
|
return {
|
||||||
|
type: "and",
|
||||||
|
left: toNNF(fml.left),
|
||||||
|
right: toNNF(fml.right),
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "not": {
|
||||||
|
switch (fml.fml.type) {
|
||||||
|
case "top":
|
||||||
|
return { ...bot, tag: undefined };
|
||||||
|
case "bot":
|
||||||
|
return { ...top, tag: undefined };
|
||||||
|
case "propvar":
|
||||||
|
return {
|
||||||
|
type: "not",
|
||||||
|
fml: { ...fml.fml, tag: undefined },
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
case "not":
|
||||||
|
return toNNF(fml.fml.fml);
|
||||||
|
case "box":
|
||||||
|
return {
|
||||||
|
type: "diamond",
|
||||||
|
fml: toNNF(not(fml.fml.fml)),
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
case "diamond":
|
||||||
|
return { type: "box", fml: toNNF(not(fml.fml.fml)), tag: undefined };
|
||||||
|
case "or":
|
||||||
|
return {
|
||||||
|
type: "and",
|
||||||
|
left: toNNF(not(fml.fml.left)),
|
||||||
|
right: toNNF(not(fml.fml.right)),
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
case "and":
|
||||||
|
return {
|
||||||
|
type: "or",
|
||||||
|
left: toNNF(not(fml.fml.left)),
|
||||||
|
right: toNNF(not(fml.fml.right)),
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
case "to":
|
||||||
|
return {
|
||||||
|
type: "and",
|
||||||
|
left: toNNF(fml.fml.left),
|
||||||
|
right: toNNF(not(fml.fml.right)),
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
case "eq":
|
||||||
|
return {
|
||||||
|
type: "or",
|
||||||
|
left: {
|
||||||
|
type: "and",
|
||||||
|
left: toNNF(fml.fml.left),
|
||||||
|
right: toNNF(not(fml.fml.right)),
|
||||||
|
tag: undefined,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
type: "and",
|
||||||
|
left: toNNF(not(fml.fml.left)),
|
||||||
|
right: toNNF(fml.fml.right),
|
||||||
|
tag: undefined,
|
||||||
|
},
|
||||||
|
tag: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tagNNF<T, U>(
|
||||||
|
fml: NNFFormula<T>,
|
||||||
|
tagger: (fml: Omit<NNFFormula<T>, "tag">, tag: T) => U,
|
||||||
|
): NNFFormula<U> {
|
||||||
|
switch (fml.type) {
|
||||||
|
case "top":
|
||||||
|
case "bot":
|
||||||
|
case "propvar":
|
||||||
|
return { ...fml, tag: tagger(fml, fml.tag) };
|
||||||
|
case "not":
|
||||||
|
return {
|
||||||
|
type: "not",
|
||||||
|
fml: {
|
||||||
|
type: "propvar",
|
||||||
|
name: fml.fml.name,
|
||||||
|
tag: tagger(fml.fml, fml.fml.tag),
|
||||||
|
},
|
||||||
|
tag: tagger(fml, fml.tag),
|
||||||
|
};
|
||||||
|
case "box":
|
||||||
|
case "diamond":
|
||||||
|
return {
|
||||||
|
type: fml.type,
|
||||||
|
fml: tagNNF(fml.fml, tagger),
|
||||||
|
tag: tagger(fml, fml.tag),
|
||||||
|
};
|
||||||
|
case "and":
|
||||||
|
case "or":
|
||||||
|
return {
|
||||||
|
type: fml.type,
|
||||||
|
left: tagNNF(fml.left, tagger),
|
||||||
|
right: tagNNF(fml.right, tagger),
|
||||||
|
tag: tagger(fml, fml.tag),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
30
packages/kripke/tests/sat.test.ts
Normal file
30
packages/kripke/tests/sat.test.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { validWorlds } from "../sat";
|
||||||
|
import { validWorlds as validWorldsNaive } from "../semantics";
|
||||||
|
import { prettyPrint } from "../syntax";
|
||||||
|
import { randomFormula, randomFrame, testFormulas } from "./utils";
|
||||||
|
|
||||||
|
const elapsed = <T>(f: () => T) => {
|
||||||
|
const start = Date.now();
|
||||||
|
const value = f();
|
||||||
|
const end = Date.now();
|
||||||
|
return { value, elapsed: end - start };
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const fml of testFormulas.concat(
|
||||||
|
[...Array(50)].map(() => randomFormula()),
|
||||||
|
)) {
|
||||||
|
test(`SAT works for ${prettyPrint(fml)}`, () => {
|
||||||
|
const count = 100;
|
||||||
|
let diff = 0;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const frame = randomFrame();
|
||||||
|
const expected = elapsed(() => validWorldsNaive(frame, fml));
|
||||||
|
const actual = elapsed(() => validWorlds(frame, fml));
|
||||||
|
|
||||||
|
expect(actual.value.sort()).toStrictEqual(expected.value.sort());
|
||||||
|
diff += actual.elapsed - expected.elapsed;
|
||||||
|
}
|
||||||
|
expect(diff / count).toBeLessThanOrEqual(10);
|
||||||
|
});
|
||||||
|
}
|
||||||
96
packages/kripke/tests/utils.ts
Normal file
96
packages/kripke/tests/utils.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { sample } from "@cannorin/utils/array";
|
||||||
|
import { parse } from "../parser";
|
||||||
|
import { getFrame, nontrivials } from "../semantics";
|
||||||
|
import {
|
||||||
|
type Formula,
|
||||||
|
and,
|
||||||
|
bot,
|
||||||
|
box,
|
||||||
|
diamond,
|
||||||
|
eq,
|
||||||
|
not,
|
||||||
|
or,
|
||||||
|
propvar,
|
||||||
|
to,
|
||||||
|
top,
|
||||||
|
} from "../syntax";
|
||||||
|
|
||||||
|
const formulaTypes = [
|
||||||
|
"top",
|
||||||
|
"bot",
|
||||||
|
"propvar",
|
||||||
|
"not",
|
||||||
|
"and",
|
||||||
|
"or",
|
||||||
|
"to",
|
||||||
|
"eq",
|
||||||
|
"box",
|
||||||
|
"diamond",
|
||||||
|
] as const satisfies Formula["type"][];
|
||||||
|
|
||||||
|
export const testFormulas: Formula[] = [
|
||||||
|
parse("L(p -> q) -> (Lp -> Lq)"),
|
||||||
|
parse("Lp -> p"),
|
||||||
|
parse("LLp -> Lp"),
|
||||||
|
parse("Lp -> LLp"),
|
||||||
|
parse("Lp -> Mp"),
|
||||||
|
parse("p -> LMp"),
|
||||||
|
parse("Mp -> LMp"),
|
||||||
|
parse("L(Lp -> p) -> Lp"),
|
||||||
|
parse("L(L(p -> Lp) -> p) -> p"),
|
||||||
|
parse("L(Lp -> q) v L(Lq -> p)"),
|
||||||
|
parse("LMp -> MLp"),
|
||||||
|
parse("MLp -> LMp"),
|
||||||
|
parse("p -> Lp"),
|
||||||
|
parse("Mp -> Lp"),
|
||||||
|
parse("Mp <-> Lp"),
|
||||||
|
parse("Lp"),
|
||||||
|
parse("Lp v L(p -> q)"),
|
||||||
|
parse("Lp v L(p -> q) v L(p&q -> r)"),
|
||||||
|
|
||||||
|
parse("L(Lp -> p)"),
|
||||||
|
parse("LLp -> p"),
|
||||||
|
parse("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))"),
|
||||||
|
];
|
||||||
|
|
||||||
|
export function randomFormula(depth = 5): Formula {
|
||||||
|
const types = new Map([
|
||||||
|
[5, ["not", "and", "or", "to", "eq", "box", "diamond"] as const],
|
||||||
|
[4, ["not", "and", "or", "to", "eq", "box", "diamond"] as const],
|
||||||
|
[0, ["top", "bot", "propvar", "propvar", "propvar", "propvar"] as const],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const type = sample(types.get(depth) ?? formulaTypes);
|
||||||
|
switch (type) {
|
||||||
|
case "top":
|
||||||
|
return top;
|
||||||
|
case "bot":
|
||||||
|
return bot;
|
||||||
|
case "propvar":
|
||||||
|
return propvar(sample(["p", "q", "r"]));
|
||||||
|
case "not":
|
||||||
|
return not(randomFormula(depth - 1));
|
||||||
|
case "and":
|
||||||
|
return and(randomFormula(depth - 1), randomFormula(depth - 1));
|
||||||
|
case "or":
|
||||||
|
return or(randomFormula(depth - 1), randomFormula(depth - 1));
|
||||||
|
case "to":
|
||||||
|
return to(randomFormula(depth - 1), randomFormula(depth - 1));
|
||||||
|
case "eq":
|
||||||
|
return eq(randomFormula(depth - 1), randomFormula(depth - 1));
|
||||||
|
case "box":
|
||||||
|
return box(randomFormula(depth - 1));
|
||||||
|
case "diamond":
|
||||||
|
return diamond(randomFormula(depth - 1));
|
||||||
|
default:
|
||||||
|
throw new Error(`impossible: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomFrame() {
|
||||||
|
return getFrame(sample(nontrivials));
|
||||||
|
}
|
||||||
@@ -39,3 +39,28 @@ export function permutations<T>(arr: readonly T[]): T[][] {
|
|||||||
}
|
}
|
||||||
return result;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
3
packages/utils/biome.json
Normal file
3
packages/utils/biome.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../../biome.json"]
|
||||||
|
}
|
||||||
1
packages/utils/index.ts
Normal file
1
packages/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
22
packages/utils/package.json
Normal file
22
packages/utils/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@cannorin/utils",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts",
|
||||||
|
"./headless": "./src/headless.ts",
|
||||||
|
"./array": "./src/array.ts",
|
||||||
|
"./vector": "./src/vector.ts",
|
||||||
|
"./multiset": "./src/multiset.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"lint": "biome check",
|
||||||
|
"fix": "biome check --fix",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "5.7.3",
|
||||||
|
"vitest": "3.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
packages/utils/src/array.ts
Normal file
12
packages/utils/src/array.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const sample = <T>(arr: readonly T[]): T =>
|
||||||
|
arr[Math.floor(Math.random() * arr.length)] as T;
|
||||||
|
|
||||||
|
export function sampleMany<T>(arr: T[], n: number = arr.length): T[] {
|
||||||
|
if (n > arr.length) return sampleMany(arr, arr.length);
|
||||||
|
const copy = [...arr];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const j = i + Math.floor(Math.random() * (copy.length - i));
|
||||||
|
[copy[i], copy[j]] = [copy[j] as T, copy[i] as T];
|
||||||
|
}
|
||||||
|
return copy.slice(0, n);
|
||||||
|
}
|
||||||
40
packages/utils/src/headless.ts
Normal file
40
packages/utils/src/headless.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export {};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface String {
|
||||||
|
startsWith<S extends string>(searchString: S): this is `${S}${string}`;
|
||||||
|
endsWith<S extends string>(searchString: S): this is `${string}${S}`;
|
||||||
|
includes<S extends string>(
|
||||||
|
searchString: S,
|
||||||
|
position?: number,
|
||||||
|
): this is `${string}${S}${string}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LiteralUnionLike<T> = T extends string
|
||||||
|
? T extends ""
|
||||||
|
? T
|
||||||
|
: T extends `${T}${T}`
|
||||||
|
? never
|
||||||
|
: T
|
||||||
|
: T extends number
|
||||||
|
? `${T}0` extends `${number}`
|
||||||
|
? T
|
||||||
|
: never
|
||||||
|
: T extends null | undefined
|
||||||
|
? T
|
||||||
|
: never;
|
||||||
|
|
||||||
|
interface Array<T> {
|
||||||
|
includes(
|
||||||
|
searchElement: T extends LiteralUnionLike<T> ? unknown : never,
|
||||||
|
fromIndex?: number,
|
||||||
|
): searchElement is T extends LiteralUnionLike<T> ? T : never;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReadonlyArray<T> {
|
||||||
|
includes(
|
||||||
|
searchElement: T extends LiteralUnionLike<T> ? unknown : never,
|
||||||
|
fromIndex?: number,
|
||||||
|
): searchElement is T extends LiteralUnionLike<T> ? T : never;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
packages/utils/tsconfig.json
Normal file
12
packages/utils/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": ["esnext"],
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
"check": {
|
"check": {
|
||||||
"cache": false
|
"cache": false
|
||||||
},
|
},
|
||||||
|
"test": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
|
|||||||
Reference in New Issue
Block a user