diff --git a/.gitignore b/.gitignore index 8560ff9..8444843 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ pkg/ /mocha-setup.js /test-loader.js /test.html + +/crates/percival/bindings/ diff --git a/Cargo.lock b/Cargo.lock index cbfd98e..7fc82ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "ahash" version = "0.3.8" @@ -183,6 +189,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "js-sys" version = "0.3.55" @@ -242,7 +254,10 @@ dependencies = [ "chumsky", "maplit", "rpds", + "serde", + "serde_json", "thiserror", + "ts-rs", ] [[package]] @@ -321,12 +336,49 @@ dependencies = [ "archery", ] +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + [[package]] name = "scoped-tls" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f972498cf015f7c0746cac89ebe1d6ef10c293b94175a243a2d9442c163d9944" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -394,6 +446,29 @@ dependencies = [ "crunchy", ] +[[package]] +name = "ts-rs" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d26cba30c9b3a2f537f765cf754126f11c983b7426d280ae0b4cef2374cab98" +dependencies = [ + "thiserror", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8e302fbcf5b60dfa1df443535967511442c5ce4eac8b4c9fa811f2274280a4" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn", + "termcolor", +] + [[package]] name = "unicode-xid" version = "0.2.2" @@ -419,6 +494,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if", + "serde", + "serde_json", "wasm-bindgen-macro", ] diff --git a/crates/percival-cli/src/main.rs b/crates/percival-cli/src/main.rs index 341955a..f846006 100644 --- a/crates/percival-cli/src/main.rs +++ b/crates/percival-cli/src/main.rs @@ -1,5 +1,6 @@ //! Crate containing code for the `percival-cli` binary. +use std::error::Error; use std::{ fs::read_to_string, io::{self, Read, Write}, @@ -7,9 +8,15 @@ use std::{ process::{self, Command, Stdio}, }; -use clap::Parser; +use clap::{ArgEnum, Parser}; -use percival::{codegen::compile, errors::format_errors, parser::Grammar}; +use percival::{codegen::compile as compile_js, errors::format_errors, parser::Grammar}; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum, Debug)] +enum Emitter { + JS, + Json, +} /// Convenience CLI for testing the Percival language compiler. #[derive(Parser, Debug)] @@ -22,6 +29,9 @@ struct Opt { /// Runs prettier and bat on the output. #[clap(short, long)] format: bool, + + #[clap(short, long, arg_enum, default_value_t = Emitter::JS)] + emit: Emitter, } /// Run the main program. @@ -49,7 +59,12 @@ fn main() { } }; - match compile(&prog) { + let emitted: Result> = match opt.emit { + Emitter::JS => compile_js(&prog).map_err(|err| err.into()), + Emitter::Json => prog.json().map_err(|err| err.into()), + }; + + match emitted { Ok(js) => { if !opt.format { println!("{}", js); diff --git a/crates/percival-wasm/Cargo.toml b/crates/percival-wasm/Cargo.toml index 873baa1..26b66a6 100644 --- a/crates/percival-wasm/Cargo.toml +++ b/crates/percival-wasm/Cargo.toml @@ -13,7 +13,7 @@ default = ["console_error_panic_hook"] [dependencies] console_error_panic_hook = { version = "0.1", optional = true } percival = { path = "../percival" } -wasm-bindgen = "0.2" +wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } yansi = "0.5.0" [dev-dependencies] diff --git a/crates/percival-wasm/src/lib.rs b/crates/percival-wasm/src/lib.rs index 4b602c0..aad19b7 100644 --- a/crates/percival-wasm/src/lib.rs +++ b/crates/percival-wasm/src/lib.rs @@ -54,6 +54,22 @@ impl CompilerResult { self.0.as_ref().ok().map(|(_, js)| js.clone()) } + /// Returns the AST of the program's source. + pub fn ast(&self) -> JsValue { + self.0 + .as_ref() + .ok() + .map(|(prog, _)| match JsValue::from_serde(prog) { + Ok(ast) => ast, + Err(err) => { + // XX: wasm-bindgen claims to have errors, but doesn't + eprintln!("{} {}", Paint::red("Error:"), err); + JsValue::UNDEFINED + } + }) + .unwrap_or(JsValue::UNDEFINED) + } + /// Returns the names of relations that are dependencies of this program. pub fn deps(&self) -> Option> { self.0.as_ref().ok().map(|(prog, _)| { diff --git a/crates/percival/Cargo.toml b/crates/percival/Cargo.toml index b014188..8564817 100644 --- a/crates/percival/Cargo.toml +++ b/crates/percival/Cargo.toml @@ -9,6 +9,9 @@ ariadne = "0.1.3" chumsky = "0.7.0" rpds = "0.11.0" thiserror = "1.0.30" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +ts-rs = "6.1" [dev-dependencies] maplit = "1.0.2" diff --git a/crates/percival/src/ast.rs b/crates/percival/src/ast.rs index b685f74..5d6e955 100644 --- a/crates/percival/src/ast.rs +++ b/crates/percival/src/ast.rs @@ -1,9 +1,23 @@ //! Abstract syntax tree definitions for the Percival language. +// How to build AST types: +// +// cargo test ast::export_bindings +// mkdir -p $(percival_wasm_pkg)/ast +// for ts in $(percival_bindings)/* ; do \ +// cp $$ts $(percival_wasm_pkg)/ast/"$$(basename "$${ts%.ts}.d.ts")" ; \ +// done +// sed -i '' -e 's~ast(): any~ast(): import("./ast/Program").Program | undefined~' $(percival_wasm_pkg)/percival_wasm.d.ts +// + +use serde::Serialize; +use serde_json::{to_string_pretty, Result as JsonResult}; use std::collections::{BTreeMap, BTreeSet}; +use ts_rs::TS; /// A program translation unit in the Percival language. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub struct Program { /// Rules that make up the program. pub rules: Vec, @@ -11,8 +25,16 @@ pub struct Program { pub imports: Vec, } +impl Program { + /// Convert the program to a JSON string. + pub fn json(&self) -> JsonResult { + to_string_pretty(self) + } +} + /// Represents a single Horn clause. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub struct Rule { /// Head or implicand of the Horn clause. pub goal: Fact, @@ -21,7 +43,8 @@ pub struct Rule { } /// An element of the right-hand side of a rule. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub enum Clause { /// Relational assumption in the rule. Fact(Fact), @@ -32,7 +55,8 @@ pub enum Clause { } /// Literal part of a Horn clause, written in terms of relations. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub struct Fact { /// Name of the relation being referenced. pub name: String, @@ -41,7 +65,8 @@ pub struct Fact { } /// A bound or unbound value assigned to part of a relation. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub enum Value { /// A simple identifier, which can be either bound or unbound. Id(String), @@ -54,7 +79,8 @@ pub enum Value { } /// Literal values supported by the Percival grammar. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub enum Literal { /// A standard floating-point number literal. Number(String), @@ -65,7 +91,8 @@ pub enum Literal { } /// An aggregate operation over stratified dependency relations. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub struct Aggregate { /// Name of the aggregate operator, such as `min` or `sum`. pub operator: String, @@ -76,7 +103,8 @@ pub struct Aggregate { } /// An external import from a static JSON dataset. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub struct Import { /// Name of the relation being imported. pub name: String, diff --git a/package-lock.json b/package-lock.json index c23714e..700089e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "remark-rehype": "^10.1.0", "svelte": "^3.46.6", "svelte-icons": "^2.1.0", + "svelte-json-tree": "^1.0.0", "unified": "^10.1.2" }, "devDependencies": { @@ -58,6 +59,7 @@ } }, "crates/percival-wasm/pkg": { + "name": "percival-wasm", "version": "0.1.0" }, "node_modules/@ampproject/remapping": { @@ -6128,6 +6130,11 @@ "resolved": "https://registry.npmjs.org/svelte-icons/-/svelte-icons-2.1.0.tgz", "integrity": "sha512-rHPQjweEc9fGSnvM0/4gA3pDHwyZyYsC5KhttCZRhSMJfLttJST5Uq0B16Czhw+HQ+HbSOk8kLigMlPs7gZtfg==" }, + "node_modules/svelte-json-tree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svelte-json-tree/-/svelte-json-tree-1.0.0.tgz", + "integrity": "sha512-scs1OdkC8uFpTN4MX0yKkOzZ1/EG3eP1ARC+xcFthXp2IfcwBaXgab0FqA4Am0vQwffNNB+1Gd1LFkJBlynWTA==" + }, "node_modules/svelte-preprocess": { "version": "4.10.4", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.4.tgz", @@ -11098,6 +11105,11 @@ "resolved": "https://registry.npmjs.org/svelte-icons/-/svelte-icons-2.1.0.tgz", "integrity": "sha512-rHPQjweEc9fGSnvM0/4gA3pDHwyZyYsC5KhttCZRhSMJfLttJST5Uq0B16Czhw+HQ+HbSOk8kLigMlPs7gZtfg==" }, + "svelte-json-tree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svelte-json-tree/-/svelte-json-tree-1.0.0.tgz", + "integrity": "sha512-scs1OdkC8uFpTN4MX0yKkOzZ1/EG3eP1ARC+xcFthXp2IfcwBaXgab0FqA4Am0vQwffNNB+1Gd1LFkJBlynWTA==" + }, "svelte-preprocess": { "version": "4.10.4", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.4.tgz", diff --git a/package.json b/package.json index 379028d..621df2b 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "remark-rehype": "^10.1.0", "svelte": "^3.46.6", "svelte-icons": "^2.1.0", + "svelte-json-tree": "^1.0.0", "unified": "^10.1.2" }, "devDependencies": { diff --git a/src/components/cell/Cell.svelte b/src/components/cell/Cell.svelte index f7d2142..87d66d4 100644 --- a/src/components/cell/Cell.svelte +++ b/src/components/cell/Cell.svelte @@ -1,6 +1,7 @@ + +{#if state.type === "code" && state.displayDebug && state.result.ok && state.result.ast !== undefined} +
+
+
+ AST + + := +
+ {#key expandAll} + + {/key} +
+
+{/if} + + diff --git a/src/lib/notebook.ts b/src/lib/notebook.ts index 3e523e8..dd710d7 100644 --- a/src/lib/notebook.ts +++ b/src/lib/notebook.ts @@ -27,6 +27,7 @@ export type CellData = MarkdownCell | CodeCellData | PlotCellData; export type CodeCellState = CodeCellData & { result: CompilerResult; status: "stale" | "pending" | "done"; + displayDebug: boolean; output?: Record; graphErrors?: string; runtimeErrors?: string; @@ -97,6 +98,7 @@ export class NotebookState { ...cell, result: build(cell.value), status: "stale", + displayDebug: false, }); } else { this.cells.set(id, { diff --git a/src/lib/runtime.ts b/src/lib/runtime.ts index 0770200..4734f7d 100644 --- a/src/lib/runtime.ts +++ b/src/lib/runtime.ts @@ -12,6 +12,7 @@ type CompilerResultOk = { evaluate: (deps: Record) => EvalPromise; deps: string[]; results: string[]; + ast: object | undefined; }; type CompilerResultErr = { @@ -51,6 +52,7 @@ export function build(src: string): CompilerResult { }, deps: result.deps()!, results: [...result.results()!], + ast: result.ast(), }; } else { return { ok: false, errors: result.err()! };