Skip to content

Commit

Permalink
block scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
fabiosantoscode committed Apr 12, 2023
1 parent 302e1eb commit 045c40c
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 60 deletions.
5 changes: 2 additions & 3 deletions lib/flow/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "./expressions.js";
import "./lambda.js";
import "./statements.js";

import { Exit, hoist_defun_decls, NOPE, sequential, World } from "./tools.js";
import { Exit, analyze_func_or_toplevel_body, NOPE, sequential, World } from "./tools.js";

/**
* @module lib/flow/flow.js
Expand Down Expand Up @@ -44,6 +44,5 @@ AST_Node.prototype._flow = function (world = new World()) {
};

AST_Toplevel.prototype._flow = function (world = new World()) {
hoist_defun_decls(this, world);
return sequential(this.body, world);
return analyze_func_or_toplevel_body(this, world);
};
14 changes: 2 additions & 12 deletions lib/flow/lambda.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {AST_Call, AST_Defun, AST_Return} from "../ast.js";
import {NOPE, World, sequential, Return, hoist_defun_decls} from "./tools.js";
import {NOPE, World, sequential, Return, analyze_func_or_toplevel_body} from "./tools.js";
import {LiteralType, FunctionType } from "./types.js";

// Defining the defun is done during hoisting (`hoist_defun_decls`)
Expand Down Expand Up @@ -46,17 +46,7 @@ AST_Call.prototype._flow = function (world = new World()) {
this.args[i]._flow(world);
}

try {
hoist_defun_decls(func_type.node, callee_world);
sequential(func_type.node.body, callee_world);
} catch(e) {
if (e instanceof Return) {
return e.returned;
}
throw e;
}

return new LiteralType(undefined);
return analyze_func_or_toplevel_body(func_type.node, callee_world, { allow_return: true });
} finally {
calls--;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/flow/statements.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ AST_If.prototype._flow = function (world = new World()) {
};

AST_BlockStatement.prototype._flow = function (world = new World()) {
return sequential(this.body, world);
return sequential(this.body, world.block_scope_world(this));
};

AST_EmptyStatement.prototype._flow = function (world = new World()) {
Expand Down
168 changes: 124 additions & 44 deletions lib/flow/tools.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AST_Const, AST_Definitions, AST_Defun, AST_Lambda, AST_Let, AST_Symbol, AST_Var, AST_VarDef, walk_parent } from "../ast.js";
import { AST_Const, AST_Definitions, AST_Defun, AST_Lambda, AST_Let, AST_Symbol, AST_Scope, AST_Var, AST_VarDef, walk_parent } from "../ast.js";
import { FunctionType, LiteralType } from "./types.js";
import { map_map_set } from "../utils/index.js";

/** walk a sequence of statements */
export function sequential(nodes, world) {
Expand All @@ -11,42 +12,91 @@ export function sequential(nodes, world) {
}

/** pre-walk to hoist declarations such as var and defun */
export function hoist_defun_decls(node_with_body, world = new World()) {
walk_parent(node_with_body, (node, info) => {
if (node === node_with_body) return;

if (node instanceof AST_Defun) {
if (info.parent() !== node_with_body) {
throw NOPE; // Defun in a block scope
export function analyze_func_or_toplevel_body(node_with_body, world = new World(), { allow_return } = {}) {
const is_function = node_with_body instanceof AST_Lambda;

const block_scopes = new Map();
const closest_block_scope = (node, info) => {
let i = 0, scope;
while ((scope = info.parent(i++))) {
if (scope.is_block_scope()) {
return scope;
}

world.define(node.name.name, new FunctionType(node, world), 'defun');
return true;
}
};

if (node instanceof AST_Definitions) {
const type =
node instanceof AST_Const ? 'const'
: node instanceof AST_Let ? 'let'
: node instanceof AST_Var ? 'var'
: null
try {
walk_parent(node_with_body, (node, info) => {
if (node === node_with_body) return;

if (node instanceof AST_Defun) {
if (info.parent() !== node_with_body) {
throw NOPE; // Defun in a block scope
}

if (!type) {
throw new Error('unexpected definition type ' + node.TYPE)
world.define(node.name.name, new FunctionType(node, world), "defun");
return true;
}

for (const definition of node.definitions) {
if (definition.name instanceof AST_Symbol) {
if (definition.name.name === "arguments") throw NOPE;
world.define(definition.name.name, undefined /* defined when _flow*/, type);
} else {
throw new Error("TODO: destructuring");
if (node instanceof AST_Definitions) {
const type =
node instanceof AST_Const ? "const"
: node instanceof AST_Let ? "let"
: node instanceof AST_Var ? "var"
: null;

if (!type) {
throw new Error("unexpected definition type " + node.TYPE);
}

if (node.definitions.some(d => d.name.name === "arguments")) {
throw NOPE;
}

const block_scope = (type === "let" || type === "const")
&& closest_block_scope(node, info);
// TDZ stuff
if (block_scope) {
for (const definition of node.definitions) {
if (definition.name instanceof AST_Symbol) {
map_map_set(block_scopes, block_scope, definition.name.name, type);
} else {
throw new Error("TODO: destructuring");
}
}

return;
}

for (const definition of node.definitions) {
if (definition.name instanceof AST_Symbol) {
world.define(definition.name.name, undefined /* defined when _flow*/, type);
} else {
throw new Error("TODO: destructuring");
}

return;
}
}
}

if (node instanceof AST_Lambda) return true;
});
if (node instanceof AST_Lambda) return true;
});

world.set_block_scopes(block_scopes);
const normal_completion = sequential(node_with_body.body, world);

if (is_function) {
return new LiteralType(undefined); // default func return value
} else {
return normal_completion;
}
} catch (e) {
if (e instanceof Return) {
if (is_function) return e.returned;
throw NOPE;
}
throw e;
}
}


Expand Down Expand Up @@ -95,20 +145,20 @@ export function conditional(world = new World(), condition, body, alternative) {
}
}

const binding_func_mask = 0b1000_0000
const binding_block_mask = 0b0100_0000
const binding_const_mask = 0b0010_0000
const binding_func_mask = 0b10000000;
const binding_block_mask = 0b01000000;
const binding_const_mask = 0b00100000;

const binding_types = {
// func scope
'var': 0b1000_0001,
'argument': 0b1000_0010,
'defun': 0b1000_0100, // defun can be block-scope too but we nope out.
"var": 0b10000001,
"argument": 0b10000010,
"defun": 0b10000100, // defun can be block-scope too but we nope out.

// block scope
'let': 0b0100_0001,
'const': 0b0110_0010,
}
"let": 0b01000001,
"const": 0b01100010,
};

/**
* A single variable binding. It has a "type" and an "ambient type" which is the OR
Expand All @@ -127,7 +177,7 @@ export class Binding {
this.type = type;
this.ambient_type = type;
if (!(binding_type in binding_types)) {
throw new Error('unknown binding type ' + binding_type)
throw new Error("unknown binding type " + binding_type);
}
this.binding_type = binding_types[binding_type];
this.reads = 0;
Expand Down Expand Up @@ -175,14 +225,14 @@ export class Binding {
}

read() {
const reads = this.reads + 1
const reads = this.reads + 1;

if (this.type == null) {
// never read or written before
if (this.binding_type & binding_block_mask) {
throw NOPE; // TDZ
}
const type = new LiteralType(undefined)
const type = new LiteralType(undefined);
return this.changed({ type, ambient_type: type, reads });
} else {
return this.changed({ reads });
Expand All @@ -193,14 +243,14 @@ export class Binding {
const writes = this.writes + 1;

if (this.type == null) {
const is_tdz_violation = !is_definition && (this.binding_type & binding_block_mask)
const is_tdz_violation = !is_definition && (this.binding_type & binding_block_mask);
if (is_tdz_violation) throw NOPE;

// First write: let's pretend this wasn't considered `undefined` before
return this.changed({ type, ambient_type: type, writes });
} else {
const is_const_violation = this.binding_type & binding_const_mask
if (is_const_violation) throw NOPE
const is_const_violation = this.binding_type & binding_const_mask;
if (is_const_violation) throw NOPE;

return this.changed({ type, ambient_type: this.type.OR(type), writes });
}
Expand All @@ -213,9 +263,24 @@ export class World {
this.module = module;
this.parent_reality = parent_reality;
this.parent_scope = parent_reality;
this.block_scopes = undefined;
if (
parent_scope && parent_scope.flow_test
|| parent_reality && parent_reality.flow_test
) {
this.flow_test = true;
}
}

set_block_scopes(s) {
if (this.block_scopes) {
throw new Error("set_block_scopes called twice");
}
this.block_scopes = s;
}

define(name, type, binding_type = 'var') {

define(name, type, binding_type = "var") {
if (this.variables.has(name) || name === "arguments" || name === "async") {
throw NOPE;
}
Expand Down Expand Up @@ -282,6 +347,7 @@ export class World {
fork() {
const new_world = new World(this);
new_world.parent_reality = this;
new_world.block_scopes = this.block_scopes;
return new_world;
}
/** Join the child world with this world. */
Expand All @@ -300,6 +366,20 @@ export class World {
return new_world;
}

block_scope_world(block_scope) {
const new_world = new World();
new_world.parent_scope = this;
new_world.set_block_scopes(this.block_scopes);

const block_scope_contents = this.block_scopes.get(block_scope);
if (block_scope_contents) {
for (const [varname, binding_type] of block_scope_contents) {
new_world.define(varname, undefined, binding_type);
}
}
return new_world;
}

fully_walked() {
// Our knowledge about bindings is considered to be complete
this.walked = true;
Expand Down
9 changes: 9 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@ function map_add(map, key, value) {
}
}

/** Set a value into a map of maps */
export function map_map_set(root_map, key1, key2, value) {
if (root_map.has(key1)) {
root_map.get(key1).set(key2, value);
} else {
root_map.set(key1, new Map([[key2, value]]));
}
}

function map_from_object(obj) {
var map = new Map();
for (var key in obj) {
Expand Down
7 changes: 7 additions & 0 deletions test/mocha/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,15 @@ describe('Flow analysis: vars', () => {
assert_properties(world.get_binding('x'), { reads: 1, writes: 1 })
});

it('allows block scope', () => {
equal(test_stat('var x = 1; { const x = 2; x }'), new LiteralType(2))
equal(test_stat('var x = 1; { const x = 2; x } x'), new LiteralType(1))
})

it('forbids weird vars and accessing globals', () => {
equal(test_stat('var x; function x() {}'), NOPE);
equal(test_stat('let x; function x() {}'), NOPE);
equal(test_stat('var x; let x'), NOPE);
equal(test_stat('var arguments'), NOPE);
equal(test_stat('unknown'), NOPE);
});
Expand Down

0 comments on commit 045c40c

Please sign in to comment.