Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨Allow importing modules as raw YAML #80

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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