Skip to content

Commit 02ff280

Browse files
authored
Fix stepper: no precheck for undefined variables (#1469)
* Fix stepper: no precheck for undefined variables (#1468) * fix: enable preludes in precheck * update snap file * update test cases and snap files * add one test case
1 parent 130a030 commit 02ff280

File tree

5 files changed

+158
-162
lines changed

5 files changed

+158
-162
lines changed

src/stepper/__tests__/__snapshots__/stepper.ts.snap

-116
Original file line numberDiff line numberDiff line change
@@ -4166,31 +4166,6 @@ f(undefined);
41664166
"
41674167
`;
41684168

4169-
exports[`correctly avoids capture by other parameter names 1`] = `
4170-
"function f(g, x) {
4171-
return g(x);
4172-
}
4173-
f(y => x + 1, 2);
4174-
4175-
function f(g, x) {
4176-
return g(x);
4177-
}
4178-
f(y => x + 1, 2);
4179-
4180-
f(y => x + 1, 2);
4181-
4182-
f(y => x + 1, 2);
4183-
4184-
(y => x + 1)(2);
4185-
4186-
(y => x + 1)(2);
4187-
4188-
x + 1;
4189-
4190-
x + 1;
4191-
"
4192-
`;
4193-
41944169
exports[`expmod 1`] = `
41954170
"function is_even(n) {
41964171
return n % 2 === 0;
@@ -7248,97 +7223,6 @@ g();
72487223
"
72497224
`;
72507225

7251-
exports[`scoping test for block expressions, with renaming 1`] = `
7252-
"function f(w) {
7253-
return g();
7254-
}
7255-
function h(f) {
7256-
function g() {
7257-
return w;
7258-
}
7259-
const w = 0;
7260-
return f(1);
7261-
}
7262-
h(f);
7263-
7264-
function f(w) {
7265-
return g();
7266-
}
7267-
function h(f) {
7268-
function g() {
7269-
return w;
7270-
}
7271-
const w = 0;
7272-
return f(1);
7273-
}
7274-
h(f);
7275-
7276-
function h(f) {
7277-
function g() {
7278-
return w;
7279-
}
7280-
const w = 0;
7281-
return f(1);
7282-
}
7283-
h(f);
7284-
7285-
function h(f) {
7286-
function g() {
7287-
return w;
7288-
}
7289-
const w = 0;
7290-
return f(1);
7291-
}
7292-
h(f);
7293-
7294-
h(f);
7295-
7296-
h(f);
7297-
7298-
{
7299-
function g_1() {
7300-
return w;
7301-
}
7302-
const w = 0;
7303-
return f(1);
7304-
};
7305-
7306-
{
7307-
function g_1() {
7308-
return w;
7309-
}
7310-
const w = 0;
7311-
return f(1);
7312-
};
7313-
7314-
{
7315-
const w = 0;
7316-
return f(1);
7317-
};
7318-
7319-
{
7320-
const w = 0;
7321-
return f(1);
7322-
};
7323-
7324-
{
7325-
return f(1);
7326-
};
7327-
7328-
{
7329-
return f(1);
7330-
};
7331-
7332-
f(1);
7333-
7334-
f(1);
7335-
7336-
g();
7337-
7338-
g();
7339-
"
7340-
`;
7341-
73427226
exports[`scoping test for blocks nested in lambda expressions 1`] = `
73437227
"const f = x => {
73447228
g();

src/stepper/__tests__/stepper.ts

+9-44
Original file line numberDiff line numberDiff line change
@@ -186,28 +186,26 @@ test('Test unary and binary boolean operations', () => {
186186

187187
test('Test ternary operator', () => {
188188
const code = `
189-
1 + -1 === 0
190-
? false ? garbage : Infinity
191-
: anotherGarbage;
189+
1 + -1 === 0 ? false ? true : Infinity : undefined;
192190
`
193191
const program = parse(code, mockContext())!
194192
const steps = getEvaluationSteps(program, mockContext(), 1000)
195193
expect(steps.map(x => codify(x[0])).join('\n')).toMatchInlineSnapshot(`
196-
"1 + -1 === 0 ? false ? garbage : Infinity : anotherGarbage;
194+
"1 + -1 === 0 ? false ? true : Infinity : undefined;
197195
198-
1 + -1 === 0 ? false ? garbage : Infinity : anotherGarbage;
196+
1 + -1 === 0 ? false ? true : Infinity : undefined;
199197
200-
0 === 0 ? false ? garbage : Infinity : anotherGarbage;
198+
0 === 0 ? false ? true : Infinity : undefined;
201199
202-
0 === 0 ? false ? garbage : Infinity : anotherGarbage;
200+
0 === 0 ? false ? true : Infinity : undefined;
203201
204-
true ? false ? garbage : Infinity : anotherGarbage;
202+
true ? false ? true : Infinity : undefined;
205203
206-
true ? false ? garbage : Infinity : anotherGarbage;
204+
true ? false ? true : Infinity : undefined;
207205
208-
false ? garbage : Infinity;
206+
false ? true : Infinity;
209207
210-
false ? garbage : Infinity;
208+
false ? true : Infinity;
211209
212210
Infinity;
213211
@@ -1013,26 +1011,6 @@ test('scoping test for block expressions, no renaming', () => {
10131011
expect(getLastStepAsString(steps)).toEqual('1;')
10141012
})
10151013

1016-
test('scoping test for block expressions, with renaming', () => {
1017-
const code = `
1018-
function f(w) {
1019-
return g();
1020-
}
1021-
function h(f) {
1022-
function g() {
1023-
return w;
1024-
}
1025-
const w = 0;
1026-
return f(1);
1027-
}
1028-
h(f);
1029-
`
1030-
const program = parse(code, mockContext())!
1031-
const steps = getEvaluationSteps(program, mockContext(), 1000)
1032-
expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot()
1033-
expect(getLastStepAsString(steps)).toEqual('g();')
1034-
})
1035-
10361014
test('return in nested blocks', () => {
10371015
const code = `
10381016
function f(x) {{ return 1; }}
@@ -1365,19 +1343,6 @@ test(`renaming of outer parameter in lambda function`, () => {
13651343
expect(getLastStepAsString(steps)).toEqual('1;')
13661344
})
13671345

1368-
test(`correctly avoids capture by other parameter names`, () => {
1369-
const code = `
1370-
function f(g, x) {
1371-
return g(x);
1372-
}
1373-
f(y => x + 1, 2);
1374-
`
1375-
const program = parse(code, mockContext())!
1376-
const steps = getEvaluationSteps(program, mockContext(), 1000)
1377-
expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot()
1378-
expect(getLastStepAsString(steps)).toEqual('x + 1;')
1379-
})
1380-
13811346
test(`removes debugger statements`, () => {
13821347
const code = `
13831348
function f(n) {

src/stepper/converter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function evaluateFunctionObject(node: substituterNodes, context: Context) {
9191
}
9292

9393
export function objectToString(value: any): string {
94-
if ('toReplString' in value) {
94+
if (value !== null && 'toReplString' in value) {
9595
return value.toReplString()
9696
}
9797
return '[Object]'

src/stepper/stepper.ts

+134-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ import {
2525
} from '../utils/dummyAstCreator'
2626
import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators'
2727
import * as rttc from '../utils/rttc'
28+
import {
29+
getFunctionDeclarationNamesInProgram,
30+
getIdentifiersInNativeStorage,
31+
getIdentifiersInProgram,
32+
getUniqueId
33+
} from '../utils/uniqueIds'
34+
import { ancestor } from '../utils/walkers'
2835
import { nodeToValue, objectToString, valueToExpression } from './converter'
2936
import * as builtin from './lib'
3037
import {
@@ -2501,7 +2508,7 @@ function jsTreeifyMain(
25012508
},
25022509

25032510
Literal: (target: es.Literal): es.Literal => {
2504-
if (typeof target.value === 'object') {
2511+
if (typeof target.value === 'object' && target.value !== null) {
25052512
target.raw = objectToString(target.value)
25062513
}
25072514
return target
@@ -3202,6 +3209,131 @@ function evaluateImports(
32023209
}
32033210
}
32043211

3212+
const globalIdNames = [
3213+
'native',
3214+
'callIfFuncAndRightArgs',
3215+
'boolOrErr',
3216+
'wrap',
3217+
'wrapSourceModule',
3218+
'unaryOp',
3219+
'binaryOp',
3220+
'throwIfTimeout',
3221+
'setProp',
3222+
'getProp',
3223+
'builtins'
3224+
] as const
3225+
type NativeIds = Record<typeof globalIdNames[number], es.Identifier>
3226+
3227+
function getNativeIds(program: es.Program, usedIdentifiers: Set<string>): NativeIds {
3228+
const globalIds = {}
3229+
for (const identifier of globalIdNames) {
3230+
globalIds[identifier] = ast.identifier(getUniqueId(usedIdentifiers, identifier))
3231+
}
3232+
return globalIds as NativeIds
3233+
}
3234+
3235+
function checkForUndefinedVariables(program: es.Program, context: Context) {
3236+
const usedIdentifiers = new Set<string>([
3237+
...getIdentifiersInProgram(program),
3238+
...getIdentifiersInNativeStorage(context.nativeStorage)
3239+
])
3240+
const globalIds = getNativeIds(program, usedIdentifiers)
3241+
3242+
const preludes = context.prelude
3243+
? getFunctionDeclarationNamesInProgram(parse(context.prelude, context)!)
3244+
: new Set<String>()
3245+
const builtins = context.nativeStorage.builtins
3246+
const identifiersIntroducedByNode = new Map<es.Node, Set<string>>()
3247+
function processBlock(node: es.Program | es.BlockStatement) {
3248+
const identifiers = new Set<string>()
3249+
for (const statement of node.body) {
3250+
if (statement.type === 'VariableDeclaration') {
3251+
identifiers.add((statement.declarations[0].id as es.Identifier).name)
3252+
} else if (statement.type === 'FunctionDeclaration') {
3253+
if (statement.id === null) {
3254+
throw new Error(
3255+
'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.'
3256+
)
3257+
}
3258+
identifiers.add(statement.id.name)
3259+
} else if (statement.type === 'ImportDeclaration') {
3260+
for (const specifier of statement.specifiers) {
3261+
identifiers.add(specifier.local.name)
3262+
}
3263+
}
3264+
}
3265+
identifiersIntroducedByNode.set(node, identifiers)
3266+
}
3267+
function processFunction(
3268+
node: es.FunctionDeclaration | es.ArrowFunctionExpression,
3269+
_ancestors: es.Node[]
3270+
) {
3271+
identifiersIntroducedByNode.set(
3272+
node,
3273+
new Set(
3274+
node.params.map(id =>
3275+
id.type === 'Identifier'
3276+
? id.name
3277+
: ((id as es.RestElement).argument as es.Identifier).name
3278+
)
3279+
)
3280+
)
3281+
}
3282+
const identifiersToAncestors = new Map<es.Identifier, es.Node[]>()
3283+
ancestor(program, {
3284+
Program: processBlock,
3285+
BlockStatement: processBlock,
3286+
FunctionDeclaration: processFunction,
3287+
ArrowFunctionExpression: processFunction,
3288+
ForStatement(forStatement: es.ForStatement, ancestors: es.Node[]) {
3289+
const init = forStatement.init!
3290+
if (init.type === 'VariableDeclaration') {
3291+
identifiersIntroducedByNode.set(
3292+
forStatement,
3293+
new Set([(init.declarations[0].id as es.Identifier).name])
3294+
)
3295+
}
3296+
},
3297+
Identifier(identifier: es.Identifier, ancestors: es.Node[]) {
3298+
identifiersToAncestors.set(identifier, [...ancestors])
3299+
},
3300+
Pattern(node: es.Pattern, ancestors: es.Node[]) {
3301+
if (node.type === 'Identifier') {
3302+
identifiersToAncestors.set(node, [...ancestors])
3303+
} else if (node.type === 'MemberExpression') {
3304+
if (node.object.type === 'Identifier') {
3305+
identifiersToAncestors.set(node.object, [...ancestors])
3306+
}
3307+
}
3308+
}
3309+
})
3310+
const nativeInternalNames = new Set(Object.values(globalIds).map(({ name }) => name))
3311+
3312+
for (const [identifier, ancestors] of identifiersToAncestors) {
3313+
const name = identifier.name
3314+
const isCurrentlyDeclared = ancestors.some(a => identifiersIntroducedByNode.get(a)?.has(name))
3315+
if (isCurrentlyDeclared) {
3316+
continue
3317+
}
3318+
const isPreviouslyDeclared = context.nativeStorage.previousProgramsIdentifiers.has(name)
3319+
if (isPreviouslyDeclared) {
3320+
continue
3321+
}
3322+
const isBuiltin = builtins.has(name)
3323+
if (isBuiltin) {
3324+
continue
3325+
}
3326+
const isPrelude = preludes.has(name)
3327+
if (isPrelude) {
3328+
continue
3329+
}
3330+
const isNativeId = nativeInternalNames.has(name)
3331+
if (!isNativeId) {
3332+
throw new errors.UndefinedVariable(name, identifier)
3333+
}
3334+
}
3335+
}
3336+
32053337
// the context here is for builtins
32063338
export function getEvaluationSteps(
32073339
program: es.Program,
@@ -3210,6 +3342,7 @@ export function getEvaluationSteps(
32103342
): [es.Program, string[][], string][] {
32113343
const steps: [es.Program, string[][], string][] = []
32123344
try {
3345+
checkForUndefinedVariables(program, context)
32133346
const limit = stepLimit === undefined ? 1000 : stepLimit % 2 === 0 ? stepLimit : stepLimit + 1
32143347
evaluateImports(program, context, true, true)
32153348
// starts with substituting predefined constants

0 commit comments

Comments
 (0)