Skip to content

Commit 32c58ef

Browse files
committed
✨Allow importing modules as raw YAML
Implement the quote system for imports documented in #79
1 parent a199129 commit 32c58ef

File tree

6 files changed

+136
-46
lines changed

6 files changed

+136
-46
lines changed

data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
PSList,
77
PSMap,
88
PSNumber,
9+
PSQuote,
910
PSRef,
1011
PSString,
1112
PSValue,
@@ -68,3 +69,7 @@ export function fn(
6869
},
6970
};
7071
}
72+
73+
export function quote(value: PSValue): PSQuote {
74+
return { type: "quote", value };
75+
}

evaluate.ts

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
PSLiteral,
44
PSMap,
55
PSMapKey,
6+
PSRef,
67
PSTemplate,
78
PSValue,
89
} from "./types.ts";
@@ -59,7 +60,6 @@ export function createYSEnv(parent = global): PSEnv {
5960
let env = createYSEnv(scope);
6061

6162
let value = yield* bind(recognize($value), scope, []);
62-
6363
if (value.type === "ref") {
6464
return value;
6565
} else if (value.type === "template") {
@@ -201,6 +201,47 @@ function isPSValue(value: unknown): value is PSValue {
201201
}
202202
}
203203

204+
function view(ref: PSRef, value: PSValue): PSValue {
205+
let { key, path } = ref;
206+
if (value.type === "external" && value.view) {
207+
let deref = value.view(path, value.value);
208+
if (!deref) {
209+
throw new ReferenceError(
210+
`'${path.join(".")}' not found at ${key}`,
211+
);
212+
}
213+
if (!isPSValue(deref)) {
214+
throw new TypeError(
215+
`external reference '${value.value}' did not resolve to a platformscript value`,
216+
);
217+
}
218+
return deref;
219+
} else if (value.type === "map") {
220+
return path.reduce((current, segment) => {
221+
if (current.type === "map") {
222+
let next = lookup(segment, current);
223+
if (next.type === "nothing") {
224+
throw new ReferenceError(
225+
`no such key '${segment}' in ${value.value}`,
226+
);
227+
} else {
228+
return next.value;
229+
}
230+
} else {
231+
throw new TypeError(
232+
`cannot de-reference key ${segment} from '${current.type}'`,
233+
);
234+
}
235+
}, value as PSValue);
236+
} else if (value.type === "quote") {
237+
return data.quote(view(ref, value.value));
238+
} else {
239+
throw new TypeError(
240+
`${value.type} '$${key}' does not support path-like references`,
241+
);
242+
}
243+
}
244+
204245
function* bind(
205246
value: PSValue,
206247
scope: PSMap,
@@ -218,41 +259,7 @@ function* bind(
218259
throw new ReferenceError(`'${value.key}' not defined`);
219260
} else {
220261
if (path.length > 0) {
221-
if (result.value.type === "external" && result.value.view) {
222-
let deref = result.value.view(path, result.value.value);
223-
if (!deref) {
224-
throw new ReferenceError(
225-
`'${path.join(".")}' not found at ${key}`,
226-
);
227-
}
228-
if (!isPSValue(deref)) {
229-
throw new TypeError(
230-
`external reference '${value.value}' did not resolve to a platformscript value`,
231-
);
232-
}
233-
return deref;
234-
} else if (result.value.type === "map") {
235-
return path.reduce((current, segment) => {
236-
if (current.type === "map") {
237-
let next = lookup(segment, current);
238-
if (next.type === "nothing") {
239-
throw new ReferenceError(
240-
`no such key '${segment}' in ${value.value}`,
241-
);
242-
} else {
243-
return next.value;
244-
}
245-
} else {
246-
throw new TypeError(
247-
`cannot de-reference key ${segment} from '${current.type}'`,
248-
);
249-
}
250-
}, result.value as PSValue);
251-
} else {
252-
throw new TypeError(
253-
`${result.value.type} '$${key}' does not support path-like references`,
254-
);
255-
}
262+
return view(value, result.value);
256263
} else {
257264
return result.value;
258265
}

load.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,24 @@ export interface LoadOptions {
1111
location: string | URL;
1212
base?: string;
1313
env?: PSEnv;
14+
quote?: boolean;
1415
}
1516

1617
export function* load(options: LoadOptions): Operation<PSModule> {
17-
let { location, base, env } = options;
18+
let { location, base, env, quote } = 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, quote });
2425
}
2526

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

3234
export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
@@ -40,7 +42,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
4042
imports: [],
4143
};
4244

43-
if (source.type !== "map") {
45+
if (source.type !== "map" || options.quote) {
4446
return mod;
4547
}
4648

@@ -61,16 +63,36 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
6163
`imported symbols should be a string, but was ${names.type}`,
6264
);
6365
}
64-
if (loc.type !== "string") {
66+
let location: string;
67+
let quote = false;
68+
if (loc.type === "string") {
69+
location = loc.value;
70+
} else if (loc.type === "map") {
71+
let l = lookup("location", loc);
72+
if (l.type === "nothing") {
73+
throw new Error(
74+
`If import specifier is a map, it must have a 'location'`,
75+
);
76+
} else if (l.value.type !== "string") {
77+
throw new Error(
78+
`'location' attribute of import specifier must be a string, but was '${l.value.type}'`,
79+
);
80+
} else {
81+
location = l.value.value;
82+
}
83+
let quoted = lookup("quote", loc);
84+
quote = quoted.type === "just" && quoted.value.value === true;
85+
} else {
6586
throw new Error(
66-
`import location should be a url string, but was ${loc.type}`,
87+
`import specifier must be either a string or a map, but was '${loc.type}'`,
6788
);
6889
}
6990
let bindings = matchBindings(names.value);
7091
let dep = yield* load({
71-
location: loc.value,
92+
location,
7293
base: url.toString(),
7394
env,
95+
quote,
7496
});
7597

7698
mod.imports.push({
@@ -82,7 +104,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
82104
let name = binding.alias ?? binding.name;
83105
let value;
84106
if (binding.all) {
85-
value = dep.value;
107+
value = quote ? data.quote(dep.value) : dep.value;
86108
} else if (dep.value.type !== "map") {
87109
throw new Error(
88110
`tried to import a name from ${dep.url}, but it is not a 'map'. It is a ${dep.value.type}`,
@@ -94,7 +116,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
94116
`module ${dep.url} does not have a member named '${binding.name}'`,
95117
);
96118
} else {
97-
value = result.value;
119+
value = quote ? data.quote(result.value) : result.value;
98120
}
99121
}
100122
scope.value.set(data.string(name), value);
@@ -112,7 +134,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
112134
for (let [key, value] of expanded.value.entries()) {
113135
let evaluated = yield* env.eval(value, scope);
114136
scope.value.set(key, evaluated);
115-
mod.value.value.set(key, yield* env.eval(value, scope));
137+
mod.value.value.set(key, evaluated);
116138
}
117139
}
118140

print.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ function toYAML(value: PSValue): yaml.Node {
5353
return toYAML(value.source);
5454
case "external":
5555
return new yaml.Scalar("<[external]>");
56+
case "quote":
57+
return toYAML(value.value);
5658
default:
5759
throw new Error(`FATAL: non exhaustive print() match.`);
5860
}

test/module.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,56 @@ main: $myfive
101101
expect(lookup$("main", mod.value)).toEqual(number(5));
102102
});
103103
});
104+
105+
it("can import a module manually via location", async () => {
106+
let mod = await runmod(`
107+
$import:
108+
five:
109+
location: nodeps.yaml
110+
main: $five
111+
`);
112+
113+
expect(lookup$("main", mod.value)).toEqual(number(5));
114+
});
115+
116+
it("can import an module as quoted platformscript", async () => {
117+
let mod = await runmod(`
118+
$import:
119+
nodeps<<:
120+
location: nodeps.yaml
121+
quote: true
122+
self(x): $x
123+
id: { $self: $nodeps.id }
124+
`);
125+
let id = lookup$("id", mod.value);
126+
expect(id.type).toEqual("quote");
127+
expect(lookup$("(x)=>", id.value)).toEqual(string("$x"));
128+
});
129+
130+
it("can import a single value as quoted platformscript", async () => {
131+
let mod = await runmod(`
132+
$import:
133+
id:
134+
location: nodeps.yaml
135+
quote: true
136+
id: $id
137+
`);
138+
let id = lookup$("id", mod.value);
139+
expect(id.type).toEqual("quote");
140+
expect(lookup$("(x)=>", id.value)).toEqual(string("$x"));
141+
});
104142
});
105143

144+
function runmod(modstring: string): Task<PSModule> {
145+
let source = parse(modstring);
146+
return run(() =>
147+
moduleEval({
148+
source,
149+
location: new URL(`modules/virtual-module.yaml`, import.meta.url),
150+
})
151+
);
152+
}
153+
106154
function loadmod(url: string): Task<PSModule> {
107155
return run(() =>
108156
load({

types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export type PSValue =
3030
| PSMap
3131
| PSFn
3232
| PSFnCall
33-
| PSExternal;
33+
| PSExternal
34+
| PSQuote;
3435

3536
export type PSMapKey =
3637
| PSNumber
@@ -89,6 +90,11 @@ export interface PSExternal {
8990
view?(path: string[], value: any): PSValue | void;
9091
}
9192

93+
export interface PSQuote {
94+
type: "quote";
95+
value: PSValue;
96+
}
97+
9298
export interface PSFn {
9399
type: "fn";
94100
param: { name: string };

0 commit comments

Comments
 (0)