Skip to content

Commit aa71d22

Browse files
Sander Toonenclaude
andcommitted
Make isUndefined(x) lazy so it can guard against unbound variables (0.6.3)
Calling `isUndefined(some_nonexistent_variable)` used to throw VariableError because evalCall eagerly evaluates each argument. That defeats the function's whole purpose: you can't write `isUndefined(x) ? "fallback" : x` to defend against missing inputs. Special-case isUndefined in both evaluators (mirroring the existing lazy `if()` pattern): catch VariableError raised while evaluating the single argument and treat the value as `undefined`. Gated on `!expr.legacy` for consistency with `if()`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9d1ef54 commit aa71d22

4 files changed

Lines changed: 59 additions & 1 deletion

File tree

packages/expreszo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pro-fa/expreszo",
3-
"version": "0.6.2",
3+
"version": "0.6.3",
44
"description": "Mathematical expression evaluator",
55
"keywords": [
66
"expression",

packages/expreszo/src/eval/async-evaluator.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,24 @@ async function evalCall(
283283
: evalNode(node.args[2], expr, values, resolver);
284284
}
285285

286+
// Lazy isUndefined(x): an unbound `x` is treated as undefined, not an error,
287+
// so the function can be used as a guard (`isUndefined(x) ? fallback : x`).
288+
if (
289+
!expr.legacy &&
290+
node.callee.type === 'Ident' &&
291+
node.callee.name === 'isUndefined' &&
292+
node.args.length === 1
293+
) {
294+
let argValue: Value;
295+
try {
296+
argValue = await evalNode(node.args[0], expr, values, resolver);
297+
} catch (err) {
298+
if (!(err instanceof VariableError)) throw err;
299+
argValue = undefined;
300+
}
301+
return argValue === undefined;
302+
}
303+
286304
const callee = await evalNode(node.callee, expr, values, resolver);
287305
const args = await Promise.all(
288306
node.args.map((arg) => evalNode(arg, expr, values, resolver))

packages/expreszo/src/eval/sync-evaluator.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,24 @@ function evalCall(
287287
: evalNode(node.args[2], expr, values, resolver);
288288
}
289289

290+
// Lazy isUndefined(x): an unbound `x` is treated as undefined, not an error,
291+
// so the function can be used as a guard (`isUndefined(x) ? fallback : x`).
292+
if (
293+
!expr.legacy &&
294+
node.callee.type === 'Ident' &&
295+
node.callee.name === 'isUndefined' &&
296+
node.args.length === 1
297+
) {
298+
let argValue: Value;
299+
try {
300+
argValue = evalNode(node.args[0], expr, values, resolver);
301+
} catch (err) {
302+
if (!(err instanceof VariableError)) throw err;
303+
argValue = undefined;
304+
}
305+
return argValue === undefined;
306+
}
307+
290308
const callee = evalNode(node.callee, expr, values, resolver);
291309
const args: Value[] = new Array(node.args.length);
292310
for (let i = 0; i < node.args.length; i++) {

packages/expreszo/test/functions/functions-type-checking.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,28 @@ describe('Type Checking Functions TypeScript Test', function () {
132132
assert.strictEqual(parser.evaluate('isUndefined({})'), false);
133133
assert.strictEqual(parser.evaluate('isUndefined(null)'), false);
134134
});
135+
it('should return true for variables that are not bound at all', function () {
136+
const parser = new Parser();
137+
assert.strictEqual(parser.evaluate('isUndefined(some_nonexistent_variable)'), true);
138+
});
139+
it('should return true for member access on a missing root variable', function () {
140+
const parser = new Parser();
141+
assert.strictEqual(parser.evaluate('isUndefined(missing.nested.path)'), true);
142+
});
143+
it('should be usable in a ternary as a guard against missing variables', function () {
144+
const parser = new Parser();
145+
assert.strictEqual(
146+
parser.evaluate('isUndefined(missing) ? "fallback" : missing'),
147+
'fallback'
148+
);
149+
});
150+
it('should still return false for bound variables when used in a ternary guard', function () {
151+
const parser = new Parser();
152+
assert.strictEqual(
153+
parser.evaluate('isUndefined(x) ? "fallback" : x', { x: 'hello' }),
154+
'hello'
155+
);
156+
});
135157
});
136158

137159
describe('isFunction(value)', function () {

0 commit comments

Comments
 (0)