Refactor (1)
This commit is contained in:
@@ -44,6 +44,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@cannorin/kripke": "workspace:*",
|
||||
"@cannorin/utils": "workspace:*",
|
||||
"@fontsource/poiret-one": "5.2.6",
|
||||
"@fontsource/zen-kaku-gothic-new": "5.2.5",
|
||||
"@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" {
|
||||
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 { RateLimiter } from "sveltekit-rate-limiter/server";
|
||||
|
||||
import { dev } from "$app/environment";
|
||||
import { MISSKEY_API_KEY } from "$env/static/private";
|
||||
import { sample } from "$lib";
|
||||
import type { InviteListResponse } from "misskey-js/entities.js";
|
||||
|
||||
const limiter = new RateLimiter({
|
||||
@@ -31,7 +31,7 @@ export async function GET(event: RequestEvent) {
|
||||
},
|
||||
);
|
||||
const json = await res.json();
|
||||
const invite = sample(json as InviteListResponse).find(
|
||||
const invite = sampleMany(json as InviteListResponse).find(
|
||||
(x) => !x.createdBy && !x.usedAt,
|
||||
);
|
||||
if (invite) return text(invite.code);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
right,
|
||||
worlds,
|
||||
} from "@cannorin/kripke";
|
||||
import type { SVGAttributes } from "svelte/elements";
|
||||
import {
|
||||
type Radian,
|
||||
type Vector,
|
||||
@@ -17,7 +16,8 @@ import {
|
||||
rotate,
|
||||
sub,
|
||||
theta,
|
||||
} from "../lib/vector";
|
||||
} from "@cannorin/utils/vector";
|
||||
import type { SVGAttributes } from "svelte/elements";
|
||||
|
||||
export interface FrameInputProps extends SVGAttributes<SVGElement> {
|
||||
frame?: Frame | undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MultiSet } from "@cannorin/utils/multiset";
|
||||
import { persisted } from "svelte-persisted-store";
|
||||
import type { Move } from "../components/game.svelte";
|
||||
import { MultiSet } from "./multiset";
|
||||
import { date } from "./system";
|
||||
|
||||
export type Daily = {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
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 LuX from "lucide-svelte/icons/x";
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"check": "turbo run check",
|
||||
"lint": "turbo run lint",
|
||||
"fix": "turbo run fix",
|
||||
"test": "turbo run test",
|
||||
"gen:env": "bash scripts/generate_env_for_apps.sh"
|
||||
},
|
||||
"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",
|
||||
"./syntax": "./syntax.ts",
|
||||
"./semantics": "./semantics.ts",
|
||||
"./parser": "./parser.ts"
|
||||
"./parser": "./parser.ts",
|
||||
"./sat": "./sat.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "biome check",
|
||||
"fix": "biome check --fix"
|
||||
"fix": "biome check --fix",
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "5.7.3"
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cannorin/utils": "workspace:*",
|
||||
"typescript-parsec": "0.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type {} from "@cannorin/utils/headless";
|
||||
import {
|
||||
type Token,
|
||||
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 const propVars: PropVar[] = ["p", "q", "r", "s"];
|
||||
export const propVars = ["p", "q", "r", "s"] as const satisfies PropVar[];
|
||||
|
||||
export type Formula =
|
||||
| { 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;
|
||||
}
|
||||
|
||||
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": {
|
||||
"cache": false
|
||||
},
|
||||
"test": {
|
||||
"cache": false
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
|
||||
Reference in New Issue
Block a user