diff --git a/data.ts b/data.ts index 930e8ba..6765c03 100644 --- a/data.ts +++ b/data.ts @@ -6,6 +6,7 @@ import type { PSList, PSMap, PSNumber, + PSQuote, PSRef, PSString, PSValue, @@ -68,3 +69,7 @@ export function fn( }, }; } + +export function quote(value: PSValue): PSQuote { + return { type: "quote", value }; +} diff --git a/evaluate.ts b/evaluate.ts index 16eeac2..9fdc199 100644 --- a/evaluate.ts +++ b/evaluate.ts @@ -3,6 +3,7 @@ import type { PSLiteral, PSMap, PSMapKey, + PSRef, PSTemplate, PSValue, } from "./types.ts"; @@ -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") { @@ -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, @@ -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; } diff --git a/load.ts b/load.ts index caafee6..96ac4e7 100644 --- a/load.ts +++ b/load.ts @@ -11,22 +11,24 @@ export interface LoadOptions { location: string | URL; base?: string; env?: PSEnv; + quote?: boolean; } export function* load(options: LoadOptions): Operation { - 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 { @@ -40,7 +42,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation { imports: [], }; - if (source.type !== "map") { + if (source.type !== "map" || options.quote) { return mod; } @@ -61,16 +63,36 @@ export function* moduleEval(options: ModuleEvalOptions): Operation { `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({ @@ -82,7 +104,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation { 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}`, @@ -94,7 +116,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation { `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); @@ -112,7 +134,7 @@ export function* moduleEval(options: ModuleEvalOptions): Operation { 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); } } diff --git a/print.ts b/print.ts index ed5e057..d59a03f 100644 --- a/print.ts +++ b/print.ts @@ -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.`); } diff --git a/test/module.test.ts b/test/module.test.ts index 2d9ca08..c48299b 100644 --- a/test/module.test.ts +++ b/test/module.test.ts @@ -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 { + let source = parse(modstring); + return run(() => + moduleEval({ + source, + location: new URL(`modules/virtual-module.yaml`, import.meta.url), + }) + ); +} + function loadmod(url: string): Task { return run(() => load({ diff --git a/types.ts b/types.ts index 881c8ca..bdab301 100644 --- a/types.ts +++ b/types.ts @@ -30,7 +30,8 @@ export type PSValue = | PSMap | PSFn | PSFnCall - | PSExternal; + | PSExternal + | PSQuote; export type PSMapKey = | PSNumber @@ -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 };