Skip to content

Commit dc730fe

Browse files
Duane Barlowmbostock
andauthored
Variable-scoped input shadowing (#349)
* add shadow and unshadow for variables * update tests * review feedback * Update test/variable/shadow-test.js Co-authored-by: Mike Bostock <[email protected]> * review feedback * Update src/module.js Co-authored-by: Mike Bostock <[email protected]> * Update src/variable.js Co-authored-by: Mike Bostock <[email protected]> * Update src/variable.js Co-authored-by: Mike Bostock <[email protected]> --------- Co-authored-by: Mike Bostock <[email protected]>
1 parent 38cb202 commit dc730fe

File tree

3 files changed

+86
-4
lines changed

3 files changed

+86
-4
lines changed

src/module.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ function module_import() {
5050
return v.import.apply(v, arguments);
5151
}
5252

53-
function module_variable(observer) {
54-
return new Variable(TYPE_NORMAL, this, observer);
53+
function module_variable(observer, options) {
54+
return new Variable(TYPE_NORMAL, this, observer, options);
5555
}
5656

5757
async function module_value(name) {

src/variable.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const TYPE_DUPLICATE = 3; // created on duplicate definition
1010

1111
export const no_observer = Symbol("no-observer");
1212

13-
export function Variable(type, module, observer) {
13+
export function Variable(type, module, observer, options) {
1414
if (!observer) observer = no_observer;
1515
Object.defineProperties(this, {
1616
_observer: {value: observer, writable: true},
@@ -26,6 +26,7 @@ export function Variable(type, module, observer) {
2626
_promise: {value: Promise.resolve(undefined), writable: true},
2727
_reachable: {value: observer !== no_observer, writable: true}, // Is this variable transitively visible?
2828
_rejector: {value: variable_rejector(this)},
29+
_shadow: {value: initShadow(module, options)},
2930
_type: {value: type},
3031
_value: {value: undefined, writable: true},
3132
_version: {value: 0, writable: true}
@@ -36,11 +37,20 @@ Object.defineProperties(Variable.prototype, {
3637
_pending: {value: variable_pending, writable: true, configurable: true},
3738
_fulfilled: {value: variable_fulfilled, writable: true, configurable: true},
3839
_rejected: {value: variable_rejected, writable: true, configurable: true},
40+
_resolve: {value: variable_resolve, writable: true, configurable: true},
3941
define: {value: variable_define, writable: true, configurable: true},
4042
delete: {value: variable_delete, writable: true, configurable: true},
4143
import: {value: variable_import, writable: true, configurable: true}
4244
});
4345

46+
function initShadow(module, options) {
47+
if (!options?.shadow) return null;
48+
return new Map(
49+
Object.entries(options.shadow)
50+
.map(([name, definition]) => [name, (new Variable(TYPE_IMPLICIT, module)).define([], definition)])
51+
);
52+
}
53+
4454
function variable_attach(variable) {
4555
variable._module._runtime._dirty.add(variable);
4656
variable._outputs.add(this);
@@ -89,11 +99,15 @@ function variable_define(name, inputs, definition) {
8999
}
90100
return variable_defineImpl.call(this,
91101
name == null ? null : String(name),
92-
inputs == null ? [] : map.call(inputs, this._module._resolve, this._module),
102+
inputs == null ? [] : map.call(inputs, this._resolve, this),
93103
typeof definition === "function" ? definition : constant(definition)
94104
);
95105
}
96106

107+
function variable_resolve(name) {
108+
return this._shadow?.get(name) ?? this._module._resolve(name);
109+
}
110+
97111
function variable_defineImpl(name, inputs, definition) {
98112
const scope = this._module._scope, runtime = this._module._runtime;
99113

test/variable/shadow-test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {Runtime} from "@observablehq/runtime";
2+
import {valueof} from "./valueof.js";
3+
import assert from "assert";
4+
5+
it("module.variable(…, {shadow}) can define a shadow input", async () => {
6+
const runtime = new Runtime();
7+
const module = runtime.module();
8+
9+
module.define("val", [], 1000);
10+
const a = module.variable(true, {shadow: {val: 100}}).define("a", ["val"], (val) => val);
11+
12+
assert.deepStrictEqual(await valueof(a), {value: 100});
13+
});
14+
15+
it("module.variable(…, {shadow}) can define shadow inputs that differ between variables", async () => {
16+
const runtime = new Runtime();
17+
const module = runtime.module();
18+
19+
module.define("val", [], 1000);
20+
const a = module.variable(true, {shadow: {val: 100}}).define("a", ["val"], (val) => val);
21+
const b = module.variable(true, {shadow: {val: 200}}).define("b", ["val"], (val) => val);
22+
assert.deepStrictEqual(await valueof(a), {value: 100});
23+
assert.deepStrictEqual(await valueof(b), {value: 200});
24+
});
25+
26+
it("module.variable(…, {shadow}) variables that are downstream will also use the shadow input", async () => {
27+
const runtime = new Runtime();
28+
const module = runtime.module();
29+
30+
module.define("val", [], 1000);
31+
const a = module.variable(true, {shadow: {val: 100}}).define("a", ["val"], (val) => val);
32+
const b = module.variable(true, {shadow: {val: 200}}).define("b", ["val"], (val) => val);
33+
const c = module.variable(true).define("c", ["a", "b"], (a, b) => `${a}, ${b}`);
34+
35+
assert.deepStrictEqual(await valueof(a), {value: 100});
36+
assert.deepStrictEqual(await valueof(b), {value: 200});
37+
assert.deepStrictEqual(await valueof(c), { value: "100, 200" });
38+
});
39+
40+
it("module.variable(…, {shadow}) variables a->b->c that shadow the same inputs will each use their own shadows", async () => {
41+
const runtime = new Runtime();
42+
const main = runtime.module();
43+
44+
const a = main
45+
.variable(true, {shadow: {val: 100}})
46+
.define("a", ["val"], (val) => val);
47+
const b = main
48+
.variable(true, {shadow: {val: 200}})
49+
.define("b", ["val", "a"], (val, a) => `${val}, ${a}`);
50+
const c = main
51+
.variable(true, {shadow: {val: 300}})
52+
.define("c", ["val", "b", "a"], (val, b, a) => `${val}, (${b}), ${a}`);
53+
54+
assert.deepStrictEqual(await valueof(a), {value: 100});
55+
assert.deepStrictEqual(await valueof(b), {value: "200, 100"});
56+
assert.deepStrictEqual(await valueof(c), {value: "300, (200, 100), 100"});
57+
});
58+
59+
it("module.variable(…, {shadow}) can shadow a non-existent variable", async () => {
60+
const runtime = new Runtime();
61+
const main = runtime.module();
62+
63+
const a = main
64+
.variable(true, {shadow: {val: 100}})
65+
.define("a", ["val"], (val) => val);
66+
67+
assert.deepStrictEqual(await valueof(a), {value: 100});
68+
});

0 commit comments

Comments
 (0)