Repository: smucclaw/l4-ide
Pull request #335: Viz: Add prelim frontend-only, boolean-operator-only what-if-style evaluation with three values
Author: login: ym-han
Labels: name: enhancement
description: New feature or request
name: ladder
Created: 2025-04-04T06:17:40Z | Merged: 2025-04-15T09:34:00Z

Closes #332 

More sub-tasks:
- [x] Refactor to avoid any mention of Lam/App, closures, etc; refactor away the overly complicated what-if eval stuff.
- [x] Keep track of the IRExpr ID => Dag<Lir> mapping; thread it through the traversal. (This will only really get used in the compatible vs. incompatible paths stuff. There's a bug here, but it's fixed in the next PR and doesn't affect anything that gets displayed in this PR.)
- [x] Refactor eval to be over a variant of the original IRExpr, instead of being over the LadderLirGraph
- [x] Expose the result value with simple UI. Result is displayed below the name / label of the DECIDE.
- [x] Review Mar 27's meeting to make sure there's nothing incompatible with what I have
- [x] Check if we really do want three values (answer from Mar 27 meeting seems to be: yes)
- [x] Look through everything once more

<img width="1453" alt="image" src="https://github.com/user-attachments/assets/592f0432-edea-4468-9c8d-9e7a2e0f2d1b" />

<img width="1471" alt="image" src="https://github.com/user-attachments/assets/f9d9d50f-8c07-4093-b83c-1d1356ebd41f" />

Compatible vs incompatible paths UI will be done in the next PR.

There are small UI tweaks that could be done in the future, like tweaking the UI to make it more obvious that the nodes are clickable.


Linked issue #332: Viz: Add prelim simple frontend-only what-if-style evaluator that incorporates a 3-valued logic and displays the result with a simple UI. No distinguishing between compatible vs incompatible paths

Diff:
diff --git a/ts-apps/decision-logic-visualizer/src/lib/data/viz-expr-to-lir.ts b/ts-apps/decision-logic-visualizer/src/lib/data/viz-expr-to-lir.ts
index ee19efaa8..7d417da45 100644
--- a/ts-apps/decision-logic-visualizer/src/lib/data/viz-expr-to-lir.ts
+++ b/ts-apps/decision-logic-visualizer/src/lib/data/viz-expr-to-lir.ts
@@ -1,4 +1,4 @@
-import type { IRDecl, IRExpr } from '@repo/viz-expr'
+import type { IRDecl, IRExpr, IRId } from '@repo/viz-expr'
 /*
 Do not use $lib for the layout-ir imports
 */
@@ -51,14 +51,19 @@ export const LadderGraphLirSource: LirSource<IRExpr, LadderGraphLirNode> = {
     const overallSource = vertex(new SourceNoAnnoLirNode(nodeInfo).getId())
     const overallSink = vertex(new SinkLirNode(nodeInfo).getId())
 
-    const middle = transform(nodeInfo, expr)
+    const { graph: middle, vizExprToLir } = transform(nodeInfo, new Map(), expr)
 
     const dag = overallSource
       .connect(middle.getSource())
       .overlay(middle)
       .overlay(middle.getSink().connect(overallSink))
 
-    const ladderGraph = new LadderGraphLirNode(nodeInfo, dag)
+    const ladderGraph = new LadderGraphLirNode(
+      nodeInfo,
+      dag,
+      vizExprToLir,
+      expr
+    )
 
     // 2. Augment with explanatory edge labels (TODO: Not sure this shld happen here)
     augmentEdgesWithExplanatoryLabel(nodeInfo.context, ladderGraph)
@@ -70,11 +75,18 @@ export const LadderGraphLirSource: LirSource<IRExpr, LadderGraphLirNode> = {
 // TODO2: Attach a group id to the label for the flownode to make it easier to debug
 // TODO3: Return the number of groups as metadata in the FlowGraph
 
+export interface ToLirResult {
+  graph: DirectedAcyclicGraph<LirId>
+  vizExprToLir: Map<IRId, DirectedAcyclicGraph<LirId>>
+}
+
 /** Internal helper */
 function transform(
   nodeInfo: LirNodeInfo,
+  /** vizExprToLirEnv */
+  env: Map<IRId, DirectedAcyclicGraph<LirId>>,
   expr: IRExpr
-): DirectedAcyclicGraph<LirId> {
+): ToLirResult {
   /*
   One of the insights behind the ladder diagram, in particular, behind Meng's `layman`:
     Reduce visual clutter, by creating dummy source and sink nodes between which to nest the subgraphs, and only connecting those.
@@ -100,28 +112,55 @@ function transform(
   return match(expr)
     .with({ $type: 'BoolVar' }, (originalVar) => {
       const boolvar = new BoolVarLirNode(nodeInfo, originalVar)
-      return vertex(boolvar.getId())
+      const graph = vertex(boolvar.getId())
+      const newEnv = new Map(env).set(originalVar.id, graph)
+      return { graph, vizExprToLir: newEnv }
     })
     .with({ $type: 'Not' }, (neg) => {
-      const negand = transform(nodeInfo, neg.negand)
+      const { graph: negand, vizExprToLir: negandEnv } = transform(
+        nodeInfo,
+        env,
+        neg.negand
+      )
       const notStart = vertex(new NotStartLirNode(nodeInfo, negand).getId())
       const notEnd = vertex(new NotEndLirNode(nodeInfo).getId())
-      return notStart
+
+      const notGraph = notStart
         .connect(negand.getSource())
         .overlay(negand)
         .overlay(negand.getSink().connect(notEnd))
+      const newEnv = new Map([...env, ...negandEnv])
+
+      return { graph: notGraph, vizExprToLir: newEnv }
     })
     .with({ $type: 'And' }, (andExpr) => {
-      const childGraphs = andExpr.args.map((arg) => transform(nodeInfo, arg))
+      const childResults = andExpr.args.map((arg) =>
+        transform(nodeInfo, env, arg)
+      )
+      const childGraphs = childResults.map((result) => result.graph)
 
-      return childGraphs.reduceRight((acc, left) => {
+      const combinedGraph = childGraphs.reduceRight((acc, left) => {
         const accSource = acc.getSource()
         const leftSink = left.getSink()
         return leftSink.connect(accSource).overlay(left).overlay(acc)
       })
+
+      const allEnvs = [
+        env,
+        ...childResults.map((result) => result.vizExprToLir),
+      ]
+      const combinedEnv = new Map(
+        allEnvs.reduceRight(
+          (accEntries, env) => [...env, ...accEntries],
+          [] as [IRId, DirectedAcyclicGraph<LirId>][]
+        )
+      )
+
+      return { graph: combinedGraph, vizExprToLir: combinedEnv }
     })
     .with({ $type: 'Or' }, (orExpr) => {
-      const children = orExpr.args.map((n) => transform(nodeInfo, n))
+      const childResults = orExpr.args.map((n) => transform(nodeInfo, env, n))
+      const childGraphs = childResults.map((result) => result.graph)
 
       const overallSource = vertex(
         new SourceWithOrAnnoLirNode(
@@ -131,16 +170,30 @@ function transform(
       )
       const overallSink = vertex(new SinkLirNode(nodeInfo).getId())
 
-      const leftEdges = children.map((child) =>
+      const leftEdges = childGraphs.map((child) =>
         overallSource.connect(child.getSource())
       )
-      const rightEdges = children.map((child) =>
+      const rightEdges = childGraphs.map((child) =>
         child.getSink().connect(overallSink)
       )
 
-      return [overallSource, ...leftEdges, ...children, ...rightEdges].reduce(
-        overlay
+      const orGraph = [
+        overallSource,
+        ...leftEdges,
+        ...childGraphs,
+        ...rightEdges,
+      ].reduce(overlay)
+
+      // Envs
+      const childEnvs = childResults.map((result) => result.vizExprToLir)
+      const newEnv = new Map(
+        childEnvs.reduceRight(
+          (entries, env) => [...env, ...entries],
+          [] as [IRId, DirectedAcyclicGraph<LirId>][]
+        )
       )
+
+      return { graph: orGraph, vizExprToLir: newEnv }
     })
     .exhaustive()
 }
diff --git a/ts-apps/decision-logic-visualizer/src/lib/displayers/flow/flow-base.svelte b/ts-apps/decision-logic-visualizer/src/lib/displayers/flow/flow-base.svelte
index 24d7bc14f..f32d31a29 100644
--- a/ts-apps/decision-logic-visualizer/src/lib/displayers/flow/flow-base.svelte
+++ b/ts-apps/decision-logic-visualizer/src/lib/displayers/flow/flow-base.svelte
@@ -31,7 +31,7 @@
     type LadderSFNode,
   } from './svelteflow-types.js'
   import { ladderGraphToSFGraph } from './ladder-lir-to-sf.js'
-  import { cycle } from '$lib/layout-ir/value.js'
+  import { cycle } from '$lib/eval/type.js'
   import { onMount } from 'svelte'
   import { Debounced, watch } from 'runed'
 
@@ -131,6 +131,7 @@
         if (debouncedSfNodes$Initialized.current) {
           doLayoutAndFitView()
         }
+        updateResultDisplay()
       }
     )
 
@@ -189,6 +190,15 @@
     }
   }
 
+  /***********************************
+            Result UI
+  ************************************/
+
+  let resultMessage: string = $state('')
+  function updateResultDisplay() {
+    resultMessage = `evaluates to ${ladderGraph.getResult(context).toPretty()} (what-if mode)`
+  }
+
   /*********************************************
         LadderGraph event listener
   **********************************************/
@@ -211,6 +221,8 @@
       sfIdToLirId = newSfGraph.sfIdToLirId
       lirIdToSfId = newSfGraph.lirIdToSFId
       // console.log('newSfGraph NODES', NODES)
+
+      updateResultDisplay()
     }
   }
 
@@ -305,6 +317,7 @@ Misc SF UI TODOs:
 <!-- The consumer containing div must set the height to, e.g., 96svh if that's what's wanted -->
 <div class="overall-container">
   <h1>{declLirNode.getFunName(context)}</h1>
+  <h2>{resultMessage}</h2>
   <div class="flow-container" style={`opacity: ${flowOpacity}`}>
     <SvelteFlow
       bind:nodes={NODES}
@@ -367,6 +380,10 @@ Misc SF UI TODOs:
     @apply text-2xl font-semibold text-center mt-1;
   }
 
+  h2 {
+    @apply text-lg text-center -mt-2;
+  }
+
   .overall-container {
     display: flex;
     flex-direction: column;
diff --git a/ts-apps/decision-logic-visualizer/src/lib/displayers/flow/svelteflow-types.ts b/ts-apps/decision-logic-visualizer/src/lib/displayers/flow/svelteflow-types.ts
index 609a60076..0c7482f03 100644
--- a/ts-apps/decision-logic-visualizer/src/lib/displayers/flow/svelteflow-types.ts
+++ b/ts-apps/decision-logic-visualizer/src/lib/displayers/flow/svelteflow-types.ts
@@ -1,7 +1,7 @@
 // Types and util functions for the Svelte Flow graph
 
 import type { Name } from '@repo/viz-expr'
-import type { BoolVal } from '$lib/layout-ir/value.js'
+import type { UBoolValue } from '$lib/eval/type.js'
 import type { LirContext, LirId } from '$lib/layout-ir/core.js'
 import { emptyEdgeLabel, EmptyEdgeStyles } from '../../algebraic-graphs/edge.js'
 import * as SF from '@xyflow/svelte'
@@ -116,7 +116,7 @@ export interface LadderSFNodeData {
 
 export interface BoolVarDisplayerData extends LadderSFNodeData {
   name: Name
-  value: BoolVal
+  value: UBoolValue
 }
 
 export interface BundlingNodeDisplayerData extends LadderSFNodeData {
diff --git a/ts-apps/decision-logic-visualizer/src/lib/eval/assignment.ts b/ts-apps/decision-logic-visualizer/src/lib/eval/assignment.ts
new file mode 100644
index 000000000..3d77da82e
--- /dev/null
+++ b/ts-apps/decision-logic-visualizer/src/lib/eval/assignment.ts
@@ -0,0 +1,81 @@
+import type { Unique } from '@repo/viz-expr'
+import type { Value } from './type.js'
+import type { LirId } from '../layout-ir/core.js'
+
+/** A mapping from Unique to Value */
+export class Assignment {
+  #subst: Array<Value>
+
+  static fromEntries(entries: Array<[Unique, Value]>) {
+    const initial: Array<Value> = []
+    entries.forEach(([unique, value]) => {
+      initial[unique] = value
+    })
+    return new Assignment(initial)
+  }
+
+  constructor(initial: Array<Value>) {
+    this.#subst = initial
+  }
+
+  /** Set binding */
+  set(unique: Unique, value: Value) {
+    this.#subst[unique] = value
+  }
+
+  /** Get binding */
+  get(unique: Unique): Value | undefined {
+    return this.#subst[unique]
+  }
+
+  getUniques(): Array<Unique> {
+    return this.#subst.map((_, i) => i)
+  }
+
+  getEntries(): Array<[Unique, Value]> {
+    // Need the filter because the underlying array can be sparse / have holes
+    return Array.from(this.#subst.entries()).filter(
+      ([, value]) => value !== undefined
+    )
+  }
+
+  asArray(): Array<Value> {
+    return this.#subst.slice()
+  }
+
+  clone() {
+    return new Assignment(this.#subst.slice())
+  }
+}
+
+/**
+ * Coreferents: mapping from Unique to Set<LirId>
+ */
+export class Corefs {
+  // A Unique is a non-negative integer
+  #coreferents: Array<Set<LirId>>
+
+  static fromEntries(entries: Array<[Unique, LirId]>): Corefs {
+    const initialCoreferents: Array<Set<LirId>> = []
+    entries.forEach(([unique, lirId]) => {
+      if (!initialCoreferents[unique]) {
+        initialCoreferents[unique] = new Set([lirId])
+      } else {
+        initialCoreferents[unique].add(lirId)
+      }
+    })
+    return new Corefs(initialCoreferents)
+  }
+
+  private constructor(initialCoreferents: Array<Set<LirId>>) {
+    this.#coreferents = initialCoreferents
+  }
+
+  getCoreferents(unique: Unique) {
+    return this.#coreferents[unique] ?? new Set<LirId>()
+  }
+
+  dispose() {
+    this.#coreferents = []
+  }
+}
diff --git a/ts-apps/decision-logic-visualizer/src/lib/eval/eval.ts b/ts-apps/decision-logic-visualizer/src/lib/eval/eval.ts
new file mode 100644
index 000000000..ba1599c56
--- /dev/null
+++ b/ts-apps/decision-logic-visualizer/src/lib/eval/eval.ts
@@ -0,0 +1,110 @@
+import type {
+  Value,
+  UBoolValue,
+  Expr,
+  EVBoolVar,
+  Not,
+  Or,
+  And,
+} from './type.js'
+import {
+  TrueVal,
+  FalseVal,
+  UnknownVal,
+  isTrueVal,
+  isFalseVal,
+  isUBoolValue,
+} from './type.js'
+import { Assignment } from './assignment.js'
+import { match } from 'ts-pattern'
+
+/**********************************************************
+                   Evaluator
+**********************************************************/
+
+/*
+------------------------------------------
+       UI vs. evaln
+------------------------------------------
+The Evaluator should be using the Assignment and NOT the get/setValue methods on LirNodes,
+since the LirNode values are meant to be for the UI
+(and hence fall under the purview of the LadderGraphLirNode).
+
+-------------------------------------------
+  Implementation: At least two approaches
+-------------------------------------------
+
+1. Do eval on the original IRExpr. 
+   Apply whatever UI stuff is needed on the Dag<LirId> using the 
+   IRId => LirId mapping
+   -- eg for incompatible paths, for each IRNode that's false, 
+      grey out the corresponding Dag<LirId>
+
+2. Do eval on the Dag<LirId> directly
+
+Ended up going with 2, since the postprocessing of the Dag<LirId> gets a bit involved,
+and since 2 might align better with synchronizing with the backend in the future.
+*/
+
+/** Boolean operator evaluator */
+export interface LadderEvaluator {
+  eval(ladder: Expr, assignment: Assignment): Value
+}
+
+export const Evaluator: LadderEvaluator = {
+  eval(ladder: Expr, assignment: Assignment): Value {
+    return eval_(ladder, assignment)
+  },
+}
+
+function eval_(ladder: Expr, assignment: Assignment): Value {
+  return match(ladder)
+    .with(
+      { $type: 'BoolVar' },
+      (expr: EVBoolVar) => assignment.get(expr.name.unique) as UBoolValue
+    )
+    .with({ $type: 'Not' }, (expr: Not) => {
+      const negandV = eval_(expr.negand, assignment)
+      if (!isUBoolValue(negandV)) {
+        throw new Error('Expected the negajnd to eval to a BoolVal')
+      }
+      return match(negandV)
+        .with({ $type: 'TrueVal' }, () => new FalseVal())
+        .with({ $type: 'FalseVal' }, () => new TrueVal())
+        .with({ $type: 'UnknownVal' }, () => new UnknownVal())
+        .exhaustive()
+    })
+    .with({ $type: 'And' }, (expr: And) => {
+      const andVals = expr.args.map((arg) => eval_(arg, assignment))
+      return evalAndChain(andVals)
+    })
+    .with({ $type: 'Or' }, (expr: Or) => {
+      const orVals = expr.args.map((arg) => eval_(arg, assignment))
+      return evalOrChain(orVals)
+    })
+    .exhaustive()
+}
+
+/***************************
+      AndChain, OrChain
+****************************/
+
+function evalAndChain(bools: UBoolValue[]) {
+  if (bools.some(isFalseVal)) {
+    return new FalseVal()
+  }
+  if (bools.every(isTrueVal)) {
+    return new TrueVal()
+  }
+  return new UnknownVal()
+}
+
+function evalOrChain(bools: UBoolValue[]) {
+  if (bools.some(isTrueVal)) {
+    return new TrueVal()
+  }
+  if (bools.every(isFalseVal)) {
+    return new FalseVal()
+  }
+  return new UnknownVal()
+}
diff --git a/ts-apps/decision-logic-visualizer/src/lib/eval/type.ts b/ts-apps/decision-logic-visualizer/src/lib/eval/type.ts
new file mode 100644
index 000000000..f198b4f7f
--- /dev/null
+++ b/ts-apps/decision-logic-visualizer/src/lib/eval/type.ts
@@ -0,0 +1,106 @@
+import { match } from 'ts-pattern'
+import { IRExpr } from '@repo/viz-expr'
+import * as VE from '@repo/viz-expr'
+
+/********************************
+          Value
+*********************************/
+
+export type Value = UBoolValue
+
+export type UBoolValue = TrueVal | FalseVal | UnknownVal
+export function isUBoolValue(val: Value): val is UBoolValue {
+  return (
+    val.$type === 'TrueVal' ||
+    val.$type === 'FalseVal' ||
+    val.$type === 'UnknownVal'
+  )
+}
+
+interface UBoolV {
+  $type: 'TrueVal' | 'FalseVal' | 'UnknownVal'
+  getClasses(): string[]
+  toPretty(): string
+}
+
+export function isTrueVal(val: UBoolValue): val is TrueVal {
+  return val.$type === 'TrueVal'
+}
+
+export function isFalseVal(val: UBoolValue): val is FalseVal {
+  return val.$type === 'FalseVal'
+}
+
+export class TrueVal implements UBoolV {
+  $type: 'TrueVal' = 'TrueVal' as const
+  constructor() {}
+
+  getClasses() {
+    return ['true-val']
+  }
+
+  toPretty() {
+    return 'TRUE'
+  }
+}
+
+export class FalseVal implements UBoolV {
+  $type: 'FalseVal' = 'FalseVal' as const
+  constructor() {}
+
+  getClasses() {
+    return ['false-val']
+  }
+
+  toPretty() {
+    return 'FALSE'
+  }
+}
+
+export function isUnknownVal(val: UBoolValue): val is UnknownVal {
+  return val.$type === 'UnknownVal'
+}
+
+export class UnknownVal implements UBoolV {
+  $type: 'UnknownVal' = 'UnknownVal' as const
+  constructor() {}
+
+  getClasses() {
+    return []
+  }
+
+  toPretty() {
+    return 'UNKNOWN'
+  }
+}
+
+export function cycle(val: UBoolValue): UBoolValue {
+  return match(val)
+    .with({ $type: 'TrueVal' }, () => new FalseVal())
+    .with({ $type: 'FalseVal' }, () => new UnknownVal())
+    .with({ $type: 'UnknownVal' }, () => new TrueVal())
+    .exhaustive()
+}
+
+/**********************************************************
+            Expr
+**********************************************************/
+
+/** This is basically IRExpr, but where the BoolVar doesn't have the `value` field,
+ * since we want to use values from the user,
+ * as opposed to the initial values.
+ */
+export type Expr = Exclude<IRExpr, { $type: 'BoolVar' }> | EVBoolVar
+
+/** The IRExpr BoolVar, except without the `value` field
+ * (since we want to use values from the user, as opposed to the initial values) */
+export type EVBoolVar = Omit<VE.BoolVar, 'value'>
+export type Not = VE.Not
+export type Or = VE.Or
+export type And = VE.And
+
+export function veExprToEvExpr(expr: IRExpr): Expr {
+  return match(expr)
+    .with({ $type: 'BoolVar' }, (expr) => expr as EVBoolVar)
+    .otherwise(() => expr)
+}
diff --git a/ts-apps/decision-logic-visualizer/src/lib/layout-ir/environment.ts b/ts-apps/decision-logic-visualizer/src/lib/layout-ir/environment.ts
deleted file mode 100644
index 1d6f2c470..000000000
--- a/ts-apps/decision-logic-visualizer/src/lib/layout-ir/environment.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { Unique } from '@repo/viz-expr'
-import type { Value } from './value.js'
-import type { LirId } from './core.js'
-
-export class Environment {
-  // A Unique is a non-negative integer
-  #coreferents: Array<Set<LirId>>
-
-  /** Strictly speaking, we don't need the #env to do what we currently do */
-  #env: Array<Value>
-
-  constructor(initialEnv: Array<Value>, initialCoreferents: Array<Set<LirId>>) {
-    this.#env = initialEnv
-    this.#coreferents = initialCoreferents
-  }
-
-  getCoreferents(unique: Unique) {
-    return this.#coreferents[unique] ?? new Set<LirId>()
-  }
-
-  /** Set binding */
-  set(binding: { unique: Unique; value: Value }) {
-    this.#env[binding.unique] = binding.value
-  }
-
-  /** Get binding */
-  get(unique: Unique): Value | undefined {
-    return this.#env[unique]
-  }
-
-  dispose() {
-    this.#env = []
-    this.#coreferents = []
-  }
-}
diff --git a/ts-apps/decision-logic-visualizer/src/lib/layout-ir/ladder-lir.svelte.ts b/ts-apps/decision-logic-visualizer/src/lib/layout-ir/ladder-lir.svelte.ts
index 9d337405a..546345931 100644
--- a/ts-apps/decision-logic-visualizer/src/lib/layout-ir/ladder-lir.svelte.ts
+++ b/ts-apps/decision-logic-visualizer/src/lib/layout-ir/ladder-lir.svelte.ts
@@ -1,13 +1,15 @@
 /* eslint-disable @typescript-eslint/no-unused-vars */
 import {
   type Value,
-  type BoolVal,
+  type UBoolValue,
   TrueVal,
   FalseVal,
   UnknownVal,
-} from './value.js'
-import type { BoolVar, Unique, Name } from '@repo/viz-expr'
-import { Environment } from './environment.js'
+  veExprToEvExpr,
+} from '../eval/type.js'
+import type { IRExpr, BoolVar, Unique, Name, IRId } from '@repo/viz-expr'
+import { Assignment, Corefs } from '../eval/assignment.js'
+import { Evaluator } from '$lib/eval/eval.js'
 import type { LirId, LirNode, LirNodeInfo } from './core.js'
 import { LirContext, DefaultLirNode } from './core.js'
 import type { Ord } from '$lib/utils.js'
@@ -327,31 +329,55 @@ will go through the LadderGraphLirNode.
 */
 export class LadderGraphLirNode extends DefaultLirNode implements LirNode {
   #dag: DirectedAcyclicGraph<LirId>
+
+  #originalExpr: IRExpr
+  #vizExprToLir: Map<IRId, DirectedAcyclicGraph<LirId>>
   /** This will have to be updated if (and only if) we change the structure of the graph.
    * No need to update it, tho, if changing edge attributes.
    */
+  #corefs: Corefs
+  /** Keeps track of what values the user has assigned to the various var ladder nodes */
+  #bindings: Assignment
+  #result: Value = new UnknownVal()
+
   #pathsList?: PathsListLirNode
-  #environment: Environment
 
-  constructor(nodeInfo: LirNodeInfo, dag: DirectedAcyclicGraph<LirId>) {
+  constructor(
+    nodeInfo: LirNodeInfo,
+    dag: DirectedAcyclicGraph<LirId>,
+    vizExprToLir: Map<IRId, DirectedAcyclicGraph<LirId>>,
+    originalExpr: IRExpr
+  ) {
     super(nodeInfo)
     this.#dag = dag
+    this.#originalExpr = originalExpr
+    this.#vizExprToLir = vizExprToLir
 
-    // Make the initial environment
     const varNodes = getVerticesFromAlgaDag(nodeInfo.context, this.#dag).filter(
-      (n) => n instanceof BoolVarLirNode
+      isBoolVarLirNode
     )
-    const initialEnv = varNodes.map((n) => n.getValue(nodeInfo.context))
-    const initialCoreferents: Array<Set<LirId>> = []
-    varNodes.forEach((n) => {
-      const unique = n.getUnique(nodeInfo.context)
-      if (!initialCoreferents[unique]) {
-        initialCoreferents[unique] = new Set<LirId>([n.getId()])
-      } else {
-        initialCoreferents[unique].add(n.getId())
-      }
-    })
-    this.#environment = new Environment(initialEnv, initialCoreferents)
+
+    // Make the initial args / assignment
+    const initialAssignmentAssocList: Array<[Unique, Value]> = varNodes.map(
+      (varN) => [
+        varN.getUnique(nodeInfo.context),
+        varN.getValue(nodeInfo.context),
+      ]
+    )
+    this.#bindings = Assignment.fromEntries(initialAssignmentAssocList)
+
+    // Make the initial corefs
+    const uniqLirIdPairs: Array<[Unique, LirId]> = varNodes.map((n) => [
+      n.getUnique(nodeInfo.context),
+      n.getId(),
+    ])
+    this.#corefs = Corefs.fromEntries(uniqLirIdPairs)
+
+    this.doEvalLadderExprWithVarBindings(nodeInfo.context)
+  }
+
+  getVizExprToLir() {
+    return this.#vizExprToLir
   }
 
   setDag(_context: LirContext, dag: DirectedAcyclicGraph<LirId>) {
@@ -482,28 +508,63 @@ export class LadderGraphLirNode extends DefaultLirNode implements LirNode {
   }
 
   /*****************************
-          Bindings
+        Eval, Bindings
   ******************************/
 
-  /** This sets off the 'compound logic' */
+  private doEvalLadderExprWithVarBindings(context: LirContext) {
+    const result = Evaluator.eval(
+      veExprToEvExpr(this.#originalExpr),
+      this.#bindings
+    )
+    this.setResult(context, result)
+
+    console.log('evaluating ', this.#bindings)
+    console.log('whatif eval result: ', result)
+  }
+
+  /** This is what gets called when the user clicks on a node, in 'WhatIf' mode */
   submitNewBinding(
     context: LirContext,
     binding: { unique: Unique; value: Value }
   ) {
-    // Set the new binding
-    this.#environment.set(binding)
+    // Update the args with the new binding
+    this.#bindings.set(binding.unique, binding.value)
 
-    // The key step: Update all VarLirNodes with this Unique with the new Value
-    const corefs = Array.from(this.#environment.getCoreferents(binding.unique))
+    // Update all VarLirNodes with this Unique with the new Value
+    const corefs = Array.from(this.#corefs.getCoreferents(binding.unique))
     // console.log('corefs: ', corefs)
     corefs.forEach((coref) => {
       const node = context.get(coref) as VarLirNode
       node._setValue(context, binding.value)
     })
 
+    /*
+    Try #WhatIf-style evaluation.
+    ---------
+    Note that
+    * the `bindings` map will have a Unique key for every Var node,
+      including Var nodes that the user hasn't specified a value for
+    * the default value for a VarNode is UnknownV
+    */
+    this.doEvalLadderExprWithVarBindings(context)
+
+    // TODO for v2: Grey out incompatible subgraphs
+
     this.getRegistry().publish(context, this.getId())
   }
 
+  /*************************************
+         Result
+  **************************************/
+
+  getResult(_context: LirContext) {
+    return this.#result
+  }
+
+  setResult(_context: LirContext, result: Value) {
+    this.#result = result
+  }
+
   /**************************************
       Zen mode,
     which is currently being treated as
@@ -536,7 +597,7 @@ export class LadderGraphLirNode extends DefaultLirNode implements LirNode {
 
   dispose(context: LirContext) {
     this.getVertices(context).map((n) => n.dispose(context))
-    this.#environment.dispose()
+    this.#corefs.dispose()
     this.#dag.dispose()
     context.clear(this.getId())
   }
@@ -556,6 +617,7 @@ function getVerticesFromAlgaDag(
           Ladder Lir Node
 ***********************************************/
 
+/** All the LirNodes that can appear in the Ladder graph */
 export type LadderLirNode =
   | BoolVarLirNode
   | NotStartLirNode
@@ -565,6 +627,7 @@ export type LadderLirNode =
 export interface VarLirNode extends FlowLirNode {
   getUnique(context: LirContext): Unique
 
+  /** This should be used only for UI purposes (and not, e.g., for evaluation) */
   getValue(context: LirContext): Value
   /** For displayers.
    *
@@ -580,7 +643,10 @@ export function isBoolVarLirNode(node: LadderLirNode): node is BoolVarLirNode {
 by the LadderGraphLirNode, as opposed to the BoolVarLirNode itself.
 */
 export class BoolVarLirNode extends BaseFlowLirNode implements VarLirNode {
-  #value: BoolVal
+  /** The value here is used only for UI purposes
+   * The actual evaluation uses a different data structure (that is nevertheless kept
+   * in sync with what values are stored on the VarLirNodes) */
+  #value: UBoolValue
   #name: Name
 
   constructor(
@@ -609,12 +675,11 @@ export class BoolVarLirNode extends BaseFlowLirNode implements VarLirNode {
     return { name: this.#name, value: this.#value }
   }
 
-  getValue(_context: LirContext): BoolVal {
+  getValue(_context: LirContext): UBoolValue {
     return this.#value
   }
 
-  /** This will only be invoked by LadderGraphLirNode */
-  _setValue(_context: LirContext, value: BoolVal) {
+  _setValue(_context: LirContext, value: UBoolValue) {
     this.#value = value
   }
 
@@ -683,7 +748,7 @@ export const emptyBundlingNodeAnno: BundlingNodeAnno = { annotation: '' }
 export function isBundlingFlowLirNode(
   node: LadderLirNode
 ): node is BundlingFlowLirNode {
-  return isSourceLirNode(node) || node instanceof SinkLirNode
+  return isSourceLirNode(node) || isSinkLirNode(node)
 }
 
 /** A Flow Lir Node that's used solely to visually group or 'bundle' other nodes.
@@ -696,6 +761,9 @@ export type SourceLirNode = SourceWithOrAnnoLirNode | SourceNoAnnoLirNode
 export function isSourceLirNode(node: LadderLirNode): node is SourceLirNode {
   return isSourceNoAnnoLirNode(node) || isSourceWithOrAnnoLirNode(node)
 }
+export function isSinkLirNode(node: LadderLirNode): node is SinkLirNode {
+  return node instanceof SinkLirNode
+}
 
 abstract class BaseBundlingFlowLirNode extends BaseFlowLirNode {
   constructor(
@@ -840,21 +908,22 @@ export function augmentEdgesWithExplanatoryLabel(
     const edgeU = context.get(edge.getU()) as LadderLirNode
     const edgeV = context.get(edge.getV()) as LadderLirNode
 
-    const edgeSourceIsNotOverallSource =
+    const targetIsEligible =
+      isBoolVarLirNode(edgeV) ||
+      isNotStartLirNode(edgeV) ||
+      isSourceWithOrAnnoLirNode(edgeV)
+
+    const sourceIsNotOverallSource =
       ladderGraph.getOverallSource(context) &&
       !edge
         .getU()
         .isEqualTo(ladderGraph.getOverallSource(context)?.getId() as LirId)
+    const sourceIsEligible =
+      !isSourceWithOrAnnoLirNode(edgeU) &&
+      sourceIsNotOverallSource &&
+      !isNotStartLirNode(edgeU)
 
-    const edgeSourceIsAnyOfAnnoSource = isSourceWithOrAnnoLirNode(edgeU)
-    return (
-      !edgeSourceIsAnyOfAnnoSource &&
-      edgeSourceIsNotOverallSource &&
-      !isNotStartLirNode(edgeU) &&
-      (isBoolVarLirNode(edgeV) ||
-        isNotStartLirNode(edgeV) ||
-        isSourceWithOrAnnoLirNode(edgeV))
-    )
+    return sourceIsEligible && targetIsEligible
   }
 
   const edgesToAddLabel = edges.filter(isEdgeToAddAndLabel)
@@ -898,7 +967,7 @@ function pprintPathGraph(
   context: LirContext,
   initialGraph: DirectedAcyclicGraph<LirId>
 ): string {
-  // Each node should only be pprinted once in the linearization of the dag
+  /** Each node should only be pprinted once in the linearization of the dag */
   const processed = new Set<LirId>()
 
   function pprintHelper(
@@ -909,15 +978,12 @@ function pprintPathGraph(
       .with(P.when(isEmpty<LirId>), () => '')
       .with(P.when(isVertex<LirId>), (v: Vertex<LirId>) => {
         if (processed.has(v.getValue())) return ''
+
         processed.add(v.getValue())
         return (context.get(v.getValue()) as LadderLirNode).toPretty(context)
       })
       .with(P.when(isOverlay<LirId>), (o: Overlay<LirId>) => {
-        return (
-          pprintHelper(context, o.getLeft()) +
-          ' ' +
-          pprintHelper(context, o.getRight())
-        )
+        return `${pprintHelper(context, o.getLeft())} ${pprintHelper(context, o.getRight())}`
       })
       .with(P.when(isConnect<LirId>), (c: Connect<LirId>) => {
         const from = pprintHelper(context, c.getFrom())
@@ -930,8 +996,8 @@ function pprintPathGraph(
               (c.getTo() as Vertex<LirId>).getValue()
             )
           )
-          const label = edgeAttrs.getLabel()
-          return `${from} ${label} ${to}`
+          const edgeLabel = edgeAttrs.getLabel()
+          return `${from} ${edgeLabel} ${to}`
         } else {
           return `${from} ${to}`
         }
diff --git a/ts-apps/decision-logic-visualizer/src/lib/layout-ir/value.ts b/ts-apps/decision-logic-visualizer/src/lib/layout-ir/value.ts
deleted file mode 100644
index 290e21c44..000000000
--- a/ts-apps/decision-logic-visualizer/src/lib/layout-ir/value.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { match } from 'ts-pattern'
-
-export type Value = BoolVal
-
-export type BoolVal = TrueVal | FalseVal | UnknownVal
-
-interface BoolV {
-  $type: 'TrueVal' | 'FalseVal' | 'UnknownVal'
-  getClasses(): string[]
-}
-
-export class TrueVal implements BoolV {
-  $type: 'TrueVal' = 'TrueVal' as const
-  constructor() {}
-
-  getClasses() {
-    return ['true-val']
-  }
-}
-
-export class FalseVal implements BoolV {
-  $type: 'FalseVal' = 'FalseVal' as const
-  constructor() {}
-
-  getClasses() {
-    return ['false-val']
-  }
-}
-
-export class UnknownVal implements BoolV {
-  $type: 'UnknownVal' = 'UnknownVal' as const
-  constructor() {}
-
-  getClasses() {
-    return []
-  }
-}
-
-export function cycle(val: BoolVal): BoolVal {
-  return match(val)
-    .with({ $type: 'TrueVal' }, () => new FalseVal())
-    .with({ $type: 'FalseVal' }, () => new UnknownVal())
-    .with({ $type: 'UnknownVal' }, () => new TrueVal())
-    .exhaustive()
-}
diff --git a/ts-shared/viz-expr/viz-expr.ts b/ts-shared/viz-expr/viz-expr.ts
index e1062885c..7681dda98 100644
--- a/ts-shared/viz-expr/viz-expr.ts
+++ b/ts-shared/viz-expr/viz-expr.ts
@@ -78,7 +78,7 @@ export interface Name {
 export type Unique = number
 
 /*******************************
-    IRId (not currently used)
+           IRId
 ********************************/
 
 /** Stable IDs useful for things like bidirectional synchronization down the road */
@@ -87,7 +87,7 @@ export const IRId = Schema.Struct({
 })
 
 /** Stable IDs useful for things like bidirectional synchronization down the road */
-interface IRId {
+export interface IRId {
   readonly id: number
 }
 
@@ -156,6 +156,7 @@ export interface Not extends IRNode {
   readonly negand: IRExpr
 }
 
+/** For the original Viz / IRExpr */
 export type BoolValue = 'False' | 'True' | 'Unknown'
 
 export interface BoolVar extends IRNode {


Reviews:
user: login: ym-han
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:04:36Z
user: login: ym-han
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:06:31Z
user: login: fendor
body: LGTM, one comment only :)
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-15T08:32:28Z
user: login: ym-han
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:03:42Z
user: login: ym-han
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:33:41Z

Review comments:
diff_hunk: @@ -70,11 +75,18 @@ export const LadderGraphLirSource: LirSource<IRExpr, LadderGraphLirNode> = {
 // TODO2: Attach a group id to the label for the flownode to make it easier to debug
 // TODO3: Return the number of groups as metadata in the FlowGraph
 
+export interface ToLirResult {
+  graph: DirectedAcyclicGraph<LirId>
+  vizExprToLir: Map<IRId, DirectedAcyclicGraph<LirId>>
path: ts-apps/decision-logic-visualizer/src/lib/data/viz-expr-to-lir.ts
user: login: ym-han
body: Ignore the threading of the `vizExprToLir`: it only really gets used in the next PR (and there's a bug in the traversal / threading that's fixed in the next PR).
created_at: 2025-04-15T08:04:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -683,7 +748,7 @@ export const emptyBundlingNodeAnno: BundlingNodeAnno = { annotation: '' }
 export function isBundlingFlowLirNode(
   node: LadderLirNode
 ): node is BundlingFlowLirNode {
-  return isSourceLirNode(node) || node instanceof SinkLirNode
+  return isSourceLirNode(node) || isSinkLirNode(node)
path: ts-apps/decision-logic-visualizer/src/lib/layout-ir/ladder-lir.svelte.ts
user: login: ym-han
body: This and the other changes in the file below is just code cleanups / refactoring that are not related to the main point of the PR. Sorry for not having put it in another PR.
created_at: 2025-04-15T08:06:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,106 @@
+import { match } from 'ts-pattern'
+import { IRExpr } from '@repo/viz-expr'
+import * as VE from '@repo/viz-expr'
+
+/********************************
+          Value
+*********************************/
+
+export type Value = UBoolValue
+
+export type UBoolValue = TrueVal | FalseVal | UnknownVal
+export function isUBoolValue(val: Value): val is UBoolValue {
+  return (
+    val.$type === 'TrueVal' ||
+    val.$type === 'FalseVal' ||
+    val.$type === 'UnknownVal'
+  )
+}
path: ts-apps/decision-logic-visualizer/src/lib/eval/type.ts
user: login: fendor
body: ```suggestion
```

This feels redundant, since `Value = UBoolValue`?
created_at: 2025-04-15T08:26:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,106 @@
+import { match } from 'ts-pattern'
+import { IRExpr } from '@repo/viz-expr'
+import * as VE from '@repo/viz-expr'
+
+/********************************
+          Value
+*********************************/
+
+export type Value = UBoolValue
+
+export type UBoolValue = TrueVal | FalseVal | UnknownVal
+export function isUBoolValue(val: Value): val is UBoolValue {
+  return (
+    val.$type === 'TrueVal' ||
+    val.$type === 'FalseVal' ||
+    val.$type === 'UnknownVal'
+  )
+}
path: ts-apps/decision-logic-visualizer/src/lib/eval/type.ts
user: login: ym-han
body: Oh I was trying to leave open the possibility of extending the Value type with other kinds of values in the future.

But yeah, maybe that doesn't actually make sense.
created_at: 2025-04-15T09:03:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,106 @@
+import { match } from 'ts-pattern'
+import { IRExpr } from '@repo/viz-expr'
+import * as VE from '@repo/viz-expr'
+
+/********************************
+          Value
+*********************************/
+
+export type Value = UBoolValue
+
+export type UBoolValue = TrueVal | FalseVal | UnknownVal
+export function isUBoolValue(val: Value): val is UBoolValue {
+  return (
+    val.$type === 'TrueVal' ||
+    val.$type === 'FalseVal' ||
+    val.$type === 'UnknownVal'
+  )
+}
path: ts-apps/decision-logic-visualizer/src/lib/eval/type.ts
user: login: ym-han
body: I'll merge it in for now, but I've made a note to remove this if we don't end up adding other kinds of values (eg numbers)
created_at: 2025-04-15T09:33:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: kosmikus
created_at: 2025-04-04T12:00:28Z
body: This looks much more ambitious than what we discussed before? We said we'd want that the visualiser can evaluate logical expressions on its own. With this, I was assuming expressions corresponding to the Haskell `IRExpr` datatype:
```haskell
data IRExpr
  = And ID [IRExpr]
  | Or ID [IRExpr]
  | Not ID IRExpr
  | BoolVar ID Name BoolValue
  deriving (Show, Eq, Generic)
```
It now looks like you have environments and closures and are trying to effectively build an evaluator for the entire language as well?
author_association: COLLABORATOR
user: login: ym-han
created_at: 2025-04-04T12:38:51Z
body: > This looks much more ambitious than what we discussed before? We said we'd want that the visualiser can evaluate logical expressions on its own. With this, I was assuming expressions corresponding to the Haskell `IRExpr` datatype:
> 
> ```haskell
> data IRExpr
>   = And ID [IRExpr]
>   | Or ID [IRExpr]
>   | Not ID IRExpr
>   | BoolVar ID Name BoolValue
>   deriving (Show, Eq, Generic)
> ```
> 
> It now looks like you have environments and closures and are trying to effectively build an evaluator for the entire language as well?

The closures and so forth are used in exactly one place: to create a boolean function `f` whose params correspond to the var nodes of the ladder graph and whose body corresponds to the ladder graph; and to then frame the 'WhatIf'-style evaluation as being about evaluating the application of this function to  the values that have been specified by the user, or pre-specified from the backend. So, for instance, I would **not** have translated the L4 Lam to this Lam. This Lam and App do **not** correspond to the L4 Lam/App.

Another way to put this is: if you look at https://github.com/smucclaw/l4-ide/blob/45bdcb362751f5b4e4bdecd74981dde34fe62e12/ts-apps/decision-logic-visualizer/src/lib/eval/eval.ts#L130 and type.ts, you will see that there are no lambdas etc that can appear within the LadderGraphExpr. The semantics of the stuff that can appear within the LadderGraphExpr is limited to the stuff you quoted.

Having said this, maybe setting things up this way is too confusing and overly complicated, and I should just remove any kind of Lam/App, even if the Lam/App is not the L4 Lam/App.

author_association: CONTRIBUTOR
user: login: ym-han
created_at: 2025-04-04T12:45:35Z
body: I'll just refactor it to remove any mention of Lam/App etc, though again, I was taking all of the above to be consistent with what we had agreed on.
author_association: CONTRIBUTOR
user: login: ym-han
created_at: 2025-04-06T04:53:27Z
body: > This looks much more ambitious than what we discussed before? We said we'd want that the visualiser can evaluate logical expressions on its own. With this, I was assuming expressions corresponding to the Haskell `IRExpr` datatype:
> 
> ```haskell
> data IRExpr
>   = And ID [IRExpr]
>   | Or ID [IRExpr]
>   | Not ID IRExpr
>   | BoolVar ID Name BoolValue
>   deriving (Show, Eq, Generic)
> ```
> 
> It now looks like you have environments and closures and are trying to effectively build an evaluator for the entire language as well?

Thank you for pointing this out. Sorry about having made things unnecessarily confusing and complicated; I had been having sleep issues and wasn't thinking well. It feels simpler and cleaner now.
author_association: CONTRIBUTOR
user: login: ym-han
created_at: 2025-04-09T09:38:22Z
body: This should be ready for review now. I'm still not well, but there's also not much else that can go seriously wrong.
author_association: CONTRIBUTOR