Skip to content

Commit 3ef3d1c

Browse files
committed
✨Introduce the --canon-- module
One of the principles of PlatformScript is for there to be very little magic, and what magic there is should be confined to limited cases. This runs afoul of the "magical" values that are needed to make things actually work. These are the "magical" values like React constructors on the browser, or file system access on Unix systems. It is anticipated that PS will run in many different contexts and so we have to be able to swap out what is magically available without that process seeming magical. To do this, we make all magical values appear within a normal module. Instead of them just appearing in your scope, they are accessed by a special module called `--canon--`. The "canon" can be passed into the module loader and any module that requests the`--canon--` shall receive it. Eventually, we can use this canonical value to typecheck code that accesses the canon. The word canon was chosen because it refers to an established body of work that can be drawn upon to perform contemporty tasks. Like the canon of law, or the canon of prayer. Also because "env" was taken already and we want to reserve "context" for a runtime api.
1 parent a199129 commit 3ef3d1c

File tree

7 files changed

+80
-83
lines changed

7 files changed

+80
-83
lines changed

load.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { PSEnv, PSModule, PSValue } from "./types.ts";
1+
import type { PSEnv, PSMap, PSModule, PSValue } from "./types.ts";
22
import type { Operation } from "./deps.ts";
33

44
import { expect, useAbortSignal } from "./deps.ts";
@@ -11,30 +11,32 @@ export interface LoadOptions {
1111
location: string | URL;
1212
base?: string;
1313
env?: PSEnv;
14+
canon?: PSMap;
1415
}
1516

1617
export function* load(options: LoadOptions): Operation<PSModule> {
17-
let { location, base, env } = options;
18+
let { location, base, env, canon } = options;
1819
let url = typeof location === "string" ? new URL(location, base) : location;
1920

2021
let content = yield* read(url);
2122
let source = parse(content);
2223

23-
return yield* moduleEval({ source, location: url, env });
24+
return yield* moduleEval({ source, location: url, env, canon });
2425
}
2526

2627
export interface ModuleEvalOptions {
2728
location: string | URL;
2829
source: PSValue;
2930
env?: PSEnv;
31+
canon?: PSMap;
3032
}
3133

3234
export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
3335
let { location, source, env = createYSEnv() } = options;
3436
let url = typeof location === "string" ? new URL(location) : location;
3537

3638
let mod: PSModule = {
37-
url,
39+
location: location.toString(),
3840
source,
3941
value: source,
4042
imports: [],
@@ -45,6 +47,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
4547
}
4648

4749
let scope = data.map({});
50+
let canon = options.canon ?? data.map({});
4851

4952
let imports = lookup("$import", source);
5053
let rest = exclude(["$import"], source);
@@ -67,11 +70,19 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
6770
);
6871
}
6972
let bindings = matchBindings(names.value);
70-
let dep = yield* load({
71-
location: loc.value,
72-
base: url.toString(),
73-
env,
74-
});
73+
74+
let dep = loc.value === "--canon--"
75+
? ({
76+
location: loc.value,
77+
source: canon,
78+
value: canon,
79+
imports: [],
80+
})
81+
: yield* load({
82+
location: loc.value,
83+
base: url.toString(),
84+
env,
85+
});
7586

7687
mod.imports.push({
7788
module: dep,
@@ -85,13 +96,13 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
8596
value = dep.value;
8697
} else if (dep.value.type !== "map") {
8798
throw new Error(
88-
`tried to import a name from ${dep.url}, but it is not a 'map'. It is a ${dep.value.type}`,
99+
`tried to import a name from ${dep.location}, but it is not a 'map'. It is a ${dep.value.type}`,
89100
);
90101
} else {
91102
let result = lookup(binding.name, dep.value);
92103
if (result.type === "nothing") {
93104
throw new Error(
94-
`module ${dep.url} does not have a member named '${binding.name}'`,
105+
`module ${dep.location} does not have a member named '${binding.name}'`,
95106
);
96107
} else {
97108
value = result.value;

platformscript.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { PSEnv, PSFn, PSMap, PSModule, PSValue } from "./types.ts";
22
import type { Operation, Task } from "./deps.ts";
33
import { createYSEnv, global } from "./evaluate.ts";
4-
import { concat } from "./psmap.ts";
54
import { load, moduleEval } from "./load.ts";
65
import { map } from "./data.ts";
76
import { run as $run } from "./deps.ts";
@@ -14,16 +13,11 @@ export interface PlatformScript {
1413
load(url: string | URL, base?: string): Task<PSModule>;
1514
}
1615

17-
export function createPlatformScript(
18-
globals?: (ps: PlatformScript) => PSMap,
19-
): PlatformScript {
20-
let env = lazy(() => {
21-
let ext = globals ? globals(platformscript) : map({});
22-
return createYSEnv(concat(global, ext));
23-
});
16+
export function createPlatformScript(canon = map({})): PlatformScript {
17+
let env = createYSEnv(global);
2418

2519
function run<T>(block: (env: PSEnv) => Operation<T>): Task<T> {
26-
return $run(() => block(env()));
20+
return $run(() => block(env));
2721
}
2822

2923
let platformscript: PlatformScript = {
@@ -35,20 +29,13 @@ export function createPlatformScript(
3529
return run((env) => env.eval(value, bindings));
3630
},
3731
moduleEval(value, url) {
38-
return run((env) => moduleEval({ source: value, location: url, env }));
32+
return run((env) =>
33+
moduleEval({ source: value, location: url, env, canon })
34+
);
3935
},
4036
load(location, base) {
41-
return run((env) => load({ location, base, env }));
37+
return run((env) => load({ location, base, env, canon }));
4238
},
4339
};
4440
return platformscript;
4541
}
46-
47-
function lazy<T>(create: () => T): () => T {
48-
let thunk = () => {
49-
let value = create();
50-
thunk = () => value;
51-
return value;
52-
};
53-
return () => thunk();
54-
}

test/external.test.ts

Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,58 +6,46 @@ import * as _ from "https://raw.githubusercontent.com/lodash/lodash/4.17.21-es/l
66
describe("external", () => {
77
it("can represent any arbitrary javascript value", async () => {
88
let obj = { unqiue: "object" };
9-
let ps = createPlatformScript(() =>
10-
map({
11-
"extern": external(obj),
12-
})
13-
);
14-
expect((await ps.eval(parse("$extern"))).value).toBe(obj);
9+
let binding = map({
10+
"extern": external(obj),
11+
});
12+
13+
let ps = createPlatformScript();
14+
expect((await ps.eval(parse("$extern"), binding)).value).toBe(obj);
1515
});
1616

1717
it("can define new PS values from the external value", async () => {
1818
let obj = { I: { contain: { the: { number: number(5) } } } };
19-
let ps = createPlatformScript(() =>
20-
map({
21-
"truly": external(obj, (path, o) => _.get(o, path)),
22-
})
23-
);
19+
let binding = map({
20+
"truly": external(obj, (path, o) => _.get(o, path)),
21+
});
22+
let ps = createPlatformScript();
2423

2524
let program = parse("$truly.I.contain.the.number");
26-
expect((await ps.eval(program)).value).toEqual(5);
25+
expect((await ps.eval(program, binding)).value).toEqual(5);
2726
});
2827
it("errors if a dereference is undefined", async () => {
29-
let ps = createPlatformScript(() =>
30-
map({
31-
"oops": external({}, () => void 0),
32-
})
33-
);
34-
try {
35-
await ps.eval(parse("$oops.i.did.it.again"));
36-
throw new Error("expected block to throw, but it did not");
37-
} catch (error) {
38-
expect(error.name).toEqual("ReferenceError");
39-
}
28+
let binding = map({
29+
"oops": external({}, () => void 0),
30+
});
31+
let ps = createPlatformScript();
32+
33+
await expect(ps.eval(parse("$oops.i.did.it.again"), binding)).rejects
34+
.toHaveProperty("name", "ReferenceError");
4035
});
4136
it("errors if a derefenence does not return a PSValue", async () => {
42-
let ps = createPlatformScript(() =>
43-
map({
44-
"oops": external(
45-
{ wrong: "type" },
46-
//@ts-expect-error situation could happen if you are using JavaScript...
47-
() => ({ type: "WAT!!", value: "hi" }),
48-
),
49-
})
37+
let binding = map({
38+
"oops": external(
39+
{ wrong: "type" },
40+
//@ts-expect-error situation could happen if you are using JavaScript...
41+
() => ({ type: "WAT!!", value: "hi" }),
42+
),
43+
});
44+
let ps = createPlatformScript();
45+
await expect(ps.eval(parse("$oops.wrong"), binding)).rejects.toHaveProperty(
46+
"name",
47+
"TypeError",
5048
);
51-
52-
try {
53-
await ps.eval(parse("$oops.wrong"));
54-
throw new Error("expected block to throw, but it did not");
55-
} catch (error) {
56-
expect(error.message).toMatch(
57-
/did not resolve to a platformscript value/,
58-
);
59-
expect(error.name).toEqual("TypeError");
60-
}
6149
});
6250
// it("can handle an exception that happens during dereference");
6351
});

test/fn.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,12 @@ $say: Hello
7373
});
7474

7575
it("can reference values from arguments to native functions", async () => {
76-
let interp = ps.createPlatformScript(() =>
77-
ps.map({
78-
id: ps.fn(function* $id({ arg, env }) {
79-
return yield* env.eval(arg);
80-
}, { name: "x" }),
81-
})
82-
);
76+
let binding = ps.map({
77+
id: ps.fn(function* $id({ arg, env }) {
78+
return yield* env.eval(arg);
79+
}, { name: "x" }),
80+
});
81+
let interp = ps.createPlatformScript();
8382

8483
let program = ps.parse(`
8584
$let:
@@ -89,7 +88,7 @@ $do:
8988
$myid: hello world
9089
`);
9190

92-
let result = await interp.eval(program);
91+
let result = await interp.eval(program, binding);
9392
expect(result.value).toEqual("hello world");
9493
});
9594

test/module.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { PSMap, PSModule } from "../types.ts";
22
import type { Task } from "../deps.ts";
33

44
import { describe, expect, it, useStaticFileServer } from "./suite.ts";
5-
import { load, moduleEval, number, parse, ps2js, string } from "../mod.ts";
5+
import { load, map, moduleEval, number, parse, ps2js, string } from "../mod.ts";
66
import { run } from "../deps.ts";
77
import { lookup, lookup$ } from "../psmap.ts";
88

@@ -101,6 +101,18 @@ main: $myfive
101101
expect(lookup$("main", mod.value)).toEqual(number(5));
102102
});
103103
});
104+
105+
it("supports importing from the canonical module", async () => {
106+
await run(function* () {
107+
let source = parse(`$import:\n five: --canon--\nhi: $five`);
108+
let mod = yield* moduleEval({
109+
source,
110+
location: new URL(`modules/virtual-module.yaml`, import.meta.url),
111+
canon: map({ "five": number(5) }),
112+
});
113+
expect(lookup$("hi", mod.value)).toEqual(number(5));
114+
});
115+
});
104116
});
105117

106118
function loadmod(url: string): Task<PSModule> {

test/suite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from "https://deno.land/[email protected]/testing/bdd.ts";
2-
export { expect } from "https://deno.land/x/expect@v0.2.9/mod.ts";
2+
export { expect } from "https://deno.land/x/expect@v0.3.0/mod.ts";
33
export * from "https://deno.land/[email protected]/testing/asserts.ts";
44

55
import { createPlatformScript, js2ps, parse, ps2js, PSMap } from "../mod.ts";

types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export interface PSEnv {
77
}
88

99
export interface PSModule {
10-
url: URL;
10+
location: string;
1111
source: PSValue;
1212
value: PSValue;
1313
imports: {

0 commit comments

Comments
 (0)