From 6e10a5f84b3f7ca9077c279cab7a2e376033042f Mon Sep 17 00:00:00 2001 From: osmarks Date: Tue, 28 Nov 2023 16:47:17 +0000 Subject: [PATCH] web build --- .gitignore | 4 +- Cargo.lock | 230 ++++++++++-------- Cargo.toml | 17 +- buildcalc.py | 44 ++++ index.html | 72 ++++++ src/lib.rs | 658 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 614 +---------------------------------------------- 7 files changed, 930 insertions(+), 709 deletions(-) create mode 100644 buildcalc.py create mode 100644 index.html create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index 08b7716..ba13579 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /target osmarkscalculator.zip osmarkscalculator.tar -src.zip \ No newline at end of file +src.zip +dist +vgcore* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c2f2556..ad1ea11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,10 +9,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] -name = "autocfg" -version = "1.0.1" +name = "bumpalo" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" @@ -20,65 +26,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "crossbeam-channel" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" -dependencies = [ - "cfg-if", - "lazy_static", -] - [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "inlinable_string" version = "0.1.14" @@ -94,36 +47,32 @@ dependencies = [ "either", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.114" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] -name = "memoffset" -version = "0.6.5" +name = "log" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ - "autocfg", + "cfg-if 1.0.0", ] [[package]] -name = "num_cpus" -version = "1.13.1" +name = "memory_units" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "osmarkscalculator" @@ -132,36 +81,129 @@ dependencies = [ "anyhow", "inlinable_string", "itertools", - "rayon", + "wasm-bindgen", + "wee_alloc", ] [[package]] -name = "rayon" -version = "1.5.1" +name = "proc-macro2" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ - "autocfg", - "crossbeam-deque", - "either", - "rayon-core", + "unicode-ident", ] [[package]] -name = "rayon-core" -version = "1.9.1" +name = "quote" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "lazy_static", - "num_cpus", + "proc-macro2", ] [[package]] -name = "scopeguard" -version = "1.1.0" +name = "syn" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index dc28a8d..a2a9cae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,23 @@ name = "osmarkscalculator" version = "0.1.0" edition = "2018" +[lib] +crate-type = ["cdylib", "rlib"] +name = "osmarkscalculator" + +[[bin]] +name = "osmarkscalculator" +path = "src/main.rs" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1" inlinable_string = "0.1" -rayon = "1.5" -itertools = "0.10" \ No newline at end of file +itertools = "0.10" +wasm-bindgen = "0.2.63" +wee_alloc = "0.4.5" + +[profile.release] +opt-level = "s" +lto = true \ No newline at end of file diff --git a/buildcalc.py b/buildcalc.py new file mode 100644 index 0000000..8c1c4f5 --- /dev/null +++ b/buildcalc.py @@ -0,0 +1,44 @@ +BINDING = """ +let loaded = false +onmessage = async ev => { + if (!loaded) { + await wasm_bindgen("./osmarkscalculator.wasm") + loaded = true + } + var [fn, ...args] = ev.data + let init = false + if (fn === "deinit") { + wasm_bindgen.deinit_context() + init = false + } else if (fn === "run") { + const start = performance.now() + try { + if (!init) { + wasm_bindgen.init_context() + wasm_bindgen.load_defaults() + init = true; + } + postMessage(["ok", wasm_bindgen.run_program(args[0]), performance.now() - start]) + } catch(e) { + postMessage(["error", e.toString(), performance.now() - start]) + } + } +} +""" +HEADER = """ +--- +title: osmarkscalculator +description: Unholy horrors moved from the depths of my projects directory to your browser. Theoretically, this is a calculator. Good luck using it. +--- +""".strip() +import subprocess, rjsmin, os, shutil +subprocess.run(["wasm-pack", "build", "--target=no-modules"]) +minified = rjsmin.jsmin(open("pkg/osmarkscalculator.js", "r").read() + BINDING) +os.makedirs("dist", exist_ok=True) +subprocess.run(["wasm-opt", "-Oz", "pkg/osmarkscalculator_bg.wasm", "-o", "dist/osmarkscalculator.wasm"]) +open("dist/osmarkscalculator.js", "w").write(minified) +with open("index.html") as f: + g = HEADER + f.read().replace("""""", "") +with open("dist/index.html", "w") as h: + h.write(g) +shutil.copytree("dist/.", "../website/experiments/osmarkscalculator", dirs_exist_ok=True) \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..5f8d8ee --- /dev/null +++ b/index.html @@ -0,0 +1,72 @@ + + +

+
+
+
+
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..6dfbc2a
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,658 @@
+use anyhow::{Result, Context, bail};
+use inlinable_string::InlinableString;
+use std::collections::HashMap;
+use std::borrow::Cow;
+use std::convert::TryInto;
+use std::sync::Arc;
+use wasm_bindgen::prelude::*;
+
+#[global_allocator]
+static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
+
+mod parse;
+mod value;
+mod util;
+mod env;
+
+use value::Value;
+use env::{Rule, Ruleset, Env, Bindings, RuleResult, Operation};
+
+// Main pattern matcher function; 
+fn match_and_bind(expr: &Value, rule: &Rule, env: &Env) -> Result> {
+    fn go(expr: &Value, cond: &Value, env: &Env, already_bound: &Bindings) -> Result> {
+        match (expr, cond) {
+            // numbers match themselves
+            (Value::Num(a), Value::Num(b)) => if a == b { Ok(Some(HashMap::new())) } else { Ok(None) },
+            // handle predicated value - check all predicates, succeed with binding if they match
+            (val, Value::Call(x, args)) if x == "#" => {
+                let preds = &args[1..];
+                let (mut success, mut bindings) = match go(val, &args[0], env, already_bound)? {
+                    Some(bindings) => (true, bindings),
+                    None => (false, already_bound.clone())
+                };
+
+                for pred in preds {
+                    match pred {
+                        // "Num" predicate matches successfully if something is a number
+                        Value::Identifier(i) if i.as_ref() == "Num" => {
+                            match val {
+                                Value::Num(_) => (),
+                                _ => success = false
+                            }
+                        },
+                        // "Ident" does the same for idents
+                        Value::Identifier(i) if i.as_ref() == "Ident" => {
+                            match val {
+                                Value::Identifier(_) => (),
+                                _ => success = false
+                            }
+                        },
+                        // Invert match success
+                        Value::Identifier(i) if i.as_ref() == "Not" => {
+                            success = !success
+                        },
+                        Value::Call(head, args) if head.as_ref() == "And" => {
+                            // Try all patterns it's given, and if any fails then fail the match
+                            for arg in args.iter() {
+                                match go(val, arg, env, &bindings)? {
+                                    Some(new_bindings) => bindings.extend(new_bindings),
+                                    None => success = false
+                                }
+                            }
+                        },
+                        Value::Call(head, args) if head.as_ref() == "Eq" => {
+                            // Evaluate all arguments and check if they are equal
+                            let mut compare_against = None;
+                            for arg in args.iter() {
+                                let mut evaluated_value = arg.subst(&bindings);
+                                run_rewrite(&mut evaluated_value, env).context("evaluating Eq predicate")?;
+                                match compare_against {
+                                    Some(ref x) => if x != &evaluated_value {
+                                        success = false
+                                    },
+                                    None => compare_against = Some(evaluated_value)
+                                }
+                            }
+                        },
+                        Value::Call(head, args) if head.as_ref() == "Gte" => {
+                            // Evaluate all arguments and do comparison.
+                            let mut x = args[0].subst(&bindings);
+                            let mut y = args[1].subst(&bindings);
+                            run_rewrite(&mut x, env).context("evaluating Gte predicate")?;
+                            run_rewrite(&mut y, env).context("evaluating Gte predicate")?;
+                            success &= x >= y;
+                        },
+                        Value::Call(head, args) if head.as_ref() == "Or" => {
+                            // Tries all patterns it's given and will set the match to successful if *any* of them works
+                            for arg in args.iter() {
+                                match go(val, arg, env, &bindings)? {
+                                    Some(new_bindings) => {
+                                        bindings.extend(new_bindings);
+                                        success = true
+                                    },
+                                    None => ()
+                                }
+                            }
+                        },
+                        _ => bail!("invalid predicate {:?}", pred)
+                    }
+                }
+                Ok(match success {
+                    true => Some(bindings),
+                    false => None
+                })
+            },
+            (Value::Call(exp_head, exp_args), Value::Call(rule_head, rule_args)) => {
+                let mut exp_args = Cow::Borrowed(exp_args);
+                // Regardless of any special casing for associativity etc., different heads mean rules can never match
+                if exp_head != rule_head { return Ok(None) }
+
+                let op = env.get_op(exp_head);
+    
+                // Copy bindings from the upper-level matching, so that things like "a+(b+a)" work.
+                let mut out_bindings = already_bound.clone();
+
+                // Special case for associative expressions: split off extra arguments into a new tree
+                if op.associative && rule_args.len() < exp_args.len() {
+                    let exp_args = exp_args.to_mut();
+                    let rest = exp_args.split_off(rule_args.len() - 1);
+                    let rem = Value::Call(exp_head.clone(), rest);
+                    exp_args.push(rem);
+                }
+                if rule_args.len() != exp_args.len() { return Ok(None) }
+    
+                // Try and match all "adjacent" arguments to each other
+                for (rule_arg, exp_arg) in rule_args.iter().zip(&*exp_args) {
+                    match go(exp_arg, rule_arg, env, &out_bindings)? {
+                        Some(x) => out_bindings.extend(x),
+                        None => return Ok(None)
+                    }
+                }
+                Ok(Some(out_bindings))
+            },
+            // identifier pattern matches anything, unless the identifier has already been bound to something else
+            (x, Value::Identifier(a)) => {
+                if let Some(b) = already_bound.get(a) {
+                    if b != x {
+                        return Ok(None);
+                    }
+                };
+                Ok(Some(vec![(a.clone(), x.clone())].into_iter().collect()))
+            },
+            // anything else doesn't match
+            _ => Ok(None)
+        }
+    }
+    // special case at top level of expression - try to match pattern to different subranges of input
+    match (expr, &rule.condition) {
+        // this only applies to matching one "call" against a "call" pattern (with same head)
+        (Value::Call(ehead, eargs), Value::Call(rhead, rargs)) => {
+            // and also only to associative operations
+            if env.get_op(ehead).associative && eargs.len() > rargs.len() && ehead == rhead {
+                // consider all possible subranges of the arguments of the appropriate length
+                for range_start in 0..=(eargs.len() - rargs.len()) {
+                    // extract the arguments & convert into new Value
+                    let c_args = eargs[range_start..range_start + rargs.len()].iter().cloned().collect();
+                    let c_call = Value::Call(ehead.clone(), c_args);
+                    // attempt to match the new subrange against the current rule
+                    if let Some(r) = match_and_bind(&c_call, rule, env)? {
+                        // generate new output with result
+                        let mut new_args = Vec::with_capacity(3);
+                        // add back extra start items
+                        if range_start != 0 {
+                            new_args.push(Value::Call(ehead.clone(), eargs[0..range_start].iter().cloned().collect()))
+                        }
+                        new_args.push(r);
+                        // add back extra end items
+                        if range_start + rargs.len() != eargs.len() {
+                            new_args.push(Value::Call(ehead.clone(), eargs[range_start + rargs.len()..eargs.len()].iter().cloned().collect()))
+                        }
+                        let new_exp = Value::Call(ehead.clone(), new_args);
+                        return Ok(Some(new_exp))
+                    }
+                }
+            }
+        }
+        _ => ()
+    }
+    // substitute bindings from matching into either an intrinsic or the output of the rule
+    if let Some(bindings) = go(expr, &rule.condition, env, &HashMap::new())? {
+        Ok(Some(match &rule.result {
+            RuleResult::Intrinsic(id) => env.intrinsics.get(id).unwrap()(&bindings).with_context(|| format!("applying intrinsic {}", id))?,
+            RuleResult::Exp(e) => e.subst(&bindings)
+        }))
+    } else {
+        Ok(None)
+    }
+}
+
+// Sort any commutative expressions
+fn canonical_sort(v: &mut Value, env: &Env) -> Result<()> {
+    match v {
+        Value::Call(head, args) => if env.get_op(head).commutative {
+            args.sort();
+        },
+        _ => ()
+    }
+    Ok(())
+}
+
+// Associative expression flattening.
+fn flatten_tree(v: &mut Value, env: &Env) -> Result<()> {
+    match v {
+        Value::Call(head, args) => {
+            if env.get_op(head).associative {
+                // Not the most efficient algorithm, but does work.
+                // Repeatedly find the position of a flatten-able child node, and splice it into the argument list.
+                loop {
+                    let mut move_pos = None;
+                    for (i, child) in args.iter().enumerate() {
+                        if let Some(child_head) = child.head() {
+                            if *head == child_head {
+                                move_pos = Some(i)
+                            }
+                        }
+                    }
+                    match move_pos {
+                        Some(pos) => {
+                            let removed = std::mem::replace(&mut args[pos], Value::Num(0));
+                            // We know that removed will be a Call (because its head wasn't None earlier). Unfortunately, rustc does not know this.
+                            match removed {
+                                Value::Call(_, removed_child_args) => args.splice(pos..=pos, removed_child_args.into_iter()),
+                                _ => unreachable!()
+                            };
+                        },
+                        None => break
+                    }
+                }
+            }
+            // Also do sorting after flattening, to avoid any weirdness with ordering.
+            canonical_sort(v, env)?;
+            return Ok(())
+        },
+        _ => return Ok(())
+    }
+}
+
+// Applies rewrite rulesets to an expression.
+fn run_rewrite(v: &mut Value, env: &Env) -> Result<()> {
+    loop {
+        // Compare original and final hash instead of storing a copy of the original value and checking equality
+        // Collision probability is negligible and this is substantially faster than storing/comparing copies.
+        let original_hash = v.get_hash();
+        
+        flatten_tree(v, env).context("flattening tree")?;
+        // Call expressions can be rewritten using pattern matching rules; identifiers can be substituted for bindings if available
+        match v {
+            Value::Call(head, args) => {
+                let head = head.clone();
+                
+                // Rewrite sub-expressions using existing environment
+                args.iter_mut().try_for_each(|arg| run_rewrite(arg, env).with_context(|| format!("rewriting {}", arg.render_to_string(env))))?;
+                
+                // Try to apply all applicable rules from all rulesets, in sequence
+                for ruleset in env.ruleset.iter() {
+                    if let Some(rules) = ruleset.get(&head) {
+                        // Within a ruleset, rules are applied backward. This is nicer for users using the program interactively.
+                        for rule in rules.iter().rev() {
+                            if let Some(result) = match_and_bind(v, rule, env).with_context(|| format!("applying rule {} -> {:?}", rule.condition.render_to_string(env), rule.result))? {
+                                *v = result;
+                                flatten_tree(v, env).context("flattening tree after rule application")?;
+                            }
+                        }
+                    }
+                }
+            },
+            // Substitute in bindings which have been provided
+            Value::Identifier(ident) => {
+                match env.bindings.get(ident) {
+                    Some(val) => {
+                        *v = val.clone();
+                    },
+                    None => return Ok(())
+                }
+            },
+            _ => {
+                return Ok(())
+            }
+        }
+        if original_hash == v.get_hash() {
+            break
+        }
+    }
+    Ok(())
+}
+
+// Utility function for defining intrinsic functions for binary operators.
+// Converts a function which does the actual operation to a function from bindings to a value.
+fn wrap_binop Result + Sync + Send>(op: F) -> Box Result + Sync + Send> {
+    Box::new(move |bindings: &Bindings| {
+        let a = bindings.get(&InlinableString::from("a")).context("binop missing first argument")?.assert_num("binop first argument")?;
+        let b = bindings.get(&InlinableString::from("b")).context("binop missing second argument")?.assert_num("binop second argument")?;
+        op(a, b).map(Value::Num)
+    })
+}
+
+// Provides a basic environment with operator commutativity/associativity operations and intrinsics.
+fn make_initial_env() -> Env {
+    let mut ops = HashMap::new();
+    ops.insert(InlinableString::from("+"), Operation { commutative: true, associative: true });
+    ops.insert(InlinableString::from("*"), Operation { commutative: true, associative: true });
+    ops.insert(InlinableString::from("-"), Operation { commutative: false, associative: false });
+    ops.insert(InlinableString::from("/"), Operation { commutative: false, associative: false });
+    ops.insert(InlinableString::from("^"), Operation { commutative: false, associative: false });
+    ops.insert(InlinableString::from("="), Operation { commutative: false, associative: false });
+    ops.insert(InlinableString::from("#"), Operation { commutative: false, associative: true });
+    let ops = Arc::new(ops);
+    let mut intrinsics = HashMap::new();
+    intrinsics.insert(0, wrap_binop(|a, b| a.checked_add(b).context("integer overflow")));
+    intrinsics.insert(1, wrap_binop(|a, b| a.checked_sub(b).context("integer overflow")));
+    intrinsics.insert(2, wrap_binop(|a, b| a.checked_mul(b).context("integer overflow")));
+    intrinsics.insert(3, wrap_binop(|a, b| a.checked_div(b).context("division by zero")));
+    intrinsics.insert(4, wrap_binop(|a, b| {
+        // The "pow" function takes a usize (machine-sized unsigned integer) and an i128 may not fit into this, so an extra conversion is needed
+        Ok(a.pow(b.try_into()?))
+    }));
+    intrinsics.insert(5, Box::new(|bindings| {
+        // Substitute a single, given binding var=value into a target expression
+        let var = bindings.get(&InlinableString::from("var")).unwrap();
+        let value = bindings.get(&InlinableString::from("value")).unwrap();
+        let target = bindings.get(&InlinableString::from("target")).unwrap();
+        let name = var.assert_ident("Subst")?;
+        let mut new_bindings = HashMap::new();
+        new_bindings.insert(name, value.clone());
+        Ok(target.subst(&new_bindings))
+    }));
+    intrinsics.insert(6, wrap_binop(|a, b| a.checked_rem(b).context("division by zero")));
+    let intrinsics = Arc::new(intrinsics);
+    Env {
+        ruleset: vec![],
+        ops: ops.clone(),
+        intrinsics: intrinsics.clone(),
+        bindings: HashMap::new()
+    }
+}
+
+pub const BUILTINS: &str = "
+SetStage[all]
+a#Num + b#Num = Intrinsic[0]
+a#Num - b#Num = Intrinsic[1]
+a#Num * b#Num = Intrinsic[2]
+a#Num / b#Num = Intrinsic[3]
+a#Num ^ b#Num = Intrinsic[4]
+Subst[var=value, target] = Intrinsic[5]
+Mod[a#Num, b#Num] = Intrinsic[6]
+PushRuleset[builtins]
+";
+
+pub const GENERAL_RULES: &str = "
+SetStage[all]
+(a*b#Num)+(a*c#Num) = (b+c)*a
+Negate[a] = 0 - a
+a^b*a^c = a^(b+c)
+a^0 = 1
+a^1 = a
+(a^b)^c = a^(b*c)
+0*a = 0
+0+a = a
+1*a = a
+x/x = 1
+(n*x)/x = n
+PushRuleset[general_rules]
+";
+
+pub const NORMALIZATION_RULES: &str = "
+SetStage[norm]
+a/b = a*b^Negate[1]
+a+b#Num*a = (b+1)*a
+a^b#Num#Gte[b, 2] = a*a^(b-1)
+a-c#Num*b = a+Negate[c]*b
+a+a = 2*a
+a*(b+c) = a*b+a*c
+a-b = a+Negate[1]*b
+PushRuleset[normalization]
+";
+
+pub const DENORMALIZATION_RULES: &str = "
+SetStage[denorm]
+a*a = a^2
+a^b#Num*a = a^(b+1)
+c+a*b#Num#Gte[0, b] = c-a*Negate[b]
+PushRuleset[denormalization]
+";
+
+pub const DIFFERENTIATION_DEFINITION: &str = "
+SetStage[all]
+D[x, x] = 1
+D[a#Num, x] = 0
+D[f+g, x] = D[f, x] + D[g, x]
+D[f*g, x] = D[f, x] * g + D[g, x] * f
+D[a#Num*f, x] = a * D[f, x]
+PushRuleset[differentiation]
+";
+
+pub const FACTOR_DEFINITION: &str = "
+SetStage[post_norm]
+Factor[x, a*x+b] = x * (a + Factor[x, b] / x)
+PushRuleset[factor]
+SetStage[pre_denorm]
+Factor[x, a] = a
+PushRuleset[factor_postprocess]
+SetStage[denorm]
+x^n/x = x^(n-1)
+(a*x^n)/x = a*x^(n-1)
+PushRuleset[factor_postpostprocess]
+";
+
+
+pub struct ImperativeCtx {
+    bindings: Bindings,
+    current_ruleset_stage: InlinableString,
+    current_ruleset: Ruleset,
+    rulesets: HashMap>,
+    stages: Vec<(InlinableString, Vec)>,
+    pub base_env: Env
+}
+
+impl ImperativeCtx {
+    // Make a new imperative context
+    // Stages are currently hardcoded, as adding a way to manage them would add lots of complexity
+    // for limited benefit
+    pub fn init() -> Self {
+        let stages = [
+            "pre_norm",
+            "norm",
+            "post_norm",
+            "pre_denorm",
+            "denorm",
+            "post_denorm"
+        ].iter().map(|name| (InlinableString::from(*name), vec![])).collect();
+        ImperativeCtx {
+            bindings: HashMap::new(),
+            current_ruleset_stage: InlinableString::from("post_norm"),
+            current_ruleset: HashMap::new(),
+            rulesets: HashMap::new(),
+            stages,
+            base_env: make_initial_env()
+        }
+    }
+
+    // Insert a rule into the current ruleset; handles switching out the result for a relevant intrinsic use, generating possible reorderings, and inserting into the lookup map.
+    fn insert_rule(&mut self, condition: &Value, result_val: Value) -> Result<()> {
+        let result = match result_val {
+            Value::Call(head, args) if head == "Intrinsic" => RuleResult::Intrinsic(args[0].assert_num("Intrinsic ID")? as usize),
+            _ => RuleResult::Exp(result_val)
+        };
+        for rearrangement in condition.pattern_reorderings(&self.base_env).into_iter() {
+            let rule = Rule {
+                condition: rearrangement,
+                result: result.clone()
+            };
+            self.current_ruleset.entry(condition.head().unwrap()).or_insert_with(Vec::new).push(rule);
+        }
+        Ok(())
+    }
+
+    // Run a single statement (roughly, a line of user input) on the current context
+    fn eval_statement(&mut self, mut stmt: Value) -> Result> {
+        match stmt {
+            // = sets a binding or generates a new rule.
+            Value::Call(head, args) if head.as_ref() == "=" => {
+                match &args[0] {
+                    // Create a binding if the LHS (left hand side) is just an identifier
+                    Value::Identifier(id) => {
+                        let rhs = self.eval_statement(args[1].clone())?;
+                        if let Some(val) = rhs.clone() {
+                            self.bindings.insert(id.clone(), val);
+                        }
+                        Ok(rhs)
+                    },
+                    // If the LHS is a call, then a rule should be created instead.
+                    Value::Call(_head, _args) => {
+                        let rhs = self.eval_statement(args[1].clone())?;
+                        if let Some(val) = rhs.clone() {
+                            self.insert_rule(&args[0], val)?;
+                        }
+                        Ok(rhs)
+                    },
+                    // Rebinding numbers can only bring confusion, so it is not allowed.
+                    // They also do not have a head, and so cannot be inserted into the ruleset anyway.
+                    Value::Num(_) => bail!("You cannot rebind numbers")
+                }
+            },
+            // SetStage[] calls set the stage the current ruleset will be applied at
+            Value::Call(head, args) if head.as_ref() == "SetStage" => {
+                let stage = args[0].assert_ident("SetStage requires an identifier for stage")?;
+                if stage != "all" && None == self.stages.iter().position(|s| s.0 == stage) {
+                    bail!("No such stage {}", stage);
+                }
+                self.current_ruleset_stage = stage;
+                Ok(None)
+            },
+            // Move the current ruleset from the "buffer" into the actual list of rules to be applied at each stage
+            Value::Call(head, args) if head.as_ref() == "PushRuleset" => {
+                let name = args[0].assert_ident("PushRuleset requires an identifier for ruleset name")?;
+                // Get ruleset and set the current one to empty
+                let ruleset = std::mem::replace(&mut self.current_ruleset, HashMap::new());
+                // Push ruleset to stages it specifies
+                for (stage_name, stage_rulesets) in self.stages.iter_mut() {
+                    if *stage_name == self.current_ruleset_stage || self.current_ruleset_stage == "all" {
+                        stage_rulesets.push(name.clone());
+                    }
+                }
+                // Insert actual ruleset data under its name
+                self.rulesets.insert(name, Arc::new(ruleset));
+                Ok(None)
+            },
+            // Anything not special should just be repeatedly run through each rewrite stage.
+            _ => {
+                let env = self.base_env.with_bindings(&self.bindings);
+                for (stage_name, stage_rulesets) in self.stages.iter() {
+                    // Add relevant rulesets to a new environment for this stage
+                    let mut env = env.clone();
+                    for ruleset in stage_rulesets.iter() {
+                        env = env.with_ruleset(self.rulesets[ruleset].clone());
+                    }
+                    // Also add the current ruleset if applicable
+                    if self.current_ruleset_stage == *stage_name || self.current_ruleset_stage == "all" {
+                        env = env.with_ruleset(Arc::new(self.current_ruleset.clone()));
+                    }
+                    run_rewrite(&mut stmt, &env).with_context(|| format!("failed in {} stage", stage_name))?;
+                    // If a ruleset is only meant to be applied in one particular stage, it shouldn't have any later stages applied to it, 
+                    // or the transformation it's meant to do may be undone
+                    if self.current_ruleset_stage == *stage_name {
+                        break
+                    }
+                }
+                Ok(Some(stmt))
+            }
+        }
+    }
+
+    // Evaluate an entire "program" (multiple statements delineated by ; or newlines)
+    pub fn eval_program(&mut self, program: &str) -> Result> {
+        let mut tokens = parse::lex(program)?;
+        let mut last_value = None;
+        loop {
+            // Split at the next break token
+            let remaining_tokens = tokens.iter().position(|x| *x == parse::Token::Break).map(|ix| tokens.split_off(ix + 1));
+            // Trim EOF/break tokens 
+            match tokens[tokens.len() - 1] {
+                parse::Token::Break | parse::Token::EOF => tokens.truncate(tokens.len() - 1),
+                _ => ()
+            };
+            // If the statement/line isn't blank, readd EOF for the parser, parse into an AST then Value, and evaluate the statement
+            if tokens.len() > 0 {
+                tokens.push(parse::Token::EOF);
+                let value = Value::from_ast(parse::parse(tokens)?);
+                last_value = self.eval_statement(value)?;
+            }
+            // If there was no break after the current position, this is now done. Otherwise, move onto the new remaining tokens.
+            match remaining_tokens {
+                Some(t) => { tokens = t },
+                None => break
+            }
+        }
+        Ok(last_value)
+    }
+}
+
+static mut JS_CONTEXT: Option = None;
+
+#[wasm_bindgen]
+pub fn init_context() {
+    unsafe {
+        JS_CONTEXT = Some(ImperativeCtx::init());
+    }
+}
+unsafe fn load_defaults_internal() -> Result<()> {
+    let ctx = (&mut JS_CONTEXT).as_mut().unwrap();
+    ctx.eval_program(BUILTINS)?;
+    ctx.eval_program(GENERAL_RULES)?;
+    ctx.eval_program(FACTOR_DEFINITION)?;
+    ctx.eval_program(DENORMALIZATION_RULES)?;
+    ctx.eval_program(NORMALIZATION_RULES)?;
+    ctx.eval_program(DIFFERENTIATION_DEFINITION)?;
+    Ok(())
+}
+#[wasm_bindgen]
+pub fn load_defaults() {
+    unsafe {
+        load_defaults_internal().unwrap();
+    }
+}
+#[wasm_bindgen]
+pub fn run_program(program: &str) -> String {
+    unsafe {
+        let ctx = (&mut JS_CONTEXT).as_mut().unwrap();
+        match ctx.eval_program(program) {
+            Ok(Some(result)) => result.render_to_string(&ctx.base_env).to_string(),
+            Ok(None) => String::new(),
+            Err(e) => format!("Error: {:?}", e)
+        }
+    }
+}
+#[wasm_bindgen]
+pub fn deinit_context() {
+    unsafe {
+        std::mem::take(&mut JS_CONTEXT);
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::{ImperativeCtx, BUILTINS, GENERAL_RULES, NORMALIZATION_RULES, DENORMALIZATION_RULES, DIFFERENTIATION_DEFINITION, FACTOR_DEFINITION};
+
+    #[test]
+    fn end_to_end_tests() {
+        let mut ctx = ImperativeCtx::init();
+        ctx.eval_program(BUILTINS).unwrap();
+        ctx.eval_program(GENERAL_RULES).unwrap();
+        ctx.eval_program(FACTOR_DEFINITION).unwrap();
+        ctx.eval_program(DENORMALIZATION_RULES).unwrap();
+        ctx.eval_program(NORMALIZATION_RULES).unwrap();
+        ctx.eval_program(DIFFERENTIATION_DEFINITION).unwrap();
+        let test_cases = [
+            ("Factor[x, x*3+x^2]", "(3+x)*x"),
+            ("x^a/x^(a+1)", "x^Negate[1]"),
+            ("Negate[a+b]", "Negate[1]*b-a"),
+            ("Subst[x=4, x+4+4+4+4]", "20"),
+            ("(a+b)*(c+d)*(e+f)", "a*c*e+a*c*f+a*d*e+a*d*f+b*c*e+b*c*f+b*d*e+b*d*f"),
+            ("(12+55)^3-75+16/(2*2)+5+3*4", "300709"),
+            ("D[3*x^3 + 6*x, x] ", "6+9*x^2"),
+            ("Fib[n] = Fib[n-1] + Fib[n-2] 
+            Fib[0] = 0 
+            Fib[1] = 1 
+            Fib[6]", "8"),
+            ("Subst[b=a, b+a]", "2*a"),
+            ("a = 7
+            b = Negate[4] 
+            a + b", "3"),
+            ("IsEven[x] = 0
+            IsEven[x#Eq[Mod[x, 2], 0]] = 1
+            IsEven[3] - IsEven[4]", "Negate[1]"),
+            ("(a+b+c)^2", "2*a*b+2*a*c+2*b*c+a^2+b^2+c^2"),
+            ("(x+2)^7", "128+2*x^6+12*x^5+12*x^6+16*x^3+16*x^5+24*x^4+24*x^5+32*x^2+32*x^3+32*x^5+128*x^2+256*x^4+448*x+512*x^2+512*x^3+x^7")
+        ];
+        for (input, expected_result) in test_cases {
+            let lhs = ctx.eval_program(input).unwrap();
+            let lhs = lhs.as_ref().unwrap().render_to_string(&ctx.base_env);
+            println!("{} evaluated to {}; expected {}", input, lhs, expected_result);
+            assert_eq!(lhs, expected_result);   
+        }
+
+        let error_cases = [
+            ("1/0")
+        ];
+
+        for error_case in error_cases {
+            if let Err(e) = ctx.eval_program(error_case) {
+                println!("{} produced error {:?}", error_case, e);
+            } else {
+                panic!("should have errored: {}", error_case)
+            }
+        }
+
+        println!("All tests passed.")
+    }
+}
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 82a9e55..1373dd6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,558 +1,6 @@
-use anyhow::{Result, Context, bail};
-use inlinable_string::InlinableString;
-use std::collections::HashMap;
+use osmarkscalculator::*;
 use std::io::BufRead;
-use std::borrow::Cow;
-use std::convert::TryInto;
-use std::sync::Arc;
-use rayon::prelude::*;
-
-mod parse;
-mod value;
-mod util;
-mod env;
-
-use value::Value;
-use env::{Rule, Ruleset, Env, Bindings, RuleResult, Operation};
-
-// Main pattern matcher function; 
-fn match_and_bind(expr: &Value, rule: &Rule, env: &Env) -> Result> {
-    fn go(expr: &Value, cond: &Value, env: &Env, already_bound: &Bindings) -> Result> {
-        match (expr, cond) {
-            // numbers match themselves
-            (Value::Num(a), Value::Num(b)) => if a == b { Ok(Some(HashMap::new())) } else { Ok(None) },
-            // handle predicated value - check all predicates, succeed with binding if they match
-            (val, Value::Call(x, args)) if x == "#" => {
-                let preds = &args[1..];
-                let (mut success, mut bindings) = match go(val, &args[0], env, already_bound)? {
-                    Some(bindings) => (true, bindings),
-                    None => (false, already_bound.clone())
-                };
-
-                for pred in preds {
-                    match pred {
-                        // "Num" predicate matches successfully if something is a number
-                        Value::Identifier(i) if i.as_ref() == "Num" => {
-                            match val {
-                                Value::Num(_) => (),
-                                _ => success = false
-                            }
-                        },
-                        // "Ident" does the same for idents
-                        Value::Identifier(i) if i.as_ref() == "Ident" => {
-                            match val {
-                                Value::Identifier(_) => (),
-                                _ => success = false
-                            }
-                        },
-                        // Invert match success
-                        Value::Identifier(i) if i.as_ref() == "Not" => {
-                            success = !success
-                        },
-                        Value::Call(head, args) if head.as_ref() == "And" => {
-                            // Try all patterns it's given, and if any fails then fail the match
-                            for arg in args.iter() {
-                                match go(val, arg, env, &bindings)? {
-                                    Some(new_bindings) => bindings.extend(new_bindings),
-                                    None => success = false
-                                }
-                            }
-                        },
-                        Value::Call(head, args) if head.as_ref() == "Eq" => {
-                            // Evaluate all arguments and check if they are equal
-                            let mut compare_against = None;
-                            for arg in args.iter() {
-                                let mut evaluated_value = arg.subst(&bindings);
-                                run_rewrite(&mut evaluated_value, env).context("evaluating Eq predicate")?;
-                                match compare_against {
-                                    Some(ref x) => if x != &evaluated_value {
-                                        success = false
-                                    },
-                                    None => compare_against = Some(evaluated_value)
-                                }
-                            }
-                        },
-                        Value::Call(head, args) if head.as_ref() == "Gte" => {
-                            // Evaluate all arguments and do comparison.
-                            let mut x = args[0].subst(&bindings);
-                            let mut y = args[1].subst(&bindings);
-                            run_rewrite(&mut x, env).context("evaluating Gte predicate")?;
-                            run_rewrite(&mut y, env).context("evaluating Gte predicate")?;
-                            success &= x >= y;
-                        },
-                        Value::Call(head, args) if head.as_ref() == "Or" => {
-                            // Tries all patterns it's given and will set the match to successful if *any* of them works
-                            for arg in args.iter() {
-                                match go(val, arg, env, &bindings)? {
-                                    Some(new_bindings) => {
-                                        bindings.extend(new_bindings);
-                                        success = true
-                                    },
-                                    None => ()
-                                }
-                            }
-                        },
-                        _ => bail!("invalid predicate {:?}", pred)
-                    }
-                }
-                Ok(match success {
-                    true => Some(bindings),
-                    false => None
-                })
-            },
-            (Value::Call(exp_head, exp_args), Value::Call(rule_head, rule_args)) => {
-                let mut exp_args = Cow::Borrowed(exp_args);
-                // Regardless of any special casing for associativity etc., different heads mean rules can never match
-                if exp_head != rule_head { return Ok(None) }
-
-                let op = env.get_op(exp_head);
-    
-                // Copy bindings from the upper-level matching, so that things like "a+(b+a)" work.
-                let mut out_bindings = already_bound.clone();
-
-                // Special case for associative expressions: split off extra arguments into a new tree
-                if op.associative && rule_args.len() < exp_args.len() {
-                    let exp_args = exp_args.to_mut();
-                    let rest = exp_args.split_off(rule_args.len() - 1);
-                    let rem = Value::Call(exp_head.clone(), rest);
-                    exp_args.push(rem);
-                }
-                if rule_args.len() != exp_args.len() { return Ok(None) }
-    
-                // Try and match all "adjacent" arguments to each other
-                for (rule_arg, exp_arg) in rule_args.iter().zip(&*exp_args) {
-                    match go(exp_arg, rule_arg, env, &out_bindings)? {
-                        Some(x) => out_bindings.extend(x),
-                        None => return Ok(None)
-                    }
-                }
-                Ok(Some(out_bindings))
-            },
-            // identifier pattern matches anything, unless the identifier has already been bound to something else
-            (x, Value::Identifier(a)) => {
-                if let Some(b) = already_bound.get(a) {
-                    if b != x {
-                        return Ok(None);
-                    }
-                };
-                Ok(Some(vec![(a.clone(), x.clone())].into_iter().collect()))
-            },
-            // anything else doesn't match
-            _ => Ok(None)
-        }
-    }
-    // special case at top level of expression - try to match pattern to different subranges of input
-    match (expr, &rule.condition) {
-        // this only applies to matching one "call" against a "call" pattern (with same head)
-        (Value::Call(ehead, eargs), Value::Call(rhead, rargs)) => {
-            // and also only to associative operations
-            if env.get_op(ehead).associative && eargs.len() > rargs.len() && ehead == rhead {
-                // consider all possible subranges of the arguments of the appropriate length
-                for range_start in 0..=(eargs.len() - rargs.len()) {
-                    // extract the arguments & convert into new Value
-                    let c_args = eargs[range_start..range_start + rargs.len()].iter().cloned().collect();
-                    let c_call = Value::Call(ehead.clone(), c_args);
-                    // attempt to match the new subrange against the current rule
-                    if let Some(r) = match_and_bind(&c_call, rule, env)? {
-                        // generate new output with result
-                        let mut new_args = Vec::with_capacity(3);
-                        // add back extra start items
-                        if range_start != 0 {
-                            new_args.push(Value::Call(ehead.clone(), eargs[0..range_start].iter().cloned().collect()))
-                        }
-                        new_args.push(r);
-                        // add back extra end items
-                        if range_start + rargs.len() != eargs.len() {
-                            new_args.push(Value::Call(ehead.clone(), eargs[range_start + rargs.len()..eargs.len()].iter().cloned().collect()))
-                        }
-                        let new_exp = Value::Call(ehead.clone(), new_args);
-                        return Ok(Some(new_exp))
-                    }
-                }
-            }
-        }
-        _ => ()
-    }
-    // substitute bindings from matching into either an intrinsic or the output of the rule
-    if let Some(bindings) = go(expr, &rule.condition, env, &HashMap::new())? {
-        Ok(Some(match &rule.result {
-            RuleResult::Intrinsic(id) => env.intrinsics.get(id).unwrap()(&bindings).with_context(|| format!("applying intrinsic {}", id))?,
-            RuleResult::Exp(e) => e.subst(&bindings)
-        }))
-    } else {
-        Ok(None)
-    }
-}
-
-// Sort any commutative expressions
-fn canonical_sort(v: &mut Value, env: &Env) -> Result<()> {
-    match v {
-        Value::Call(head, args) => if env.get_op(head).commutative {
-            args.sort();
-        },
-        _ => ()
-    }
-    Ok(())
-}
-
-// Associative expression flattening.
-fn flatten_tree(v: &mut Value, env: &Env) -> Result<()> {
-    match v {
-        Value::Call(head, args) => {
-            if env.get_op(head).associative {
-                // Not the most efficient algorithm, but does work.
-                // Repeatedly find the position of a flatten-able child node, and splice it into the argument list.
-                loop {
-                    let mut move_pos = None;
-                    for (i, child) in args.iter().enumerate() {
-                        if let Some(child_head) = child.head() {
-                            if *head == child_head {
-                                move_pos = Some(i)
-                            }
-                        }
-                    }
-                    match move_pos {
-                        Some(pos) => {
-                            let removed = std::mem::replace(&mut args[pos], Value::Num(0));
-                            // We know that removed will be a Call (because its head wasn't None earlier). Unfortunately, rustc does not know this.
-                            match removed {
-                                Value::Call(_, removed_child_args) => args.splice(pos..=pos, removed_child_args.into_iter()),
-                                _ => unreachable!()
-                            };
-                        },
-                        None => break
-                    }
-                }
-            }
-            // Also do sorting after flattening, to avoid any weirdness with ordering.
-            canonical_sort(v, env)?;
-            return Ok(())
-        },
-        _ => return Ok(())
-    }
-}
-
-// Applies rewrite rulesets to an expression.
-fn run_rewrite(v: &mut Value, env: &Env) -> Result<()> {
-    loop {
-        // Compare original and final hash instead of storing a copy of the original value and checking equality
-        // Collision probability is negligible and this is substantially faster than storing/comparing copies.
-        let original_hash = v.get_hash();
-        
-        flatten_tree(v, env).context("flattening tree")?;
-        // Call expressions can be rewritten using pattern matching rules; identifiers can be substituted for bindings if available
-        match v {
-            Value::Call(head, args) => {
-                let head = head.clone();
-                
-                // Rewrite sub-expressions using existing environment
-                args.par_iter_mut().try_for_each(|arg| run_rewrite(arg, env).with_context(|| format!("rewriting {}", arg.render_to_string(env))))?;
-                
-                // Try to apply all applicable rules from all rulesets, in sequence
-                for ruleset in env.ruleset.iter() {
-                    if let Some(rules) = ruleset.get(&head) {
-                        // Within a ruleset, rules are applied backward. This is nicer for users using the program interactively.
-                        for rule in rules.iter().rev() {
-                            if let Some(result) = match_and_bind(v, rule, env).with_context(|| format!("applying rule {} -> {:?}", rule.condition.render_to_string(env), rule.result))? {
-                                *v = result;
-                                flatten_tree(v, env).context("flattening tree after rule application")?;
-                            }
-                        }
-                    }
-                }
-            },
-            // Substitute in bindings which have been provided
-            Value::Identifier(ident) => {
-                match env.bindings.get(ident) {
-                    Some(val) => {
-                        *v = val.clone();
-                    },
-                    None => return Ok(())
-                }
-            },
-            _ => {
-                return Ok(())
-            }
-        }
-        if original_hash == v.get_hash() {
-            break
-        }
-    }
-    Ok(())
-}
-
-// Utility function for defining intrinsic functions for binary operators.
-// Converts a function which does the actual operation to a function from bindings to a value.
-fn wrap_binop Result + Sync + Send>(op: F) -> Box Result + Sync + Send> {
-    Box::new(move |bindings: &Bindings| {
-        let a = bindings.get(&InlinableString::from("a")).context("binop missing first argument")?.assert_num("binop first argument")?;
-        let b = bindings.get(&InlinableString::from("b")).context("binop missing second argument")?.assert_num("binop second argument")?;
-        op(a, b).map(Value::Num)
-    })
-}
-
-// Provides a basic environment with operator commutativity/associativity operations and intrinsics.
-fn make_initial_env() -> Env {
-    let mut ops = HashMap::new();
-    ops.insert(InlinableString::from("+"), Operation { commutative: true, associative: true });
-    ops.insert(InlinableString::from("*"), Operation { commutative: true, associative: true });
-    ops.insert(InlinableString::from("-"), Operation { commutative: false, associative: false });
-    ops.insert(InlinableString::from("/"), Operation { commutative: false, associative: false });
-    ops.insert(InlinableString::from("^"), Operation { commutative: false, associative: false });
-    ops.insert(InlinableString::from("="), Operation { commutative: false, associative: false });
-    ops.insert(InlinableString::from("#"), Operation { commutative: false, associative: true });
-    let ops = Arc::new(ops);
-    let mut intrinsics = HashMap::new();
-    intrinsics.insert(0, wrap_binop(|a, b| a.checked_add(b).context("integer overflow")));
-    intrinsics.insert(1, wrap_binop(|a, b| a.checked_sub(b).context("integer overflow")));
-    intrinsics.insert(2, wrap_binop(|a, b| a.checked_mul(b).context("integer overflow")));
-    intrinsics.insert(3, wrap_binop(|a, b| a.checked_div(b).context("division by zero")));
-    intrinsics.insert(4, wrap_binop(|a, b| {
-        // The "pow" function takes a usize (machine-sized unsigned integer) and an i128 may not fit into this, so an extra conversion is needed
-        Ok(a.pow(b.try_into()?))
-    }));
-    intrinsics.insert(5, Box::new(|bindings| {
-        // Substitute a single, given binding var=value into a target expression
-        let var = bindings.get(&InlinableString::from("var")).unwrap();
-        let value = bindings.get(&InlinableString::from("value")).unwrap();
-        let target = bindings.get(&InlinableString::from("target")).unwrap();
-        let name = var.assert_ident("Subst")?;
-        let mut new_bindings = HashMap::new();
-        new_bindings.insert(name, value.clone());
-        Ok(target.subst(&new_bindings))
-    }));
-    intrinsics.insert(6, wrap_binop(|a, b| a.checked_rem(b).context("division by zero")));
-    let intrinsics = Arc::new(intrinsics);
-    Env {
-        ruleset: vec![],
-        ops: ops.clone(),
-        intrinsics: intrinsics.clone(),
-        bindings: HashMap::new()
-    }
-}
-
-const BUILTINS: &str = "
-SetStage[all]
-a#Num + b#Num = Intrinsic[0]
-a#Num - b#Num = Intrinsic[1]
-a#Num * b#Num = Intrinsic[2]
-a#Num / b#Num = Intrinsic[3]
-a#Num ^ b#Num = Intrinsic[4]
-Subst[var=value, target] = Intrinsic[5]
-Mod[a#Num, b#Num] = Intrinsic[6]
-PushRuleset[builtins]
-";
-
-const GENERAL_RULES: &str = "
-SetStage[all]
-(a*b#Num)+(a*c#Num) = (b+c)*a
-Negate[a] = 0 - a
-a^b*a^c = a^(b+c)
-a^0 = 1
-a^1 = a
-(a^b)^c = a^(b*c)
-0*a = 0
-0+a = a
-1*a = a
-x/x = 1
-(n*x)/x = n
-PushRuleset[general_rules]
-";
-
-const NORMALIZATION_RULES: &str = "
-SetStage[norm]
-a/b = a*b^Negate[1]
-a+b#Num*a = (b+1)*a
-a^b#Num#Gte[b, 2] = a*a^(b-1)
-a-c#Num*b = a+Negate[c]*b
-a+a = 2*a
-a*(b+c) = a*b+a*c
-a-b = a+Negate[1]*b
-PushRuleset[normalization]
-";
-
-const DENORMALIZATION_RULES: &str = "
-SetStage[denorm]
-a*a = a^2
-a^b#Num*a = a^(b+1)
-c+a*b#Num#Gte[0, b] = c-a*Negate[b]
-PushRuleset[denormalization]
-";
-
-const DIFFERENTIATION_DEFINITION: &str = "
-SetStage[all]
-D[x, x] = 1
-D[a#Num, x] = 0
-D[f+g, x] = D[f, x] + D[g, x]
-D[f*g, x] = D[f, x] * g + D[g, x] * f
-D[a#Num*f, x] = a * D[f, x]
-PushRuleset[differentiation]
-";
-
-const FACTOR_DEFINITION: &str = "
-SetStage[post_norm]
-Factor[x, a*x+b] = x * (a + Factor[x, b] / x)
-PushRuleset[factor]
-SetStage[pre_denorm]
-Factor[x, a] = a
-PushRuleset[factor_postprocess]
-SetStage[denorm]
-x^n/x = x^(n-1)
-(a*x^n)/x = a*x^(n-1)
-PushRuleset[factor_postpostprocess]
-";
-
-struct ImperativeCtx {
-    bindings: Bindings,
-    current_ruleset_stage: InlinableString,
-    current_ruleset: Ruleset,
-    rulesets: HashMap>,
-    stages: Vec<(InlinableString, Vec)>,
-    base_env: Env
-}
-
-impl ImperativeCtx {
-    // Make a new imperative context
-    // Stages are currently hardcoded, as adding a way to manage them would add lots of complexity
-    // for limited benefit
-    fn init() -> Self {
-        let stages = [
-            "pre_norm",
-            "norm",
-            "post_norm",
-            "pre_denorm",
-            "denorm",
-            "post_denorm"
-        ].iter().map(|name| (InlinableString::from(*name), vec![])).collect();
-        ImperativeCtx {
-            bindings: HashMap::new(),
-            current_ruleset_stage: InlinableString::from("post_norm"),
-            current_ruleset: HashMap::new(),
-            rulesets: HashMap::new(),
-            stages,
-            base_env: make_initial_env()
-        }
-    }
-
-    // Insert a rule into the current ruleset; handles switching out the result for a relevant intrinsic use, generating possible reorderings, and inserting into the lookup map.
-    fn insert_rule(&mut self, condition: &Value, result_val: Value) -> Result<()> {
-        let result = match result_val {
-            Value::Call(head, args) if head == "Intrinsic" => RuleResult::Intrinsic(args[0].assert_num("Intrinsic ID")? as usize),
-            _ => RuleResult::Exp(result_val)
-        };
-        for rearrangement in condition.pattern_reorderings(&self.base_env).into_iter() {
-            let rule = Rule {
-                condition: rearrangement,
-                result: result.clone()
-            };
-            self.current_ruleset.entry(condition.head().unwrap()).or_insert_with(Vec::new).push(rule);
-        }
-        Ok(())
-    }
-
-    // Run a single statement (roughly, a line of user input) on the current context
-    fn eval_statement(&mut self, mut stmt: Value) -> Result> {
-        match stmt {
-            // = sets a binding or generates a new rule.
-            Value::Call(head, args) if head.as_ref() == "=" => {
-                match &args[0] {
-                    // Create a binding if the LHS (left hand side) is just an identifier
-                    Value::Identifier(id) => {
-                        let rhs = self.eval_statement(args[1].clone())?;
-                        if let Some(val) = rhs.clone() {
-                            self.bindings.insert(id.clone(), val);
-                        }
-                        Ok(rhs)
-                    },
-                    // If the LHS is a call, then a rule should be created instead.
-                    Value::Call(_head, _args) => {
-                        let rhs = self.eval_statement(args[1].clone())?;
-                        if let Some(val) = rhs.clone() {
-                            self.insert_rule(&args[0], val)?;
-                        }
-                        Ok(rhs)
-                    },
-                    // Rebinding numbers can only bring confusion, so it is not allowed.
-                    // They also do not have a head, and so cannot be inserted into the ruleset anyway.
-                    Value::Num(_) => bail!("You cannot rebind numbers")
-                }
-            },
-            // SetStage[] calls set the stage the current ruleset will be applied at
-            Value::Call(head, args) if head.as_ref() == "SetStage" => {
-                let stage = args[0].assert_ident("SetStage requires an identifier for stage")?;
-                if stage != "all" && None == self.stages.iter().position(|s| s.0 == stage) {
-                    bail!("No such stage {}", stage);
-                }
-                self.current_ruleset_stage = stage;
-                Ok(None)
-            },
-            // Move the current ruleset from the "buffer" into the actual list of rules to be applied at each stage
-            Value::Call(head, args) if head.as_ref() == "PushRuleset" => {
-                let name = args[0].assert_ident("PushRuleset requires an identifier for ruleset name")?;
-                // Get ruleset and set the current one to empty
-                let ruleset = std::mem::replace(&mut self.current_ruleset, HashMap::new());
-                // Push ruleset to stages it specifies
-                for (stage_name, stage_rulesets) in self.stages.iter_mut() {
-                    if *stage_name == self.current_ruleset_stage || self.current_ruleset_stage == "all" {
-                        stage_rulesets.push(name.clone());
-                    }
-                }
-                // Insert actual ruleset data under its name
-                self.rulesets.insert(name, Arc::new(ruleset));
-                Ok(None)
-            },
-            // Anything not special should just be repeatedly run through each rewrite stage.
-            _ => {
-                let env = self.base_env.with_bindings(&self.bindings);
-                for (stage_name, stage_rulesets) in self.stages.iter() {
-                    // Add relevant rulesets to a new environment for this stage
-                    let mut env = env.clone();
-                    for ruleset in stage_rulesets.iter() {
-                        env = env.with_ruleset(self.rulesets[ruleset].clone());
-                    }
-                    // Also add the current ruleset if applicable
-                    if self.current_ruleset_stage == *stage_name || self.current_ruleset_stage == "all" {
-                        env = env.with_ruleset(Arc::new(self.current_ruleset.clone()));
-                    }
-                    run_rewrite(&mut stmt, &env).with_context(|| format!("failed in {} stage", stage_name))?;
-                    // If a ruleset is only meant to be applied in one particular stage, it shouldn't have any later stages applied to it, 
-                    // or the transformation it's meant to do may be undone
-                    if self.current_ruleset_stage == *stage_name {
-                        break
-                    }
-                }
-                Ok(Some(stmt))
-            }
-        }
-    }
-
-    // Evaluate an entire "program" (multiple statements delineated by ; or newlines)
-    fn eval_program(&mut self, program: &str) -> Result> {
-        let mut tokens = parse::lex(program)?;
-        let mut last_value = None;
-        loop {
-            // Split at the next break token
-            let remaining_tokens = tokens.iter().position(|x| *x == parse::Token::Break).map(|ix| tokens.split_off(ix + 1));
-            // Trim EOF/break tokens 
-            match tokens[tokens.len() - 1] {
-                parse::Token::Break | parse::Token::EOF => tokens.truncate(tokens.len() - 1),
-                _ => ()
-            };
-            // If the statement/line isn't blank, readd EOF for the parser, parse into an AST then Value, and evaluate the statement
-            if tokens.len() > 0 {
-                tokens.push(parse::Token::EOF);
-                let value = Value::from_ast(parse::parse(tokens)?);
-                last_value = self.eval_statement(value)?;
-            }
-            // If there was no break after the current position, this is now done. Otherwise, move onto the new remaining tokens.
-            match remaining_tokens {
-                Some(t) => { tokens = t },
-                None => break
-            }
-        }
-        Ok(last_value)
-    }
-}
+use anyhow::Result;
 
 fn main() -> Result<()> {
     let mut ctx = ImperativeCtx::init();
@@ -572,62 +20,4 @@ fn main() -> Result<()> {
         }
     }
     Ok(())
-}
-
-#[cfg(test)]
-mod test {
-    use crate::{ImperativeCtx, BUILTINS, GENERAL_RULES, NORMALIZATION_RULES, DENORMALIZATION_RULES, DIFFERENTIATION_DEFINITION, FACTOR_DEFINITION};
-
-    #[test]
-    fn end_to_end_tests() {
-        let mut ctx = ImperativeCtx::init();
-        ctx.eval_program(BUILTINS).unwrap();
-        ctx.eval_program(GENERAL_RULES).unwrap();
-        ctx.eval_program(FACTOR_DEFINITION).unwrap();
-        ctx.eval_program(DENORMALIZATION_RULES).unwrap();
-        ctx.eval_program(NORMALIZATION_RULES).unwrap();
-        ctx.eval_program(DIFFERENTIATION_DEFINITION).unwrap();
-        let test_cases = [
-            ("Factor[x, x*3+x^2]", "(3+x)*x"),
-            ("x^a/x^(a+1)", "x^Negate[1]"),
-            ("Negate[a+b]", "Negate[1]*b-a"),
-            ("Subst[x=4, x+4+4+4+4]", "20"),
-            ("(a+b)*(c+d)*(e+f)", "a*c*e+a*c*f+a*d*e+a*d*f+b*c*e+b*c*f+b*d*e+b*d*f"),
-            ("(12+55)^3-75+16/(2*2)+5+3*4", "300709"),
-            ("D[3*x^3 + 6*x, x] ", "6+9*x^2"),
-            ("Fib[n] = Fib[n-1] + Fib[n-2] 
-            Fib[0] = 0 
-            Fib[1] = 1 
-            Fib[6]", "8"),
-            ("Subst[b=a, b+a]", "2*a"),
-            ("a = 7
-            b = Negate[4] 
-            a + b", "3"),
-            ("IsEven[x] = 0
-            IsEven[x#Eq[Mod[x, 2], 0]] = 1
-            IsEven[3] - IsEven[4]", "Negate[1]"),
-            ("(a+b+c)^2", "2*a*b+2*a*c+2*b*c+a^2+b^2+c^2"),
-            ("(x+2)^7", "128+2*x^6+12*x^5+12*x^6+16*x^3+16*x^5+24*x^4+24*x^5+32*x^2+32*x^3+32*x^5+128*x^2+256*x^4+448*x+512*x^2+512*x^3+x^7")
-        ];
-        for (input, expected_result) in test_cases {
-            let lhs = ctx.eval_program(input).unwrap();
-            let lhs = lhs.as_ref().unwrap().render_to_string(&ctx.base_env);
-            println!("{} evaluated to {}; expected {}", input, lhs, expected_result);
-            assert_eq!(lhs, expected_result);   
-        }
-
-        let error_cases = [
-            ("1/0")
-        ];
-
-        for error_case in error_cases {
-            if let Err(e) = ctx.eval_program(error_case) {
-                println!("{} produced error {:?}", error_case, e);
-            } else {
-                panic!("should have errored: {}", error_case)
-            }
-        }
-
-        println!("All tests passed.")
-    }
 }
\ No newline at end of file