diff --git a/lib/util/fixpoint.js b/lib/util/fixpoint.js
new file mode 100644
index 0000000..c149fce
--- /dev/null
+++ b/lib/util/fixpoint.js
@@ -0,0 +1,87 @@
+// Based on:
+// https://github.com/plug-obp/obp3-algos/blob/1f15cf8c829c7ecd08adb7180aa3c3aefa4f9466/src/main/java/obp3/fixer/Fixer.java
+// which is in turn based on:
+// FRANCOIS POTTIER, Lazy Least Fixed Points in ML
+// OCAML Code
+
+export const fixer = (eqsFunction, lattice) => {
+ const mFixed = new Map();
+ const mTransient = new Map();
+ const mParents = new Map();
+
+ const mWorkset = []; // Note: we use a Stack (LIFO), but FIFO queue or any other collection would also work.
+
+ let inactive = true;
+
+ const ensureTransient = node => {
+ if (mTransient.has(node)) return;
+ mTransient.set(node, lattice.bottom());
+ mParents.set(node, []);
+ mWorkset.push(node);
+ }
+
+ const solve = current => {
+ if (mFixed.get(current) !== undefined) return;
+ const currentChildren = [];
+ const alive = [true];
+ const requestFunction = node => {
+ if (!alive[0]) throw new Error("must be alive!");
+ const property = mFixed.get(node);
+ if (property !== undefined) return property;
+ ensureTransient(node);
+ currentChildren.push(node);
+ return mTransient.get(node);
+ }
+ const newProperty = eqsFunction(current)(requestFunction);
+ alive[0] = false;
+
+ const isMaximal = lattice.isMaximal(newProperty);
+ if (!isMaximal) {
+ for (const child of currentChildren) {
+ const parents = mParents.get(child);
+ if (parents === undefined) {
+ mParents.set(child, [current]);
+ } else {
+ parents.push(current);
+ }
+ }
+
+ if (!lattice.equality(mTransient.get(current), newProperty)) {
+ mTransient.put(current, newProperty);
+ mWorkset.push(...(mParents.get(current) || []));
+ }
+ }
+ else {
+ mFixed.set(current, newProperty);
+ const oldProperty = mTransient.get(current);
+ mTransient.delete(current);
+ if (!lattice.equality(oldProperty, newProperty)) {
+ mWorkset.push(...(mParents.get(current) || []));
+ }
+ }
+ }
+
+ const get = node => {
+ const property = mFixed.get(node);
+ if (property !== undefined) return property; // return from cache
+
+ if (!inactive) throw new Error("must be inactive!");
+ inactive = false;
+
+ ensureTransient(node);
+
+ while (mWorkset.length > 0) {
+ const current = mWorkset.pop(); // LIFO
+ solve(current);
+ }
+
+ for (const [key, value] of mTransient) {
+ mFixed.set(key, value);
+ }
+ mTransient.clear();
+ inactive = true;
+ return mFixed.get(node);
+ }
+
+ return get;
+}
diff --git a/tests/fixpoint.js b/tests/fixpoint.js
new file mode 100644
index 0000000..8942723
--- /dev/null
+++ b/tests/fixpoint.js
@@ -0,0 +1,74 @@
+import { fixer } from "../lib/util/fixpoint.js";
+
+const booleanLattice = {
+ isMaximal: (value) => value,
+ equality: (l, r) => l === r,
+ bottom: () => false,
+};
+
+const nullable = grammar => nonterminal => request => {
+ console.log(nonterminal);
+ const f = nt => {
+ return {
+ Epsilon: () => true,
+ T: () => false,
+ NT: () => request(nt.nt),
+ Seq: () => f(nt.l) && f(nt.r),
+ Alt: () => f(nt.l) || f(nt.r),
+ }[nt.kind]();
+ }
+ return f(grammar[nonterminal]);
+}
+
+// S → A B C
+// A → a A | ε
+// B → b B | A C
+// C → c C | ε
+const exampleGrammar = {
+ S: {kind: "Seq",
+ l: {kind: "NT", nt: "A"},
+ r: {kind: "Seq",
+ l: {kind: "NT", nt: "B"},
+ r: {kind: "NT", nt: "C"},
+ },
+ },
+
+ A: {kind: "Alt",
+ l: {kind: "Seq",
+ l: {kind: "T", token: "a"},
+ r: {kind: "NT", nt: "A"}
+ },
+ r: {kind: "Epsilon"},
+ },
+
+ B: {kind: "Alt",
+ l: {kind: "Seq",
+ l: {kind: "T", token: "b"},
+ r: {kind: "NT", nt: "B"}
+ },
+ r: {kind: "Seq",
+ l: {kind: "NT", nt: "A"},
+ r: {kind: "NT", nt: "C"}
+ },
+ },
+
+ C: {kind: "Alt",
+ l: {kind: "Seq",
+ l: {kind: "T", token: "c"},
+ r: {kind: "NT", nt: "C"}
+ },
+ r: {kind: "Epsilon"},
+ },
+};
+// A is nullable because it has a production A → ε.
+// C is nullable because it has a production C → ε.
+// B is nullable because B → A C, and both A and C are nullable.
+// S is nullable because S → A B C, and A, B, and C are all nullable.
+
+
+const isNullable = fixer(nullable(exampleGrammar), booleanLattice);
+
+console.log(isNullable("S"));
+console.log(isNullable("A"));
+console.log(isNullable("B"));
+console.log(isNullable("C"));
\ No newline at end of file