Skip to main content

Abstract Interpretation

Abstract interpretation is the theoretical foundation of Nudo. Instead of running code with concrete values (like a test) or analyzing code without running it (like TypeScript), Nudo executes code with symbolic type values — and the execution itself produces types.

Three Approaches Compared​

ApproachInputOutputCompleteness
Unit testsConcrete values (1, "hello")Concrete resultOnly test cases
NudoType values (number(), string())Type valuesAll values in the type set
TypeScriptAST (no execution)TypesAll syntactic paths

When Nudo executes transform(string()), the engine propagates string() through the function body. At typeof x === "string", the engine knows that branch is taken. At x.toUpperCase(), the engine knows the result is string(). The result is not a concrete value — it is a type.


Evaluation Engine Architecture​

┌─────────────────────────────────────────────────────┐
│ Nudo Engine │
│ │
│ ┌───────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Parser │──▶│ Directive │──▶│ Evaluator │ │
│ │ (Babel) │ │ Extractor │ │ (AST Walker) │ │
│ └───────────┘ └────────────┘ └──────┬───────┘ │
│ │ │
│ ┌───────────────────────┐│ │
│ │ algebra surface / ││ │
│ │ arithmetic / route │◀ │
│ └───────────────────────┘ │
│ │
│ ┌──────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ Environment │ │ Branch │ │ Type │ │
│ │ (Scope) │ │ Executor │ │ Emitter │ │
│ └──────────────┘ └─────────────┘ └───────────┘ │
└─────────────────────────────────────────────────────┘
ComponentResponsibility
ParserParse JS/TS source into AST (delegates to Babel)
Directive ExtractorExtract @nudo:* directives from comments
EvaluatorB-path transpile+exec (ast-eval fallback): evaluate each node with Abs
surface / arithmetic / abs-routeOperator semantics on Abs for arithmetic, comparison, unary, spread
EnvironmentManage variable scopes and bindings (name → Abs)
Branch ExecutorHandle conditional branches: fork, narrow, evaluate, merge
Type EmitterSerialize final Abs results (optionally to TypeScript types)

Evaluation Rules​

The evaluator executes the function body with Abs values. On the primary B path the source is transpiled and run with Abs operands; the ast-eval fallback walks the AST directly with the same Abs rules. For each AST node type there is a corresponding evaluation rule.

Literals​

eval(NumericLiteral { value: 42 })   →  numLit(42)
eval(StringLiteral { value: "hi" }) → strLit("hi")
eval(BooleanLiteral { value: true }) → boolLit(true)
eval(NullLiteral) → null Abs

Variables​

eval(Identifier { name: "x" })  →  env.vars.get("x")

Binary Expressions​

eval(BinaryExpression { left, op, right })  →  tryAbsBinary(op, eval(left), eval(right))

Assignment​

eval(AssignmentExpression { left: "x", right: expr })
→ env = withVar(env, "x", eval(expr))

Conditional (if-else)​

This is where the engine differs fundamentally from a normal interpreter. Instead of choosing one branch, it may evaluate both branches with narrowed Abs values:

eval(IfStatement { test, consequent, alternate }) →
condition = eval(test)

// Case 1: condition is a known literal
if isDefinitelyTrue(condition) → eval(consequent)
if isDefinitelyFalse(condition) → eval(alternate)

// Case 2: condition is abstract → fork both branches
[envTrue, envFalse] = narrow(env, test)
resultTrue = eval(consequent, envTrue)
resultFalse = eval(alternate, envFalse)
return joinAbs(resultTrue, resultFalse)

Function Declaration​

eval(FunctionDeclaration { id: "foo", params, body })
→ env.fns.set("foo", absFunction(params, { body, closure: env }))

Function Call​

eval(CallExpression { callee: "foo", args })
→ fn = env.fns.get("foo")
argValues = args.map(eval)
fnEnv = fn.closure.extend(zip(fn.params, argValues))
eval(fn.body, fnEnv)

Narrowing Rules​

Narrowing refines values based on conditions. The engine supports these patterns:

PatternTrue branchFalse branch
typeof x === "string"x ∩ stringx - string
typeof x === "number"x ∩ numberx - number
x === nullx ∩ nullx - null
x === undefinedx ∩ undefinedx - undefined
x === <literal>x ∩ lit(v)x - lit(v)
Array.isArray(x)x ∩ arrayx - array
x (truthiness)x - null - undefined - lit(0) - lit("") - lit(false)complement
x instanceof Cx ∩ instance(C)x - instance(C)
"key" in xunion members with key propertyunion members without key
x?.propnormal member access (short-circuits to undefined for nullish)—
a ?? ba with null/undefined removed—
switch(x) { case v: ... }x ∩ lit(v) per caseremaining after all cases
x.kind === "a" (discriminated union)union members where kind matches literalunion members where kind differs

Where ∩ is type intersection and - is type subtraction.


Advanced Behaviors​

Loops (Bounded Unrolling)​

When the loop bound is concrete, the engine unrolls the loop that many times. When the bound is abstract, the condition is never definitely false, so the engine unrolls up to a bounded cap (DEFAULT_MAX_LOOP_ITERS = 8) — a termination guard for abstract conditions, not a fixed-point join. Within the cap, the loop exits early when the test becomes definitely false, or when two adjacent loop states stop changing (leqAbs).

Closures and Higher-Order Functions​

Functions are first-class Abs values (fn shape). When a function is passed as an argument, the engine evaluates calls through its Abs representation (parameters, body, closure environment):

map(number[], (x) => x + 1)
// Engine evaluates: fn(number) → number + lit(1) → number
// Result: number[]

Recursion (Call Budget)​

Recursion is bounded by a call budget (MAX_CALL_DEPTH = 64), not refined to a fixed point:

  1. A recursive call is evaluated with the current arguments.
  2. When a signature is re-entered past the budget, the call is truncated.
  3. The truncated result is widened to unknown, reported as nudo:recursion-truncated (a warning in nudo check).

Concrete base cases inside the budget still evaluate to literals; symbolic self-recursion widens to unknown rather than diverging.

Async / Promise​

Promises are modeled as an effect shape (eff):

  • await expr unwraps promise<V> to V
  • async function wraps the return value in promise<...>

Exception and throws Tracking​

Nudo treats exceptions as a first-class property of function types. Every function's inferred type includes both returns and throws:

function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// divide(number, number):
// returns: number
// throws: instance(Error)

try-catch absorbs thrown types. The catch parameter receives the union of all thrown types from the try block. If the function never throws, throws is never.

Mutability (Reference Semantics, Copy-on-Write)​

Object Abs values use reference semantics — assignment copies references, not values. Multiple variables can point to the same object Abs value.

When entering conditional branches, the engine deep-copies modified objects so each branch has its own copy. On merge, overlapping properties become unions. Without branching, mutations are applied in-place with no overhead.