mirror of
https://github.com/osmarks/osmarkscalculator.git
synced 2024-10-30 03:26:16 +00:00
web build
This commit is contained in:
parent
b8eddc0837
commit
6e10a5f84b
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@
|
|||||||
osmarkscalculator.zip
|
osmarkscalculator.zip
|
||||||
osmarkscalculator.tar
|
osmarkscalculator.tar
|
||||||
src.zip
|
src.zip
|
||||||
|
dist
|
||||||
|
vgcore*
|
230
Cargo.lock
generated
230
Cargo.lock
generated
@ -9,10 +9,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "bumpalo"
|
||||||
version = "1.0.1"
|
version = "3.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
@ -20,65 +26,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
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]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
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]]
|
[[package]]
|
||||||
name = "inlinable_string"
|
name = "inlinable_string"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
@ -94,36 +47,32 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy_static"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.114"
|
version = "0.2.137"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50"
|
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "log"
|
||||||
version = "0.6.5"
|
version = "0.4.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
|
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"cfg-if 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "memory_units"
|
||||||
version = "1.13.1"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
|
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
|
||||||
dependencies = [
|
|
||||||
"hermit-abi",
|
[[package]]
|
||||||
"libc",
|
name = "once_cell"
|
||||||
]
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "osmarkscalculator"
|
name = "osmarkscalculator"
|
||||||
@ -132,36 +81,129 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"inlinable_string",
|
"inlinable_string",
|
||||||
"itertools",
|
"itertools",
|
||||||
"rayon",
|
"wasm-bindgen",
|
||||||
|
"wee_alloc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "proc-macro2"
|
||||||
version = "1.5.1"
|
version = "1.0.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
|
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"unicode-ident",
|
||||||
"crossbeam-deque",
|
|
||||||
"either",
|
|
||||||
"rayon-core",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon-core"
|
name = "quote"
|
||||||
version = "1.9.1"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
|
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"proc-macro2",
|
||||||
"crossbeam-deque",
|
|
||||||
"crossbeam-utils",
|
|
||||||
"lazy_static",
|
|
||||||
"num_cpus",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "syn"
|
||||||
version = "1.1.0"
|
version = "1.0.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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"
|
||||||
|
15
Cargo.toml
15
Cargo.toml
@ -3,10 +3,23 @@ name = "osmarkscalculator"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2018"
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
inlinable_string = "0.1"
|
inlinable_string = "0.1"
|
||||||
rayon = "1.5"
|
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
|
wasm-bindgen = "0.2.63"
|
||||||
|
wee_alloc = "0.4.5"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
44
buildcalc.py
Normal file
44
buildcalc.py
Normal file
@ -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("""<meta charset="UTF-8">""", "")
|
||||||
|
with open("dist/index.html", "w") as h:
|
||||||
|
h.write(g)
|
||||||
|
shutil.copytree("dist/.", "../website/experiments/osmarkscalculator", dirs_exist_ok=True)
|
72
index.html
Normal file
72
index.html
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<meta charset="UTF-8">
|
||||||
|
<textarea id="program" style="width: 100%; resize: vertical" rows="5"></textarea>
|
||||||
|
<pre id="output"></pre>
|
||||||
|
<button id="go">Go</button>
|
||||||
|
<button id="clear">Clear Context</button>
|
||||||
|
<select id="examples">
|
||||||
|
</select>
|
||||||
|
<script>
|
||||||
|
const examples = {
|
||||||
|
"blank": "",
|
||||||
|
"factorial": `Fac[n] = Fac[n-1]*n
|
||||||
|
Fac[0] = 1
|
||||||
|
Fac[17]
|
||||||
|
`,
|
||||||
|
"expand": "(a+b)*(c+d)*(e+f)*(g+h)",
|
||||||
|
"expand2": "(a+b)^3*(b+c)-d",
|
||||||
|
"fibonacci": `Fib[n] = Fib[n-1] + Fib[n-2]
|
||||||
|
Fib[0] = 0
|
||||||
|
Fib[1] = 1
|
||||||
|
Fib[6]
|
||||||
|
`,
|
||||||
|
"predicate": `IsEven[x] = 0
|
||||||
|
IsEven[x#Eq[Mod[x, 2], 0]] = 1
|
||||||
|
IsEven[3] - IsEven[4]`,
|
||||||
|
"derivative": `D[3*x^3 + 6*x, x]`,
|
||||||
|
"simplify": `x^a/x^(a+1)`,
|
||||||
|
"simplify2": "Negate[a+b] + b",
|
||||||
|
"arith": `(12+55)^3-75+16/(2*2)+5+3*4`,
|
||||||
|
"subst": "Subst[x=4, x+4+4+4+4]"
|
||||||
|
}
|
||||||
|
const examplesSelector = document.querySelector("#examples")
|
||||||
|
const program = document.querySelector("#program")
|
||||||
|
for (const name of Object.keys(examples)) {
|
||||||
|
const opt = document.createElement("option")
|
||||||
|
opt.value = name
|
||||||
|
opt.appendChild(document.createTextNode(name))
|
||||||
|
examplesSelector.appendChild(opt)
|
||||||
|
}
|
||||||
|
examplesSelector.addEventListener("change", () => {
|
||||||
|
program.value = examples[examplesSelector.value]
|
||||||
|
})
|
||||||
|
var worker = new Worker("osmarkscalculator.js")
|
||||||
|
const forceKill = () => {
|
||||||
|
console.warn("Force-terminating worker.")
|
||||||
|
worker.terminate()
|
||||||
|
worker = new Worker("osmarkscalculator.js")
|
||||||
|
}
|
||||||
|
const write = data => {
|
||||||
|
const out = document.querySelector("#output")
|
||||||
|
while (out.firstChild) { out.removeChild(out.firstChild) }
|
||||||
|
out.appendChild(document.createTextNode(data))
|
||||||
|
}
|
||||||
|
document.querySelector("#go").addEventListener("click", () => {
|
||||||
|
console.log(program.value)
|
||||||
|
write("Running...")
|
||||||
|
worker.postMessage(["run", program.value])
|
||||||
|
var timeout = setTimeout(() => {
|
||||||
|
forceKill()
|
||||||
|
write("Execution timeout")
|
||||||
|
}, 5000)
|
||||||
|
worker.onmessage = ev => {
|
||||||
|
const [status, result, time] = ev.data
|
||||||
|
if (status === "ok") {
|
||||||
|
write(result + `\nin ${time}ms`)
|
||||||
|
} else {
|
||||||
|
write("Internal error: " + result + `\nin ${time}ms`)
|
||||||
|
}
|
||||||
|
clearInterval(timeout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
document.querySelector("#clear").addEventListener("click", () => worker.postMessage(["deinit"]))
|
||||||
|
</script>
|
658
src/lib.rs
Normal file
658
src/lib.rs
Normal file
@ -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<Option<Value>> {
|
||||||
|
fn go(expr: &Value, cond: &Value, env: &Env, already_bound: &Bindings) -> Result<Option<Bindings>> {
|
||||||
|
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<F: 'static + Fn(i128, i128) -> Result<i128> + Sync + Send>(op: F) -> Box<dyn Fn(&Bindings) -> Result<Value> + 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<InlinableString, Arc<Ruleset>>,
|
||||||
|
stages: Vec<(InlinableString, Vec<InlinableString>)>,
|
||||||
|
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<Option<Value>> {
|
||||||
|
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<Option<Value>> {
|
||||||
|
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<ImperativeCtx> = 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.")
|
||||||
|
}
|
||||||
|
}
|
614
src/main.rs
614
src/main.rs
@ -1,558 +1,6 @@
|
|||||||
use anyhow::{Result, Context, bail};
|
use osmarkscalculator::*;
|
||||||
use inlinable_string::InlinableString;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use std::borrow::Cow;
|
use anyhow::Result;
|
||||||
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<Option<Value>> {
|
|
||||||
fn go(expr: &Value, cond: &Value, env: &Env, already_bound: &Bindings) -> Result<Option<Bindings>> {
|
|
||||||
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<F: 'static + Fn(i128, i128) -> Result<i128> + Sync + Send>(op: F) -> Box<dyn Fn(&Bindings) -> Result<Value> + 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<InlinableString, Arc<Ruleset>>,
|
|
||||||
stages: Vec<(InlinableString, Vec<InlinableString>)>,
|
|
||||||
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<Option<Value>> {
|
|
||||||
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<Option<Value>> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let mut ctx = ImperativeCtx::init();
|
let mut ctx = ImperativeCtx::init();
|
||||||
@ -573,61 +21,3 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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.")
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user