Skip to content

Commit

Permalink
✨Allow importing modules as raw YAML
Browse files Browse the repository at this point in the history
Implement the quote system for imports documented in
#79
  • Loading branch information
cowboyd committed Feb 14, 2023
1 parent a199129 commit 32c58ef
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 46 deletions.
5 changes: 5 additions & 0 deletions data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
PSList,
PSMap,
PSNumber,
PSQuote,
PSRef,
PSString,
PSValue,
Expand Down Expand Up @@ -68,3 +69,7 @@ export function fn(
},
};
}

export function quote(value: PSValue): PSQuote {
return { type: "quote", value };
}
79 changes: 43 additions & 36 deletions evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
PSLiteral,
PSMap,
PSMapKey,
PSRef,
PSTemplate,
PSValue,
} from "./types.ts";
Expand Down Expand Up @@ -59,7 +60,6 @@ export function createYSEnv(parent = global): PSEnv {
let env = createYSEnv(scope);

let value = yield* bind(recognize($value), scope, []);

if (value.type === "ref") {
return value;
} else if (value.type === "template") {
Expand Down Expand Up @@ -201,6 +201,47 @@ function isPSValue(value: unknown): value is PSValue {
}
}

function view(ref: PSRef, value: PSValue): PSValue {
let { key, path } = ref;
if (value.type === "external" && value.view) {
let deref = value.view(path, value.value);
if (!deref) {
throw new ReferenceError(
`'${path.join(".")}' not found at ${key}`,
);
}
if (!isPSValue(deref)) {
throw new TypeError(
`external reference '${value.value}' did not resolve to a platformscript value`,
);
}
return deref;
} else if (value.type === "map") {
return path.reduce((current, segment) => {
if (current.type === "map") {
let next = lookup(segment, current);
if (next.type === "nothing") {
throw new ReferenceError(
`no such key '${segment}' in ${value.value}`,
);
} else {
return next.value;
}
} else {
throw new TypeError(
`cannot de-reference key ${segment} from '${current.type}'`,
);
}
}, value as PSValue);
} else if (value.type === "quote") {
return data.quote(view(ref, value.value));
} else {
throw new TypeError(
`${value.type} '$${key}' does not support path-like references`,
);
}
}

function* bind(
value: PSValue,
scope: PSMap,
Expand All @@ -218,41 +259,7 @@ function* bind(
throw new ReferenceError(`'${value.key}' not defined`);
} else {
if (path.length > 0) {
if (result.value.type === "external" && result.value.view) {
let deref = result.value.view(path, result.value.value);
if (!deref) {
throw new ReferenceError(
`'${path.join(".")}' not found at ${key}`,
);
}
if (!isPSValue(deref)) {
throw new TypeError(
`external reference '${value.value}' did not resolve to a platformscript value`,
);
}
return deref;
} else if (result.value.type === "map") {
return path.reduce((current, segment) => {
if (current.type === "map") {
let next = lookup(segment, current);
if (next.type === "nothing") {
throw new ReferenceError(
`no such key '${segment}' in ${value.value}`,
);
} else {
return next.value;
}
} else {
throw new TypeError(
`cannot de-reference key ${segment} from '${current.type}'`,
);
}
}, result.value as PSValue);
} else {
throw new TypeError(
`${result.value.type} '$${key}' does not support path-like references`,
);
}
return view(value, result.value);
} else {
return result.value;
}
Expand Down
40 changes: 31 additions & 9 deletions load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@ export interface LoadOptions {
location: string | URL;
base?: string;
env?: PSEnv;
quote?: boolean;
}

export function* load(options: LoadOptions): Operation<PSModule> {
let { location, base, env } = options;
let { location, base, env, quote } = options;
let url = typeof location === "string" ? new URL(location, base) : location;

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

return yield* moduleEval({ source, location: url, env });
return yield* moduleEval({ source, location: url, env, quote });
}

export interface ModuleEvalOptions {
location: string | URL;
source: PSValue;
env?: PSEnv;
quote?: boolean;
}

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

if (source.type !== "map") {
if (source.type !== "map" || options.quote) {
return mod;
}

Expand All @@ -61,16 +63,36 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
`imported symbols should be a string, but was ${names.type}`,
);
}
if (loc.type !== "string") {
let location: string;
let quote = false;
if (loc.type === "string") {
location = loc.value;
} else if (loc.type === "map") {
let l = lookup("location", loc);
if (l.type === "nothing") {
throw new Error(
`If import specifier is a map, it must have a 'location'`,
);
} else if (l.value.type !== "string") {
throw new Error(
`'location' attribute of import specifier must be a string, but was '${l.value.type}'`,
);
} else {
location = l.value.value;
}
let quoted = lookup("quote", loc);
quote = quoted.type === "just" && quoted.value.value === true;
} else {
throw new Error(
`import location should be a url string, but was ${loc.type}`,
`import specifier must be either a string or a map, but was '${loc.type}'`,
);
}
let bindings = matchBindings(names.value);
let dep = yield* load({
location: loc.value,
location,
base: url.toString(),
env,
quote,
});

mod.imports.push({
Expand All @@ -82,7 +104,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
let name = binding.alias ?? binding.name;
let value;
if (binding.all) {
value = dep.value;
value = quote ? data.quote(dep.value) : dep.value;
} else if (dep.value.type !== "map") {
throw new Error(
`tried to import a name from ${dep.url}, but it is not a 'map'. It is a ${dep.value.type}`,
Expand All @@ -94,7 +116,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
`module ${dep.url} does not have a member named '${binding.name}'`,
);
} else {
value = result.value;
value = quote ? data.quote(result.value) : result.value;
}
}
scope.value.set(data.string(name), value);
Expand All @@ -112,7 +134,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation<PSModule> {
for (let [key, value] of expanded.value.entries()) {
let evaluated = yield* env.eval(value, scope);
scope.value.set(key, evaluated);
mod.value.value.set(key, yield* env.eval(value, scope));
mod.value.value.set(key, evaluated);
}
}

Expand Down
2 changes: 2 additions & 0 deletions print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ function toYAML(value: PSValue): yaml.Node {
return toYAML(value.source);
case "external":
return new yaml.Scalar("<[external]>");
case "quote":
return toYAML(value.value);
default:
throw new Error(`FATAL: non exhaustive print() match.`);
}
Expand Down
48 changes: 48 additions & 0 deletions test/module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,56 @@ main: $myfive
expect(lookup$("main", mod.value)).toEqual(number(5));
});
});

it("can import a module manually via location", async () => {
let mod = await runmod(`
$import:
five:
location: nodeps.yaml
main: $five
`);

expect(lookup$("main", mod.value)).toEqual(number(5));
});

it("can import an module as quoted platformscript", async () => {
let mod = await runmod(`
$import:
nodeps<<:
location: nodeps.yaml
quote: true
self(x): $x
id: { $self: $nodeps.id }
`);
let id = lookup$("id", mod.value);
expect(id.type).toEqual("quote");
expect(lookup$("(x)=>", id.value)).toEqual(string("$x"));
});

it("can import a single value as quoted platformscript", async () => {
let mod = await runmod(`
$import:
id:
location: nodeps.yaml
quote: true
id: $id
`);
let id = lookup$("id", mod.value);
expect(id.type).toEqual("quote");
expect(lookup$("(x)=>", id.value)).toEqual(string("$x"));
});
});

function runmod(modstring: string): Task<PSModule> {
let source = parse(modstring);
return run(() =>
moduleEval({
source,
location: new URL(`modules/virtual-module.yaml`, import.meta.url),
})
);
}

function loadmod(url: string): Task<PSModule> {
return run(() =>
load({
Expand Down
8 changes: 7 additions & 1 deletion types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export type PSValue =
| PSMap
| PSFn
| PSFnCall
| PSExternal;
| PSExternal
| PSQuote;

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

export interface PSQuote {
type: "quote";
value: PSValue;
}

export interface PSFn {
type: "fn";
param: { name: string };
Expand Down

0 comments on commit 32c58ef

Please sign in to comment.