@classytic/stage 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/assets/index.d.mts +3 -0
- package/dist/assets/index.mjs +5 -0
- package/dist/assets/kit/glyphs.d.mts +61 -0
- package/dist/assets/kit/glyphs.mjs +256 -0
- package/dist/assets/kit/index.d.mts +2 -0
- package/dist/assets/kit/index.mjs +4 -0
- package/dist/assets/kit/svg-defs.d.mts +6 -0
- package/dist/assets/kit/svg-defs.mjs +137 -0
- package/dist/builder/Palette.d.mts +30 -0
- package/dist/builder/Palette.mjs +100 -0
- package/dist/builder/SceneBuilder.d.mts +26 -0
- package/dist/builder/SceneBuilder.mjs +340 -0
- package/dist/builder/controls.d.mts +18 -0
- package/dist/builder/controls.mjs +29 -0
- package/dist/builder/editor.d.mts +46 -0
- package/dist/builder/editor.mjs +130 -0
- package/dist/builder/icons.d.mts +14 -0
- package/dist/builder/icons.mjs +143 -0
- package/dist/builder/index.d.mts +6 -0
- package/dist/builder/index.mjs +7 -0
- package/dist/builder/tools.d.mts +47 -0
- package/dist/builder/tools.mjs +303 -0
- package/dist/chem/index.d.mts +118 -0
- package/dist/chem/index.mjs +131 -0
- package/dist/core/clock.d.mts +41 -0
- package/dist/core/clock.mjs +62 -0
- package/dist/core/context.d.mts +13 -0
- package/dist/core/context.mjs +29 -0
- package/dist/core/control.d.mts +58 -0
- package/dist/core/control.mjs +113 -0
- package/dist/core/coords.d.mts +51 -0
- package/dist/core/coords.mjs +44 -0
- package/dist/core/index.d.mts +8 -0
- package/dist/core/index.mjs +9 -0
- package/dist/core/learner.d.mts +44 -0
- package/dist/core/learner.mjs +23 -0
- package/dist/core/motion.d.mts +73 -0
- package/dist/core/motion.mjs +103 -0
- package/dist/core/richText.d.mts +28 -0
- package/dist/core/richText.mjs +52 -0
- package/dist/core/vec.d.mts +47 -0
- package/dist/core/vec.mjs +144 -0
- package/dist/field/index.d.mts +58 -0
- package/dist/field/index.mjs +156 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.mjs +55 -0
- package/dist/interaction/MovableDot.d.mts +33 -0
- package/dist/interaction/MovableDot.mjs +66 -0
- package/dist/interaction/hitTest.d.mts +16 -0
- package/dist/interaction/hitTest.mjs +39 -0
- package/dist/interaction/index.d.mts +5 -0
- package/dist/interaction/index.mjs +6 -0
- package/dist/interaction/useDraggable.d.mts +51 -0
- package/dist/interaction/useDraggable.mjs +116 -0
- package/dist/interaction/useScreenToMath.d.mts +12 -0
- package/dist/interaction/useScreenToMath.mjs +25 -0
- package/dist/logic/ast.d.mts +29 -0
- package/dist/logic/ast.mjs +37 -0
- package/dist/logic/index.d.mts +25 -0
- package/dist/logic/index.mjs +36 -0
- package/dist/logic/latex.d.mts +6 -0
- package/dist/logic/latex.mjs +33 -0
- package/dist/logic/minimize.d.mts +37 -0
- package/dist/logic/minimize.mjs +175 -0
- package/dist/logic/parse.d.mts +6 -0
- package/dist/logic/parse.mjs +68 -0
- package/dist/logic/table.d.mts +24 -0
- package/dist/logic/table.mjs +65 -0
- package/dist/logic/tokenize.mjs +258 -0
- package/dist/math/ast.d.mts +35 -0
- package/dist/math/ast.mjs +94 -0
- package/dist/math/calculus.d.mts +9 -0
- package/dist/math/calculus.mjs +188 -0
- package/dist/math/defs.mjs +51 -0
- package/dist/math/index.d.mts +26 -0
- package/dist/math/index.mjs +42 -0
- package/dist/math/latex.d.mts +7 -0
- package/dist/math/latex.mjs +115 -0
- package/dist/math/parse.d.mts +6 -0
- package/dist/math/parse.mjs +91 -0
- package/dist/math/tokenize.mjs +112 -0
- package/dist/primitives/CanvasLayer.d.mts +26 -0
- package/dist/primitives/CanvasLayer.mjs +87 -0
- package/dist/primitives/Dot.d.mts +27 -0
- package/dist/primitives/Dot.mjs +40 -0
- package/dist/primitives/Grid.d.mts +30 -0
- package/dist/primitives/Grid.mjs +122 -0
- package/dist/primitives/Label.d.mts +33 -0
- package/dist/primitives/Label.mjs +45 -0
- package/dist/primitives/Lines.d.mts +50 -0
- package/dist/primitives/Lines.mjs +82 -0
- package/dist/primitives/Plot.d.mts +50 -0
- package/dist/primitives/Plot.mjs +87 -0
- package/dist/primitives/Shapes.d.mts +76 -0
- package/dist/primitives/Shapes.mjs +79 -0
- package/dist/primitives/Tex.d.mts +27 -0
- package/dist/primitives/Tex.mjs +54 -0
- package/dist/primitives/index.d.mts +10 -0
- package/dist/primitives/index.mjs +10 -0
- package/dist/primitives/props.d.mts +15 -0
- package/dist/scene/Scene.d.mts +31 -0
- package/dist/scene/Scene.mjs +168 -0
- package/dist/scene/assets.d.mts +37 -0
- package/dist/scene/assets.mjs +31 -0
- package/dist/scene/commands.d.mts +47 -0
- package/dist/scene/commands.mjs +162 -0
- package/dist/scene/evaluators.d.mts +17 -0
- package/dist/scene/evaluators.mjs +210 -0
- package/dist/scene/index.d.mts +13 -0
- package/dist/scene/index.mjs +13 -0
- package/dist/scene/migrate.d.mts +11 -0
- package/dist/scene/migrate.mjs +35 -0
- package/dist/scene/render.d.mts +14 -0
- package/dist/scene/render.mjs +126 -0
- package/dist/scene/resolve.d.mts +12 -0
- package/dist/scene/resolve.mjs +95 -0
- package/dist/scene/schema.d.mts +9 -0
- package/dist/scene/schema.mjs +51 -0
- package/dist/scene/sims.d.mts +18 -0
- package/dist/scene/sims.mjs +54 -0
- package/dist/scene/store.d.mts +22 -0
- package/dist/scene/store.mjs +37 -0
- package/dist/scene/types.d.mts +228 -0
- package/dist/scene/types.mjs +27 -0
- package/dist/sim/equilibrium.d.mts +36 -0
- package/dist/sim/equilibrium.mjs +54 -0
- package/dist/sim/index.d.mts +9 -0
- package/dist/sim/index.mjs +9 -0
- package/dist/sim/particles.d.mts +43 -0
- package/dist/sim/particles.mjs +136 -0
- package/dist/sim/rate.d.mts +36 -0
- package/dist/sim/rate.mjs +39 -0
- package/dist/sim/registry.d.mts +10 -0
- package/dist/sim/registry.mjs +27 -0
- package/dist/sim/sampler.d.mts +47 -0
- package/dist/sim/sampler.mjs +79 -0
- package/dist/sim/thermal.d.mts +78 -0
- package/dist/sim/thermal.mjs +172 -0
- package/dist/sim/types.d.mts +24 -0
- package/dist/sim/wave.d.mts +34 -0
- package/dist/sim/wave.mjs +61 -0
- package/dist/steps/index.d.mts +62 -0
- package/dist/steps/index.mjs +133 -0
- package/dist/thermo/index.d.mts +82 -0
- package/dist/thermo/index.mjs +133 -0
- package/dist/view/Stage.d.mts +39 -0
- package/dist/view/Stage.mjs +91 -0
- package/dist/view/index.d.mts +4 -0
- package/dist/view/index.mjs +5 -0
- package/dist/view/useElementSize.d.mts +13 -0
- package/dist/view/useElementSize.mjs +40 -0
- package/dist/view/useInView.d.mts +9 -0
- package/dist/view/useInView.mjs +33 -0
- package/package.json +141 -0
- package/styles.css +70 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/logic/latex.ts
|
|
2
|
+
const OP_TEX = {
|
|
3
|
+
and: "\\land",
|
|
4
|
+
or: "\\lor",
|
|
5
|
+
xor: "\\oplus",
|
|
6
|
+
implies: "\\rightarrow",
|
|
7
|
+
iff: "\\leftrightarrow"
|
|
8
|
+
};
|
|
9
|
+
const PREC = {
|
|
10
|
+
and: 50,
|
|
11
|
+
xor: 40,
|
|
12
|
+
or: 30,
|
|
13
|
+
implies: 20,
|
|
14
|
+
iff: 10
|
|
15
|
+
};
|
|
16
|
+
function toLatex(n) {
|
|
17
|
+
const walk = (x, parentPrec) => {
|
|
18
|
+
switch (x.kind) {
|
|
19
|
+
case "var": return x.name;
|
|
20
|
+
case "const": return x.value ? "\\top" : "\\bot";
|
|
21
|
+
case "not": return `\\neg ${walk(x.a, 100)}`;
|
|
22
|
+
case "bin": {
|
|
23
|
+
const p = PREC[x.op];
|
|
24
|
+
const s = `${walk(x.a, p)} ${OP_TEX[x.op]} ${walk(x.b, p + 1)}`;
|
|
25
|
+
return p < parentPrec ? `\\left(${s}\\right)` : s;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
return walk(n, 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { toLatex };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LNode } from "./ast.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/logic/minimize.d.ts
|
|
4
|
+
interface Cube {
|
|
5
|
+
value: number;
|
|
6
|
+
mask: number;
|
|
7
|
+
}
|
|
8
|
+
interface Minimization {
|
|
9
|
+
vars: string[];
|
|
10
|
+
minterms: number[];
|
|
11
|
+
dontCares: number[];
|
|
12
|
+
primeImplicants: Cube[];
|
|
13
|
+
/** A minimal SOP cover (subset of the prime implicants). */
|
|
14
|
+
cover: Cube[];
|
|
15
|
+
/** The simplified sum-of-products expression ('0' / '1' for the constants). */
|
|
16
|
+
expression: string;
|
|
17
|
+
}
|
|
18
|
+
/** All minterms a cube covers, within an n-variable space. */
|
|
19
|
+
declare function cubeCovers(c: Cube, n: number): number[];
|
|
20
|
+
/** The product term for a cube — a literal per FIXED bit (MSB = first var). */
|
|
21
|
+
declare function cubeTerm(c: Cube, vars: string[], neg?: string, join?: string): string;
|
|
22
|
+
/** Quine–McCluskey prime implicants of a set of minterms (+ optional don't-cares). */
|
|
23
|
+
declare function primeImplicants(minterms: number[], n: number, dontCares?: number[]): Cube[];
|
|
24
|
+
/** A minimal SOP cover: essential prime implicants + a greedy fill of the rest. */
|
|
25
|
+
declare function minimalCover(minterms: number[], n: number, dontCares?: number[]): Cube[];
|
|
26
|
+
/** Full minimisation of a formula (or explicit minterms) → cover + SOP string. */
|
|
27
|
+
declare function minimize(input: LNode | {
|
|
28
|
+
minterms: number[];
|
|
29
|
+
vars: string[];
|
|
30
|
+
dontCares?: number[];
|
|
31
|
+
}): Minimization;
|
|
32
|
+
/** Is `selected` a legal implicant (a sub-cube) of an n-var map? If so return
|
|
33
|
+
* the tightest cube containing exactly it; else null. (Used by the K-map lab to
|
|
34
|
+
* validate a learner's hand-drawn group.) */
|
|
35
|
+
declare function cubeOfSelection(selected: number[], n: number): Cube | null;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { Cube, Minimization, cubeCovers, cubeOfSelection, cubeTerm, minimalCover, minimize, primeImplicants };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { freeVars } from "./ast.mjs";
|
|
2
|
+
import { truthTable } from "./table.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/logic/minimize.ts
|
|
5
|
+
/**
|
|
6
|
+
* Boolean minimisation — Quine–McCluskey: the algorithm a Karnaugh map performs
|
|
7
|
+
* by eye. Given a function's minterms (the rows where it is 1) over n variables,
|
|
8
|
+
* it finds the PRIME IMPLICANTS (maximal "don't-care cubes" — groups on the
|
|
9
|
+
* K-map) and a minimal SUM-OF-PRODUCTS cover (essential PIs + a greedy fill).
|
|
10
|
+
* Pure + dependency-free, the boolean sibling of `simplify` in the expr engine;
|
|
11
|
+
* the K-map lab renders these cubes as the coloured groups, and the same routine
|
|
12
|
+
* powers ICT boolean-algebra simplification.
|
|
13
|
+
*
|
|
14
|
+
* A cube is {value, mask}: a minterm m is covered iff (m & mask) === value, so
|
|
15
|
+
* `mask` marks the FIXED bits (a 0 bit is "don't care" = a dimension the group
|
|
16
|
+
* spans). popcount(~mask) free bits ⇒ the cube covers 2^free cells.
|
|
17
|
+
*/
|
|
18
|
+
const popcount = (x) => {
|
|
19
|
+
let c = 0;
|
|
20
|
+
while (x) {
|
|
21
|
+
x &= x - 1;
|
|
22
|
+
c++;
|
|
23
|
+
}
|
|
24
|
+
return c;
|
|
25
|
+
};
|
|
26
|
+
const cubeKey = (c) => `${c.value}|${c.mask}`;
|
|
27
|
+
/** All minterms a cube covers, within an n-variable space. */
|
|
28
|
+
function cubeCovers(c, n) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const total = 1 << n;
|
|
31
|
+
for (let m = 0; m < total; m++) if ((m & c.mask) === c.value) out.push(m);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** The product term for a cube — a literal per FIXED bit (MSB = first var). */
|
|
35
|
+
function cubeTerm(c, vars, neg = "¬", join = " ∧ ") {
|
|
36
|
+
const n = vars.length;
|
|
37
|
+
const lits = [];
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const bit = 1 << n - 1 - i;
|
|
40
|
+
if (c.mask & bit) lits.push(c.value & bit ? vars[i] : `${neg}${vars[i]}`);
|
|
41
|
+
}
|
|
42
|
+
return lits.length ? lits.join(join) : "T";
|
|
43
|
+
}
|
|
44
|
+
/** Quine–McCluskey prime implicants of a set of minterms (+ optional don't-cares). */
|
|
45
|
+
function primeImplicants(minterms, n, dontCares = []) {
|
|
46
|
+
const fullMask = (1 << n) - 1;
|
|
47
|
+
const seed = [...new Set([...minterms, ...dontCares])].sort((a, b) => a - b);
|
|
48
|
+
if (seed.length === 0) return [];
|
|
49
|
+
if (seed.length === 1 << n) return [{
|
|
50
|
+
value: 0,
|
|
51
|
+
mask: 0
|
|
52
|
+
}];
|
|
53
|
+
let current = seed.map((m) => ({
|
|
54
|
+
value: m,
|
|
55
|
+
mask: fullMask
|
|
56
|
+
}));
|
|
57
|
+
const primes = /* @__PURE__ */ new Map();
|
|
58
|
+
while (current.length) {
|
|
59
|
+
const used = /* @__PURE__ */ new Set();
|
|
60
|
+
const nextMap = /* @__PURE__ */ new Map();
|
|
61
|
+
for (let i = 0; i < current.length; i++) for (let j = i + 1; j < current.length; j++) {
|
|
62
|
+
const a = current[i], b = current[j];
|
|
63
|
+
if (a.mask !== b.mask) continue;
|
|
64
|
+
const diff = a.value ^ b.value;
|
|
65
|
+
if (popcount(diff) !== 1) continue;
|
|
66
|
+
const merged = {
|
|
67
|
+
value: a.value & ~diff,
|
|
68
|
+
mask: a.mask & ~diff
|
|
69
|
+
};
|
|
70
|
+
nextMap.set(cubeKey(merged), merged);
|
|
71
|
+
used.add(cubeKey(a));
|
|
72
|
+
used.add(cubeKey(b));
|
|
73
|
+
}
|
|
74
|
+
for (const c of current) if (!used.has(cubeKey(c))) primes.set(cubeKey(c), c);
|
|
75
|
+
current = [...nextMap.values()];
|
|
76
|
+
}
|
|
77
|
+
return [...primes.values()];
|
|
78
|
+
}
|
|
79
|
+
/** Minterms (required, NOT don't-cares) each prime implicant covers. */
|
|
80
|
+
function coverageOf(pis, required) {
|
|
81
|
+
const map = /* @__PURE__ */ new Map();
|
|
82
|
+
pis.forEach((pi, idx) => {
|
|
83
|
+
const got = [];
|
|
84
|
+
for (const m of required) if ((m & pi.mask) === pi.value) got.push(m);
|
|
85
|
+
map.set(idx, got);
|
|
86
|
+
});
|
|
87
|
+
return map;
|
|
88
|
+
}
|
|
89
|
+
/** A minimal SOP cover: essential prime implicants + a greedy fill of the rest. */
|
|
90
|
+
function minimalCover(minterms, n, dontCares = []) {
|
|
91
|
+
const required = new Set(minterms);
|
|
92
|
+
if (required.size === 0) return [];
|
|
93
|
+
const pis = primeImplicants(minterms, n, dontCares);
|
|
94
|
+
if (pis.length === 1 && pis[0].mask === 0) return pis;
|
|
95
|
+
const cov = coverageOf(pis, required);
|
|
96
|
+
const chosen = /* @__PURE__ */ new Set();
|
|
97
|
+
const uncovered = new Set(required);
|
|
98
|
+
for (const m of required) {
|
|
99
|
+
const owners = pis.map((_, i) => i).filter((i) => cov.get(i).includes(m));
|
|
100
|
+
if (owners.length === 1) chosen.add(owners[0]);
|
|
101
|
+
}
|
|
102
|
+
for (const i of chosen) for (const m of cov.get(i)) uncovered.delete(m);
|
|
103
|
+
while (uncovered.size) {
|
|
104
|
+
let best = -1, bestCount = -1;
|
|
105
|
+
pis.forEach((_, i) => {
|
|
106
|
+
if (chosen.has(i)) return;
|
|
107
|
+
const c = cov.get(i).filter((m) => uncovered.has(m)).length;
|
|
108
|
+
if (c > bestCount) {
|
|
109
|
+
bestCount = c;
|
|
110
|
+
best = i;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (best < 0 || bestCount <= 0) break;
|
|
114
|
+
chosen.add(best);
|
|
115
|
+
for (const m of cov.get(best)) uncovered.delete(m);
|
|
116
|
+
}
|
|
117
|
+
return [...chosen].map((i) => pis[i]);
|
|
118
|
+
}
|
|
119
|
+
/** Full minimisation of a formula (or explicit minterms) → cover + SOP string. */
|
|
120
|
+
function minimize(input) {
|
|
121
|
+
let vars;
|
|
122
|
+
let minterms;
|
|
123
|
+
let dontCares;
|
|
124
|
+
if ("minterms" in input) {
|
|
125
|
+
vars = input.vars;
|
|
126
|
+
minterms = [...input.minterms].sort((a, b) => a - b);
|
|
127
|
+
dontCares = (input.dontCares ?? []).slice();
|
|
128
|
+
} else {
|
|
129
|
+
vars = freeVars(input);
|
|
130
|
+
minterms = truthTable(input, vars).rows.map((r, i) => r.value ? i : -1).filter((i) => i >= 0);
|
|
131
|
+
dontCares = [];
|
|
132
|
+
}
|
|
133
|
+
const n = vars.length;
|
|
134
|
+
const cover = minimalCover(minterms, n, dontCares);
|
|
135
|
+
const pis = primeImplicants(minterms, n, dontCares);
|
|
136
|
+
let expression;
|
|
137
|
+
if (minterms.length === 0) expression = "0";
|
|
138
|
+
else if (cover.length === 1 && cover[0].mask === 0) expression = "1";
|
|
139
|
+
else expression = cover.map((c) => cover.length > 1 ? `(${cubeTerm(c, vars)})` : cubeTerm(c, vars)).join(" ∨ ");
|
|
140
|
+
return {
|
|
141
|
+
vars,
|
|
142
|
+
minterms,
|
|
143
|
+
dontCares,
|
|
144
|
+
primeImplicants: pis,
|
|
145
|
+
cover,
|
|
146
|
+
expression
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Is `selected` a legal implicant (a sub-cube) of an n-var map? If so return
|
|
150
|
+
* the tightest cube containing exactly it; else null. (Used by the K-map lab to
|
|
151
|
+
* validate a learner's hand-drawn group.) */
|
|
152
|
+
function cubeOfSelection(selected, n) {
|
|
153
|
+
if (selected.length === 0) return null;
|
|
154
|
+
if ((selected.length & selected.length - 1) !== 0) return null;
|
|
155
|
+
const fullMask = (1 << n) - 1;
|
|
156
|
+
const first = selected[0];
|
|
157
|
+
let mask = fullMask;
|
|
158
|
+
for (let i = 0; i < n; i++) {
|
|
159
|
+
const bit = 1 << i;
|
|
160
|
+
const v0 = first & bit;
|
|
161
|
+
if (selected.some((m) => (m & bit) !== v0)) mask &= ~bit;
|
|
162
|
+
}
|
|
163
|
+
const cube = {
|
|
164
|
+
value: first & mask,
|
|
165
|
+
mask
|
|
166
|
+
};
|
|
167
|
+
const covers = cubeCovers(cube, n);
|
|
168
|
+
if (covers.length !== selected.length) return null;
|
|
169
|
+
const set = new Set(selected);
|
|
170
|
+
if (!covers.every((m) => set.has(m))) return null;
|
|
171
|
+
return cube;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
export { cubeCovers, cubeOfSelection, cubeTerm, minimalCover, minimize, primeImplicants };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { tokenizeLogic } from "./tokenize.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/logic/parse.ts
|
|
4
|
+
const PREC = {
|
|
5
|
+
and: 50,
|
|
6
|
+
xor: 40,
|
|
7
|
+
or: 30,
|
|
8
|
+
implies: 20,
|
|
9
|
+
iff: 10
|
|
10
|
+
};
|
|
11
|
+
const RIGHT_ASSOC = { implies: true };
|
|
12
|
+
function parseLogic(src) {
|
|
13
|
+
const toks = tokenizeLogic(src);
|
|
14
|
+
let pos = 0;
|
|
15
|
+
const peek = () => toks[pos];
|
|
16
|
+
const next = () => {
|
|
17
|
+
const t = toks[pos];
|
|
18
|
+
if (!t) throw new Error("Unexpected end of formula");
|
|
19
|
+
pos++;
|
|
20
|
+
return t;
|
|
21
|
+
};
|
|
22
|
+
const parsePrefix = () => {
|
|
23
|
+
const t = next();
|
|
24
|
+
if (t.type === "not") return {
|
|
25
|
+
kind: "not",
|
|
26
|
+
a: parsePrefix()
|
|
27
|
+
};
|
|
28
|
+
if (t.type === "lparen") {
|
|
29
|
+
const inner = parseExpr(0);
|
|
30
|
+
if (next().type !== "rparen") throw new Error("Expected \")\"");
|
|
31
|
+
return inner;
|
|
32
|
+
}
|
|
33
|
+
if (t.type === "var") return {
|
|
34
|
+
kind: "var",
|
|
35
|
+
name: t.value
|
|
36
|
+
};
|
|
37
|
+
if (t.type === "const") return {
|
|
38
|
+
kind: "const",
|
|
39
|
+
value: t.value === "true"
|
|
40
|
+
};
|
|
41
|
+
throw new Error(`Expected a variable, "(", or ¬ but found "${t.value}"`);
|
|
42
|
+
};
|
|
43
|
+
const parseExpr = (minPrec) => {
|
|
44
|
+
let left = parsePrefix();
|
|
45
|
+
for (;;) {
|
|
46
|
+
const t = peek();
|
|
47
|
+
if (!t || t.type !== "bin") break;
|
|
48
|
+
const op = t.value;
|
|
49
|
+
const prec = PREC[op];
|
|
50
|
+
if (prec < minPrec) break;
|
|
51
|
+
pos++;
|
|
52
|
+
const right = parseExpr(RIGHT_ASSOC[op] ? prec : prec + 1);
|
|
53
|
+
left = {
|
|
54
|
+
kind: "bin",
|
|
55
|
+
op,
|
|
56
|
+
a: left,
|
|
57
|
+
b: right
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return left;
|
|
61
|
+
};
|
|
62
|
+
const node = parseExpr(0);
|
|
63
|
+
if (pos < toks.length) throw new Error(`Unexpected "${toks[pos].value}" after a complete formula`);
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { parseLogic };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LNode } from "./ast.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/logic/table.d.ts
|
|
4
|
+
interface TruthRow {
|
|
5
|
+
env: Record<string, boolean>;
|
|
6
|
+
value: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface TruthTable {
|
|
9
|
+
vars: string[];
|
|
10
|
+
rows: TruthRow[];
|
|
11
|
+
}
|
|
12
|
+
/** Enumerate all 2^vars assignments (MSB = first var), evaluating `n` on each. */
|
|
13
|
+
declare function truthTable(n: LNode, varsIn?: string[]): TruthTable;
|
|
14
|
+
type Classification = 'tautology' | 'contradiction' | 'contingency';
|
|
15
|
+
declare function classify(n: LNode): Classification;
|
|
16
|
+
/** Two formulas are equivalent iff they agree on every assignment of the
|
|
17
|
+
* COMBINED variable set. */
|
|
18
|
+
declare function equivalent(a: LNode, b: LNode): boolean;
|
|
19
|
+
/** Disjunctive normal form (sum of minterms) read straight off the true rows. */
|
|
20
|
+
declare function toDNF(n: LNode): string;
|
|
21
|
+
/** Conjunctive normal form (product of maxterms) read off the false rows. */
|
|
22
|
+
declare function toCNF(n: LNode): string;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { Classification, TruthRow, TruthTable, classify, equivalent, toCNF, toDNF, truthTable };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { evalBool, freeVars } from "./ast.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/logic/table.ts
|
|
4
|
+
/**
|
|
5
|
+
* Truth tables + the things derived from them: classification (tautology /
|
|
6
|
+
* contradiction / contingency), logical equivalence, and DNF/CNF. Everything is
|
|
7
|
+
* a fold over the 2ⁿ assignments — the truth table is the source of truth, so
|
|
8
|
+
* equivalence is just "same output column" (no algebraic law engine needed).
|
|
9
|
+
*/
|
|
10
|
+
/** Enumerate all 2^vars assignments (MSB = first var), evaluating `n` on each. */
|
|
11
|
+
function truthTable(n, varsIn) {
|
|
12
|
+
const vars = varsIn ?? freeVars(n);
|
|
13
|
+
const rows = [];
|
|
14
|
+
const total = 1 << vars.length;
|
|
15
|
+
for (let mask = 0; mask < total; mask++) {
|
|
16
|
+
const env = {};
|
|
17
|
+
vars.forEach((v, i) => {
|
|
18
|
+
env[v] = (mask & 1 << vars.length - 1 - i) !== 0;
|
|
19
|
+
});
|
|
20
|
+
rows.push({
|
|
21
|
+
env,
|
|
22
|
+
value: evalBool(n, env)
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
vars,
|
|
27
|
+
rows
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function classify(n) {
|
|
31
|
+
const { rows } = truthTable(n);
|
|
32
|
+
const t = rows.some((r) => r.value);
|
|
33
|
+
const f = rows.some((r) => !r.value);
|
|
34
|
+
if (t && !f) return "tautology";
|
|
35
|
+
if (f && !t) return "contradiction";
|
|
36
|
+
return "contingency";
|
|
37
|
+
}
|
|
38
|
+
/** Two formulas are equivalent iff they agree on every assignment of the
|
|
39
|
+
* COMBINED variable set. */
|
|
40
|
+
function equivalent(a, b) {
|
|
41
|
+
const vars = [...new Set([...freeVars(a), ...freeVars(b)])].sort();
|
|
42
|
+
const ta = truthTable(a, vars);
|
|
43
|
+
const tb = truthTable(b, vars);
|
|
44
|
+
return ta.rows.every((r, i) => r.value === tb.rows[i].value);
|
|
45
|
+
}
|
|
46
|
+
const term = (vars, env, positiveWhenTrue, join, neg) => vars.map((v) => env[v] === positiveWhenTrue ? v : `${neg}${v}`).join(join);
|
|
47
|
+
/** Disjunctive normal form (sum of minterms) read straight off the true rows. */
|
|
48
|
+
function toDNF(n) {
|
|
49
|
+
const { vars, rows } = truthTable(n);
|
|
50
|
+
const minterms = rows.filter((r) => r.value);
|
|
51
|
+
if (minterms.length === 0) return "F";
|
|
52
|
+
if (minterms.length === rows.length) return "T";
|
|
53
|
+
return minterms.map((r) => `(${term(vars, r.env, true, " ∧ ", "¬")})`).join(" ∨ ");
|
|
54
|
+
}
|
|
55
|
+
/** Conjunctive normal form (product of maxterms) read off the false rows. */
|
|
56
|
+
function toCNF(n) {
|
|
57
|
+
const { vars, rows } = truthTable(n);
|
|
58
|
+
const maxterms = rows.filter((r) => !r.value);
|
|
59
|
+
if (maxterms.length === 0) return "T";
|
|
60
|
+
if (maxterms.length === rows.length) return "F";
|
|
61
|
+
return maxterms.map((r) => `(${term(vars, r.env, false, " ∨ ", "¬")})`).join(" ∧ ");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
export { classify, equivalent, toCNF, toDNF, truthTable };
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
//#region src/logic/tokenize.ts
|
|
2
|
+
const TRUE = new Set([
|
|
3
|
+
"true",
|
|
4
|
+
"t",
|
|
5
|
+
"1",
|
|
6
|
+
"⊤"
|
|
7
|
+
]);
|
|
8
|
+
const FALSE = new Set([
|
|
9
|
+
"false",
|
|
10
|
+
"f",
|
|
11
|
+
"0",
|
|
12
|
+
"⊥"
|
|
13
|
+
]);
|
|
14
|
+
const WORD_OPS = {
|
|
15
|
+
not: { type: "not" },
|
|
16
|
+
and: {
|
|
17
|
+
type: "bin",
|
|
18
|
+
op: "and"
|
|
19
|
+
},
|
|
20
|
+
or: {
|
|
21
|
+
type: "bin",
|
|
22
|
+
op: "or"
|
|
23
|
+
},
|
|
24
|
+
xor: {
|
|
25
|
+
type: "bin",
|
|
26
|
+
op: "xor"
|
|
27
|
+
},
|
|
28
|
+
implies: {
|
|
29
|
+
type: "bin",
|
|
30
|
+
op: "implies"
|
|
31
|
+
},
|
|
32
|
+
iff: {
|
|
33
|
+
type: "bin",
|
|
34
|
+
op: "iff"
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** Multi-char symbol operators, longest-match first. */
|
|
38
|
+
const SYMBOLS = [
|
|
39
|
+
{
|
|
40
|
+
re: "<->",
|
|
41
|
+
tok: {
|
|
42
|
+
type: "bin",
|
|
43
|
+
value: "iff"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
re: "<=>",
|
|
48
|
+
tok: {
|
|
49
|
+
type: "bin",
|
|
50
|
+
value: "iff"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
re: "->",
|
|
55
|
+
tok: {
|
|
56
|
+
type: "bin",
|
|
57
|
+
value: "implies"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
re: "=>",
|
|
62
|
+
tok: {
|
|
63
|
+
type: "bin",
|
|
64
|
+
value: "implies"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
re: "&&",
|
|
69
|
+
tok: {
|
|
70
|
+
type: "bin",
|
|
71
|
+
value: "and"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
re: "||",
|
|
76
|
+
tok: {
|
|
77
|
+
type: "bin",
|
|
78
|
+
value: "or"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
re: "∧",
|
|
83
|
+
tok: {
|
|
84
|
+
type: "bin",
|
|
85
|
+
value: "and"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
re: "·",
|
|
90
|
+
tok: {
|
|
91
|
+
type: "bin",
|
|
92
|
+
value: "and"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
re: "&",
|
|
97
|
+
tok: {
|
|
98
|
+
type: "bin",
|
|
99
|
+
value: "and"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
re: "∨",
|
|
104
|
+
tok: {
|
|
105
|
+
type: "bin",
|
|
106
|
+
value: "or"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
re: "+",
|
|
111
|
+
tok: {
|
|
112
|
+
type: "bin",
|
|
113
|
+
value: "or"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
re: "|",
|
|
118
|
+
tok: {
|
|
119
|
+
type: "bin",
|
|
120
|
+
value: "or"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
re: "⊕",
|
|
125
|
+
tok: {
|
|
126
|
+
type: "bin",
|
|
127
|
+
value: "xor"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
re: "^",
|
|
132
|
+
tok: {
|
|
133
|
+
type: "bin",
|
|
134
|
+
value: "xor"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
re: "↔",
|
|
139
|
+
tok: {
|
|
140
|
+
type: "bin",
|
|
141
|
+
value: "iff"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
re: "≡",
|
|
146
|
+
tok: {
|
|
147
|
+
type: "bin",
|
|
148
|
+
value: "iff"
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
re: "→",
|
|
153
|
+
tok: {
|
|
154
|
+
type: "bin",
|
|
155
|
+
value: "implies"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
re: "⊃",
|
|
160
|
+
tok: {
|
|
161
|
+
type: "bin",
|
|
162
|
+
value: "implies"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
re: "¬",
|
|
167
|
+
tok: {
|
|
168
|
+
type: "not",
|
|
169
|
+
value: "not"
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
re: "~",
|
|
174
|
+
tok: {
|
|
175
|
+
type: "not",
|
|
176
|
+
value: "not"
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
re: "!",
|
|
181
|
+
tok: {
|
|
182
|
+
type: "not",
|
|
183
|
+
value: "not"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
re: "(",
|
|
188
|
+
tok: {
|
|
189
|
+
type: "lparen",
|
|
190
|
+
value: "("
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
re: ")",
|
|
195
|
+
tok: {
|
|
196
|
+
type: "rparen",
|
|
197
|
+
value: ")"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
];
|
|
201
|
+
const isIdentStart = (c) => /[A-Za-z]/.test(c);
|
|
202
|
+
const isIdent = (c) => /[A-Za-z0-9_]/.test(c);
|
|
203
|
+
function tokenizeLogic(src) {
|
|
204
|
+
const out = [];
|
|
205
|
+
let i = 0;
|
|
206
|
+
const s = src;
|
|
207
|
+
outer: while (i < s.length) {
|
|
208
|
+
const c = s[i];
|
|
209
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
210
|
+
i++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
for (const { re, tok } of SYMBOLS) if (s.startsWith(re, i)) {
|
|
214
|
+
out.push({ ...tok });
|
|
215
|
+
i += re.length;
|
|
216
|
+
continue outer;
|
|
217
|
+
}
|
|
218
|
+
if (isIdentStart(c)) {
|
|
219
|
+
let j = i + 1;
|
|
220
|
+
while (j < s.length && isIdent(s[j])) j++;
|
|
221
|
+
const word = s.slice(i, j);
|
|
222
|
+
const lower = word.toLowerCase();
|
|
223
|
+
i = j;
|
|
224
|
+
if (TRUE.has(lower)) out.push({
|
|
225
|
+
type: "const",
|
|
226
|
+
value: "true"
|
|
227
|
+
});
|
|
228
|
+
else if (FALSE.has(lower)) out.push({
|
|
229
|
+
type: "const",
|
|
230
|
+
value: "false"
|
|
231
|
+
});
|
|
232
|
+
else if (WORD_OPS[lower]) {
|
|
233
|
+
const w = WORD_OPS[lower];
|
|
234
|
+
out.push({
|
|
235
|
+
type: w.type,
|
|
236
|
+
value: w.op ?? "not"
|
|
237
|
+
});
|
|
238
|
+
} else out.push({
|
|
239
|
+
type: "var",
|
|
240
|
+
value: word
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (c === "1" || c === "0") {
|
|
245
|
+
out.push({
|
|
246
|
+
type: "const",
|
|
247
|
+
value: c === "1" ? "true" : "false"
|
|
248
|
+
});
|
|
249
|
+
i++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
throw new Error(`Unexpected character "${c}" at ${i}`);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
//#endregion
|
|
258
|
+
export { tokenizeLogic };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/math/ast.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* expr/ast — the Abstract Syntax Tree: node types, evaluation, free-variable
|
|
4
|
+
* collection, and small builder helpers. The AST is the shared representation
|
|
5
|
+
* that evaluation, differentiation, simplification, and LaTeX printing all
|
|
6
|
+
* operate on.
|
|
7
|
+
*/
|
|
8
|
+
type BinOp = '+' | '-' | '*' | '/' | '%' | '^';
|
|
9
|
+
type Node = {
|
|
10
|
+
type: 'num';
|
|
11
|
+
value: number;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'var';
|
|
14
|
+
name: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'neg';
|
|
17
|
+
arg: Node;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'binary';
|
|
20
|
+
op: BinOp;
|
|
21
|
+
left: Node;
|
|
22
|
+
right: Node;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'call';
|
|
25
|
+
fn: string;
|
|
26
|
+
args: Node[];
|
|
27
|
+
};
|
|
28
|
+
/** Evaluate the AST against a scope of variable values. Returns NaN for unknowns. */
|
|
29
|
+
declare function evaluate(node: Node, scope: Record<string, number>): number;
|
|
30
|
+
/** Free variables referenced (excludes constants & function names). */
|
|
31
|
+
declare function freeVars(node: Node, into?: Set<string>): Set<string>;
|
|
32
|
+
/** Compile to a fast closure (a thin wrapper over `evaluate`). */
|
|
33
|
+
declare const compileNode: (node: Node) => (scope: Record<string, number>) => number;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { BinOp, Node, compileNode, evaluate, freeVars };
|