Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions graph/javascript/engine/resolution/frameworks.dl
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ assign_like_site(ce, tgt, src) :- expr_kind(_, "CALL", _, ce), call_site(_, "MET
assign_like_site(ce, tgt, src) :- expr_kind(_, "CALL", _, ce), callee_value(ce, "func", m), model_assign_like_function(m),
call_arg(ce, 0, tgt), call_arg(ce, 1, src).
model_assign_like_function(m) :- module_export_value(mod, "default", "func", m), module_package("lib", "merge-descriptors", mod).
member_write(k, i, n, k2, i2) :- assign_like_site(_, tgt, src), expr_value(tgt, k, i), k != "ambient",
expr_value(src, k0, i0), prop_value(k0, i0, n, k2, i2).
member_write(k, i, n, k2, i2) :- assign_like_site(_, tgt, src), expr_value(tgt, k, i), !shared_identity_kind(k),
expr_value(src, k0, i0), prop_value(k0, i0, n, k2, i2), (k != "shape" ; shape_stores_kind(k2)).
// The call evaluates to its target.
expr_value(ce, k, i) :- assign_like_site(ce, tgt, _), expr_value(tgt, k, i).

Expand All @@ -49,7 +49,7 @@ object_create_site(ce, src) :- expr_kind(_, "CALL", _, ce), call_site(_, "METHOD
expr_child(_, ce, "RECEIVER", _, recv), expr_value(recv, "ambient", "Object"),
call_arg(ce, 0, src).
expr_value(ce, "obj", ce) :- object_create_site(ce, _).
inherits_from(k, i, k0, i0) :- inherit_like_site(_, tgt, src), expr_value(tgt, k, i), k != "ambient", expr_value(src, k0, i0).
inherits_from(k, i, k0, i0) :- inherit_like_site(_, tgt, src), expr_value(tgt, k, i), !shared_identity_kind(k), expr_value(src, k0, i0).
inherits_from("obj", ce, k0, i0) :- object_create_site(ce, src), expr_value(src, k0, i0).
prop_value(k, i, n, k2, i2) :- inherits_from(k, i, k0, i0), prop_value(k0, i0, n, k2, i2).
// `Object.setPrototypeOf(Child.prototype, Parent.prototype)` — the spelling of
Expand Down Expand Up @@ -164,17 +164,18 @@ descriptor_value(d, k, i) :- literal_owns_method(d, g), method_decl(_, "get", _,
define_property_site(ce, tgt, key, desc) :- expr_kind(_, "CALL", _, ce), call_site(_, "METHOD_CALL", "defineProperty", _, _, _, ce, _, _),
expr_child(_, ce, "RECEIVER", _, recv), expr_value(recv, "ambient", "Object"),
call_arg(ce, 0, tgt), call_arg(ce, 1, key), call_arg(ce, 2, desc).
member_write(k, i, n, k2, i2) :- define_property_site(_, tgt, key, desc), expr_value(tgt, k, i), k != "ambient",
expr_value(key, "str", n), descriptor_value(desc, k2, i2).
member_write(k, i, n, k2, i2) :- define_property_site(_, tgt, key, desc), expr_value(tgt, k, i), !shared_identity_kind(k),
expr_value(key, "str", n), descriptor_value(desc, k2, i2), (k != "shape" ; shape_stores_kind(k2)).
// `Object.defineProperties(o, { n: desc, … })` and `Object.create(proto, { n: desc })`.
descriptors_literal_entry(lit, n, desc) :- expr_child(_, lit, "PROPERTY_KEY", idx, key), expr_name(_, n, key),
expr_child(_, lit, "PROPERTY_VALUE", idx, desc).
descriptors_literal_entry(lit, n, desc) :- expr_child(_, lit, "COMPUTED_KEY", idx, key), expr_value(key, "str", n),
expr_child(_, lit, "PROPERTY_VALUE", idx, desc).
member_write(k, i, n, k2, i2) :- expr_kind(_, "CALL", _, ce), call_site(_, "METHOD_CALL", "defineProperties", _, _, _, ce, _, _),
expr_child(_, ce, "RECEIVER", _, recv), expr_value(recv, "ambient", "Object"),
call_arg(ce, 0, tgt), expr_value(tgt, k, i), k != "ambient",
call_arg(ce, 1, lit), descriptors_literal_entry(lit, n, desc), descriptor_value(desc, k2, i2).
call_arg(ce, 0, tgt), expr_value(tgt, k, i), !shared_identity_kind(k),
call_arg(ce, 1, lit), descriptors_literal_entry(lit, n, desc), descriptor_value(desc, k2, i2),
(k != "shape" ; shape_stores_kind(k2)).
member_write("obj", ce, n, k2, i2) :- object_create_site(ce, _), call_arg(ce, 1, lit),
descriptors_literal_entry(lit, n, desc), descriptor_value(desc, k2, i2).
// The call evaluates to its target (`Object.defineProperty(o, …)` returns o).
Expand Down
77 changes: 63 additions & 14 deletions graph/javascript/engine/resolution/value-flow.dl
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,14 @@ path_source(k, i) :- var_binding_path(_, _, v), var_init(_, _, e, v), expr_value
path_source(k, i) :- expr_param(_, p, e), expr_binding_path(_, _, e), param_value(p, k, i).
path_source("module", mod) :- import_decl(_, _, _, "DESTRUCTURED", n, _, _, _, imp), contains(".", n), import_module(imp, mod).
path_source(k, i) :- path_step(_, _, _, k, i).
path_value(k, i, p, k2, i2) :- path_rest_base(p, b), path_parent(b, par), match("[0-9]+", substr(b, strlen(par) + 1, strlen(b) - strlen(par) - 1)),
path_value(k, i, par, k2, i2).
path_value(k, i, p, k2, i2) :- path_rest_base(p, b), b != "", !match("([^.]*[.])*[0-9]+", b), path_value(k, i, b, k2, i2).
path_value(k, i, p, k2, i2) :- path_rest_index_parent(p, par), path_value(k, i, par, k2, i2).
path_value(k, i, p, k2, i2) :- path_rest_named_base(p, b), path_value(k, i, b, k2, i2).
// The two tests above are on the path TEXT only, so they are decided once here, below
// the flow, instead of by a regex on every pass of the recursion (#732: the second
// clause was 29 s of a 200 s solve on a real bundler's source when it matched inline).
path_rest_index_parent(p, par) :- path_rest_base(p, b), path_parent(b, par),
match("[0-9]+", substr(b, strlen(par) + 1, strlen(b) - strlen(par) - 1)).
path_rest_named_base(p, b) :- path_rest_base(p, b), b != "", !match("([^.]*[.])*[0-9]+", b).
path_parent(b, substr(b, 0, n)) :- path_rest_base(_, b), contains(".", b), n = range(1, strlen(b) - 1), substr(b, n, 1) = ".",
!contains(".", substr(b, n + 1, strlen(b) - n - 1)).
binding_path(par) :- path_parent(_, par).
Expand Down Expand Up @@ -499,14 +504,46 @@ expr_value(e, "free", cat(mod, ":", n)) :- expr_kind(_, "IDENTIFIER", _, e),
// every `@param {Object} opts` in the program, so a write through any of them would
// be a read through all of them — a global merge point that made one held-out
// solve 35x slower. The platform's properties are not tracked.
//
// The same holds for `str` (#732): a string value is identified by its TEXT, so
// `"request"` is one value wherever the literal is written, and a write through a
// binding that may hold it landed on every other binding that may hold the same text.
// At runtime a property write onto a primitive is dropped (or throws in strict code),
// so nothing real is lost. On a real bundler's own source these were 37K member_write
// rows, one of the points where its widest value sets were manufactured.
//
// A `shape` is a JSDoc `@typedef {Object}` REFERENCE, also one identity per typedef:
// `@param {StatsModule} m; m.moduleName = x` wrote x into every StatsModule in the
// program. On that bundler these were 36K member_write rows onto 66 typedefs, and
// they are where its widest value sets were made: 513 parameters of 620 unrelated
// functions all held the same 552 values. But where the object behind a typedef is
// not tracked, the shape is the only carrier of a HANDLER stored on it:
// `context.cachedGetErrors = c => …`, later `cachedGetErrors(compilation)`, was 9
// real edges on that bundler. So a write through a shape keeps every value a call
// can later be made on or through (shape_stores_kind): a function or a class; an
// instance, object literal or module namespace whose members are called
// (`ctx.logger = new Logger()`, then `ctx.logger.log()`, where nothing but the
// typedef carries ctx); and an array or collection of handlers that is iterated
// (`ctx.list = [a, b]; for (const f of ctx.list) f()`). It drops the string,
// symbol and other scalar stores, which were the width. Both tests are on kinds,
// so the rules stay monotone.
shared_identity_kind("ambient").
shared_identity_kind("str").
shape_stores_kind("func").
shape_stores_kind("ctor").
shape_stores_kind("inst").
shape_stores_kind("obj").
shape_stores_kind("module").
shape_stores_kind("arr").
shape_stores_kind("coll").
member_write(k, i, n, k2, i2) :- expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, op, a), assignment_op_writes_value(op),
expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_kind(_, "PROPERTY_ACCESS", _, tgt), expr_name(_, n, tgt),
expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, i), k != "ambient",
expr_child(_, a, "ASSIGNMENT_VALUE", _, val), expr_value(val, k2, i2).
expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, i), !shared_identity_kind(k),
expr_child(_, a, "ASSIGNMENT_VALUE", _, val), expr_value(val, k2, i2), (k != "shape" ; shape_stores_kind(k2)).
member_write(k, i, n, k2, i2) :- computed_key_write(a, tgt, key, val), !keyed_copy_write(a, _, _), !generic_setter_write(a),
key_name(key, n),
expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, i), k != "ambient", k != "arr", k != "coll",
expr_value(val, k2, i2).
expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, i), !shared_identity_kind(k), k != "arr", k != "coll",
expr_value(val, k2, i2), (k != "shape" ; shape_stores_kind(k2)).
computed_key_write(a, tgt, key, val) :- expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, op, a), assignment_op_writes_value(op),
expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_kind(_, "ELEMENT_ACCESS", _, tgt),
expr_child(_, tgt, "COMPUTED_KEY", _, key), expr_child(_, a, "ASSIGNMENT_VALUE", _, val).
Expand Down Expand Up @@ -539,10 +576,15 @@ same_key_binding(k1, k2) :- expr_binding(_, v, k1), expr_binding(_, v, k2).
generic_setter_write(a) :- computed_key_write(a, tgt, key, _), expr_child(_, tgt, "ACCESS_TARGET", _, r),
expr_param(_, _, r), expr_param(_, _, key), !keyed_copy_write(a, _, _).
member_write_refused(a, "generic_setter") :- generic_setter_write(a).
// A write whose target may hold a string (above): declared, so the
// dropped store is countable. `ambient` targets were never tracked and are not listed.
member_write_refused(a, "shared_identity") :- expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, op, a), assignment_op_writes_value(op),
expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_child(_, tgt, "ACCESS_TARGET", _, r),
expr_value(r, k, _), shared_identity_kind(k), k != "ambient".
member_write(k, i, n, k2, i2) :- keyed_copy_write(a, key, src), computed_key_write(a, tgt, key, _),
key_name(key, n),
expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, i), k != "ambient", k != "arr", k != "coll",
expr_value(src, k0, i0), prop_value(k0, i0, n, k2, i2).
expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, i), !shared_identity_kind(k), k != "arr", k != "coll",
expr_value(src, k0, i0), prop_value(k0, i0, n, k2, i2), (k != "shape" ; shape_stores_kind(k2)).
// A copy whose keys are ENUMERATED FROM THE SOURCE (#707): `for (const k in src)
// dst[k] = src[k]`, `for (const k of Object.keys(src))`, `Object.keys(src).forEach(k
// => { dst[k] = src[k]; })`, the same through `Object.getOwnPropertyNames`, an alias
Expand All @@ -558,8 +600,9 @@ enumerated_copy(a, src) :- keyed_copy_write(a, key, src), key_enumerates(key2, o
same_subject(e1, e2) :- same_key_binding(e1, e2).
same_subject(e1, e2) :- expr_kind(_, "THIS", _, e1), expr_kind(_, "THIS", _, e2), expr_owner(_, m, _, e1), expr_owner(_, m, _, e2).
member_write(k, i, n, k2, i2) :- enumerated_copy(a, src), computed_key_write(a, tgt, _, _),
expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, i), k != "ambient", k != "arr", k != "coll",
expr_value(src, k0, i0), prop_value(k0, i0, n, k2, i2), n != "constructor", n != "prototype".
expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, i), !shared_identity_kind(k), k != "arr", k != "coll",
expr_value(src, k0, i0), prop_value(k0, i0, n, k2, i2), n != "constructor", n != "prototype",
(k != "shape" ; shape_stores_kind(k2)).
// key_enumerates(KeyExpr, Source): a reference to a binding that enumerates Source's
// own property names.
key_enumerates(key, src) :- loop_head(v, src), for_in_iterable(src), expr_binding(_, v, key).
Expand Down Expand Up @@ -590,9 +633,15 @@ object_keys_call(ce, o) :- expr_kind(_, "CALL", _, ce), expr_resolves_to_method(
// keys_helper(Method): `function keys(o) { const a = []; for (const k in o) a.push(k);
// return a; }`, or `function keys(o) { return Object.keys(o); }`: returns an array of
// its first parameter's property names.
keys_helper(m) :- param_decl(_, _, "0", m, p), loop_head(v, e), for_in_iterable(e), expr_param(_, p, e),
expr_binding(_, v, kref), array_method_call(pce, "push", a), call_arg(pce, _, kref), return_value(m, "arr", a).
keys_helper(m) :- param_decl(_, _, "0", m, p), object_keys_call(ce, e), expr_param(_, p, e), return_value(m, "arr", ce).
// ATOM ORDER MATTERS (#732): Souffle joins in the order written, and these rules sit in
// the value-flow recursion, so they are re-run every iteration. Written with
// `param_decl` first they began with the product of every first parameter and every
// loop head (no shared variable), and on a real bundler's source that product was the
// most expensive clause of the solve: 76 s of 200, deriving nothing. Each atom below
// is bound by the one before it.
keys_helper(m) :- loop_head(v, e), for_in_iterable(e), expr_param(_, p, e), param_decl(_, _, "0", m, p),
expr_binding(_, v, kref), call_arg(pce, _, kref), array_method_call(pce, "push", a), return_value(m, "arr", a).
keys_helper(m) :- object_keys_call(ce, e), expr_param(_, p, e), param_decl(_, _, "0", m, p), return_value(m, "arr", ce).
// `Object.assign` and its library cousins: resolution/frameworks.dl.

// ── literal_owns_method(LiteralExpr, MethodHash) — PD-JS-1 workaround ───────
Expand Down
4 changes: 4 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@
.decl keys_array_expr(c0:symbol, c1:symbol)
.decl object_keys_call(c0:symbol, c1:symbol)
.decl keys_helper(c0:symbol)
.decl shared_identity_kind(c0:symbol)
.decl shape_stores_kind(c0:symbol)
.decl path_rest_index_parent(c0:symbol, c1:symbol)
.decl path_rest_named_base(c0:symbol, c1:symbol)
.decl generic_setter_write(c0:symbol)
.decl member_write_refused(c0:symbol, c1:symbol)
.decl literal_contains_method(c0:symbol, c1:symbol)
Expand Down
77 changes: 77 additions & 0 deletions graph/test/javascript/cases/64-shared-identity-writes/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// A write through a string is not a store (#732). A string is one identity per TEXT,
// so a write through one binding holding 'k' landed on every other expression holding
// 'k'; at runtime a property written onto a primitive is dropped.
function alpha() { return 'a'; }
function beta() { return 'b'; }

const key = 'k';
key.fn = beta;
function viaString() { return 'k'.fn(); } // nothing: a string has no such member
/** @param {string} s */
function tagString(s) { s.fn = alpha; }
tagString('k');
function viaStringParam() { return 'k'.fn(); } // nothing either

// A JSDoc typedef is one identity for every value documented with it, so a DATA store
// through it wrote into every other value of that typedef. The objects that really
// flow in keep the store; the typedef keeps only what a call can be made on (a
// function, a class, an instance, an object literal), so both sides reach goEntry.
class Left { go() { return 'l'; } }
class Right { go() { return 'r'; } }
/**
* @typedef {Object} Entry
* @property {string} name
*/
/** @param {Entry} e */
function putLeft(e) { e.side = new Left(); }
/** @param {Entry} e */
function putRight(e) { e.side = new Right(); }
/** @param {Entry} e */
function goEntry(e) { return e.side.go(); } // Left.go, and Right.go through the typedef
const first = { name: 'first' };
putLeft(first);
function viaTypedef() { return goEntry(first); }
const second = { name: 'second' };
putRight(second);

// CONTROL: where the object behind a typedef is not tracked, the typedef is the only
// carrier of a handler stored on it, so a FUNCTION written through it still reads back.
/**
* @typedef {Object} Hooks
* @property {string} name
*/
/** @param {Hooks} hooks */
function install(hooks) { hooks.onError = alpha; }
/** @param {Hooks} hooks */
function report(hooks) { return hooks.onError(); } // alpha, through the typedef
// The same holds for an instance or an object literal whose methods are called later,
// a module namespace, and an array of handlers that is iterated.
class Logger { log() { return 'g'; } }
const util = require('./util');
/** @param {Hooks} hooks */
function installLogger(hooks) { hooks.logger = new Logger(); hooks.api = { fetch() { return 'f'; } }; }
/** @param {Hooks} hooks */
function useLogger(hooks) { hooks.logger.log(); return hooks.api.fetch(); } // Logger.log, fetch
/** @param {Hooks} hooks */
function installMore(hooks) { hooks.util = util; hooks.list = [alpha, beta]; hooks.label = 'x'; }
/** @param {Hooks} hooks */
function useMore(hooks) { hooks.util.helper(); for (const f of hooks.list) f(); } // helper, alpha, beta
/** @param {Hooks} hooks */
function useLabel(hooks) { return hooks.label.toUpperCase(); } // nothing: a string store through a typedef is dropped

// The controls. An object literal and a class instance keep their writes, and a
// typedef's DECLARED members still read.
class Widget { render() { return 'w'; } }
/**
* @typedef {Object} Ctx
* @property {Widget} widget
*/
/** @param {Ctx} c */
function draw(c) { return c.widget.render(); } // Widget.render, from the declaration
const holder = {};
holder.run = alpha;
function viaLiteral() { return holder.run(); } // alpha
class Box { constructor() { this.run = beta; } }
function viaInstance() { return new Box().run(); } // beta
module.exports = { viaString, viaStringParam, viaTypedef, install, report, installLogger, useLogger, installMore, useMore,
useLabel, draw, viaLiteral, viaInstance };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "name": "64-shared-identity-writes" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';
function helper() { return 'h'; }
module.exports = { helper };
4 changes: 4 additions & 0 deletions graph/test/javascript/expected/64-shared-identity-writes.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package_entry 64-shared-identity-writes . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved main.js:13:36 METHOD_CALL fn no_target
unresolved main.js:60:35 METHOD_CALL toUpperCase receiver_untyped
unresolved main.js:9:31 METHOD_CALL fn no_target
22 changes: 22 additions & 0 deletions graph/test/javascript/expected/64-shared-identity-writes.edges
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
main.js:12:1 FUNCTION_CALL tagString -> known_edge main.js:11:1 tagString
main.js:13:36 METHOD_CALL 'k'.fn -> ambient_terminal -
main.js:26:32 CONSTRUCTOR_CALL Left -> implicit_constructor -
main.js:28:33 CONSTRUCTOR_CALL Right -> implicit_constructor -
main.js:30:30 METHOD_CALL e.side.go -> multi_inferred main.js:19:14 go
main.js:30:30 METHOD_CALL e.side.go -> multi_inferred main.js:20:15 go
main.js:32:1 FUNCTION_CALL putLeft -> known_edge main.js:26:1 putLeft
main.js:33:32 FUNCTION_CALL goEntry -> known_edge main.js:30:1 goEntry
main.js:35:1 FUNCTION_CALL putRight -> known_edge main.js:28:1 putRight
main.js:46:33 METHOD_CALL hooks.onError -> known_edge main.js:4:1 alpha
main.js:52:48 CONSTRUCTOR_CALL Logger -> implicit_constructor -
main.js:54:29 METHOD_CALL hooks.logger.log -> known_edge main.js:49:16 log
main.js:54:56 METHOD_CALL hooks.api.fetch -> known_edge main.js:52:76 fetch
main.js:58:27 METHOD_CALL hooks.util.helper -> known_edge util.js:2:1 helper
main.js:58:76 FUNCTION_CALL f -> multi_inferred main.js:4:1 alpha
main.js:58:76 FUNCTION_CALL f -> multi_inferred main.js:5:1 beta
main.js:60:35 METHOD_CALL hooks.label.toUpperCase -> ambiguous_unknown -
main.js:70:27 METHOD_CALL c.widget.render -> known_edge main.js:64:16 render
main.js:73:32 METHOD_CALL holder.run -> known_edge main.js:4:1 alpha
main.js:75:33 CONSTRUCTOR_CALL Box -> known_edge main.js:74:13 <constructor>
main.js:75:33 METHOD_CALL new Box().run -> known_edge main.js:5:1 beta
main.js:9:31 METHOD_CALL 'k'.fn -> ambient_terminal -
12 changes: 12 additions & 0 deletions graph/test/javascript/expected/64-shared-identity-writes.oracle
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
main.js:12:1 FUNCTION_CALL tagString EXACT main.js:11:1
main.js:26:32 CONSTRUCTOR_CALL Left SYNTHESIZED_OK
main.js:28:33 CONSTRUCTOR_CALL Right SYNTHESIZED_OK
main.js:32:1 FUNCTION_CALL putLeft EXACT main.js:26:1
main.js:33:32 FUNCTION_CALL goEntry EXACT main.js:30:1
main.js:35:1 FUNCTION_CALL putRight EXACT main.js:28:1
main.js:52:48 CONSTRUCTOR_CALL Logger SYNTHESIZED_OK
main.js:70:27 METHOD_CALL render EXACT main.js:64:16
main.js:73:32 METHOD_CALL run EXACT main.js:4:1
main.js:75:33 CONSTRUCTOR_CALL Box EXACT main.js:74:13
main.js:75:33 METHOD_CALL run EXACT main.js:5:1
# defects: 0
Loading