Skip to main content

Language Semantics

Nudo infers types by executing your code with symbolic values, so the quality of inference is exactly the quality of the evaluator's JavaScript semantics. This guide lists the language behaviors the evaluator models precisely on the call-site path — every output block below is a real nudo infer run of the code above it — followed by the constructs that still degrade to unknown and should be verified before you rely on them. Precise semantics are also what make call-site discovery effective: harvested call shapes only pay off if the evaluator can actually follow them.

Modeled Precisely​

String Methods on Literals​

String methods on literal receivers fold at evaluation time.

function upper() { return "hello".toUpperCase(); }
upper(); // → "HELLO"

function slen() { return "hello".length; }
slen(); // → 5

function sli() { return "hello".slice(1, 3); }
sli(); // → "el"
=== upper ===

call@L2: () => "HELLO"

toUpperCase, toLowerCase, slice, .length, and split (literal receiver and separator) produce exact results — "a,b,c".split(",") folds to ["a", "b", "c"] at the call site, and a comma-free receiver like "abc".split("b") folds to ["a", "c"] under an @nudo:case directive. The directive path cannot express a comma-containing receiver: the directive parser splits case arguments on commas, so @nudo:case "split" ("a,b,c") arrives as three unknown parameters rather than one string. Prefix/suffix/membership checks — startsWith, endsWith, includes — fold to a definite boolean on literal receivers. indexOf yields the number primitive without the literal index.

Loops with Concrete Bounds​

A for loop with a concrete bound evaluates to its exact result.

function sumTo(n) {
let sum = 0;
for (let i = 0; i < n; i++) {
sum = sum + i;
}
return sum;
}
sumTo(5);
=== sumTo ===

call@L8: (5) => 10

for...of over a concrete array evaluates the same way:

function sumArr(arr) {
let s = 0;
for (const x of arr) {
s = s + x;
}
return s;
}
sumArr([1, 2, 3]); // → 6

break Keeps the Exiting Value​

Loop jumps are signals: the value bound in the exiting iteration is preserved.

function findBig() {
let found;
for (const x of [1, 2, 3, 4]) {
if (x > 2) {
found = x;
break;
}
}
return found;
}
findBig();
=== findBig ===

call@L11: () => 3

The result is the literal 3 — the value bound when the loop broke.

Object.keys on a Concrete Shape​

Object.keys on a concrete object returns the exact key tuple.

function keysOf() { return Object.keys({ port: 3000, host: "x" }); }
keysOf();
=== keysOf ===

call@L2: () => ["port", "host"]

Math Methods​

Math methods on literal numeric arguments fold at evaluation time — on both the call-site and @nudo:case paths.

function root(n) { return Math.sqrt(n); }
root(9);
=== root ===

call@L2: (9) => 3

sqrt, pow, abs, floor, ceil, round, sign, min, and max all fold to their exact numeric result on literal arguments; symbolic arguments widen to number.

Primitive Conversions & Parsing​

The global coercion constructors and numeric parsers fold literals to exact results at the call site and under @nudo:case alike:

function strOf(x) { return String(x); }
strOf(5); // → "5"

function boolOf(x) { return Boolean(x); }
boolOf("hi"); // → true

function numOf(x) { return Number(x); }
numOf("42"); // → 42

function intOf(s) { return parseInt(s); }
intOf("42px"); // → 42

function floatOf(s) { return parseFloat(s); }
floatOf("3.14"); // → 3.14
=== strOf ===

call@L2: (5) => "5"

String(x), Number(x), and Boolean(x) fold number/string/boolean literals to the exact coerced literal; parseInt(s) / parseFloat(s) fold string/number literals to the exact numeric prefix/parse. Symbolic arguments widen to the target primitive (string / number / boolean). Repo example (CI-pinned): docs/examples/algebra/l-primitive-conversion.js.

Method Calls and this​

Method calls made inside an analyzed function bind this to the receiver — on both the call-site and @nudo:case paths.

class Circle {
constructor(r) { this.radius = r; }
area() { return this.radius * this.radius; }
}

function compute(r) {
const circle = new Circle(r);
return circle.area();
}
compute(5);
=== compute ===

call@L11: (5) => 25

The directive path is equally precise when the argument is a literal (@nudo:case "member" (5) → (5) => 25); with an empty argument list (()) the parameter is unknown, so the result degrades to unknown #partial. The remaining gap is call-site collection, not evaluation: a bare top-level member call (circle.area() as a statement) produces no call@ case — member callees are not collected as call sites. Wrap the member call in a function to see it.

Recursion Unrolls per Call Site​

A recursive function is evaluated per observed call: each top-level call is fully unrolled and reported as its own call@ case with the exact result.

function walk(n) {
if (n <= 0) return 0;
return n + walk(n - 1);
}

walk(0);
walk(1);
walk(2);
=== walk ===

call@L6: (0) => 0
call@L7: (1) => 1
call@L8: (2) => 3

Observed: 0 | 1 | 3

More calls than the precise-case cap aggregate into a call@symbolic case with widened arguments instead.

Narrowing Guards​

=== comparisons, typeof, Array.isArray, and switch narrow per concrete call site — see Control Flow Narrowing for the verified patterns.

Not Modeled Yet​

These constructs currently evaluate to unknown (often with a nudo:unknown-recv or nudo:builtin-unknown diagnostic). Prefer the modeled alternatives listed beside each one.

ConstructBehavior todayModeled alternative
== / != literal folding1 == "1" → trueAbstract Equality on double literals (C2.3)
Primitive autoboxing"nudo".constructor → unknown.length, string methods above
Object.prototype methods({}).hasOwnProperty("key") → unknownObject.keys(...) / shape checks
Symbol.iterator in x→ unknownArray.isArray(x)
for...of over Set / Mapelements → unknownarrays / .map callbacks
Promise executornew Promise((r) => r("done")) → promise<unknown>@nudo:mock + async functions
try/catch parametermodeled — catch (err) binds thrown Abs; new Error("boom") → err.message is "boom"use Error family / object / literal throws
Per-iteration let closuresfns[i]() → unknowndirect iteration results
arguments→ unknown (nudo:builtin-unknown)named parameters
JSON.parseJSON.parse('{"port": 3000}') → unknownobject literals
Number formatting methods(cents / 100).toFixed(2) → unknown (nudo:no-method)Math.round / arithmetic
String.fromCharCode→ unknownstring literals
Exponentiation **→ unknownx * x

Env modules and the @types harvester cover a large slice of common Node/Web APIs. They do not remove the need for mocks everywhere. Categories that are still recommended for handwritten mock (or that remain honest unknown / entry@ results) — aligned with the call-site ceiling in docs/design-limitations.md §八:

CategoryWhy mock / why unknownWorkaround
Native bindingschild_process.spawn, native addons — env may hold a signature, not side effects@nudo:mock or treat return as opaque
Dynamic requireComputed module graphs are not statically resolved@nudo:mock-module / static import
Stream machine callbacksNode Transform internals are driven by the runtime; no call-site record to harvestMock the stream factory; do not expect internal callbacks to infer
Dual-entry browser/node variantsCall-site records do not cross files (attribution is file-scoped)Analyze the entry you ship; mock the other
No call-site functionsentry@ fallback when tests never touch an internal helperAdd a call site, or accept entry@ as the honest result
Promise executor internalsnew Promise((r) => r(...)) → promise<unknown> without mock@nudo:mock + async wrappers

Coverage baselines (pnpm run coverage:env → docs/reports/env-coverage-baseline.md) report resolution rate, not completeness. Do not read a high resolved ratio as a soundness guarantee — see the mock boundary in the harvester API as well.

Summary​

CapabilityExampleResult
String methods"hello".toUpperCase()"HELLO"
Concrete-bound loopssumTo(5)10
breakloop exit value3
Object.keysconcrete shape["port", "host"]
Recursionwalk(2)3
Narrowingtypeof / === / Array.isArray / switchper-call-site precision