Skip to content
Closed
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
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ version = "0.2.0"
authors = ["Julian Hyde <jhyde@apache.org>"]
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"
Expand All @@ -37,6 +44,12 @@ 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`
# causes the unifier to print timing and iteration statistics to stdout.
profiling = []


[lints.rust]
Expand Down
19 changes: 4 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
20 changes: 19 additions & 1 deletion src/shell/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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);
}
}
29 changes: 21 additions & 8 deletions src/unify/unifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 }
Expand All @@ -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 {
Expand Down Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -1166,6 +1178,7 @@ impl Unifier {
.resolve()
);
}

return Ok(Substitution { substitutions });
}
}
Expand Down
52 changes: 52 additions & 0 deletions src/wasm.rs
Original file line number Diff line number Diff line change
@@ -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<String, String> {
self.0
.process_statement(input, None)
.map_err(|e| e.to_string())
}
}
35 changes: 35 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -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.
59 changes: 59 additions & 0 deletions web/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Morel Web Demo</title>

<!-- Use Tailwind CSS via CDN (best for development, not production). -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>

<body class="bg-gray-50">
<div class="w-full max-w-2xl mx-auto p-6">
<textarea id="output" readonly
class="w-full h-96 p-4 border rounded mb-4 font-mono text-sm bg-white resize-none"></textarea>
<input id="input" type="text" placeholder="Enter Morel statement..."
class="w-full p-3 border rounded font-mono text-sm">
</div>

<script type="module">
import init, { MorelShell } from '../pkg/morel.js';

let shell;

async function initShell() {
await init();
shell = new MorelShell();
output.value = "";
}

const output = document.getElementById('output');
const input = document.getElementById('input');

input.addEventListener('keydown', async (e) => {
if (e.key === 'Enter' && input.value.trim()) {
const statement = input.value + '\n';
input.value = '';

// Echo the input to the output text area.
output.value += '$ ' + statement;

try {
const result = shell.process_statement(statement);
output.value += result + '\n';
} catch (err) {
output.value += 'Error: ' + err + '\n';
}

output.scrollTop = output.scrollHeight;
}
});

initShell();
</script>
</body>

</html>
Loading