Refactor (1)

This commit is contained in:
2025-09-19 18:54:12 +09:00
committed by cannorin
parent 6b15542c40
commit c3b1bf39a4
26 changed files with 1684 additions and 105 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,3 @@
{
"extends": ["../../biome.json"]
}

1
packages/utils/index.ts Normal file
View File

@@ -0,0 +1 @@
export {};

View 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"
}
}

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

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

View 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
}
}

View File

@@ -15,6 +15,9 @@
"check": { "check": {
"cache": false "cache": false
}, },
"test": {
"cache": false
},
"dev": { "dev": {
"cache": false, "cache": false,
"persistent": true "persistent": true

979
yarn.lock

File diff suppressed because it is too large Load Diff