diff --git a/README.md b/README.md index e1f1cad7..19bac87d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,14 @@ You can try programming interactively with the Playground. You can run with: $ crochet playground ``` -You do need to specify a package currently because that's how Crochet tracks +For node projects you need to specify `node` as your Playground execution +target, since the default is running the package in the browser: + +```shell +$ crochet playground --target node +``` + +You do need to specify a package because that's how Crochet tracks dependencies and capabilities. All code you type in the Playground will be executed in the context of the given package. And all dependencies of that package will be loaded first. diff --git a/package-lock.json b/package-lock.json index d4fb9c19..e31c8dd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.13.1-rc1", "dependencies": { "@types/codemirror": "^5.60.5", + "@types/ws": "^8.2.3", "express": "^4.17.1", "immutable": "^4.0.0-rc.14", "ohm-js": "^15.3.0", @@ -129,8 +130,7 @@ "node_modules/@types/node": { "version": "14.17.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.9.tgz", - "integrity": "sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==", - "dev": true + "integrity": "sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==" }, "node_modules/@types/qs": { "version": "6.9.7", @@ -168,6 +168,14 @@ "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-ahRJZquUYCdOZf/rCsWg88S0/+cb9wazUBHv6HZEe3XdYaBe2zr/slM8J28X07Hn88Pnm4ezo7N8/ofnOgrPVQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -3987,8 +3995,7 @@ "@types/node": { "version": "14.17.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.9.tgz", - "integrity": "sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==", - "dev": true + "integrity": "sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==" }, "@types/qs": { "version": "6.9.7", @@ -4026,6 +4033,14 @@ "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", "dev": true }, + "@types/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-ahRJZquUYCdOZf/rCsWg88S0/+cb9wazUBHv6HZEe3XdYaBe2zr/slM8J28X07Hn88Pnm4ezo7N8/ofnOgrPVQ==", + "requires": { + "@types/node": "*" + } + }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", diff --git a/package.json b/package.json index 83186b6a..2c9604e1 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@types/codemirror": "^5.60.5", + "@types/ws": "^8.2.3", "express": "^4.17.1", "immutable": "^4.0.0-rc.14", "ohm-js": "^15.3.0", diff --git a/source/node-cli/index.ts b/source/node-cli/index.ts index ea95f1da..260a0433 100644 --- a/source/node-cli/index.ts +++ b/source/node-cli/index.ts @@ -13,10 +13,12 @@ import { ResolvedPackage, Target, target_web, + parse as parse_pkg, + target_spec, } from "../pkg"; import * as ChildProcess from "child_process"; import { serve_docs } from "./docs"; -import { Ok, try_parse } from "../utils/spec"; +import { Ok, parse, try_parse } from "../utils/spec"; import { StorageConfig } from "../storage"; import { random_uuid } from "../utils/uuid"; @@ -49,13 +51,13 @@ interface Options { disclose_debug: boolean; capabilities: Set; interactive: boolean; + target?: Target; web: { port: number; www_root: string; }; docs: { port: number; - target: Target; }; packaging: { out_dir: string; @@ -78,6 +80,7 @@ function parse_options(args0: string[]) { options.interactive = true; options.capabilities = new Set([]); options.verbose = false; + options.target = undefined; options.test = { show_success: false, }; @@ -90,7 +93,6 @@ function parse_options(args0: string[]) { }; options.docs = { port: 8080, - target: Crochet.pkg.target_any(), }; options.app_args = []; @@ -151,10 +153,11 @@ function parse_options(args0: string[]) { case "--target": { const target = try_parse(args0[current + 1], Crochet.pkg.target_spec); if (target instanceof Ok) { - options.docs.target = target.value; + options.target = target.value; } else { throw new Error(`Invalid target ${args0[current + 1]}`); } + current += 2; continue; } @@ -325,7 +328,7 @@ async function setup_web_capabilities(file: string, options: Options) { false ); const pkg = crochet.read_package_from_file(file); - const rpkg = new ResolvedPackage(pkg, target_web()); + const rpkg = new ResolvedPackage(pkg, options.target ?? target_web()); const required = [...pkg.meta.capabilities.requires.values()]; const cap_map = new Map(required.map((x) => [x, [rpkg]])); const config = StorageConfig.load(); @@ -346,16 +349,25 @@ async function setup_web_capabilities(file: string, options: Options) { ); } } + + return new Set(required); } async function run_web([file]: string[], options: Options) { - setup_web_capabilities(file, options); + const cap = await setup_web_capabilities(file, options); await build([file], options); - await Server(file, options.web.port, options.web.www_root, "/"); + await Server( + file, + options.web.port, + options.web.www_root, + "/", + target_web(), + cap + ); } async function playground([file]: string[], options: Options) { - setup_web_capabilities(file, options); + const cap = await setup_web_capabilities(file, options); await build([file], options); await build( [Path.join(__dirname, "../../stdlib/crochet.debug.ui/crochet.json")], @@ -365,7 +377,9 @@ async function playground([file]: string[], options: Options) { file, options.web.port, Path.join(__dirname, "../../www"), - "/playground" + "/playground", + options.target ?? target_web(), + cap ); } @@ -507,7 +521,6 @@ function help(command?: string) { " crochet playground [--port PORT]\n", " crochet docs [--port PORT --target ('node' | 'browser' | '*')]\n", " crochet package [--package-to OUT_DIR]\n", - " crochet repl \n", " crochet test [--test-title PATTERN --test-module PATTERN --test-package PATTERN --test-show-ok]\n", " crochet build \n", " crochet show-ir \n", diff --git a/source/node-cli/server.ts b/source/node-cli/server.ts index cec0237e..79d5a39e 100644 --- a/source/node-cli/server.ts +++ b/source/node-cli/server.ts @@ -1,18 +1,36 @@ import * as Path from "path"; import * as FS from "fs"; import * as Package from "../pkg"; +import * as REPL from "../node-repl"; import type * as Express from "express"; import { CrochetForNode, build_file } from "../targets/node"; import { random_uuid } from "../utils/uuid"; +import { randomUUID } from "crypto"; const repo_root = Path.resolve(__dirname, "../../"); +function get_kind(x: Package.Target) { + switch (x.tag) { + case Package.TargetTag.NODE: + return "node"; + case Package.TargetTag.ANY: + return "browser"; + case Package.TargetTag.WEB: + return "browser"; + default: + throw new Error(`Unknown target`); + } +} + export default async ( root: string, port: number, www: string, - start_page: string + start_page: string, + target: Package.Target, + capabilities: Set ) => { + // -- Templating const template = (filename: string) => (config: unknown) => { const config_str = JSON.stringify(config).replace(/ { const config = { + session_id: session_id, token: random_uuid(), library_root: "/library", app_root: "/app/crochet.json", @@ -88,6 +111,8 @@ export default async ( app.get("/playground", (req, res) => { const config = { + session_id: session_id, + kind: get_kind(target), token: random_uuid(), library_root: "/library", app_root: "/app/crochet.json", @@ -102,6 +127,36 @@ export default async ( app.use("/", express.static(www)); app.use("/library", express.static(Path.join(repo_root, "stdlib"))); + app.use("/playground/api", express.json()); + + app.post("/playground/api/:id/make-page", async (req, res) => { + if (req.params.id !== session_id) { + return res.send(403); + } + + try { + const page = await repl!.make_page(); + res.send({ ok: true, page_id: page.id }); + } catch (e) { + res.send({ ok: false, reason: String(e) }); + } + }); + + app.post("/playground/api/:id/pages/:page_id/run-code", async (req, res) => { + if (req.params.id !== session_id) { + return res.send(403); + } + + try { + const page = repl!.get_page(req.params.page_id); + const result = await page.run_code(req.body.language, req.body.code); + res.send({ ok: true, representations: result?.representations }); + } catch (e) { + res.send({ ok: false, error: String(e) }); + } + }); + + // -- File system capabilities const pkg_tokens = new Map(); for (const x of graph.serialise(rpkg)) { const assets = x.assets_root; @@ -113,6 +168,18 @@ export default async ( } } + if (target.tag === Package.TargetTag.NODE) { + repl = await REPL.NodeRepl.bootstrap( + root, + target, + capabilities, + randomUUID(), + session_id, + pkg_tokens + ); + } + + // -- Starting servers app.listen(port, () => { const url = new URL("http://localhost"); url.port = String(port); diff --git a/source/node-repl/compiler.ts b/source/node-repl/compiler.ts index ad51d025..a3c482eb 100644 --- a/source/node-repl/compiler.ts +++ b/source/node-repl/compiler.ts @@ -2,14 +2,24 @@ import * as Compiler from "../compiler"; import * as IR from "../ir"; import * as Ast from "../generated/crochet-grammar"; import * as VM from "../vm"; -import type { CrochetForNode } from "../targets/node"; +import type { BootedCrochet } from "../crochet/index"; + +type Representation = { + name: string; + document: string; // JSON +}; + +type ReplResponse = null | { + value: VM.CrochetValue; + representations: Representation[]; +}; export abstract class ReplExpr { abstract evaluate( - vm: CrochetForNode, + vm: BootedCrochet, module: VM.CrochetModule, env: VM.Environment - ): Promise; + ): Promise; } export class ReplDeclaration extends ReplExpr { @@ -22,13 +32,15 @@ export class ReplDeclaration extends ReplExpr { } async evaluate( - vm: CrochetForNode, + vm: BootedCrochet, module: VM.CrochetModule, env: VM.Environment - ): Promise { + ): Promise { for (const x of this.declarations) { - await vm.system.load_declaration(x, module); + await vm.load_declaration(x, module); } + + return null; } } @@ -42,19 +54,29 @@ export class ReplStatements extends ReplExpr { } async evaluate( - vm: CrochetForNode, + vm: BootedCrochet, module: VM.CrochetModule, env: VM.Environment - ): Promise { + ): Promise { const new_env = VM.Environments.clone(env); - const value = await vm.system.run_block(this.block, new_env); + const value = await vm.run_block(this.block, new_env); // Copy only non-generated bindings back to the top-level environment for (const [k, v] of new_env.bindings.entries()) { if (!/\$/.test(k)) { env.define(k, v); } } - console.log(VM.Location.simple_value(value)); + + const perspectives = await vm.debug_perspectives(value); + const representations = await vm.debug_representations(value, perspectives); + + return { + value, + representations: representations.map((x) => ({ + name: x.name, + document: JSON.stringify(x.document), + })), + }; } } diff --git a/source/node-repl/repl.ts b/source/node-repl/repl.ts index 10b651a7..08cd8c1c 100644 --- a/source/node-repl/repl.ts +++ b/source/node-repl/repl.ts @@ -3,8 +3,10 @@ import * as Path from "path"; import * as Read from "readline"; import * as Compiler from "./compiler"; import * as VM from "../vm"; +import * as Pkg from "../pkg"; import { CrochetForNode } from "../targets/node"; import { logger } from "../utils/logger"; +import { randomUUID } from "crypto"; async function readline(rl: Read.Interface, prompt: string) { return new Promise((resolve, reject) => { @@ -46,8 +48,11 @@ export async function repl(vm: CrochetForNode, pkg_name: string) { while (true) { try { const ast = await get_line(rl); - await ast.evaluate(vm, module, env); - } catch (error) { + const result = await ast.evaluate(vm.system, module, env); + if (result != null) { + console.log(VM.Location.simple_value(result.value)); + } + } catch (error: any) { if (error instanceof SyntaxError) { console.error(error.name + ":", error.message); } else if (logger.verbose) { @@ -71,3 +76,76 @@ export function resolve_file(x: string) { return x; } } + +export class NodeRepl { + readonly pages: Map = new Map(); + constructor(readonly vm: CrochetForNode, readonly session: string) {} + + get_page(id: string) { + const page = this.pages.get(id); + if (page == null) { + throw new Error(`Unknown page id ${id}`); + } + return page; + } + + static async bootstrap( + file: string, + target: Pkg.Target, + capabilities: Set, + universe: string, + session: string, + package_tokens: Map + ) { + const disclose_debug = false; + const library_paths: string[] = []; + const interactive = false; + const safe_mode = false; + const crochet = new CrochetForNode( + { universe: universe, packages: package_tokens }, + disclose_debug, + library_paths, + capabilities, + interactive, + safe_mode + ); + await crochet.boot_from_file(file, target); + return new NodeRepl(crochet, session); + } + + async make_page() { + const id = randomUUID(); + const root_pkg = this.vm.root; + const root_cpkg = this.vm.system.universe.world.packages.get( + root_pkg.meta.name + ); + if (root_cpkg == null) { + throw new Error(`internal: root package not found`); + } + + const module = new VM.CrochetModule(root_cpkg, "(playground)", null); + const env = new VM.Environment(null, null, module, null); + const page = new ReplPage(id, this.vm, root_cpkg, module, env); + this.pages.set(id, page); + return page; + } +} + +export class ReplPage { + constructor( + readonly id: string, + readonly vm: CrochetForNode, + readonly pkg: VM.CrochetPackage, + readonly module: VM.CrochetModule, + readonly env: VM.Environment + ) {} + + async run_code(language: string, code: string) { + if (language !== "crochet") { + throw new Error(`Language ${language} is currently unsupported`); + } + + const expr = await Compiler.compile(code); + return await expr.evaluate(this.vm.system, this.module, this.env); + } +} diff --git a/source/targets/browser/index.ts b/source/targets/browser/index.ts index 7dc89de5..95409140 100644 --- a/source/targets/browser/index.ts +++ b/source/targets/browser/index.ts @@ -4,7 +4,8 @@ import * as Binary from "../../binary"; import * as VM from "../../vm"; import * as Package from "../../pkg"; import * as AST from "../../generated/crochet-grammar"; +import * as REPL from "../../node-repl/compiler"; export * from "./browser"; -export { Package, IR, Compiler, Binary, VM, AST }; +export { Package, IR, Compiler, Binary, VM, AST, REPL }; diff --git a/stdlib/crochet.debug.ui/native/kernel.ts b/stdlib/crochet.debug.ui/native/kernel.ts index e0b8c7a4..97cb05b7 100644 --- a/stdlib/crochet.debug.ui/native/kernel.ts +++ b/stdlib/crochet.debug.ui/native/kernel.ts @@ -8,6 +8,7 @@ import type { VM, Compiler, AST, + REPL, } from "../../../build/targets/browser"; declare var Crochet: { @@ -18,87 +19,111 @@ declare var Crochet: { VM: typeof VM; Compiler: typeof Compiler; AST: typeof AST; + REPL: typeof REPL; }; declare var crypto: { randomUUID(): string }; export default (ffi: ForeignInterface) => { - type Meta = Map; + type RunResult = + | { ok: true; value: CrochetValue } + | { ok: false; error: unknown }; - abstract class ReplExpr { - abstract evaluate( - page: KernelPage - ): Promise< - { ok: true; value: CrochetValue } | { ok: false; error: unknown } - >; + async function run(page: KernelPage, code: string) { + try { + const expr = Crochet.REPL.compile(code); + const result = await expr.evaluate(page.vm.system, page.module, page.env); + if (result == null) { + return { ok: true, value: ffi.nothing }; + } else { + return { + ok: true, + value: ffi.record( + new Map([ + ["raw-value", ffi.box(result.value)], + [ + "representations", + ffi.list( + result.representations.map((x) => + ffi.record( + new Map([ + ["name", ffi.text(x.name)], + ["document", ffi.text(x.document)], + ]) + ) + ) + ), + ], + ]) + ), + }; + } + } catch (e) { + return { ok: false, error: e }; + } + } + + abstract class BasePage { + abstract run_code(language: string, code: string): Promise; } - class ReplDeclaration extends ReplExpr { + class KernelPage extends BasePage { constructor( - readonly declarations: IR.Declaration[], - readonly source: string, - readonly meta: Meta + readonly id: string, + readonly vm: CrochetForBrowser, + readonly pkg: VM.CrochetPackage, + readonly module: VM.CrochetModule, + readonly env: VM.Environment ) { super(); } - async evaluate( - page: KernelPage - ): Promise< - { ok: true; value: CrochetValue } | { ok: false; error: unknown } - > { - try { - for (const x of this.declarations) { - await page.vm.system.load_declaration(x, page.module); - } - return { ok: true, value: ffi.nothing }; - } catch (e) { - return { ok: false, error: e }; + async run_code(language: string, code: string) { + if (language !== "crochet") { + throw new Error(`Language ${language} is currently unsupported`); } + + return (await run(this, code)) as RunResult; } } - class ReplStatements extends ReplExpr { - constructor( - readonly block: IR.BasicBlock, - readonly source: string, - readonly meta: Meta - ) { + class FarKernelPage extends BasePage { + constructor(readonly session_id: string, readonly page_id: string) { super(); } - async evaluate( - page: KernelPage - ): Promise< - { ok: true; value: CrochetValue } | { ok: false; error: unknown } - > { - const new_env = Crochet.VM.Environments.clone(page.env); - try { - const value = await page.vm.system.run_block(this.block, new_env); - for (const [k, v] of new_env.bindings.entries()) { - if (!/\$/.test(k)) { - page.env.define(k, v); + async run_code(language: string, code: string) { + const result = await ( + await fetch( + `/playground/api/${encodeURIComponent( + this.session_id + )}/pages/${encodeURIComponent(this.page_id)}/run-code`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + language, + code, + }), } - } - - const perspectives = await page.vm.system.debug_perspectives(value); - const representations = await page.vm.system.debug_representations( - value, - perspectives - ); + ) + ).json(); + if (!result.ok) { + return result; + } else if (Array.isArray(result.representations)) { return { ok: true, value: ffi.record( new Map([ - ["raw-value", ffi.box(value)], + ["raw-value", ffi.nothing], [ "representations", ffi.list( - representations.map((x) => + result.representations.map((x: any) => ffi.record( new Map([ ["name", ffi.text(x.name)], - ["document", ffi.text(JSON.stringify(x.document))], + ["document", ffi.text(x.document)], ]) ) ) @@ -107,57 +132,77 @@ export default (ffi: ForeignInterface) => { ]) ), }; - } catch (e) { - return { ok: false, error: e }; + } else { + return { ok: true, value: ffi.nothing }; } } } - function lower(x: AST.REPL, source: string) { - return x.match({ - Declarations: (xs) => { - const { declarations, meta } = Crochet.Compiler.lower_declarations( - source, - xs - ); - return new ReplDeclaration(declarations, source, meta); - }, - - Statements: (xs) => { - const { block, meta } = Crochet.Compiler.lower_statements(source, xs); - return new ReplStatements(block, source, meta); - }, - - Command: (command) => { - throw new Error(`internal: Unsupported`); - }, - }); + abstract class BaseVM { + abstract make_page(kernel: BaseKernel): Promise; } - function compile(source: string) { - const ast = Crochet.Compiler.parse_repl(source, "(playground)"); - return lower(ast, source); - } + class KernelVM extends BaseVM { + constructor(readonly id: string, readonly vm: CrochetForBrowser) { + super(); + } - class KernelPage { - constructor( - readonly id: string, - readonly vm: CrochetForBrowser, - readonly pkg: VM.CrochetPackage, - readonly module: VM.CrochetModule, - readonly env: VM.Environment - ) {} + async make_page(kernel: BaseKernel) { + const root_pkg = this.vm.root; + const root_cpkg = this.vm.system.universe.world.packages.get( + root_pkg.meta.name + ); + if (root_cpkg == null) { + throw new Error(`internal: root package not found`); + } + const module = new Crochet.VM.CrochetModule( + root_cpkg, + "(playground)", + null + ); + const env = new Crochet.VM.Environment(null, null, module, null); + + const id = crypto.randomUUID(); + const page = new KernelPage(id, this.vm, root_cpkg, module, env); + kernel.add_page(this.id, page); + + return page; + } } - class KernelVM { - constructor(readonly id: string, readonly vm: CrochetForBrowser) {} + class FarKernelVM extends BaseVM { + constructor(readonly session_id: string, readonly id: string) { + super(); + } + + async make_page(kernel: BaseKernel) { + const result = await ( + await fetch( + `/playground/api/${encodeURIComponent(this.session_id)}/make-page`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + } + ) + ).json(); + + if (!result.ok) { + throw new Error(result.reason); + } + + const page = new FarKernelPage(this.session_id, result.page_id); + kernel.add_page(page.page_id, page); + return page; + } } - class Kernel { - private vms: Map = new Map(); - private pages: Map = new Map(); + abstract class BaseKernel { + private vms: Map = new Map(); + private pages: Map = new Map(); constructor( + readonly session_id: string, readonly library_root: string, readonly capabilities: string[], readonly package_tokens: Map, @@ -172,14 +217,14 @@ export default (ffi: ForeignInterface) => { return vm; } - add_vm(id: string, vm: KernelVM) { + add_vm(id: string, vm: BaseVM) { if (this.vms.has(id)) { throw new Error(`Duplicated VM id: ${id}`); } this.vms.set(id, vm); } - add_page(id: string, page: KernelPage) { + add_page(id: string, page: BasePage) { if (this.pages.has(id)) { throw new Error(`Duplicated kernel page: ${id}`); } @@ -193,11 +238,70 @@ export default (ffi: ForeignInterface) => { } return page; } + + abstract make_vm(): Promise< + { ok: true; value: BaseVM } | { ok: false; reason: string } + >; + } + + class BrowserKernel extends BaseKernel { + async make_vm(): Promise< + { ok: true; value: BaseVM } | { ok: false; reason: string } + > { + const universe = crypto.randomUUID(); + const session = crypto.randomUUID(); + + const crochet = new Crochet.CrochetForBrowser( + { + universe: universe, + packages: this.package_tokens, + }, + this.library_root, + new Set(this.capabilities), + false + ); + + const result = await crochet + .boot_from_file(this.app_root, Crochet.Package.target_web()) + .then( + (_) => true, + (e) => String(e) + ); + + if (result !== true) { + return { ok: false, reason: result as string }; + } + + const vm = new KernelVM(session, crochet); + this.add_vm(session, vm); + + return { ok: true, value: vm }; + } + } + + class FarKernel extends BaseKernel { + async make_vm(): Promise< + { ok: true; value: BaseVM } | { ok: false; reason: string } + > { + const id = crypto.randomUUID(); + const vm = new FarKernelVM(this.session_id, id); + this.add_vm(id, vm); + return { ok: true, value: vm }; + } } ffi.defun( "kernel.make-kernel", - (library_root0, capabilities0, package_tokens0, app_root0) => { + ( + kind0, + session_id0, + library_root0, + capabilities0, + package_tokens0, + app_root0 + ) => { + const kind = ffi.text_to_string(kind0); + const session_id = ffi.text_to_string(session_id0); const library_root = ffi.text_to_string(library_root0); const capabilities = ffi .list_to_array(capabilities0) @@ -209,119 +313,90 @@ export default (ffi: ForeignInterface) => { }) ); const app_root = ffi.text_to_string(app_root0); - return ffi.box( - new Kernel(library_root, capabilities, package_tokens, app_root) - ); + + switch (kind) { + case "node": + return ffi.box( + new FarKernel( + session_id, + library_root, + capabilities, + package_tokens, + app_root + ) + ); + + case "browser": + return ffi.box( + new BrowserKernel( + session_id, + library_root, + capabilities, + package_tokens, + app_root + ) + ); + + default: + throw new Error(`internal: Unknown kind ${kind}`); + } } ); ffi.defmachine("kernel.make-vm", function* (kernel0) { - const kernel = ffi.unbox_typed(Kernel, kernel0); - - const universe = crypto.randomUUID(); - const session = crypto.randomUUID(); - - const crochet = new Crochet.CrochetForBrowser( - { - universe: universe, - packages: kernel.package_tokens, - }, - kernel.library_root, - new Set(kernel.capabilities), - false - ); - + const kernel = ffi.unbox_typed(BaseKernel, kernel0); const result = ffi.unbox( - yield ffi.await( - crochet - .boot_from_file(kernel.app_root, Crochet.Package.target_web()) - .then( - (_) => ffi.box(true), - (e) => ffi.box(String(e)) - ) - ) - ); + yield ffi.await(kernel.make_vm().then((x) => ffi.box(x))) + ) as any; - if (result !== true) { + if (result.ok) { + return ffi.record( + new Map([ + ["ok", ffi.boolean(true)], + ["value", ffi.box(result.value)], + ]) + ); + } else { return ffi.record( new Map([ ["ok", ffi.boolean(false)], - ["reason", ffi.text(result as any)], + ["reason", ffi.text(String(result.reason))], ]) ); } - - const vm = new KernelVM(session, crochet); - kernel.add_vm(session, vm); - - return ffi.record( - new Map([ - ["ok", ffi.boolean(true)], - ["value", ffi.box(vm)], - ]) - ); }); - ffi.defun("kernel.make-page", (kernel0, vm0) => { - const kernel = ffi.unbox_typed(Kernel, kernel0); - const vm = ffi.unbox_typed(KernelVM, vm0).vm; - - const root_pkg = vm.root; - const root_cpkg = vm.system.universe.world.packages.get(root_pkg.meta.name); - if (root_cpkg == null) { - throw new Error(`internal: root package not found`); - } - const module = new Crochet.VM.CrochetModule( - root_cpkg, - "(playground)", - null - ); - const env = new Crochet.VM.Environment(null, null, module, null); - - const id = crypto.randomUUID(); - const page = new KernelPage(id, vm, root_cpkg, module, env); - - kernel.add_page(id, page); + ffi.defmachine("kernel.make-page", function* (kernel0, vm0) { + const kernel = ffi.unbox_typed(BaseKernel, kernel0); + const vm = ffi.unbox_typed(BaseVM, vm0); + const page = yield ffi.await(vm.make_page(kernel).then((x) => ffi.box(x))); return ffi.box(page); }); ffi.defmachine("kernel.run-code", function* (page0, language0, code0) { - const page = ffi.unbox_typed(KernelPage, page0); + const page = ffi.unbox_typed(BasePage, page0); const language = ffi.text_to_string(language0); - if (language !== "crochet") { - throw new Error(`Language ${language} is currently unsupported`); - } - - const code = ffi.text_to_string(code0); - let expr: ReplExpr; - try { - expr = compile(code); - } catch (error) { - return ffi.record( - new Map([ - ["ok", ffi.boolean(false)], - ["reason", ffi.text(String(error))], - ]) - ); - } - const value: - | { ok: true; value: CrochetValue } - | { ok: false; error: unknown } = ffi.unbox( - yield ffi.await(expr.evaluate(page).then((x) => ffi.box(x))) + const result: RunResult = ffi.unbox( + yield ffi.await( + page + .run_code(language, ffi.text_to_string(code0)) + .then((x) => ffi.box(x)) + ) ) as any; - if (value.ok) { + + if (result.ok) { return ffi.record( new Map([ ["ok", ffi.boolean(true)], - ["value", value.value], + ["value", result.value], ]) ); } else { return ffi.record( new Map([ ["ok", ffi.boolean(false)], - ["reason", ffi.text(String(value.error))], + ["reason", ffi.text(String(result.error))], ]) ); } diff --git a/stdlib/crochet.debug.ui/source/main.crochet b/stdlib/crochet.debug.ui/source/main.crochet index f5b70dc7..0dafc53e 100644 --- a/stdlib/crochet.debug.ui/source/main.crochet +++ b/stdlib/crochet.debug.ui/source/main.crochet @@ -20,6 +20,12 @@ do agata show: new welcome-screen; let Kernel = #playground-kernel bootstrap: new kernel-config( + session-id -> + package seal: Config.session-id, + + kind -> + #kernel-kind from-enum-text: Config.kind, + library-root -> Config.library-root, diff --git a/stdlib/crochet.debug.ui/source/playground/effects.crochet b/stdlib/crochet.debug.ui/source/playground/effects.crochet index b9f7a9c7..6ca84995 100644 --- a/stdlib/crochet.debug.ui/source/playground/effects.crochet +++ b/stdlib/crochet.debug.ui/source/playground/effects.crochet @@ -25,6 +25,8 @@ end handler browser-kernel with on playground.bootstrap(Config) do let Kernel = foreign kernel.make-kernel( + Config.kind to-enum-text, + package unseal: Config.session-id, Config.library-root, Config.capabilities values, Config.package-tokens @@ -68,6 +70,12 @@ handler browser-kernel with when X is nothing => continue with #result ok: foreign-value-none; + when X.raw-value is nothing do + let Representations = X.representations map: (package reify-value-representation: _); + let Value = new foreign-value-far(Page.vm, Representations); + continue with #result ok: Value; + end + otherwise do let Representations = X.representations map: (package reify-value-representation: _); let Value = new foreign-value-near(Page.vm, X.raw-value, Representations); diff --git a/stdlib/crochet.debug.ui/source/playground/foreign-values.crochet b/stdlib/crochet.debug.ui/source/playground/foreign-values.crochet index e23058f9..6ec46b03 100644 --- a/stdlib/crochet.debug.ui/source/playground/foreign-values.crochet +++ b/stdlib/crochet.debug.ui/source/playground/foreign-values.crochet @@ -8,8 +8,19 @@ type foreign-value-near( global representations is list, ) is foreign-value; +type foreign-value-far( + vm is playground-vm, + global representations is list, +) is foreign-value; + singleton foreign-value-none is foreign-value; command foreign-value-near internal-representation = - foreign repr.internal(self.value); \ No newline at end of file + foreign repr.internal(self.value); + +command foreign-value-far internal-representation = + "(far reference)"; + +command foreign-value-none internal-representation = + "(nothing)"; \ No newline at end of file diff --git a/stdlib/crochet.debug.ui/source/playground/kernel.crochet b/stdlib/crochet.debug.ui/source/playground/kernel.crochet index 49186ddd..e6865f2b 100644 --- a/stdlib/crochet.debug.ui/source/playground/kernel.crochet +++ b/stdlib/crochet.debug.ui/source/playground/kernel.crochet @@ -1,7 +1,13 @@ % crochet +enum kernel-kind = + node, + browser; + /// Information needed to spawn new Crochet VMs for the underlying package. type kernel-config( + kind is kernel-kind, + session-id is secret, library-root is text, capabilities is set, package-tokens is map>, diff --git a/stdlib/crochet.debug.ui/source/ui/playground-page.crochet b/stdlib/crochet.debug.ui/source/ui/playground-page.crochet index 0ea1896d..125b08bf 100644 --- a/stdlib/crochet.debug.ui/source/ui/playground-page.crochet +++ b/stdlib/crochet.debug.ui/source/ui/playground-page.crochet @@ -105,4 +105,15 @@ command widget-playground-page render-value: (X is foreign-value-near) do } ) | selected: (#observable-cell with-value: 1) +end + +command widget-playground-page render-value: (X is foreign-value-far) do + #widget tabbed-panel: ( + X representations enumerate map: { R in + #tab id: R index + | header: R value name + | content: (#dom-widget from-html-element: (force (R value document).rendered)) + } + ) + | selected: (#observable-cell with-value: 1) end \ No newline at end of file diff --git a/www/playground.html b/www/playground.html index b1a0b93d..ffd2bac3 100644 --- a/www/playground.html +++ b/www/playground.html @@ -63,6 +63,8 @@ const app = Crochet.VM.Values.instantiate(app_type, []); const app_config = crochet.ffi.record( new Map([ + ["session-id", crochet.ffi.text(config.session_id)], + ["kind", crochet.ffi.text(config.kind)], ["library-root", crochet.ffi.text(config.library_root)], [ "capabilities",