From d1485ad850567c852b57a144b1ddb6ac15b6b480 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Mon, 3 Nov 2025 17:37:18 -0800 Subject: [PATCH 1/3] Start 'Morel in the browser' (#13) Fixes #13 To make it easy to call Morel from WASM, we need a simple interface to the shell. We already have `process_line`, which takes a string and returns a string. It is stateful, so variables assigned in one statement are available in the next statement: ```sml let mut shell = Shell::new(&[]); let mut result; let in_1 = "val x = 5\n\ and y = 6\n"; let out_1 = "> val x = 5 : int\n\ > val y = 6 : int\n"; result = shell.process_statement(in_1, None).unwrap(); assert_eq!(result, out_1); let in_2 = "x + y\n"; let out_2 = "> val it = 11 : int\n"; result = shell.process_statement(in_2, None).unwrap(); assert_eq!(result, out_2); ``` The current API is string-in, string-out. The input must be a single declaration or expression (without a semicolon). The output prints a value and its type. Future enhancements could be multi-statement input (separated by semicolons) and output as a structured value. --- src/shell/main.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/shell/main.rs b/src/shell/main.rs index e2b1ef6..12b9603 100644 --- a/src/shell/main.rs +++ b/src/shell/main.rs @@ -305,7 +305,7 @@ impl Shell { } /// Processes a single statement. - fn process_statement( + pub fn process_statement( &mut self, code: &str, expected_output: Option<&str>, @@ -627,4 +627,22 @@ mod tests { "#; assert_eq!(comment_depth(s), 0); } + + #[test] + fn test_line_mode() { + let mut shell = Shell::new(&[]); + let mut result; + + let in_1 = "val x = 5\n\ + and y = 6\n"; + let out_1 = "> val x = 5 : int\n\ + > val y = 6 : int\n"; + result = shell.process_statement(in_1, None).unwrap(); + assert_eq!(result, out_1); + + let in_2 = "x + y\n"; + let out_2 = "> val it = 11 : int\n"; + result = shell.process_statement(in_2, None).unwrap(); + assert_eq!(result, out_2); + } } From d786db42940e0bb223849b36d70b19e69c727a4a Mon Sep 17 00:00:00 2001 From: Will Noble Date: Wed, 5 Nov 2025 11:13:30 -0800 Subject: [PATCH 2/3] Hide profiling logic in unifier behind feature flag --- Cargo.toml | 5 +++++ src/unify/unifier.rs | 29 +++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11bc1b1..a24a00f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,11 @@ similar = "2.7.0" strum = "0.27.2" strum_macros = "0.27.2" +[features] +# Enabling profiling with `cargo build --features=profiling` +# causes the unifier to print timing and iteration statistics to stdout. +profiling = [] + [lints.rust] # lint: sort until '^$' where '^[a-z]' diff --git a/src/unify/unifier.rs b/src/unify/unifier.rs index b0fced3..741c08a 100644 --- a/src/unify/unifier.rs +++ b/src/unify/unifier.rs @@ -31,6 +31,8 @@ use std::collections::{BTreeMap, HashMap, VecDeque}; use std::fmt::{self, Debug, Display, Formatter, Write}; use std::iter::zip; use std::rc::Rc; + +#[cfg(feature = "profiling")] use std::time::Instant; /// Trait for things that behave like terms. @@ -1029,6 +1031,7 @@ impl Unifier { unify::unifier_parser::generate_program(term_pairs) ); } + #[cfg(feature = "profiling")] let start = Instant::now(); // delete: G u { t = t } @@ -1053,13 +1056,18 @@ impl Unifier { // if x in vars(f(s0, ..., sk)) let mut work = Work::new(tracer, term_pairs); - if false { - println!("Before: {}", work); - } + + #[cfg(feature = "profiling")] + println!("Before: {}", work); + #[cfg(feature = "profiling")] let mut iteration = 0; + loop { - iteration += 1; - // println!("iteration {} work {}", iteration, work); + #[cfg(feature = "profiling")] + { + iteration += 1; + println!("iteration {} work {}", iteration, work); + } let seq_pair = work.seq_seq_queue.borrow_mut().pop_front(); if let Some((left, right)) = seq_pair { @@ -1140,8 +1148,9 @@ impl Unifier { continue; } - let duration = Instant::now() - start; - if false { + #[cfg(feature = "profiling")] + { + let duration = Instant::now() - start; println!( "Term count {} iterations {} \ duration {} nanos ({} nanos per iteration)\n", @@ -1152,11 +1161,14 @@ impl Unifier { ); println!("Result: {}", work); } + let mut substitutions = BTreeMap::new(); work.result.iter().for_each(|(var, term)| { substitutions.insert(var.clone(), term.clone()); }); - if false { + + #[cfg(feature = "profiling")] + { println!( "After: {}\n{}", work, @@ -1166,6 +1178,7 @@ impl Unifier { .resolve() ); } + return Ok(Substitution { substitutions }); } } From b91c32a5cab955bedb14c350cea7a93dbd03ef68 Mon Sep 17 00:00:00 2001 From: Will Noble Date: Tue, 4 Nov 2025 23:19:54 -0800 Subject: [PATCH 3/3] Add web demo --- Cargo.toml | 8 +++++++ src/lib.rs | 19 ++++------------- src/wasm.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++ web/README.md | 35 ++++++++++++++++++++++++++++++ web/demo.html | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 src/wasm.rs create mode 100644 web/README.md create mode 100644 web/demo.html diff --git a/Cargo.toml b/Cargo.toml index a24a00f..3967610 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,13 @@ version = "0.2.0" authors = ["Julian Hyde "] edition = "2024" +# Generate portable dynamic code when building the library. +# This is necessary for compiling WebAssembly modules. +# We could also add an `rlib` option +# if we ever want to release the library as a Rust crate as well. +[lib] +crate-type = ["cdylib"] + [[bin]] name = "main" path = "src/main.rs" @@ -37,6 +44,7 @@ rustc_version = "0.4" similar = "2.7.0" strum = "0.27.2" strum_macros = "0.27.2" +wasm-bindgen = "0.2" [features] # Enabling profiling with `cargo build --features=profiling` diff --git a/src/lib.rs b/src/lib.rs index 48f56be..320cc28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,25 +15,14 @@ // language governing permissions and limitations under the // License. -#![allow(dead_code)] - -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - pub mod compile; pub mod eval; pub mod shell; pub mod syntax; pub mod unify; -#[cfg(test)] -mod tests { - use super::*; +#[cfg(target_arch = "wasm32")] +pub mod wasm; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +#[cfg(target_arch = "wasm32")] +pub use wasm::MorelShell; diff --git a/src/wasm.rs b/src/wasm.rs new file mode 100644 index 0000000..5f7a52d --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,52 @@ +// Licensed to Julian Hyde under one or more contributor license +// agreements. See the NOTICE file distributed with this work +// for additional information regarding copyright ownership. +// Julian Hyde licenses this file to you under the Apache +// License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. You may obtain a +// copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +// either express or implied. See the License for the specific +// language governing permissions and limitations under the +// License. + +use crate::shell::Shell; +use crate::shell::config::Config; +use wasm_bindgen::prelude::*; + +/// A stateful shell that implements a Morel REPL for Wasm. +/// +/// # Example +/// ```javascript +/// const shell = new MorelShell(); +/// const result = shell.process_statement('val x = 42\n'); +/// ``` +#[wasm_bindgen] +pub struct MorelShell(Shell); + +#[wasm_bindgen] +impl MorelShell { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + // In Wasm, we can't use filesystem operations like current_dir(), + // so we need `Shell::with_config` instead of `Shell::new`. + let config = Config::default(); + + Self(Shell::with_config(config)) + } + + /// Process a complete Morel statement, which must end in a newline, + /// returning the result as a string on success, + /// or an error message on failure. + #[wasm_bindgen] + pub fn process_statement(&mut self, input: &str) -> Result { + self.0 + .process_statement(input, None) + .map_err(|e| e.to_string()) + } +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..c699a3d --- /dev/null +++ b/web/README.md @@ -0,0 +1,35 @@ +# Morel WebAssembly Build + +This project can be compiled to WebAssembly for use in the browser +using [`wasm-pack`](https://github.com/drager/wasm-pack). + +## Build + +If you don't already have `wasm-pack` installed: + +```bash +cargo install wasm-pack +``` + +To build the Wasm module: + +```bash +wasm-pack build --target web +``` + +The resulting module can be found under `target/wasm32-unknown-unknown/`. +This will also generate a new directory called `pkg/` +containing some JS glue code that can be imported as an ES6 module. + +## Demo + +There is a bare-bones demo page showcasing the browser build in action. +After building the Wasm target, start up a web server. +If you just open the demo as a `file://` URI, +you'll probably get a CORS error when it tries to load the module. + +```bash +python -m http.server 8000 +``` + +Then, navigate to http://127.0.0.1:8000/web/demo.html in your browser. diff --git a/web/demo.html b/web/demo.html new file mode 100644 index 0000000..01f4306 --- /dev/null +++ b/web/demo.html @@ -0,0 +1,59 @@ + + + + + + + + Morel Web Demo + + + + + + +
+ + +
+ + + + +