diff --git a/Cargo.lock b/Cargo.lock index 0c8e6aa..51367bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,16 +30,15 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] @@ -69,14 +68,25 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -128,33 +138,31 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.3.4" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.4" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ "anstream", "anstyle", - "bitflags 1.3.2", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.2" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", @@ -164,9 +172,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "colorchoice" @@ -183,6 +191,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "cpufeatures" version = "0.2.7" @@ -192,6 +220,30 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -285,9 +337,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ "ahash", "allocator-api2", @@ -301,54 +353,54 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "io-lifetimes" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ - "hermit-abi", "libc", - "windows-sys", ] [[package]] -name = "is-terminal" -version = "0.4.7" +name = "intern-all" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "d79d55e732e243f6762e0fc7b245bfd9fa0e0246356ed6cfdba62d9c707e36c1" dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix 0.37.19", - "windows-sys", + "hashbrown", + "lazy_static", ] [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" dependencies = [ "either", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[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.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" -[[package]] -name = "linux-raw-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" - [[package]] name = "linux-raw-sys" version = "0.4.7" @@ -370,6 +422,30 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memorize" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b505dbd3a88b64417e29469500c32af2b538ba5f703100761f657540a1c442d" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "never" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" + [[package]] name = "num-traits" version = "0.2.15" @@ -380,24 +456,38 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.18.0" +name = "numtoa" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "orchidlang" version = "0.2.2" dependencies = [ "clap", + "const_format", "dyn-clone", "hashbrown", + "intern-all", "itertools", + "memorize", + "never", + "once_cell", "ordered-float", "paste", "polling", + "rayon", "rust-embed", + "substack", "take_mut", + "termsize", "trait-set", "unicode-segmentation", ] @@ -425,14 +515,14 @@ checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "polling" -version = "3.0.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51348b98db9d4a18ada4fdf7ff5274666e7e6c5a50c42a7d77c5e5c0cb6b036b" +checksum = "e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531" dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", - "rustix 0.38.13", + "rustix", "tracing", "windows-sys", ] @@ -455,6 +545,41 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_termios" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" + [[package]] name = "regex" version = "1.7.3" @@ -507,20 +632,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rustix" -version = "0.37.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.7", - "windows-sys", -] - [[package]] name = "rustix" version = "0.38.13" @@ -530,7 +641,7 @@ dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.7", + "linux-raw-sys", "windows-sys", ] @@ -543,6 +654,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.160" @@ -566,6 +683,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "substack" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffccc3d80f0a489de67aa74ff31ab852abb973e1c6dacf3704889e00ca544e7f" + [[package]] name = "syn" version = "1.0.109" @@ -594,6 +717,31 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + +[[package]] +name = "termsize" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e86d824a8e90f342ad3ef4bd51ef7119a9b681b0cc9f8ee7b2852f02ccd2517" +dependencies = [ + "atty", + "kernel32-sys", + "libc", + "termion", + "winapi 0.2.8", +] + [[package]] name = "tracing" version = "0.1.37" @@ -640,6 +788,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "utf8parse" version = "0.2.1" @@ -662,6 +816,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -672,6 +832,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -684,7 +850,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ad36a76..22ae53a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,20 @@ doc = false [dependencies] hashbrown = "0.14" ordered-float = "4.1" -itertools = "0.11" +itertools = "0.12" dyn-clone = "1.0" -clap = { version = "4.3", features = ["derive"] } +clap = { version = "4.4", features = ["derive"] } trait-set = "0.3" paste = "1.0" rust-embed = { version = "8.0", features = ["include-exclude"] } take_mut = "0.2.2" unicode-segmentation = "1.10.1" -polling = "3.0.0" +polling = "3.3.0" +never = "0.1.0" +memorize = "2.0.0" +substack = "1.1.0" +rayon = "1.8.0" +intern-all = "0.2.0" +once_cell = "1.19.0" +const_format = "0.2.32" +termsize = "0.1.6" diff --git a/examples/file-browser/main.orc b/examples/file-browser/main.orc index d7b042e..59906b8 100644 --- a/examples/file-browser/main.orc +++ b/examples/file-browser/main.orc @@ -1,7 +1,8 @@ import system::(io, fs, async) import std::(to_string, to_uint, inspect) -const folder_view := (path, next) => do{ +--[ +const folder_view_old := \path. do{ cps println $ "Contents of " ++ fs::os_print path; cps entries = async::block_on $ fs::read_dir path; cps list::enumerate entries @@ -13,13 +14,13 @@ const folder_view := (path, next) => do{ cps choice = readln; if (choice == "..") then do { let parent_path = fs::pop_path path - |> option::unwrap + |> option::assume |> tuple::pick 0 2; next parent_path } else do { let t[subname, is_dir] = to_uint choice |> (list::get entries) - |> option::unwrap; + |> option::assume; let subpath = fs::join_paths path subname; if is_dir then next subpath else do { @@ -30,8 +31,16 @@ const folder_view := (path, next) => do{ } } } +]-- -const main := loop_over (path = fs::cwd) { - cps path = folder_view path; +const folder_view := \path. do cps { + cps println $ "Contents of " ++ fs::os_print path; + cps entries = async::block_on $ fs::read_dir path; + let t[name, is_dir] = option::assume $ list::get entries 0; + cps println $ to_string name ++ " " ++ fs::os_print is_dir +} + +const main := loop_over (path = fs::cwd) { + cps folder_view path; } diff --git a/examples/fizz-buzz/main.orc b/examples/fizz-buzz/main.orc index 52bf896..c893d75 100644 --- a/examples/fizz-buzz/main.orc +++ b/examples/fizz-buzz/main.orc @@ -1,4 +1,4 @@ -import std::to_string +import std::conv::to_string const fizz_buzz := n => ( (recursive r (i=0) list::cons i $ r (i + 1)) @@ -10,7 +10,7 @@ const fizz_buzz := n => ( ) |> list::take n |> list::reduce ((l, r) => l ++ "\n" ++ r) - |> option::unwrap + |> option::assume ) const main := fizz_buzz 100 diff --git a/examples/hello-world/main.orc b/examples/hello-world/main.orc index 50d1518..3e0df95 100644 --- a/examples/hello-world/main.orc +++ b/examples/hello-world/main.orc @@ -1,7 +1,9 @@ import std::exit_status +import std::conv -const main := ( +const main2 := ( println "Hello, world!" exit_status::success ) --- main := "Hello, World!\n" + +const main := conv::to_string t[1, 2, 3] diff --git a/examples/list-processing/main.orc b/examples/list-processing/main.orc index fc18433..7700818 100644 --- a/examples/list-processing/main.orc +++ b/examples/list-processing/main.orc @@ -1,4 +1,4 @@ -import std::to_string +import std::conv::to_string export const main := do{ let foo = list::new[1, 2, 3, 4, 5, 6]; @@ -7,7 +7,7 @@ export const main := do{ |> list::skip 2 |> list::take 3 |> list::reduce ((a, b) => a + b) - |> option::unwrap; + |> option::assume; cps println $ to_string sum; 0 } diff --git a/examples/maps/main.orc b/examples/maps/main.orc index 3f8ab35..2c8b0f1 100644 --- a/examples/maps/main.orc +++ b/examples/maps/main.orc @@ -8,7 +8,7 @@ export const main := do{ "bar" = 4 ]; let num = map::get foo "bar" - |> option::unwrap; + |> option::assume; cps println $ to_string num; 0 } diff --git a/examples/match/main.orc b/examples/match/main.orc index 4c7e9e4..053bfb3 100644 --- a/examples/match/main.orc +++ b/examples/match/main.orc @@ -1,4 +1,4 @@ -import std::to_string +import std::(conv, reflect) const foo := t[option::some "world!", option::none] @@ -18,4 +18,6 @@ const test2 := match bar { map::having ["is_alive" = true, "greeting" = foo] => foo } -const main := test2 ++ ", " ++ test1 +const tests := test2 ++ ", " ++ test1 + +const main := conv::to_string bar diff --git a/rustfmt.toml b/rustfmt.toml index a7a1b63..7746353 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -14,10 +14,10 @@ wrap_comments = true overflow_delimited_expr = true use_small_heuristics = "Max" fn_single_line = true +where_single_line = true # literals hex_literal_case = "Lower" -format_strings = true # delimiters match_arm_blocks = false diff --git a/src/bin/features/macro_debug.rs b/src/bin/features/macro_debug.rs new file mode 100644 index 0000000..1aae14a --- /dev/null +++ b/src/bin/features/macro_debug.rs @@ -0,0 +1,62 @@ +use itertools::Itertools; +use orchidlang::facade::macro_runner::MacroRunner; +use orchidlang::libs::std::exit_status::ExitStatus; +use orchidlang::name::Sym; + +use crate::cli::cmd_prompt; + +/// A little utility to step through the resolution of a macro set +pub fn main(macro_runner: MacroRunner, sym: Sym) -> ExitStatus { + let outname = sym.iter().join("::"); + let (mut code, location) = match macro_runner.consts.get(&sym) { + Some(rep) => (rep.value.clone(), rep.range.clone()), + None => { + let valid = macro_runner.consts.keys(); + let valid_str = valid.map(|t| t.iter().join("::")).join("\n\t"); + eprintln!("Symbol {outname} not found\nvalid symbols: \n\t{valid_str}\n"); + return ExitStatus::Failure; + }, + }; + print!("Debugging macros in {outname} defined at {location}"); + println!("\nInitial state: {code}"); + // print_for_debug(&code); + let mut steps = macro_runner.step(sym).enumerate(); + loop { + let (cmd, _) = cmd_prompt("\ncmd> ").unwrap(); + match cmd.trim() { + "" | "n" | "next" => match steps.next() { + None => print!("Halted"), + Some((idx, c)) => { + code = c; + print!("Step {idx}: {code}"); + }, + }, + "p" | "print" => { + let glossary = code.value.collect_names(); + let gl_str = glossary.iter().map(|t| t.iter().join("::")).join(", "); + print!("code: {code}\nglossary: {gl_str}") + }, + "d" | "dump" => print!("Rules: {}", macro_runner.repo), + "q" | "quit" => return ExitStatus::Success, + "complete" => { + match steps.last() { + Some((idx, c)) => print!("Step {idx}: {c}"), + None => print!("Already halted"), + } + return ExitStatus::Success; + }, + "h" | "help" => print!( + "Available commands: + \t, n, next\t\ttake a step + \tp, print\t\tprint the current state + \td, dump\t\tprint the rule table + \tq, quit\t\texit + \th, help\t\tprint this text" + ), + _ => { + print!("unrecognized command \"{}\", try \"help\"", cmd); + continue; + }, + } + } +} diff --git a/src/bin/features/mod.rs b/src/bin/features/mod.rs new file mode 100644 index 0000000..fc9f03b --- /dev/null +++ b/src/bin/features/mod.rs @@ -0,0 +1,2 @@ +pub mod macro_debug; +pub mod print_project; diff --git a/src/bin/features/print_project.rs b/src/bin/features/print_project.rs new file mode 100644 index 0000000..abcf788 --- /dev/null +++ b/src/bin/features/print_project.rs @@ -0,0 +1,59 @@ +use itertools::Itertools; +use orchidlang::pipeline::project::{ItemKind, ProjItem, ProjectMod}; +use orchidlang::tree::{ModEntry, ModMember}; + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub struct ProjPrintOpts { + pub width: u16, + pub hide_locations: bool, +} + +fn indent(amount: u16) -> String { " ".repeat(amount.into()) } + +pub fn print_proj_mod( + module: &ProjectMod, + lvl: u16, + opts: ProjPrintOpts, +) -> String { + let mut acc = String::new(); + let tab = indent(lvl); + for (key, ModEntry { member, x }) in &module.entries { + let mut line_acc = String::new(); + for c in &x.comments { + line_acc += &format!("{tab}, --[|{}|]--\n", c); + } + if x.exported { + line_acc += &format!("{tab}export "); + } else { + line_acc += &tab + } + match member { + ModMember::Sub(module) => { + line_acc += &format!("module {key} {{\n"); + line_acc += &print_proj_mod(module, lvl + 1, opts); + line_acc += &format!("{tab}}}"); + }, + ModMember::Item(ProjItem { kind: ItemKind::None }) => { + line_acc += &format!("keyword {key}"); + }, + ModMember::Item(ProjItem { kind: ItemKind::Alias(tgt) }) => { + line_acc += &format!("alias {key} => {tgt}"); + }, + ModMember::Item(ProjItem { kind: ItemKind::Const(val) }) => { + line_acc += &format!("const {key} := {val}"); + }, + } + if !x.locations.is_empty() && !opts.hide_locations { + let locs = x.locations.iter().map(|l| l.to_string()).join(", "); + let line_len = line_acc.split('\n').last().unwrap().len(); + match usize::from(opts.width).checked_sub(locs.len() + line_len + 4) { + Some(padding) => line_acc += &" ".repeat(padding), + None => line_acc += &format!("\n{tab} @ "), + } + line_acc += &locs; + } + line_acc += "\n"; + acc += &line_acc + } + acc +} diff --git a/src/bin/orcx.rs b/src/bin/orcx.rs index 3260e07..5fb49d7 100644 --- a/src/bin/orcx.rs +++ b/src/bin/orcx.rs @@ -1,39 +1,81 @@ mod cli; +mod features; use std::fs::File; use std::io::BufReader; -use std::path::PathBuf; +use std::num::NonZeroUsize; +use std::path::{Path, PathBuf}; use std::process::ExitCode; +use std::thread::available_parallelism; -use clap::Parser; +use clap::{Parser, Subcommand}; +use hashbrown::{HashMap, HashSet}; use itertools::Itertools; -use orchidlang::facade::{Environment, PreMacro}; -use orchidlang::systems::asynch::AsynchSystem; -use orchidlang::systems::stl::{ExitStatus, StlConfig}; -use orchidlang::systems::{directfs, io, scheduler}; -use orchidlang::{ast, interpreted, interpreter, Interner, Sym, VName}; +use orchidlang::error::{ProjectError, ProjectErrorObj, ProjectResult}; +use orchidlang::facade::loader::Loader; +use orchidlang::facade::macro_runner::MacroRunner; +use orchidlang::facade::merge_trees::merge_trees; +use orchidlang::facade::process::Process; +use orchidlang::foreign::inert::Inert; +use orchidlang::interpreter::context::Halt; +use orchidlang::interpreter::nort; +use orchidlang::libs::asynch::system::AsynchSystem; +use orchidlang::libs::directfs::DirectFS; +use orchidlang::libs::io::{IOService, Stream}; +use orchidlang::libs::scheduler::system::SeqScheduler; +use orchidlang::libs::std::exit_status::ExitStatus; +use orchidlang::libs::std::std_system::StdConfig; +use orchidlang::location::{CodeGenInfo, CodeLocation}; +use orchidlang::name::Sym; +use orchidlang::tree::{ModMemberRef, TreeTransforms}; +use rayon::prelude::ParallelIterator; +use rayon::slice::ParallelSlice; -use crate::cli::cmd_prompt; +use crate::features::macro_debug; +use crate::features::print_project::{print_proj_mod, ProjPrintOpts}; +#[derive(Subcommand, Debug)] +enum Command { + /// Run unit tests, any constant annotated --[[ test ]]-- + Test { + /// Specify an exact test to run + #[arg(long)] + only: Option, + #[arg(long, short)] + threads: Option, + #[arg(long)] + system: Option, + }, + #[command(arg_required_else_help = true)] + MacroDebug { + symbol: String, + }, + ListMacros, + ProjectTree { + #[arg(long, default_value_t = false)] + hide_locations: bool, + #[arg(long)] + width: Option, + }, +} /// Orchid interpreter #[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] +#[command(name = "Orchid Executor")] +#[command(author = "Lawrence Bethlenfalvy ")] +#[command(long_about = Some("Execute Orchid projects from the file system"))] struct Args { /// Folder containing main.orc or the manually specified entry module #[arg(short, long, default_value = ".")] pub dir: String, - /// Entrypoint for the interpreter - #[arg(short, long, default_value = "main::main")] - pub main: String, + /// Alternative entrypoint for the interpreter + #[arg(short, long)] + pub main: Option, /// Maximum number of steps taken by the macro executor #[arg(long, default_value_t = 10_000)] pub macro_limit: usize, - /// Print the parsed ruleset and exit - #[arg(long)] - pub list_macros: bool, - /// Step through the macro execution process in the specified symbol - #[arg(long, default_value = "")] - pub macro_debug: String, + + #[command(subcommand)] + pub command: Option, } impl Args { /// Validate the project directory and the @@ -42,21 +84,25 @@ impl Args { if !dir_path.is_dir() { return Err(format!("{} is not a directory", dir_path.display())); } - let segs = self.main.split("::").collect::>(); + let segs = match &self.main { + Some(s) => s.split("::").collect::>(), + None => match File::open("./main.orc") { + Ok(_) => return Ok(()), + Err(e) => return Err(format!("Cannot open './main.orc'\n{e}")), + }, + }; if segs.len() < 2 { return Err("Entry point too short".to_string()); - } - let (pathsegs, _) = segs.split_at(segs.len() - 1); + }; + let (_, pathsegs) = segs.split_last().unwrap(); let mut possible_files = pathsegs.iter().scan(dir_path, |path, seg| { path.push(seg); Some(path.with_extension("orc")) }); if possible_files.all(|p| File::open(p).is_err()) { - return Err(format!( - "{} not found in {}", - pathsegs.join("::"), - PathBuf::from(&self.dir).display() - )); + let out_path = pathsegs.join("::"); + let pbuf = PathBuf::from(&self.dir); + return Err(format!("{out_path} not found in {}", pbuf.display())); } Ok(()) } @@ -64,121 +110,216 @@ impl Args { pub fn chk_proj(&self) -> Result<(), String> { self.chk_dir_main() } } -#[must_use] -pub fn to_vname(data: &str, i: &Interner) -> VName { - data.split("::").map(|s| i.i(s)).collect::>() -} - -fn print_for_debug(e: &ast::Expr) { - print!( - "code: {}\nglossary: {}", - e, - (e.value.collect_names().into_iter()) - .map(|t| t.iter().join("::")) - .join(", ") - ) -} - -/// A little utility to step through the resolution of a macro set -pub fn macro_debug(premacro: PreMacro, sym: Sym) -> ExitCode { - let (mut code, location) = (premacro.consts.get(&sym)) - .unwrap_or_else(|| { - panic!( - "Symbol {} not found\nvalid symbols: \n\t{}\n", - sym.iter().join("::"), - (premacro.consts.keys()).map(|t| t.iter().join("::")).join("\n\t") - ) - }) - .clone(); - println!( - "Debugging macros in {} defined at {}. - Initial state: ", - sym.iter().join("::"), - location - ); - print_for_debug(&code); - let mut steps = premacro.step(sym).enumerate(); - loop { - let (cmd, _) = cmd_prompt("\ncmd> ").unwrap(); - match cmd.trim() { - "" | "n" | "next" => - if let Some((idx, c)) = steps.next() { - code = c; - print!("Step {idx}: "); - print_for_debug(&code); - } else { - print!("Halted") - }, - "p" | "print" => print_for_debug(&code), - "d" | "dump" => print!("Rules: {}", premacro.repo), - "q" | "quit" => return ExitCode::SUCCESS, - "complete" => { - if let Some((idx, c)) = steps.last() { - code = c; - print!("Step {idx}: "); - print_for_debug(&code); - } else { - print!("Already halted") - } - return ExitCode::SUCCESS; - }, - "h" | "help" => print!( - "Available commands: - \t, n, next\t\ttake a step - \tp, print\t\tprint the current state - \td, dump\t\tprint the rule table - \tq, quit\t\texit - \th, help\t\tprint this text" - ), - _ => { - print!("unrecognized command \"{}\", try \"help\"", cmd); - continue; +macro_rules! unwrap_exit { + ($param:expr) => { + match $param { + Ok(v) => v, + Err(e) => { + eprintln!("{e}"); + return ExitCode::FAILURE; }, } + }; +} + +pub fn with_std_proc( + dir: &Path, + macro_limit: usize, + f: impl for<'a> FnOnce(Process<'a>) -> ProjectResult, +) -> ProjectResult { + with_std_env(|env| { + let mr = MacroRunner::new(&env.load_dir(dir.to_owned())?)?; + let source_syms = mr.run_macros(Some(macro_limit))?; + let consts = merge_trees(source_syms, env.systems())?; + let proc = Process::new(consts, env.handlers()); + f(proc) + }) +} + +// TODO +pub fn run_test(proc: &mut Process, name: Sym) -> ProjectResult<()> { Ok(()) } +pub fn run_tests( + dir: &Path, + macro_limit: usize, + threads: Option, + tests: &[Sym], +) -> ProjectResult<()> { + with_std_proc(dir, macro_limit, |proc| proc.validate_refs())?; + let threads = threads + .or_else(|| available_parallelism().ok().map(NonZeroUsize::into)) + .unwrap_or(1); + rayon::ThreadPoolBuilder::new().num_threads(threads).build_global().unwrap(); + let batch_size = tests.len().div_ceil(threads); + let errors = tests + .par_chunks(batch_size) + .map(|tests| { + let res = with_std_proc(dir, macro_limit, |mut proc| { + let mut errors = HashMap::new(); + for test in tests { + if let Err(e) = run_test(&mut proc, test.clone()) { + errors.insert(test.clone(), e); + } + } + Ok(errors) + }); + res.expect("Tested earlier") + }) + .reduce(HashMap::new, |l, r| l.into_iter().chain(r).collect()); + if errors.is_empty() { Ok(()) } else { Err(TestsFailed(errors).pack()) } +} + +pub struct TestsFailed(HashMap); +impl ProjectError for TestsFailed { + const DESCRIPTION: &'static str = "Various tests failed"; + fn message(&self) -> String { + format!( + "{} tests failed. Errors:\n{}", + self.0.len(), + self.0.iter().map(|(k, e)| format!("In {k}, {e}")).join("\n") + ) } } +fn get_tree_tests(dir: &Path) -> ProjectResult> { + with_std_env(|env| { + env.load_dir(dir.to_owned()).map(|tree| { + (tree.all_consts().into_iter()) + .filter(|(_, rep)| rep.comments.iter().any(|s| s.trim() == "test")) + .map(|(k, _)| k.clone()) + .collect::>() + }) + }) +} + +pub fn with_std_env(cb: impl for<'a> FnOnce(Loader<'a>) -> T) -> T { + let mut asynch = AsynchSystem::new(); + let scheduler = SeqScheduler::new(&mut asynch); + let std_streams = [ + ("stdin", Stream::Source(BufReader::new(Box::new(std::io::stdin())))), + ("stdout", Stream::Sink(Box::new(std::io::stdout()))), + ("stderr", Stream::Sink(Box::new(std::io::stderr()))), + ]; + let env = Loader::new() + .add_system(StdConfig { impure: true }) + .add_system(asynch) + .add_system(scheduler.clone()) + .add_system(IOService::new(scheduler.clone(), std_streams)) + .add_system(DirectFS::new(scheduler)); + cb(env) +} + pub fn main() -> ExitCode { let args = Args::parse(); - args.chk_proj().unwrap_or_else(|e| panic!("{e}")); - let dir = PathBuf::try_from(args.dir).unwrap(); - let i = Interner::new(); - let main = to_vname(&args.main, &i); - let mut asynch = AsynchSystem::new(); - let scheduler = scheduler::SeqScheduler::new(&mut asynch); - let std_streams = [ - ("stdin", io::Stream::Source(BufReader::new(Box::new(std::io::stdin())))), - ("stdout", io::Stream::Sink(Box::new(std::io::stdout()))), - // ("stderr", io::Stream::Sink(Box::new(std::io::stderr()))), - ]; - let env = Environment::new(&i) - .add_system(StlConfig { impure: true }) - .add_system(asynch) - .add_system(scheduler.clone()) - .add_system(io::Service::new(scheduler.clone(), std_streams)) - .add_system(directfs::DirectFS::new(scheduler)); - let premacro = env.load_dir(&dir, &main).unwrap(); - if args.list_macros { - println!("Parsed rules: {}", premacro.repo); - return ExitCode::SUCCESS; - } - if !args.macro_debug.is_empty() { - let sym = i.i(&to_vname(&args.macro_debug, &i)); - return macro_debug(premacro, sym); - } - let mut proc = premacro.build_process(Some(args.macro_limit)).unwrap(); - proc.validate_refs().unwrap(); - let main = interpreted::Clause::Constant(i.i(&main)).wrap(); - let ret = proc.run(main, None).unwrap(); - let interpreter::Return { state, inert, .. } = ret; - drop(proc); - assert!(inert, "Gas is not used, only inert data should be yielded"); - match state.clone().downcast::() { - Ok(ExitStatus::Success) => ExitCode::SUCCESS, - Ok(ExitStatus::Failure) => ExitCode::FAILURE, - Err(_) => { - println!("{}", state.expr().clause); + unwrap_exit!(args.chk_proj()); + let dir = PathBuf::from(args.dir); + let main = args.main.map_or_else( + || Sym::literal("tree::main::main"), + |main| Sym::parse(&main).expect("--main cannot be empty"), + ); + + // subcommands + match args.command { + Some(Command::ListMacros) => with_std_env(|env| { + let tree = unwrap_exit!(env.load_main(dir, main)); + let mr = unwrap_exit!(MacroRunner::new(&tree)); + println!("Parsed rules: {}", mr.repo); + ExitCode::SUCCESS + }), + Some(Command::ProjectTree { hide_locations, width }) => { + let tree = unwrap_exit!(with_std_env(|env| env.load_main(dir, main))); + let w = width.or_else(|| termsize::get().map(|s| s.cols)).unwrap_or(74); + let print_opts = ProjPrintOpts { width: w, hide_locations }; + println!("Project tree: {}", print_proj_mod(&tree.0, 0, print_opts)); ExitCode::SUCCESS }, + Some(Command::MacroDebug { symbol }) => with_std_env(|env| { + let tree = unwrap_exit!(env.load_main(dir, main)); + let symbol = Sym::parse(&symbol).expect("macro-debug needs an argument"); + macro_debug::main(unwrap_exit!(MacroRunner::new(&tree)), symbol).code() + }), + Some(Command::Test { only: Some(_), threads: Some(_), .. }) => { + eprintln!( + "Each test case runs in a single thread. + --only and --threads cannot both be specified" + ); + ExitCode::FAILURE + }, + Some(Command::Test { only: Some(_), system: Some(_), .. }) => { + eprintln!( + "Conflicting test filters applied. --only runs a single test by + symbol name, while --system runs all tests in a system" + ); + ExitCode::FAILURE + }, + Some(Command::Test { only: None, threads, system: None }) => { + let tree_tests = unwrap_exit!(get_tree_tests(&dir)); + unwrap_exit!(run_tests(&dir, args.macro_limit, threads, &tree_tests)); + ExitCode::SUCCESS + }, + Some(Command::Test { only: Some(symbol), threads: None, system: None }) => { + let symbol = Sym::parse(&symbol).expect("Test needs an argument"); + unwrap_exit!(run_tests(&dir, args.macro_limit, Some(1), &[symbol])); + ExitCode::SUCCESS + }, + Some(Command::Test { only: None, threads, system: Some(system) }) => { + let subtrees = unwrap_exit!(with_std_env(|env| { + match env.systems().find(|s| s.name == system) { + None => Err(format!("System {system} not found")), + Some(sys) => { + let mut paths = HashSet::new(); + sys.code.search_all((), |path, node, ()| { + if matches!(node, ModMemberRef::Item(_)) { + let name = Sym::new(path.unreverse()) + .expect("Empty path means global file"); + paths.insert(name); + } + }); + Ok(paths) + }, + } + })); + let in_subtrees = + |sym: Sym| subtrees.iter().any(|sub| sym[..].starts_with(&sub[..])); + let tests = unwrap_exit!(with_std_env(|env| -> ProjectResult<_> { + let tree = env.load_main(dir.clone(), main.clone())?; + let mr = MacroRunner::new(&tree)?; + let src_consts = mr.run_macros(Some(args.macro_limit))?; + let consts = merge_trees(src_consts, env.systems())?; + let test_names = (consts.into_iter()) + .filter(|(k, v)| { + in_subtrees(k.clone()) + && v.comments.iter().any(|c| c.trim() == "test") + }) + .map(|p| p.0) + .collect_vec(); + Ok(test_names) + })); + eprintln!("Running {} tests", tests.len()); + unwrap_exit!(run_tests(&dir, args.macro_limit, threads, &tests)); + eprintln!("All tests pass"); + ExitCode::SUCCESS + }, + None => with_std_env(|env| { + let tree = unwrap_exit!(env.load_main(dir, main.clone())); + let mr = unwrap_exit!(MacroRunner::new(&tree)); + let src_consts = unwrap_exit!(mr.run_macros(Some(args.macro_limit))); + let consts = unwrap_exit!(merge_trees(src_consts, env.systems())); + let mut proc = Process::new(consts, env.handlers()); + unwrap_exit!(proc.validate_refs()); + let main = nort::Clause::Constant(main.clone()) + .to_expr(CodeLocation::Gen(CodeGenInfo::no_details("entrypoint"))); + let ret = unwrap_exit!(proc.run(main, None)); + let Halt { state, inert, .. } = ret; + drop(proc); + assert!(inert, "Gas is not used, only inert data should be yielded"); + match state.clone().downcast() { + Ok(Inert(ExitStatus::Success)) => ExitCode::SUCCESS, + Ok(Inert(ExitStatus::Failure)) => ExitCode::FAILURE, + Err(_) => { + println!("{}", state.clause); + ExitCode::SUCCESS + }, + } + }), } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..5c5be40 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,185 @@ +//! Abstractions for handling various code-related errors under a common trait +//! object. + +use core::fmt; +use std::any::Any; +use std::fmt::{Debug, Display}; +use std::sync::Arc; + +use dyn_clone::{clone_box, DynClone}; + +use crate::location::CodeLocation; +use crate::utils::boxed_iter::{box_once, BoxedIter}; +#[allow(unused)] // for doc +use crate::virt_fs::CodeNotFound; + +/// A point of interest in resolving the error, such as the point where +/// processing got stuck, a command that is likely to be incorrect +pub struct ErrorPosition { + /// The suspected location + pub location: CodeLocation, + /// Any information about the role of this location + pub message: Option, +} +impl From for ErrorPosition { + fn from(location: CodeLocation) -> Self { Self { location, message: None } } +} + +/// Errors addressed to the developer which are to be resolved with +/// code changes +pub trait ProjectError: Sized + Send + Sync + 'static { + /// A general description of this type of error + const DESCRIPTION: &'static str; + /// A formatted message that includes specific parameters + #[must_use] + fn message(&self) -> String { self.description().to_string() } + /// Code positions relevant to this error. If you don't implement this, you + /// must implement [ProjectError::one_position] + #[must_use] + fn positions(&self) -> impl IntoIterator { + box_once(ErrorPosition { location: self.one_position(), message: None }) + } + /// Short way to provide a single location. If you don't implement this, you + /// must implement [ProjectError::positions] + #[must_use] + fn one_position(&self) -> CodeLocation { unimplemented!() } + /// Convert the error into an `Arc` to be able to + /// handle various errors together + #[must_use] + fn pack(self) -> ProjectErrorObj { Arc::new(self) } +} + +/// Object-safe version of [ProjectError]. Implement that instead of this. +pub trait DynProjectError: Send + Sync { + /// Access type information about this error + #[must_use] + fn as_any(&self) -> &dyn Any; + /// A general description of this type of error + #[must_use] + fn description(&self) -> &str; + /// A formatted message that includes specific parameters + #[must_use] + fn message(&self) -> String { self.description().to_string() } + /// Code positions relevant to this error. + #[must_use] + fn positions(&self) -> BoxedIter; +} + +impl DynProjectError for T +where T: ProjectError +{ + fn as_any(&self) -> &dyn Any { self } + fn description(&self) -> &str { T::DESCRIPTION } + fn message(&self) -> String { ProjectError::message(self) } + fn positions(&self) -> BoxedIter { + Box::new(ProjectError::positions(self).into_iter()) + } +} + +impl Display for dyn DynProjectError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let description = self.description(); + let message = self.message(); + let positions = self.positions().collect::>(); + writeln!(f, "Project error: {description}\n{message}")?; + if positions.is_empty() { + writeln!(f, "No locations specified")?; + } else { + for ErrorPosition { location, message } in positions { + match message { + None => writeln!(f, "@{location}"), + Some(msg) => writeln!(f, "@{location}: {msg}"), + }? + } + } + Ok(()) + } +} + +impl Debug for dyn DynProjectError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self}") + } +} + +/// Type-erased [ProjectError] implementor through the [DynProjectError] +/// object-trait +pub type ProjectErrorObj = Arc; +/// Alias for a result with an error of [Rc] of [ProjectError] trait object. +/// This is the type of result most commonly returned by pre-run operations. +pub type ProjectResult = Result; + +/// A trait for error types that are only missing a location. Do not depend on +/// this trait, refer to [DynErrorSansLocation] instead. +pub trait ErrorSansLocation: Clone + Sized + Send + Sync + 'static { + /// General description of the error condition + const DESCRIPTION: &'static str; + /// Specific description of the error including code fragments or concrete + /// data if possible + fn message(&self) -> String { Self::DESCRIPTION.to_string() } + /// Convert the error to a type-erased structure for handling on shared + /// channels + fn pack(self) -> ErrorSansLocationObj { Box::new(self) } +} + +/// Object-safe equivalent to [ErrorSansLocation]. Implement that one instead of +/// this. Typically found as [ErrorSansLocationObj] +pub trait DynErrorSansLocation: Any + Send + Sync + DynClone { + /// Allow to downcast the base object to distinguish between various errors. + /// The main intended purpose is to trigger a fallback when [CodeNotFound] is + /// encountered, but the possibilities are not limited to that. + fn as_any_ref(&self) -> &dyn Any; + /// Generic description of the error condition + fn description(&self) -> &str; + /// Specific description of this particular error + fn message(&self) -> String; +} + +/// Type-erased [ErrorSansLocation] implementor through the object-trait +/// [DynErrorSansLocation]. This can be turned into a [ProjectErrorObj] with +/// [bundle_location]. +pub type ErrorSansLocationObj = Box; +/// A generic project result without location +pub type ResultSansLocation = Result; + +impl DynErrorSansLocation for T { + fn description(&self) -> &str { Self::DESCRIPTION } + fn message(&self) -> String { self.message() } + fn as_any_ref(&self) -> &dyn Any { self } +} +impl Clone for ErrorSansLocationObj { + fn clone(&self) -> Self { clone_box(&**self) } +} +impl DynErrorSansLocation for ErrorSansLocationObj { + fn description(&self) -> &str { (**self).description() } + fn message(&self) -> String { (**self).message() } + fn as_any_ref(&self) -> &dyn Any { (**self).as_any_ref() } +} +impl Display for ErrorSansLocationObj { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{}\nLocation missing from error", self.message()) + } +} +impl Debug for ErrorSansLocationObj { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self}") + } +} + +struct LocationBundle(CodeLocation, Box); +impl DynProjectError for LocationBundle { + fn as_any(&self) -> &dyn Any { self.1.as_any_ref() } + fn description(&self) -> &str { self.1.description() } + fn message(&self) -> String { self.1.message() } + fn positions(&self) -> BoxedIter { + box_once(ErrorPosition { location: self.0.clone(), message: None }) + } +} + +/// Add a location to an [ErrorSansLocation] +pub fn bundle_location( + location: &CodeLocation, + details: &dyn DynErrorSansLocation, +) -> ProjectErrorObj { + Arc::new(LocationBundle(location.clone(), clone_box(details))) +} diff --git a/src/error/assertion_error.rs b/src/error/assertion_error.rs deleted file mode 100644 index 03097c7..0000000 --- a/src/error/assertion_error.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::fmt::Display; -use std::sync::Arc; - -use crate::foreign::{ExternError, XfnResult}; -use crate::Location; - -/// Some expectation (usually about the argument types of a function) did not -/// hold. -#[derive(Clone)] -pub struct AssertionError { - location: Location, - message: &'static str, -} - -impl AssertionError { - /// Construct, upcast and wrap in a Result that never succeeds for easy - /// short-circuiting - pub fn fail(location: Location, message: &'static str) -> XfnResult { - Err(Self::ext(location, message)) - } - - /// Construct and upcast to [ExternError] - pub fn ext( - location: Location, - message: &'static str, - ) -> Arc { - Self { location, message }.into_extern() - } -} - -impl Display for AssertionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Error: expected {}", self.message)?; - if self.location != Location::Unknown { - write!(f, " at {}", self.location)?; - } - Ok(()) - } -} - -impl ExternError for AssertionError {} diff --git a/src/error/conflicting_roles.rs b/src/error/conflicting_roles.rs deleted file mode 100644 index e0e184e..0000000 --- a/src/error/conflicting_roles.rs +++ /dev/null @@ -1,31 +0,0 @@ -use itertools::Itertools; - -use super::{ErrorPosition, ProjectError}; -use crate::utils::BoxedIter; -use crate::{Location, VName}; - -/// Error raised if the same name ends up assigned to more than one thing. -/// A name in Orchid has exactly one meaning, either a value or a module. -pub struct ConflictingRoles { - /// Name assigned to multiple things - pub name: VName, - /// Location of at least two occurrences - pub locations: Vec, -} -impl ProjectError for ConflictingRoles { - fn description(&self) -> &str { - "The same name is assigned multiple times to conflicting items" - } - fn message(&self) -> String { - format!( - "{} has multiple conflicting meanings", - self.name.iter().map(|t| t.as_str()).join("::") - ) - } - fn positions(&self) -> BoxedIter { - Box::new( - (self.locations.iter()) - .map(|l| ErrorPosition { location: l.clone(), message: None }), - ) - } -} diff --git a/src/error/import_all.rs b/src/error/import_all.rs deleted file mode 100644 index 3730860..0000000 --- a/src/error/import_all.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::sync::Arc; - -use itertools::Itertools; - -use super::ProjectError; -use crate::representations::location::Location; -use crate::VName; - -/// Error produced for the statement `import *` -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ImportAll { - /// The file containing the offending import - pub offender_file: Arc, - /// The module containing the offending import - pub offender_mod: Arc, -} -impl ProjectError for ImportAll { - fn description(&self) -> &str { "a top-level glob import was used" } - fn message(&self) -> String { - format!("{} imports *", self.offender_mod.iter().join("::")) - } - - fn one_position(&self) -> Location { - Location::File(self.offender_file.clone()) - } -} diff --git a/src/error/mod.rs b/src/error/mod.rs deleted file mode 100644 index a45c10c..0000000 --- a/src/error/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Various errors the pipeline can produce -mod assertion_error; -mod conflicting_roles; -mod import_all; -mod no_targets; -mod not_exported; -mod project_error; -mod runtime_error; -mod too_many_supers; -mod unexpected_directory; -mod visibility_mismatch; - -pub use assertion_error::AssertionError; -pub use conflicting_roles::ConflictingRoles; -pub use import_all::ImportAll; -pub use no_targets::NoTargets; -pub use not_exported::NotExported; -pub use project_error::{ErrorPosition, ProjectError, ProjectResult}; -pub use runtime_error::RuntimeError; -pub use too_many_supers::TooManySupers; -pub use unexpected_directory::UnexpectedDirectory; -pub use visibility_mismatch::VisibilityMismatch; diff --git a/src/error/no_targets.rs b/src/error/no_targets.rs deleted file mode 100644 index 049a4d4..0000000 --- a/src/error/no_targets.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::{ErrorPosition, ProjectError}; -#[allow(unused)] // for doc -use crate::parse_layer; -use crate::utils::boxed_iter::box_empty; -use crate::utils::BoxedIter; - -/// Error produced when [parse_layer] is called without targets. This function -/// produces an error instead of returning a straightforward empty tree because -/// the edge case of no targets is often an error and should generally be -/// handled explicitly -#[derive(Debug)] -pub struct NoTargets; - -impl ProjectError for NoTargets { - fn description(&self) -> &str { - "No targets were specified for layer parsing" - } - - fn positions(&self) -> BoxedIter { box_empty() } -} diff --git a/src/error/not_exported.rs b/src/error/not_exported.rs deleted file mode 100644 index 09d12e2..0000000 --- a/src/error/not_exported.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::sync::Arc; - -use super::{ErrorPosition, ProjectError}; -use crate::representations::location::Location; -use crate::utils::BoxedIter; -use crate::{Interner, VName}; - -/// An import refers to a symbol which exists but is not exported. -#[derive(Debug)] -pub struct NotExported { - /// The containing file - files are always exported - pub file: VName, - /// The path leading to the unexported module - pub subpath: VName, - /// The offending file - pub referrer_file: VName, - /// The module containing the offending import - pub referrer_subpath: VName, -} -impl ProjectError for NotExported { - fn description(&self) -> &str { - "An import refers to a symbol that exists but isn't exported" - } - fn positions(&self) -> BoxedIter { - Box::new( - [ - ErrorPosition { - location: Location::File(Arc::new(self.file.clone())), - message: Some(format!( - "{} isn't exported", - Interner::extern_all(&self.subpath).join("::") - )), - }, - ErrorPosition { - location: Location::File(Arc::new(self.referrer_file.clone())), - message: Some(format!( - "{} cannot see this symbol", - Interner::extern_all(&self.referrer_subpath).join("::") - )), - }, - ] - .into_iter(), - ) - } -} diff --git a/src/error/project_error.rs b/src/error/project_error.rs deleted file mode 100644 index 8ec041d..0000000 --- a/src/error/project_error.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::fmt::{Debug, Display}; -use std::rc::Rc; - -use crate::representations::location::Location; -use crate::utils::boxed_iter::box_once; -use crate::utils::BoxedIter; - -/// A point of interest in resolving the error, such as the point where -/// processing got stuck, a command that is likely to be incorrect -pub struct ErrorPosition { - /// The suspected location - pub location: Location, - /// Any information about the role of this location - pub message: Option, -} - -/// Errors addressed to the developer which are to be resolved with -/// code changes -pub trait ProjectError { - /// A general description of this type of error - #[must_use] - fn description(&self) -> &str; - /// A formatted message that includes specific parameters - #[must_use] - fn message(&self) -> String { self.description().to_string() } - /// Code positions relevant to this error. If you don't implement this, you - /// must implement [ProjectError::one_position] - #[must_use] - fn positions(&self) -> BoxedIter { - box_once(ErrorPosition { location: self.one_position(), message: None }) - } - /// Short way to provide a single location. If you don't implement this, you - /// must implement [ProjectError::positions] - #[must_use] - fn one_position(&self) -> Location { unimplemented!() } - /// Convert the error into an `Rc` to be able to - /// handle various errors together - #[must_use] - fn rc(self) -> Rc - where - Self: Sized + 'static, - { - Rc::new(self) - } -} - -impl Display for dyn ProjectError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let description = self.description(); - let message = self.message(); - let positions = self.positions(); - writeln!(f, "Project error: {description}\n{message}")?; - for ErrorPosition { location, message } in positions { - writeln!( - f, - "@{location}: {}", - message.unwrap_or("location of interest".to_string()) - )? - } - Ok(()) - } -} - -impl Debug for dyn ProjectError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self}") - } -} - -/// Alias for a result with an error of [Rc] of [ProjectError] trait object. -/// This is the type of result most commonly returned by pre-run operations. -pub type ProjectResult = Result>; diff --git a/src/error/too_many_supers.rs b/src/error/too_many_supers.rs deleted file mode 100644 index 626d194..0000000 --- a/src/error/too_many_supers.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::ProjectError; -use crate::representations::location::Location; -use crate::{Interner, VName}; - -/// Error produced when an import path starts with more `super` segments -/// than the current module's absolute path -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct TooManySupers { - /// The offending import path - pub path: VName, - /// The faulty import statement - pub location: Location, -} -impl ProjectError for TooManySupers { - fn description(&self) -> &str { - "an import path starts with more `super` segments than the current \ - module's absolute path" - } - fn message(&self) -> String { - format!( - "path {} contains too many `super` steps.", - Interner::extern_all(&self.path).join("::"), - ) - } - - fn one_position(&self) -> Location { self.location.clone() } -} diff --git a/src/error/unexpected_directory.rs b/src/error/unexpected_directory.rs deleted file mode 100644 index 636c121..0000000 --- a/src/error/unexpected_directory.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::sync::Arc; - -use super::ProjectError; -use crate::{Interner, Location, VName}; - -/// Produced when a stage that deals specifically with code encounters -/// a path that refers to a directory -#[derive(Debug)] -pub struct UnexpectedDirectory { - /// Path to the offending collection - pub path: VName, -} -impl ProjectError for UnexpectedDirectory { - fn description(&self) -> &str { - "A stage that deals specifically with code encountered a path that refers \ - to a directory" - } - fn one_position(&self) -> crate::Location { - Location::File(Arc::new(self.path.clone())) - } - - fn message(&self) -> String { - format!( - "{} was expected to be a file but a directory was found", - Interner::extern_all(&self.path).join("/") - ) - } -} diff --git a/src/error/visibility_mismatch.rs b/src/error/visibility_mismatch.rs deleted file mode 100644 index f99c201..0000000 --- a/src/error/visibility_mismatch.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::sync::Arc; - -use super::project_error::ProjectError; -use crate::representations::location::Location; -use crate::{Interner, VName}; - -/// Multiple occurences of the same namespace with different visibility -#[derive(Debug)] -pub struct VisibilityMismatch { - /// The namespace with ambiguous visibility - pub namespace: VName, - /// The file containing the namespace - pub file: VName, -} -impl ProjectError for VisibilityMismatch { - fn description(&self) -> &str { - "Some occurences of a namespace are exported but others are not" - } - fn message(&self) -> String { - format!( - "{} is opened multiple times with different visibilities", - Interner::extern_all(&self.namespace).join("::") - ) - } - fn one_position(&self) -> Location { - Location::File(Arc::new(self.file.clone())) - } -} diff --git a/src/facade/environment.rs b/src/facade/environment.rs deleted file mode 100644 index 9fd1dc7..0000000 --- a/src/facade/environment.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::iter; -use std::path::Path; - -use hashbrown::HashMap; - -use super::system::{IntoSystem, System}; -use super::PreMacro; -use crate::error::ProjectResult; -use crate::pipeline::file_loader; -use crate::sourcefile::FileEntry; -use crate::utils::never; -use crate::{ - from_const_tree, parse_layer, vname_to_sym_tree, Interner, ProjectTree, Stok, - VName, -}; - -/// A compiled environment ready to load user code. It stores the list of -/// systems and combines with usercode to produce a [Process] -pub struct Environment<'a> { - /// [Interner] pseudo-global - pub i: &'a Interner, - systems: Vec>, -} -impl<'a> Environment<'a> { - /// Initialize a new environment - #[must_use] - pub fn new(i: &'a Interner) -> Self { Self { i, systems: Vec::new() } } - - /// Register a new system in the environment - #[must_use] - pub fn add_system<'b: 'a>(mut self, is: impl IntoSystem<'b> + 'b) -> Self { - self.systems.push(Box::new(is).into_system(self.i)); - self - } - - /// Compile the environment from the set of systems and return it directly. - /// See [#load_dir] - pub fn compile(self) -> ProjectResult> { - let Self { i, systems, .. } = self; - let mut tree = from_const_tree(HashMap::new(), &[i.i("none")]); - for sys in systems.iter() { - let system_tree = from_const_tree(sys.constants.clone(), &sys.vname(i)); - tree = ProjectTree(never::unwrap_always(tree.0.overlay(system_tree.0))); - } - let mut lexer_plugins = vec![]; - let mut line_parsers = vec![]; - let mut prelude = vec![]; - for sys in systems.iter() { - lexer_plugins.extend(sys.lexer_plugins.iter().map(|b| &**b)); - line_parsers.extend(sys.line_parsers.iter().map(|b| &**b)); - if !sys.code.is_empty() { - tree = parse_layer( - sys.code.keys().map(|sym| &sym[..]), - &|k, referrer| sys.load_file(k, referrer), - &tree, - &prelude, - &lexer_plugins, - &line_parsers, - i, - )?; - } - prelude.extend_from_slice(&sys.prelude); - } - Ok(CompiledEnv { prelude, tree, systems }) - } - - /// Load a directory from the local file system as an Orchid project. - pub fn load_dir( - self, - dir: &Path, - target: &[Stok], - ) -> ProjectResult> { - let i = self.i; - let CompiledEnv { prelude, systems, tree } = self.compile()?; - let file_cache = file_loader::mk_dir_cache(dir.to_path_buf()); - let lexer_plugins = (systems.iter()) - .flat_map(|s| s.lexer_plugins.iter().map(|b| &**b)) - .collect::>(); - let line_parsers = (systems.iter()) - .flat_map(|s| s.line_parsers.iter().map(|b| &**b)) - .collect::>(); - let vname_tree = parse_layer( - iter::once(target), - &|path, _| file_cache.find(path), - &tree, - &prelude, - &lexer_plugins, - &line_parsers, - i, - )?; - let tree = vname_to_sym_tree(vname_tree, i); - PreMacro::new(tree, systems, i) - } -} - -/// Compiled environment waiting for usercode. An intermediate step between -/// [Environment] and [Process] -pub struct CompiledEnv<'a> { - /// Namespace tree for pre-defined symbols with symbols at the leaves and - /// rules defined on the nodes - pub tree: ProjectTree, - /// Lines prepended to each usercode file - pub prelude: Vec, - /// List of systems to source handlers for the interpreter - pub systems: Vec>, -} diff --git a/src/facade/loader.rs b/src/facade/loader.rs new file mode 100644 index 0000000..6eabab9 --- /dev/null +++ b/src/facade/loader.rs @@ -0,0 +1,143 @@ +use std::path::{Path, PathBuf}; +use std::{fs, iter}; + +use intern_all::{i, Tok}; +use substack::Substack; + +use super::system::{IntoSystem, System}; +use crate::error::ProjectResult; +use crate::gen::tree::ConstTree; +use crate::interpreter::handler::HandlerTable; +use crate::location::{CodeGenInfo, CodeLocation}; +use crate::name::{Sym, VPath}; +use crate::pipeline::load_solution::{load_solution, SolutionContext}; +use crate::pipeline::project::ProjectTree; +use crate::utils::combine::Combine; +use crate::utils::sequence::Sequence; +use crate::utils::unwrap_or::unwrap_or; +use crate::virt_fs::{DeclTree, DirNode, VirtFS}; + +/// A compiled environment ready to load user code. It stores the list of +/// systems and combines with usercode to produce a [Process] +pub struct Loader<'a> { + systems: Vec>, +} +impl<'a> Loader<'a> { + /// Initialize a new environment + #[must_use] + pub fn new() -> Self { Self { systems: Vec::new() } } + + /// Retrieve the list of systems + pub fn systems(&self) -> impl Iterator> { + self.systems.iter() + } + + /// Register a new system in the environment + #[must_use] + pub fn add_system<'b: 'a>(mut self, is: impl IntoSystem<'b> + 'b) -> Self { + self.systems.push(Box::new(is).into_system()); + self + } + + /// Extract the systems from the environment + pub fn into_systems(self) -> Vec> { self.systems } + + /// Initialize an environment with a prepared list of systems + pub fn from_systems(sys: impl IntoIterator>) -> Self { + Self { systems: sys.into_iter().collect() } + } + + /// Combine the `constants` fields of all systems + pub fn constants(&self) -> ConstTree { + (self.systems()) + .try_fold(ConstTree::tree::<&str>([]), |acc, sys| { + acc.combine(sys.constants.clone()) + }) + .expect("Conflicting const trees") + } + + pub fn handlers(self) -> HandlerTable<'a> { + (self.systems.into_iter()) + .fold(HandlerTable::new(), |t, sys| t.combine(sys.handlers)) + } + + /// Compile the environment from the set of systems and return it directly. + /// See [#load_dir] + pub fn solution_ctx(&self) -> ProjectResult { + Ok(SolutionContext { + lexer_plugins: Sequence::new(|| { + self.systems().flat_map(|sys| &sys.lexer_plugins).map(|b| &**b) + }), + line_parsers: Sequence::new(|| { + self.systems().flat_map(|sys| &sys.line_parsers).map(|b| &**b) + }), + preludes: Sequence::new(|| self.systems().flat_map(|sys| &sys.prelude)), + }) + } + + /// Combine source code from all systems with the specified directory into a + /// common [VirtFS] + pub fn make_dir_tree(&self, dir: PathBuf) -> DeclTree { + let dir_node = DirNode::new(dir, ".orc").rc(); + let base = DeclTree::tree([("tree", DeclTree::leaf(dir_node))]); + (self.systems().try_fold(base, |acc, sub| acc.combine(sub.code.clone()))) + .expect("Conflicting system trees") + } + + /// Load a directory from the local file system as an Orchid project. + /// File loading proceeds along import statements and ignores all files + /// not reachable from the specified file. + pub fn load_main( + &self, + dir: PathBuf, + target: Sym, + ) -> ProjectResult { + let ctx = self.solution_ctx()?; + let tgt_loc = + CodeLocation::Gen(CodeGenInfo::no_details("facade::entrypoint")); + let root = self.make_dir_tree(dir.clone()); + let targets = iter::once((target, tgt_loc)); + let constants = self.constants().unwrap_mod(); + load_solution(ctx, targets, &constants, &root) + } + + /// Load every orchid file in a directory + pub fn load_dir(&self, dir: PathBuf) -> ProjectResult { + let ctx = self.solution_ctx()?; + let tgt_loc = + CodeLocation::Gen(CodeGenInfo::no_details("facade::entrypoint")); + let mut orc_files: Vec = Vec::new(); + find_all_orc_files(&dir, &mut orc_files, Substack::Bottom); + let root = self.make_dir_tree(dir.clone()); + let constants = self.constants().unwrap_mod(); + let targets = (orc_files.into_iter()) + .map(|p| (p.as_suffix_of(i("tree")).to_sym(), tgt_loc.clone())); + load_solution(ctx, targets, &constants, &root) + } +} + +impl<'a> Default for Loader<'a> { + fn default() -> Self { Self::new() } +} + +fn find_all_orc_files( + path: &Path, + paths: &mut Vec, + stack: Substack<'_, Tok>, +) { + assert!(path.exists(), "find_all_orc_files encountered missing path"); + if path.is_symlink() { + let path = unwrap_or!(fs::read_link(path).ok(); return); + find_all_orc_files(&path, paths, stack) + } else if path.is_file() { + if path.extension().and_then(|t| t.to_str()) == Some("orc") { + paths.push(VPath(stack.unreverse())) + } + } else if path.is_dir() { + let entries = unwrap_or!(path.read_dir().ok(); return); + for entry in entries.filter_map(Result::ok) { + let name = unwrap_or!(entry.file_name().into_string().ok(); return); + find_all_orc_files(&entry.path(), paths, stack.push(i(&name))) + } + } +} diff --git a/src/facade/macro_runner.rs b/src/facade/macro_runner.rs new file mode 100644 index 0000000..f17d608 --- /dev/null +++ b/src/facade/macro_runner.rs @@ -0,0 +1,84 @@ +use std::iter; + +use hashbrown::HashMap; + +use crate::error::{ProjectError, ProjectResult}; +use crate::location::CodeLocation; +use crate::name::Sym; +use crate::parse::parsed; +use crate::pipeline::project::{ + ConstReport, ProjectTree, +}; +use crate::rule::repository::Repo; + +pub struct MacroRunner { + /// Optimized catalog of substitution rules + pub repo: Repo, + /// Runtime code containing macro invocations + pub consts: HashMap, +} +impl MacroRunner { + pub fn new(tree: &ProjectTree) -> ProjectResult { + let rules = tree.all_rules(); + let repo = Repo::new(rules).map_err(|(rule, e)| e.to_project(&rule))?; + Ok(Self { repo, consts: tree.all_consts().into_iter().collect() }) + } + + pub fn run_macros( + &self, + timeout: Option, + ) -> ProjectResult> { + let mut symbols = HashMap::new(); + for (name, report) in self.consts.iter() { + let value = match timeout { + None => (self.repo.pass(&report.value)) + .unwrap_or_else(|| report.value.clone()), + Some(limit) => { + let (o, leftover_gas) = self.repo.long_step(&report.value, limit + 1); + match leftover_gas { + 1.. => o, + _ => { + let err = MacroTimeout { + location: CodeLocation::Source(report.range.clone()), + symbol: name.clone(), + limit, + }; + return Err(err.pack()); + }, + } + }, + }; + symbols.insert(name.clone(), ConstReport { value, ..report.clone() }); + } + Ok(symbols) + } + + /// Obtain an iterator that steps through the preprocessing of a constant + /// for debugging macros + pub fn step(&self, sym: Sym) -> impl Iterator + '_ { + let mut target = + self.consts.get(&sym).expect("Target not found").value.clone(); + iter::from_fn(move || { + target = self.repo.step(&target)?; + Some(target.clone()) + }) + } +} + +/// Error raised when a macro runs too long +#[derive(Debug)] +pub struct MacroTimeout { + location: CodeLocation, + symbol: Sym, + limit: usize, +} +impl ProjectError for MacroTimeout { + const DESCRIPTION: &'static str = "Macro execution has not halted"; + + fn message(&self) -> String { + let Self { symbol, limit, .. } = self; + format!("Macro processing in {symbol} took more than {limit} steps") + } + + fn one_position(&self) -> CodeLocation { self.location.clone() } +} diff --git a/src/facade/merge_trees.rs b/src/facade/merge_trees.rs new file mode 100644 index 0000000..632e773 --- /dev/null +++ b/src/facade/merge_trees.rs @@ -0,0 +1,61 @@ +use std::sync::Arc; + +use hashbrown::HashMap; +use never::Never; +use substack::Substack; + +use super::system::System; +use crate::error::ProjectResult; +use crate::intermediate::ast_to_ir::ast_to_ir; +use crate::intermediate::ir_to_nort::ir_to_nort; +use crate::interpreter::nort; +use crate::location::{CodeGenInfo, CodeLocation}; +use crate::name::{Sym, VPath}; +use crate::pipeline::project::ConstReport; +use crate::tree::{ModMember, ModMemberRef, TreeTransforms}; + +/// Equivalent of [crate::pipeline::project::ConstReport] for the interpreter's +/// representation, [crate::interpreter::nort]. +pub struct NortConst { + /// Comments associated with the constant which may affect its interpretation + pub comments: Vec>, + /// Location of the definition, if known + pub location: CodeLocation, + /// Value assigned to the constant + pub value: nort::Expr, +} + +/// Combine a list of symbols loaded from source and the constant trees from +/// each system. +pub fn merge_trees<'a: 'b, 'b>( + source: impl IntoIterator + 'b, + systems: impl IntoIterator> + 'b, +) -> ProjectResult + 'static> { + let mut out = HashMap::new(); + for (name, rep) in source { + out.insert(name.clone(), NortConst { + value: ir_to_nort(&ast_to_ir(rep.value, name)?), + location: CodeLocation::Source(rep.range), + comments: rep.comments, + }); + } + for sys in systems { + let const_module = sys.constants.unwrap_mod_ref(); + const_module.search_all((), |path, node, ()| { + let m = if let ModMemberRef::Mod(m) = node { m } else { return }; + for (key, ent) in &m.entries { + if let ModMember::Item(c) = &ent.member { + let path = VPath::new(path.unreverse()).as_prefix_of(key.clone()); + let location = CodeLocation::Gen(CodeGenInfo::details( + "constant from", + format!("system.name={}", sys.name), + )); + let value = c.gen_nort(location.clone()); + let crep = NortConst { value, comments: vec![], location }; + out.insert(path.to_sym(), crep); + } + } + }); + } + Ok(out) +} diff --git a/src/facade/mod.rs b/src/facade/mod.rs index 5044b89..74f7c6c 100644 --- a/src/facade/mod.rs +++ b/src/facade/mod.rs @@ -1,12 +1,8 @@ //! A simplified set of commands each grouping a large subset of the operations //! exposed by Orchid to make writing embeddings faster in the typical case. -mod environment; -mod pre_macro; -mod process; -mod system; - -pub use environment::{CompiledEnv, Environment}; -pub use pre_macro::{MacroTimeout, PreMacro}; -pub use process::Process; -pub use system::{IntoSystem, MissingSystemCode, System}; +pub mod loader; +pub mod macro_runner; +pub mod process; +pub mod system; +pub mod merge_trees; diff --git a/src/facade/pre_macro.rs b/src/facade/pre_macro.rs index c39f7eb..b9ca874 100644 --- a/src/facade/pre_macro.rs +++ b/src/facade/pre_macro.rs @@ -1,98 +1,80 @@ use std::iter; -use std::sync::Arc; use hashbrown::HashMap; +use never::Never; -use super::{Process, System}; -use crate::error::{ProjectError, ProjectResult}; -use crate::interpreter::HandlerTable; -use crate::rule::Repo; -use crate::{ - ast, ast_to_interpreted, collect_consts, collect_rules, rule, Interner, - Location, ProjectTree, Sym, +use super::process::Process; +use super::system::System; +use crate::error::{ErrorPosition, ProjectError, ProjectResult}; +use crate::intermediate::ast_to_ir::ast_to_ir; +use crate::intermediate::ir_to_nort::ir_to_nort; +use crate::interpreter::handler::HandlerTable; +use crate::location::{CodeGenInfo, CodeLocation}; +use crate::name::{Sym, VPath}; +use crate::parse::parsed; +use crate::pipeline::project::{ + collect_consts, collect_rules, ConstReport, ProjectTree, }; +use crate::rule::repository::Repo; +use crate::tree::ModMember; /// Everything needed for macro execution, and constructing the process pub struct PreMacro<'a> { /// Optimized catalog of substitution rules pub repo: Repo, /// Runtime code containing macro invocations - pub consts: HashMap, Location)>, + pub consts: HashMap, /// Libraries and plug-ins pub systems: Vec>, - /// [Interner] pseudo-global - pub i: &'a Interner, } impl<'a> PreMacro<'a> { /// Build a [PreMacro] from a source tree and system list pub fn new( - tree: ProjectTree, + tree: &ProjectTree, systems: Vec>, - i: &'a Interner, ) -> ProjectResult { - let consts = collect_consts(&tree, i); - let rules = collect_rules(&tree); - let repo = match rule::Repo::new(rules, i) { - Ok(r) => r, - Err((rule, error)) => { - return Err(error.to_project_error(&rule)); - }, - }; Ok(Self { repo, consts: (consts.into_iter()) .map(|(name, expr)| { - // Figure out the location of the constant - let location = (name.split_last()) - .and_then(|(_, path)| { - let origin = (tree.0) - .walk_ref(&[], path, false) - .unwrap_or_else(|_| panic!("path sourced from symbol names")); - (origin.extra.file.as_ref()).cloned() - }) - .map(|p| Location::File(Arc::new(p))) - .unwrap_or(Location::Unknown); + let (ent, _) = (tree.0) + .walk1_ref(&[], &name.split_last().1[..], |_| true) + .expect("path sourced from symbol names"); + let location = (ent.x.locations.first().cloned()) + .unwrap_or_else(|| CodeLocation::Source(expr.value.range.clone())); (name, (expr, location)) }) .collect(), - i, systems, }) } /// Run all macros to termination or the optional timeout. If a timeout does /// not occur, returns a process which can execute Orchid code - pub fn build_process( + pub fn run_macros( self, timeout: Option, ) -> ProjectResult> { - let Self { i, systems, repo, consts } = self; - let mut symbols = HashMap::new(); - for (name, (source, source_location)) in consts.iter() { - let unmatched = if let Some(limit) = timeout { - let (unmatched, steps_left) = repo.long_step(source, limit + 1); - if steps_left == 0 { - return Err( - MacroTimeout { - location: source_location.clone(), - symbol: name.clone(), - limit, - } - .rc(), - ); - } else { - unmatched + let Self { systems, repo, consts } = self; + for sys in systems.iter() { + let const_module = sys.constants.unwrap_mod_ref(); + let _ = const_module.search_all((), &mut |path, module, ()| { + for (key, ent) in &module.entries { + if let ModMember::Item(c) = &ent.member { + let path = VPath::new(path.unreverse()).as_prefix_of(key.clone()); + let cginfo = CodeGenInfo::details( + "constant from", + format!("system.name={}", sys.name), + ); + symbols + .insert(path.to_sym(), c.gen_nort(CodeLocation::Gen(cginfo))); + } } - } else { - repo.pass(source).unwrap_or_else(|| source.clone()) - }; - let runtree = - ast_to_interpreted(&unmatched, name.clone()).map_err(|e| e.rc())?; - symbols.insert(name.clone(), runtree); + Ok::<(), Never>(()) + }); } Ok(Process { symbols, - i, handlers: (systems.into_iter()) .fold(HandlerTable::new(), |tbl, sys| tbl.combine(sys.handlers)), }) @@ -100,8 +82,9 @@ impl<'a> PreMacro<'a> { /// Obtain an iterator that steps through the preprocessing of a constant /// for debugging macros - pub fn step(&self, sym: Sym) -> impl Iterator> + '_ { - let mut target = self.consts.get(&sym).expect("Target not found").0.clone(); + pub fn step(&self, sym: Sym) -> impl Iterator + '_ { + let mut target = + self.consts.get(&sym).expect("Target not found").0.value.clone(); iter::from_fn(move || { target = self.repo.step(&target)?; Some(target.clone()) @@ -112,20 +95,17 @@ impl<'a> PreMacro<'a> { /// Error raised when a macro runs too long #[derive(Debug)] pub struct MacroTimeout { - location: Location, + location: CodeLocation, symbol: Sym, limit: usize, } impl ProjectError for MacroTimeout { - fn description(&self) -> &str { "Macro execution has not halted" } + const DESCRIPTION: &'static str = "Macro execution has not halted"; fn message(&self) -> String { - format!( - "Macro execution during the processing of {} took more than {} steps", - self.symbol.extern_vec().join("::"), - self.limit - ) + let Self { symbol, limit, .. } = self; + format!("Macro processing in {symbol} took more than {limit} steps") } - fn one_position(&self) -> Location { self.location.clone() } + fn one_position(&self) -> CodeLocation { self.location.clone() } } diff --git a/src/facade/process.rs b/src/facade/process.rs index 56f3b9a..26e0c8a 100644 --- a/src/facade/process.rs +++ b/src/facade/process.rs @@ -1,23 +1,32 @@ use hashbrown::HashMap; use itertools::Itertools; +use super::merge_trees::NortConst; use crate::error::{ErrorPosition, ProjectError, ProjectResult}; -use crate::interpreted::{self, ExprInst}; -#[allow(unused)] // for doc -use crate::interpreter; -use crate::interpreter::{ - run_handler, Context, HandlerTable, Return, RuntimeError, -}; -use crate::{Interner, Location, Sym}; +use crate::interpreter::context::{Halt, RunContext}; +use crate::interpreter::error::RunError; +use crate::interpreter::handler::{run_handler, HandlerTable}; +use crate::interpreter::nort::{Clause, Expr}; +use crate::location::CodeLocation; +use crate::name::Sym; +use crate::utils::boxed_iter::BoxedIter; /// This struct ties the state of systems to loaded code, and allows to call /// Orchid-defined functions pub struct Process<'a> { - pub(crate) symbols: HashMap, + pub(crate) symbols: HashMap, pub(crate) handlers: HandlerTable<'a>, - pub(crate) i: &'a Interner, } impl<'a> Process<'a> { + /// Build a process from the return value of [crate::facade::merge_trees] and + pub fn new( + consts: impl IntoIterator, + handlers: HandlerTable<'a>, + ) -> Self { + let symbols = consts.into_iter().map(|(k, v)| (k, v.value)).collect(); + Self { handlers, symbols } + } + /// Execute the given command in this process. If gas is specified, at most as /// many steps will be executed and then the partial result returned. /// @@ -25,24 +34,23 @@ impl<'a> Process<'a> { /// yields pub fn run( &mut self, - prompt: ExprInst, + prompt: Expr, gas: Option, - ) -> Result { - let ctx = Context { gas, interner: self.i, symbols: &self.symbols }; + ) -> Result { + let ctx = RunContext { gas, symbols: &self.symbols }; run_handler(prompt, &mut self.handlers, ctx) } /// Find all unbound constant names in a symbol. This is often useful to /// identify dynamic loading targets. #[must_use] - pub fn unbound_refs(&self, key: Sym) -> Vec<(Sym, Location)> { + pub fn unbound_refs(&self, key: Sym) -> Vec<(Sym, CodeLocation)> { let mut errors = Vec::new(); let sym = self.symbols.get(&key).expect("symbol must exist"); - sym.search_all(&mut |s: &ExprInst| { - let expr = s.expr(); - if let interpreted::Clause::Constant(sym) = &expr.clause { + sym.search_all(&mut |s: &Expr| { + if let Clause::Constant(sym) = &*s.clause.cls() { if !self.symbols.contains_key(sym) { - errors.push((sym.clone(), expr.location.clone())) + errors.push((sym.clone(), s.location())) } } None::<()> @@ -51,8 +59,8 @@ impl<'a> Process<'a> { } /// Assert that the code contains no invalid constants. This ensures that, - /// unless [interpreted::Clause::Constant]s are created procedurally, - /// a [interpreter::RuntimeError::MissingSymbol] cannot be produced + /// unless [Clause::Constant]s are created procedurally, + /// a [crate::interpreter::error::RunError::MissingSymbol] cannot be produced pub fn validate_refs(&self) -> ProjectResult<()> { let mut errors = Vec::new(); for key in self.symbols.keys() { @@ -66,45 +74,39 @@ impl<'a> Process<'a> { } match errors.is_empty() { true => Ok(()), - false => Err(MissingSymbols { errors }.rc()), + false => Err(MissingSymbols { errors }.pack()), } } } #[derive(Debug, Clone)] -pub struct MissingSymbol { +struct MissingSymbol { referrer: Sym, - location: Location, + location: CodeLocation, symbol: Sym, } #[derive(Debug)] -pub struct MissingSymbols { +struct MissingSymbols { errors: Vec, } impl ProjectError for MissingSymbols { - fn description(&self) -> &str { - "A name not referring to a known symbol was found in the source after \ + const DESCRIPTION: &'static str = "A name not referring to a known symbol was found in the source after \ macro execution. This can either mean that a symbol name was mistyped, or \ - that macro execution didn't correctly halt." - } + that macro execution didn't correctly halt."; fn message(&self) -> String { format!( "The following symbols do not exist:\n{}", (self.errors.iter()) - .map(|e| format!( - "{} referenced in {} ", - e.symbol.extern_vec().join("::"), - e.referrer.extern_vec().join("::") + .map(|MissingSymbol { symbol, referrer, .. }| format!( + "{symbol} referenced in {referrer}" )) .join("\n") ) } - fn positions(&self) -> crate::utils::BoxedIter { - Box::new( - (self.errors.clone().into_iter()) - .map(|i| ErrorPosition { location: i.location, message: None }), - ) + fn positions(&self) -> impl IntoIterator { + (self.errors.iter()) + .map(|i| ErrorPosition { location: i.location.clone(), message: None }) } } diff --git a/src/facade/system.rs b/src/facade/system.rs index 3e86929..8264a6f 100644 --- a/src/facade/system.rs +++ b/src/facade/system.rs @@ -1,59 +1,41 @@ -use hashbrown::HashMap; - use crate::error::{ErrorPosition, ProjectError}; -use crate::interpreter::HandlerTable; -use crate::parse::{LexerPlugin, LineParser}; -use crate::pipeline::file_loader::{IOResult, Loaded}; -use crate::sourcefile::FileEntry; -use crate::utils::boxed_iter::box_empty; -use crate::utils::BoxedIter; -use crate::{ConstTree, Interner, Tok, VName}; +use crate::gen::tree::ConstTree; +use crate::interpreter::handler::HandlerTable; +use crate::name::VName; +use crate::parse::lex_plugin::LexerPlugin; +use crate::parse::parse_plugin::ParseLinePlugin; +use crate::pipeline::load_solution::Prelude; +use crate::virt_fs::DeclTree; /// A description of every point where an external library can hook into Orchid. /// Intuitively, this can be thought of as a plugin pub struct System<'a> { /// An identifier for the system used eg. in error reporting. - pub name: Vec, + pub name: &'a str, /// External functions and other constant values defined in AST form - pub constants: HashMap, ConstTree>, + pub constants: ConstTree, /// Orchid libraries defined by this system - pub code: HashMap, - /// Prelude lines to be added to **subsequent** systems and usercode to - /// expose the functionality of this system. The prelude is not added during - /// the loading of this system - pub prelude: Vec, + pub code: DeclTree, + /// Prelude lines to be added to the head of files to expose the + /// functionality of this system. A glob import from the first path is + /// added to every file outside the prefix specified by the second path + pub prelude: Vec, /// Handlers for actions defined in this system pub handlers: HandlerTable<'a>, /// Custom lexer for the source code representation atomic data. /// These take priority over builtin lexers so the syntax they /// match should be unambiguous - pub lexer_plugins: Vec>, + pub lexer_plugins: Vec>, /// Parser that processes custom line types into their representation in the /// module tree - pub line_parsers: Vec>, + pub line_parsers: Vec>, } impl<'a> System<'a> { /// Intern the name of the system so that it can be used as an Orchid /// namespace #[must_use] - pub fn vname(&self, i: &Interner) -> VName { - self.name.iter().map(|s| i.i(s)).collect::>() - } - - /// Load a file from the system - pub fn load_file( - &self, - path: &[Tok], - referrer: &[Tok], - ) -> IOResult { - (self.code.get(path)).cloned().ok_or_else(|| { - let err = MissingSystemCode { - path: path.to_vec(), - system: self.name.clone(), - referrer: referrer.to_vec(), - }; - err.rc() - }) + pub fn vname(&self) -> VName { + VName::parse(self.name).expect("Systems must have a non-empty name") } } @@ -66,23 +48,22 @@ pub struct MissingSystemCode { referrer: VName, } impl ProjectError for MissingSystemCode { - fn description(&self) -> &str { - "A system tried to import a path that doesn't exist" - } + const DESCRIPTION: &'static str = + "A system tried to import a path that doesn't exist"; fn message(&self) -> String { format!( "Path {} imported by {} is not defined by {} or any system before it", - Interner::extern_all(&self.path).join("::"), - Interner::extern_all(&self.referrer).join("::"), + self.path, + self.referrer, self.system.join("::") ) } - fn positions(&self) -> BoxedIter { box_empty() } + fn positions(&self) -> impl IntoIterator { [] } } /// Trait for objects that can be converted into a [System] in the presence /// of an [Interner]. pub trait IntoSystem<'a> { /// Convert this object into a system using an interner - fn into_system(self, i: &Interner) -> System<'a>; + fn into_system(self) -> System<'a>; } diff --git a/src/foreign/atom.rs b/src/foreign/atom.rs index 5821282..1f6ef98 100644 --- a/src/foreign/atom.rs +++ b/src/foreign/atom.rs @@ -1,45 +1,79 @@ use std::any::Any; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; +use std::sync::{Arc, Mutex}; -use dyn_clone::DynClone; +use never::Never; -use super::XfnResult; -use crate::ddispatch::request; -use crate::error::AssertionError; -use crate::interpreted::{ExprInst, TryFromExprInst}; -use crate::interpreter::{Context, RuntimeError}; -use crate::representations::interpreted::Clause; -use crate::utils::ddispatch::Responder; -use crate::{ast, NameLike}; +use super::error::{ExternError, ExternResult}; +use crate::interpreter::apply::CallData; +use crate::interpreter::context::RunContext; +use crate::interpreter::error::RunError; +use crate::interpreter::nort; +use crate::interpreter::run::RunData; +use crate::location::{CodeLocation, SourceRange}; +use crate::name::NameLike; +use crate::parse::parsed; +use crate::utils::ddispatch::{request, Request, Responder}; /// Information returned by [Atomic::run]. This mirrors /// [crate::interpreter::Return] but with a clause instead of an Expr. pub struct AtomicReturn { /// The next form of the expression - pub clause: Clause, + pub clause: nort::Clause, /// Remaining gas pub gas: Option, /// Whether further normalization is possible by repeated calls to /// [Atomic::run] pub inert: bool, } +impl AtomicReturn { + /// Report indicating that the value is inert + pub fn inert(this: T, ctx: RunContext) -> Result { + Ok(Self { clause: this.atom_cls(), gas: ctx.gas, inert: true }) + } + /// Report indicating that the value has been processed + pub fn run(clause: nort::Clause, run: RunData) -> Result { + Ok(Self { clause, gas: run.ctx.gas, inert: false }) + } +} /// Returned by [Atomic::run] -pub type AtomicResult = Result; +pub type AtomicResult = Result; -/// Trait for things that are _definitely_ equal. -pub trait StrictEq { - /// must return true if the objects were produced via the exact same sequence - /// of transformations, including any relevant context data. Must return false - /// if the objects are of different type, or if their type is [PartialEq] - /// and [PartialEq::eq] returns false. - fn strict_eq(&self, other: &dyn Any) -> bool; +/// General error produced when a non-function [Atom] is applied to something as +/// a function. +#[derive(Clone)] +pub struct NotAFunction(pub nort::Expr); +impl ExternError for NotAFunction {} +impl Display for NotAFunction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?} is not a function", self.0) + } } /// Functionality the interpreter needs to handle a value -pub trait Atomic: Any + Debug + DynClone + StrictEq + Responder + Send -where - Self: 'static, +/// +/// # Lifecycle methods +/// +/// Atomics expose the methods [Atomic::redirect], [Atomic::run], +/// [Atomic::apply] and [Atomic::apply_ref] to interact with the interpreter. +/// The interpreter first tries to call `redirect` to find a subexpression to +/// normalize. If it returns `None` or the subexpression is inert, `run` is +/// called. `run` takes ownership of the value and returns a new one. +/// +/// If `run` indicated in its return value that the result is inert and the atom +/// is in the position of a function, `apply` or `apply_ref` is called depending +/// upon whether the atom is referenced elsewhere. `apply` falls back to +/// `apply_ref` so implementing it is considered an optimization to avoid +/// excessive copying. +/// +/// Atoms don't generally have to be copyable because clauses are refcounted in +/// the interpreter, but Orchid code is always free to duplicate the references +/// and apply them as functions to multiple different arguments so atoms that +/// represent functions have to support application by-ref without consuming the +/// function itself. +pub trait Atomic: Any + Debug + Responder + Send +where Self: 'static { /// Casts this value to [Any] so that its original value can be salvaged /// during introspection by other external code. @@ -55,45 +89,90 @@ where #[must_use] fn as_any_ref(&self) -> &dyn Any; + /// Returns a reference to a possible expression held inside the atom which + /// can be reduced. For an overview of the lifecycle see [Atomic] + fn redirect(&mut self) -> Option<&mut nort::ClauseInst>; + /// Attempt to normalize this value. If it wraps a value, this should report /// inert. If it wraps a computation, it should execute one logical step of - /// the computation and return a structure representing the ntext. - fn run(self: Box, ctx: Context) -> AtomicResult; + /// the computation and return a structure representing the next. + /// + /// For an overview of the lifecycle see [Atomic] + fn run(self: Box, run: RunData) -> AtomicResult; + + /// Combine the function with an argument to produce a new clause. Falls back + /// to [Atomic::apply_ref] by default. + /// + /// For an overview of the lifecycle see [Atomic] + fn apply(self: Box, call: CallData) -> ExternResult { + self.apply_ref(call) + } + + /// Combine the function with an argument to produce a new clause + /// + /// For an overview of the lifecycle see [Atomic] + fn apply_ref(&self, call: CallData) -> ExternResult; + + /// Must return true for atoms parsed from identical source. + /// If the atom cannot be parsed from source, it can safely be ignored + #[allow(unused_variables)] + fn parser_eq(&self, other: &dyn Any) -> bool { false } /// Wrap the atom in a clause to be placed in an [AtomicResult]. #[must_use] - fn atom_cls(self) -> Clause - where - Self: Sized, - { - Clause::Atom(Atom(Box::new(self))) + fn atom_cls(self) -> nort::Clause + where Self: Sized { + nort::Clause::Atom(Atom(Box::new(self))) + } + + /// Shorthand for `self.atom_cls().to_inst()` + fn atom_clsi(self) -> nort::ClauseInst + where Self: Sized { + self.atom_cls().to_inst() } /// Wrap the atom in a new expression instance to be placed in a tree #[must_use] - fn atom_exi(self) -> ExprInst - where - Self: Sized, - { - self.atom_cls().wrap() + fn atom_expr(self, location: CodeLocation) -> nort::Expr + where Self: Sized { + self.atom_clsi().to_expr(location) } /// Wrap the atom in a clause to be placed in a [sourcefile::FileEntry]. #[must_use] - fn ast_cls(self) -> ast::Clause - where - Self: Sized, - { - ast::Clause::Atom(Atom::new(self)) + fn ast_cls(self) -> parsed::Clause + where Self: Sized + Clone { + parsed::Clause::Atom(AtomGenerator::cloner(self)) } /// Wrap the atom in an expression to be placed in a [sourcefile::FileEntry]. #[must_use] - fn ast_exp(self) -> ast::Expr - where - Self: Sized, - { - self.ast_cls().into_expr() + fn ast_exp(self, range: SourceRange) -> parsed::Expr + where Self: Sized + Clone { + self.ast_cls().into_expr(range) + } +} + +/// A struct for generating any number of [Atom]s. Since atoms aren't Clone, +/// this represents the ability to create any number of instances of an atom +#[derive(Clone)] +pub struct AtomGenerator(Arc Atom + Send + Sync>); +impl AtomGenerator { + /// Use a factory function to create any number of atoms + pub fn new(f: impl Fn() -> Atom + Send + Sync + 'static) -> Self { + Self(Arc::new(f)) + } + /// Clone a representative atom when called + pub fn cloner(atom: impl Atomic + Clone) -> Self { + let lock = Mutex::new(atom); + Self::new(move || Atom::new(lock.lock().unwrap().clone())) + } + /// Generate an atom + pub fn run(&self) -> Atom { self.0() } +} +impl Debug for AtomGenerator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AtomGenerator").finish_non_exhaustive() } } @@ -123,7 +202,7 @@ impl Atom { *self.0.as_any().downcast().expect("Type mismatch on Atom::cast") } /// Normalize the contained data - pub fn run(self, ctx: Context) -> AtomicResult { self.0.run(ctx) } + pub fn run(self, run: RunData) -> AtomicResult { self.0.run(run) } /// Request a delegate from the encapsulated data pub fn request(&self) -> Option { request(self.0.as_ref()) } /// Downcast the atom to a concrete atomic type, or return the original atom @@ -134,10 +213,18 @@ impl Atom { false => Err(self), } } -} - -impl Clone for Atom { - fn clone(&self) -> Self { Self(dyn_clone::clone_box(self.data())) } + /// Downcast an atom by reference + pub fn downcast_ref(&self) -> Option<&T> { + self.0.as_any_ref().downcast_ref() + } + /// Combine the function with an argument to produce a new clause + pub fn apply(self, call: CallData) -> ExternResult { + self.0.apply(call) + } + /// Combine the function with an argument to produce a new clause + pub fn apply_ref(&self, call: CallData) -> ExternResult { + self.0.apply_ref(call) + } } impl Debug for Atom { @@ -146,12 +233,15 @@ impl Debug for Atom { } } -impl TryFromExprInst for Atom { - fn from_exi(exi: ExprInst) -> XfnResult { - let loc = exi.location(); - match exi.expr_val().clause { - Clause::Atom(a) => Ok(a), - _ => AssertionError::fail(loc, "atom"), - } +impl Responder for Never { + fn respond(&self, _request: Request) { match *self {} } +} +impl Atomic for Never { + fn as_any(self: Box) -> Box { match *self {} } + fn as_any_ref(&self) -> &dyn Any { match *self {} } + fn redirect(&mut self) -> Option<&mut nort::ClauseInst> { match *self {} } + fn run(self: Box, _: RunData) -> AtomicResult { match *self {} } + fn apply_ref(&self, _: CallData) -> ExternResult { + match *self {} } } diff --git a/src/foreign/cps_box.rs b/src/foreign/cps_box.rs index a0b97d1..bbb97c5 100644 --- a/src/foreign/cps_box.rs +++ b/src/foreign/cps_box.rs @@ -4,99 +4,97 @@ use std::fmt::Debug; use trait_set::trait_set; -use super::{Atomic, ExternFn, InertAtomic, XfnResult}; -use crate::interpreted::{Clause, ExprInst}; -use crate::interpreter::{Context, HandlerRes}; +use super::atom::{Atomic, AtomicResult, AtomicReturn, NotAFunction}; +use super::error::{ExternError, ExternResult}; +use crate::interpreter::apply::CallData; +use crate::interpreter::nort::{Clause, ClauseInst, Expr}; +use crate::interpreter::run::RunData; +use crate::location::CodeLocation; +use crate::utils::ddispatch::{Request, Responder}; use crate::utils::pure_seq::pushed_ref; -use crate::ConstTree; trait_set! { /// A "well behaved" type that can be used as payload in a CPS box pub trait CPSPayload = Clone + Debug + Send + 'static; /// A function to handle a CPS box with a specific payload - pub trait CPSHandler = FnMut(&T, &ExprInst) -> HandlerRes; + pub trait CPSHandler = FnMut(&T, &Expr) -> ExternResult; } -/// The pre-argument version of CPSBox -#[derive(Debug, Clone)] -struct CPSFn { - pub argc: usize, - pub continuations: Vec, - pub payload: T, -} -impl CPSFn { - #[must_use] - fn new(argc: usize, payload: T) -> Self { - debug_assert!( - argc > 0, - "Null-ary CPS functions are invalid, use an Atom instead" - ); - Self { argc, continuations: Vec::new(), payload } - } -} -impl ExternFn for CPSFn { - fn name(&self) -> &str { "CPS function without argument" } - fn apply(self: Box, arg: ExprInst, _ctx: Context) -> XfnResult { - let payload = self.payload.clone(); - let continuations = pushed_ref(&self.continuations, arg); - if self.argc == 1 { - Ok(CPSBox { payload, continuations }.atom_cls()) - } else { - Ok(CPSFn { argc: self.argc - 1, payload, continuations }.xfn_cls()) - } - } -} - -/// An inert Orchid Atom value encapsulating a payload and a continuation point +/// An Orchid Atom value encapsulating a payload and continuation points #[derive(Debug, Clone)] pub struct CPSBox { + /// Number of arguments not provided yet + pub argc: usize, /// Details about the command pub payload: T, /// Possible continuations, in the order they were provided - pub continuations: Vec, + pub continuations: Vec, } impl CPSBox { + /// Create a new command prepared to receive exacly N continuations + #[must_use] + pub fn new(argc: usize, payload: T) -> Self { + debug_assert!(argc > 0, "Null-ary CPS functions are invalid"); + Self { argc, continuations: Vec::new(), payload } + } /// Unpack the wrapped command and the continuation #[must_use] - pub fn unpack1(self) -> (T, ExprInst) { - let [cont]: [ExprInst; 1] = - self.continuations.try_into().expect("size checked"); - (self.payload, cont) + pub fn unpack1(&self) -> (&T, Expr) { + match &self.continuations[..] { + [cont] => (&self.payload, cont.clone()), + _ => panic!("size mismatch"), + } } /// Unpack the wrapped command and 2 continuations (usually an async and a /// sync) #[must_use] - pub fn unpack2(self) -> (T, ExprInst, ExprInst) { - let [c1, c2]: [ExprInst; 2] = - self.continuations.try_into().expect("size checked"); - (self.payload, c1, c2) + pub fn unpack2(&self) -> (&T, Expr, Expr) { + match &self.continuations[..] { + [c1, c2] => (&self.payload, c1.clone(), c2.clone()), + _ => panic!("size mismatch"), + } } /// Unpack the wrapped command and 3 continuations (usually an async success, /// an async fail and a sync) #[must_use] - pub fn unpack3(self) -> (T, ExprInst, ExprInst, ExprInst) { - let [c1, c2, c3]: [ExprInst; 3] = - self.continuations.try_into().expect("size checked"); - (self.payload, c1, c2, c3) + pub fn unpack3(&self) -> (&T, Expr, Expr, Expr) { + match &self.continuations[..] { + [c1, c2, c3] => (&self.payload, c1.clone(), c2.clone(), c3.clone()), + _ => panic!("size mismatch"), + } + } + + fn assert_applicable(&self, err_loc: &CodeLocation) -> ExternResult<()> { + match self.argc { + 0 => Err(NotAFunction(self.clone().atom_expr(err_loc.clone())).rc()), + _ => Ok(()), + } } } - -impl InertAtomic for CPSBox { - fn type_str() -> &'static str { "a CPS box" } +impl Responder for CPSBox { + fn respond(&self, _request: Request) {} } - -/// Like [init_cps] but wrapped in a [ConstTree] for init-time usage -#[must_use] -pub fn const_cps(argc: usize, payload: T) -> ConstTree { - ConstTree::xfn(CPSFn::new(argc, payload)) -} - -/// Construct a CPS function which takes an argument and then acts inert -/// so that command executors can receive it. -/// -/// This function is meant to be used in an external function defined with -/// [crate::define_fn]. For usage in a [ConstTree], see [mk_const] -#[must_use] -pub fn init_cps(argc: usize, payload: T) -> Clause { - CPSFn::new(argc, payload).xfn_cls() +impl Atomic for CPSBox { + fn as_any(self: Box) -> Box { self } + fn as_any_ref(&self) -> &dyn std::any::Any { self } + fn parser_eq(&self, _: &dyn std::any::Any) -> bool { false } + fn redirect(&mut self) -> Option<&mut ClauseInst> { None } + fn run(self: Box, run: RunData) -> AtomicResult { + AtomicReturn::inert(*self, run.ctx) + } + fn apply(mut self: Box, call: CallData) -> ExternResult { + self.assert_applicable(&call.location)?; + self.argc -= 1; + self.continuations.push(call.arg); + Ok(self.atom_cls()) + } + fn apply_ref(&self, call: CallData) -> ExternResult { + self.assert_applicable(&call.location)?; + let new = Self { + argc: self.argc - 1, + continuations: pushed_ref(&self.continuations, call.arg), + payload: self.payload.clone(), + }; + Ok(new.atom_cls()) + } } diff --git a/src/foreign/error.rs b/src/foreign/error.rs new file mode 100644 index 0000000..f9cf981 --- /dev/null +++ b/src/foreign/error.rs @@ -0,0 +1,68 @@ +use std::error::Error; +use std::fmt::{Debug, Display}; +use std::sync::Arc; + +use dyn_clone::DynClone; + +use crate::location::CodeLocation; + +/// Errors produced by external code +pub trait ExternError: Display + Send + Sync + DynClone { + /// Convert into trait object + #[must_use] + fn rc(self) -> Arc + where Self: 'static + Sized { + Arc::new(self) + } +} + +impl Debug for dyn ExternError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ExternError({self})") + } +} + +impl Error for dyn ExternError {} + +/// An error produced by Rust code called form Orchid. The error is type-erased. +pub type ExternResult = Result>; + +/// Some expectation (usually about the argument types of a function) did not +/// hold. +#[derive(Clone)] +pub struct AssertionError { + location: CodeLocation, + message: &'static str, + details: String, +} + +impl AssertionError { + /// Construct, upcast and wrap in a Result that never succeeds for easy + /// short-circuiting + pub fn fail( + location: CodeLocation, + message: &'static str, + details: String, + ) -> ExternResult { + Err(Self::ext(location, message, details)) + } + + /// Construct and upcast to [ExternError] + pub fn ext( + location: CodeLocation, + message: &'static str, + details: String, + ) -> Arc { + Self { location, message, details }.rc() + } +} + +impl Display for AssertionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Error: expected {}", self.message)?; + write!(f, " at {}", self.location)?; + write!(f, " details: {}", self.details) + } +} + +impl ExternError for AssertionError {} diff --git a/src/foreign/extern_fn.rs b/src/foreign/extern_fn.rs deleted file mode 100644 index 91c2eca..0000000 --- a/src/foreign/extern_fn.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::error::Error; -use std::fmt::{Debug, Display}; -use std::hash::Hash; -use std::sync::Arc; - -use dyn_clone::{clone_box, DynClone}; - -use super::XfnResult; -use crate::interpreted::ExprInst; -use crate::interpreter::Context; -use crate::representations::interpreted::Clause; -use crate::{ast, NameLike}; - -/// Errors produced by external code -pub trait ExternError: Display + Send + Sync + DynClone { - /// Convert into trait object - #[must_use] - fn into_extern(self) -> Arc - where - Self: 'static + Sized, - { - Arc::new(self) - } -} - -impl Debug for dyn ExternError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self}") - } -} - -impl Error for dyn ExternError {} - -/// Represents an externally defined function from the perspective of -/// the executor. Since Orchid lacks basic numerical operations, -/// these are also external functions. -pub trait ExternFn: DynClone + Send { - /// Display name of the function - #[must_use] - fn name(&self) -> &str; - /// Combine the function with an argument to produce a new clause - fn apply(self: Box, arg: ExprInst, ctx: Context) -> XfnResult; - /// Hash the name to get a somewhat unique hash. - fn hash(&self, mut state: &mut dyn std::hash::Hasher) { - self.name().hash(&mut state) - } - /// Wrap this function in a clause to be placed in an [AtomicResult]. - #[must_use] - fn xfn_cls(self) -> Clause - where - Self: Sized + 'static, - { - Clause::ExternFn(ExFn(Box::new(self))) - } - /// Wrap this function in a clause to be placed in a [FileEntry]. - #[must_use] - fn xfn_ast_cls(self) -> ast::Clause - where - Self: Sized + 'static, - { - ast::Clause::ExternFn(ExFn(Box::new(self))) - } -} - -impl Eq for dyn ExternFn {} -impl PartialEq for dyn ExternFn { - fn eq(&self, other: &Self) -> bool { self.name() == other.name() } -} -impl Hash for dyn ExternFn { - fn hash(&self, state: &mut H) { - self.name().hash(state) - } -} -impl Debug for dyn ExternFn { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "##EXTERN[{}]##", self.name()) - } -} - -/// Represents a black box function that can be applied to a [Clause] to produce -/// a new [Clause], typically an [Atom] representing external work, a new [ExFn] -/// to take additional arguments, or an Orchid tree to return control to the -/// interpreter -#[derive(Debug)] -pub struct ExFn(pub Box); -impl ExFn { - /// Combine the function with an argument to produce a new clause - pub fn apply(self, arg: ExprInst, ctx: Context) -> XfnResult { - self.0.apply(arg, ctx) - } -} -impl Clone for ExFn { - fn clone(&self) -> Self { Self(clone_box(self.0.as_ref())) } -} diff --git a/src/foreign/fn_bridge.rs b/src/foreign/fn_bridge.rs index ada2228..f7cad49 100644 --- a/src/foreign/fn_bridge.rs +++ b/src/foreign/fn_bridge.rs @@ -1,47 +1,16 @@ +use std::any::{Any, TypeId}; use std::fmt::Debug; use std::marker::PhantomData; -use std::sync::Arc; -use super::atom::StrictEq; -use super::{ - Atomic, AtomicResult, AtomicReturn, ExternError, ExternFn, XfnResult, -}; -use crate::ddispatch::Responder; -use crate::interpreted::{Clause, ExprInst, TryFromExprInst}; -use crate::interpreter::{run, Context, Return}; -use crate::systems::codegen::{opt, res}; -use crate::OrcString; - -/// A trait for things that are infallibly convertible to [Clause]. These types -/// can be returned by callbacks passed to the [super::xfn_1ary] family of -/// functions. -pub trait ToClause: Clone { - /// Convert the type to a [Clause]. - fn to_clause(self) -> Clause; - /// Convert to an expression instance via [ToClause]. - fn to_exi(self) -> ExprInst { self.to_clause().wrap() } -} - -impl ToClause for T { - fn to_clause(self) -> Clause { self.atom_cls() } -} -impl ToClause for Clause { - fn to_clause(self) -> Clause { self } -} -impl ToClause for ExprInst { - fn to_clause(self) -> Clause { self.expr_val().clause } -} -impl ToClause for String { - fn to_clause(self) -> Clause { OrcString::from(self).atom_cls() } -} -impl ToClause for Option { - fn to_clause(self) -> Clause { opt(self.map(|t| t.to_clause().wrap())) } -} -impl ToClause for Result { - fn to_clause(self) -> Clause { - res(self.map(|t| t.to_clause().wrap()).map_err(|u| u.to_clause().wrap())) - } -} +use super::atom::{Atomic, AtomicResult, AtomicReturn}; +use super::error::ExternResult; +use super::to_clause::ToClause; +use super::try_from_expr::TryFromExpr; +use crate::interpreter::apply::CallData; +use crate::interpreter::context::Halt; +use crate::interpreter::nort::{Clause, ClauseInst, Expr}; +use crate::interpreter::run::{run, RunData}; +use crate::utils::ddispatch::Responder; /// Return a unary lambda wrapped in this struct to take an additional argument /// in a function passed to Orchid through a member of the [super::xfn_1ary] @@ -51,6 +20,11 @@ impl ToClause for Result { /// type. Rust functions are never overloaded, but inexplicably the [Fn] traits /// take the argument tuple as a generic parameter which means that it cannot /// be a unique dispatch target. +/// +/// If the function takes an instance of [Lazy], it will contain the expression +/// the function was applied to without any specific normalization. If it takes +/// any other type, the argument will be fully normalized and cast using the +/// type's [TryFromExpr] impl. pub struct Param { data: F, _t: PhantomData, @@ -60,9 +34,7 @@ unsafe impl Send for Param {} impl Param { /// Wrap a new function in a parametric struct pub fn new(f: F) -> Self - where - F: FnOnce(T) -> Result>, - { + where F: FnOnce(T) -> U { Self { data: f, _t: PhantomData, _u: PhantomData } } /// Take out the function @@ -74,75 +46,91 @@ impl Clone for Param { } } -impl< - T: 'static + TryFromExprInst, - U: 'static + ToClause, - F: 'static + Clone + Send + FnOnce(T) -> Result>, -> ToClause for Param -{ - fn to_clause(self) -> Clause { self.xfn_cls() } +/// A marker struct that gets assigned an expression without normalizing it. +/// This behaviour cannot be replicated in usercode, it's implemented with an +/// explicit runtime [TypeId] check invoked by [Param]. +#[derive(Debug, Clone)] +pub struct Thunk(pub Expr); +impl TryFromExpr for Thunk { + fn from_expr(expr: Expr) -> ExternResult { Ok(Thunk(expr)) } } struct FnMiddleStage { - argument: ExprInst, + arg: Expr, f: Param, } -impl StrictEq for FnMiddleStage { - fn strict_eq(&self, _other: &dyn std::any::Any) -> bool { - unimplemented!("This should never be able to appear in a pattern") - } -} impl Clone for FnMiddleStage { - fn clone(&self) -> Self { - Self { argument: self.argument.clone(), f: self.f.clone() } - } + fn clone(&self) -> Self { Self { arg: self.arg.clone(), f: self.f.clone() } } } impl Debug for FnMiddleStage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("FnMiddleStage") - .field("argument", &self.argument) + .field("argument", &self.arg) .finish_non_exhaustive() } } impl Responder for FnMiddleStage {} impl< - T: 'static + TryFromExprInst, + T: 'static + TryFromExpr, U: 'static + ToClause, - F: 'static + Clone + FnOnce(T) -> Result> + Send, + F: 'static + Clone + FnOnce(T) -> U + Any + Send, > Atomic for FnMiddleStage { fn as_any(self: Box) -> Box { self } fn as_any_ref(&self) -> &dyn std::any::Any { self } - fn run(self: Box, ctx: Context) -> AtomicResult { - let Return { gas, inert, state } = run(self.argument, ctx)?; - let clause = match inert { - false => state.expr_val().clause, - true => (self.f.data)(state.downcast()?)?.to_clause(), - }; - Ok(AtomicReturn { gas, inert: false, clause }) + fn redirect(&mut self) -> Option<&mut ClauseInst> { + // this should be ctfe'd + (TypeId::of::() != TypeId::of::()).then(|| &mut self.arg.clause) + } + fn run(self: Box, r: RunData) -> AtomicResult { + let Self { arg, f: Param { data: f, .. } } = *self; + let clause = f(arg.downcast()?).to_clause(r.location); + Ok(AtomicReturn { gas: r.ctx.gas, inert: false, clause }) + } + fn apply_ref(&self, _: CallData) -> ExternResult { + panic!("Atom should have decayed") + } +} + +impl Responder for Param {} +impl Debug for Param { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Param") } } impl< - T: 'static + TryFromExprInst, + T: 'static + TryFromExpr + Clone, U: 'static + ToClause, - F: 'static + Clone + Send + FnOnce(T) -> Result>, -> ExternFn for Param + F: 'static + Clone + Send + FnOnce(T) -> U, +> Atomic for Param { - fn name(&self) -> &str { "anonymous Rust function" } - fn apply(self: Box, arg: ExprInst, _: Context) -> XfnResult { - Ok(FnMiddleStage { argument: arg, f: *self }.atom_cls()) + fn as_any(self: Box) -> Box { self } + fn as_any_ref(&self) -> &dyn std::any::Any { self } + fn redirect(&mut self) -> Option<&mut ClauseInst> { None } + fn run(self: Box, r: RunData) -> AtomicResult { + AtomicReturn::inert(*self, r.ctx) + } + fn apply_ref(&self, call: CallData) -> ExternResult { + Ok(FnMiddleStage { arg: call.arg, f: self.clone() }.atom_cls()) + } + fn apply(self: Box, call: CallData) -> ExternResult { + Ok(FnMiddleStage { arg: call.arg, f: *self }.atom_cls()) } } +/// Conversion functions from [Fn] traits into [Atomic]. Since Rust's type +/// system allows overloaded [Fn] implementations, we must specify the arity and +/// argument types for this process. Arities are only defined up to 9, but the +/// function can always return another call to `xfn_`N`ary` to consume more +/// arguments. pub mod constructors { - - - use std::sync::Arc; + use super::super::atom::Atomic; + use super::super::try_from_expr::TryFromExpr; + #[allow(unused)] // for doc + use super::Thunk; use super::{Param, ToClause}; - use crate::foreign::{ExternError, ExternFn}; - use crate::interpreted::TryFromExprInst; macro_rules! xfn_variant { ( @@ -154,18 +142,21 @@ pub mod constructors { #[doc = "Convert a function of " $number " argument(s) into a curried" " Orchid function. See also Constraints summarized:\n\n" "- the callback must live as long as `'static`\n" - "- All arguments must implement [TryFromExprInst]\n" + "- All arguments must implement [TryFromExpr]\n" "- all but the last argument must implement [Clone] and [Send]\n" "- the return type must implement [ToClause].\n\n" ] + #[doc = "Take [Lazy] to take the argument as-is,\n" + "without normalization\n\n" + ] #[doc = "Other arities: " $( "[xfn_" $alt "ary], " )+ ] pub fn [< xfn_ $number ary >] < - $( $t : TryFromExprInst + Clone + Send + 'static, )* - TLast: TryFromExprInst + 'static, + $( $t : TryFromExpr + Clone + Send + 'static, )* + TLast: TryFromExpr + Clone + 'static, TReturn: ToClause + Send + 'static, TFunction: FnOnce( $( $t , )* TLast ) - -> Result> + Clone + Send + 'static - >(function: TFunction) -> impl ExternFn { + -> TReturn + Clone + Send + 'static + >(function: TFunction) -> impl Atomic + Clone { xfn_variant!(@BODY_LOOP function ( $( ( $t [< $t:lower >] ) )* ) ( $( [< $t:lower >] )* ) @@ -178,7 +169,7 @@ pub mod constructors { $( ( $T:ident $t:ident ) )* ) $full:tt) => { Param::new(|$next : $Next| { - Ok(xfn_variant!(@BODY_LOOP $function ( $( ( $T $t ) )* ) $full)) + xfn_variant!(@BODY_LOOP $function ( $( ( $T $t ) )* ) $full) }) }; (@BODY_LOOP $function:ident () ( $( $t:ident )* )) => { diff --git a/src/foreign/implementations.rs b/src/foreign/implementations.rs new file mode 100644 index 0000000..09790b5 --- /dev/null +++ b/src/foreign/implementations.rs @@ -0,0 +1,172 @@ +use std::any::Any; +use std::fmt::Debug; +use std::sync::Arc; + +use super::atom::{Atom, Atomic, AtomicResult}; +use super::error::{ExternError, ExternResult}; +use super::process::Unstable; +use super::to_clause::ToClause; +use crate::gen::tpl; +use crate::gen::traits::Gen; +use crate::interpreter::apply::CallData; +use crate::interpreter::error::RunError; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::nort::{Clause, ClauseInst}; +use crate::interpreter::run::RunData; +use crate::location::CodeLocation; +use crate::utils::clonable_iter::Clonable; +use crate::utils::ddispatch::Responder; + +impl ToClause for Option { + fn to_clause(self, location: CodeLocation) -> Clause { + let ctx = nort_gen(location.clone()); + match self { + None => tpl::C("std::option::none").template(ctx, []), + Some(t) => tpl::A(tpl::C("std::option::some"), tpl::Slot) + .template(ctx, [t.to_clause(location)]), + } + } +} + +impl ToClause for Result { + fn to_clause(self, location: CodeLocation) -> Clause { + let ctx = nort_gen(location.clone()); + match self { + Ok(t) => tpl::A(tpl::C("std::result::ok"), tpl::Slot) + .template(ctx, [t.to_clause(location)]), + Err(e) => tpl::A(tpl::C("std::result::err"), tpl::Slot) + .template(ctx, [e.to_clause(location)]), + } + } +} + +struct PendingError(Arc); +impl Responder for PendingError {} +impl Debug for PendingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PendingError({})", self.0) + } +} +impl Atomic for PendingError { + fn as_any(self: Box) -> Box { self } + fn as_any_ref(&self) -> &dyn Any { self } + fn redirect(&mut self) -> Option<&mut ClauseInst> { None } + fn run(self: Box, _: RunData) -> AtomicResult { + Err(RunError::Extern(self.0)) + } + fn apply_ref(&self, _: CallData) -> ExternResult { + panic!("This atom decays instantly") + } +} + +impl ToClause for ExternResult { + fn to_clause(self, location: CodeLocation) -> Clause { + match self { + Err(e) => PendingError(e).atom_cls(), + Ok(t) => t.to_clause(location), + } + } +} + +struct ListGen(Clonable) +where + I: Iterator + Send, + I::Item: ToClause + Send; +impl Clone for ListGen +where + I: Iterator + Send, + I::Item: ToClause + Send, +{ + fn clone(&self) -> Self { Self(self.0.clone()) } +} +impl ToClause for ListGen +where + I: Iterator + Send + 'static, + I::Item: ToClause + Clone + Send, +{ + fn to_clause(mut self, location: CodeLocation) -> Clause { + let ctx = nort_gen(location.clone()); + match self.0.next() { + None => tpl::C("std::lit::end").template(ctx, []), + Some(val) => { + let atom = Unstable::new(|run| self.to_clause(run.location)); + tpl::a2(tpl::C("std::lit::cons"), tpl::Slot, tpl::V(atom)) + .template(ctx, [val.to_clause(location)]) + }, + } + } +} + +/// Convert an iterator into a lazy-evaluated Orchid list. +pub fn list(items: I) -> impl ToClause +where + I: IntoIterator + Clone + Send + Sync + 'static, + I::IntoIter: Send, + I::Item: ToClause + Clone + Send, +{ + Unstable::new(move |RunData { location, .. }| { + ListGen(Clonable::new( + items.clone().into_iter().map(move |t| t.to_clsi(location.clone())), + )) + }) +} + +impl ToClause for Vec { + fn to_clause(self, location: CodeLocation) -> Clause { + list(self).to_clause(location) + } +} + +impl ToClause for Atom { + fn to_clause(self, _: CodeLocation) -> Clause { Clause::Atom(self) } +} + +mod tuple_impls { + use std::sync::Arc; + + use super::ToClause; + use crate::foreign::atom::Atomic; + use crate::foreign::error::AssertionError; + use crate::foreign::implementations::ExternResult; + use crate::foreign::inert::Inert; + use crate::foreign::try_from_expr::TryFromExpr; + use crate::interpreter::nort::{Clause, Expr}; + use crate::libs::std::tuple::Tuple; + use crate::location::CodeLocation; + + macro_rules! gen_tuple_impl { + ( ($($T:ident)*) ($($t:ident)*)) => { + impl<$($T: ToClause),*> ToClause for ($($T,)*) { + fn to_clause(self, location: CodeLocation) -> Clause { + let ($($t,)*) = self; + Inert(Tuple(Arc::new(vec![ + $($t.to_expr(location.clone()),)* + ]))).atom_cls() + } + } + + impl<$($T: TryFromExpr),*> TryFromExpr for ($($T,)*) { + fn from_expr(ex: Expr) -> ExternResult { + let Inert(Tuple(slice)) = ex.clone().downcast()?; + match &slice[..] { + [$($t),*] => Ok(($($t.clone().downcast()?,)*)), + _ => AssertionError::fail(ex.location(), "Tuple length mismatch", format!("{ex}")) + } + } + } + }; + } + + gen_tuple_impl!((A)(a)); + gen_tuple_impl!((A B) (a b)); + gen_tuple_impl!((A B C) (a b c)); + gen_tuple_impl!((A B C D) (a b c d)); + gen_tuple_impl!((A B C D E) (a b c d e)); + gen_tuple_impl!((A B C D E F) (a b c d e f)); + gen_tuple_impl!((A B C D E F G) (a b c d e f g)); + gen_tuple_impl!((A B C D E F G H) (a b c d e f g h)); + gen_tuple_impl!((A B C D E F G H I) (a b c d e f g h i)); + gen_tuple_impl!((A B C D E F G H I J) (a b c d e f g h i j)); + gen_tuple_impl!((A B C D E F G H I J K) (a b c d e f g h i j k)); + gen_tuple_impl!((A B C D E F G H I J K L) (a b c d e f g h i j k l)); +} diff --git a/src/foreign/inert.rs b/src/foreign/inert.rs index 34884e0..d6ef8db 100644 --- a/src/foreign/inert.rs +++ b/src/foreign/inert.rs @@ -1,28 +1,30 @@ use std::any::Any; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; +use std::ops::{Deref, DerefMut}; use ordered_float::NotNan; -use super::atom::StrictEq; -use super::{AtomicResult, AtomicReturn, XfnResult}; -use crate::error::AssertionError; -#[allow(unused)] // for doc -// use crate::define_fn; -use crate::foreign::Atomic; -use crate::interpreted::{Clause, Expr, ExprInst, TryFromExprInst}; -use crate::interpreter::Context; -use crate::systems::stl::Numeric; +use super::atom::{Atom, Atomic, AtomicResult, AtomicReturn, NotAFunction}; +use super::error::{ExternError, ExternResult}; +use super::try_from_expr::TryFromExpr; +use crate::foreign::error::AssertionError; +use crate::interpreter::apply::CallData; +use crate::interpreter::nort::{Clause, ClauseInst, Expr}; +use crate::interpreter::run::RunData; +use crate::libs::std::number::Numeric; +use crate::libs::std::string::OrcString; use crate::utils::ddispatch::{Request, Responder}; /// A proxy trait that implements [Atomic] for blobs of data in Rust code that /// cannot be processed and always report inert. Since these are expected to be /// parameters of functions defined with [define_fn] it also automatically -/// implements [TryFromExprInst] so that a conversion doesn't have to be +/// implements [TryFromExpr] so that a conversion doesn't have to be /// provided in argument lists. -pub trait InertAtomic: Debug + Clone + Send + 'static { +pub trait InertPayload: Debug + Clone + Send + 'static { /// Typename to be shown in the error when a conversion from [ExprInst] fails - #[must_use] - fn type_str() -> &'static str; + /// + /// This will default to `type_name::()` when it becomes stable + const TYPE_STR: &'static str; /// Proxies to [Responder] so that you don't have to implmeent it manually if /// you need it, but behaves exactly as the default implementation. #[allow(unused_mut, unused_variables)] // definition should show likely usage @@ -37,59 +39,99 @@ pub trait InertAtomic: Debug + Clone + Send + 'static { /// ``` fn strict_eq(&self, _: &Self) -> bool { false } } -impl StrictEq for T { - fn strict_eq(&self, other: &dyn Any) -> bool { - other.downcast_ref().map_or(false, |other| self.strict_eq(other)) - } + +/// An atom that stores a value and rejects all interpreter interactions. It is +/// used to reference foreign data in Orchid. +#[derive(Debug, Clone)] +pub struct Inert(pub T); +impl Inert { + /// Wrap the argument in a type-erased [Atom] for embedding in Orchid + /// structures. + pub fn atom(t: T) -> Atom { Atom::new(Inert(t)) } } -impl Responder for T { + +impl Deref for Inert { + type Target = T; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Inert { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Responder for Inert { fn respond(&self, mut request: Request) { if request.can_serve::() { - request.serve(self.clone()) + request.serve(self.0.clone()) } else { - self.respond(request) + self.0.respond(request) } } } -impl Atomic for T { +impl Atomic for Inert { fn as_any(self: Box) -> Box { self } fn as_any_ref(&self) -> &dyn Any { self } - fn run(self: Box, ctx: Context) -> AtomicResult { - Ok(AtomicReturn { gas: ctx.gas, inert: true, clause: self.atom_cls() }) + fn redirect(&mut self) -> Option<&mut ClauseInst> { None } + fn run(self: Box, run: RunData) -> AtomicResult { + AtomicReturn::inert(*self, run.ctx) + } + fn apply_ref(&self, call: CallData) -> ExternResult { + Err(NotAFunction(self.clone().atom_expr(call.location)).rc()) + } + fn parser_eq(&self, other: &dyn Any) -> bool { + (other.downcast_ref::()) + .map_or(false, |other| self.0.strict_eq(&other.0)) } } -impl TryFromExprInst for T { - fn from_exi(exi: ExprInst) -> XfnResult { - let Expr { clause, location } = exi.expr_val(); - match clause { - Clause::Atom(a) => match a.0.as_any().downcast() { - Ok(t) => Ok(*t), - Err(_) => AssertionError::fail(location, Self::type_str()), +impl TryFromExpr for Inert { + fn from_expr(expr: Expr) -> ExternResult { + let Expr { clause, location } = expr; + match clause.try_unwrap() { + Ok(Clause::Atom(at)) => at.try_downcast::().map_err(|a| { + AssertionError::ext(location, T::TYPE_STR, format!("{a:?}")) + }), + Err(inst) => match &*inst.cls() { + Clause::Atom(at) => + at.downcast_ref::().cloned().ok_or_else(|| { + AssertionError::ext(location, T::TYPE_STR, format!("{inst}")) + }), + cls => AssertionError::fail(location, "atom", format!("{cls}")), }, - _ => AssertionError::fail(location, "atom"), + Ok(cls) => AssertionError::fail(location, "atom", format!("{cls}")), } } } -impl InertAtomic for bool { - fn type_str() -> &'static str { "bool" } - fn strict_eq(&self, other: &Self) -> bool { self == other } -} - -impl InertAtomic for usize { - fn type_str() -> &'static str { "usize" } - fn strict_eq(&self, other: &Self) -> bool { self == other } - fn respond(&self, mut request: Request) { - request.serve(Numeric::Uint(*self)) +impl Display for Inert { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } } -impl InertAtomic for NotNan { - fn type_str() -> &'static str { "NotNan" } +impl InertPayload for bool { + const TYPE_STR: &'static str = "bool"; fn strict_eq(&self, other: &Self) -> bool { self == other } fn respond(&self, mut request: Request) { - request.serve(Numeric::Float(*self)) + request.serve_with(|| OrcString::from(self.to_string())) + } +} + +impl InertPayload for usize { + const TYPE_STR: &'static str = "usize"; + fn strict_eq(&self, other: &Self) -> bool { self == other } + fn respond(&self, mut request: Request) { + request.serve(Numeric::Uint(*self)); + request.serve_with(|| OrcString::from(self.to_string())) + } +} + +impl InertPayload for NotNan { + const TYPE_STR: &'static str = "NotNan"; + fn strict_eq(&self, other: &Self) -> bool { self == other } + fn respond(&self, mut request: Request) { + request.serve(Numeric::Float(*self)); + request.serve_with(|| OrcString::from(self.to_string())) } } diff --git a/src/foreign/mod.rs b/src/foreign/mod.rs index 9f1d369..31ec371 100644 --- a/src/foreign/mod.rs +++ b/src/foreign/mod.rs @@ -2,24 +2,12 @@ //! //! Structures and traits used in the exposure of external functions and values //! to Orchid code -mod atom; +pub mod atom; pub mod cps_box; -mod extern_fn; -mod fn_bridge; -mod inert; - -use std::sync::Arc; - -pub use atom::{Atom, Atomic, AtomicResult, AtomicReturn, StrictEq}; -pub use extern_fn::{ExFn, ExternError, ExternFn}; -pub use fn_bridge::constructors::{ - xfn_1ary, xfn_2ary, xfn_3ary, xfn_4ary, xfn_5ary, xfn_6ary, xfn_7ary, - xfn_8ary, xfn_9ary, -}; -pub use fn_bridge::{Param, ToClause}; -pub use inert::InertAtomic; - -pub use crate::representations::interpreted::Clause; - -/// Return type of the argument to the [xfn_1ary] family of functions -pub type XfnResult = Result>; +pub mod error; +pub mod fn_bridge; +pub mod implementations; +pub mod inert; +pub mod process; +pub mod to_clause; +pub mod try_from_expr; diff --git a/src/foreign/process.rs b/src/foreign/process.rs new file mode 100644 index 0000000..2db90d0 --- /dev/null +++ b/src/foreign/process.rs @@ -0,0 +1,39 @@ +use std::fmt::Debug; + +use super::atom::{Atomic, AtomicReturn}; +use super::error::ExternResult; +use super::to_clause::ToClause; +use crate::interpreter::apply::CallData; +use crate::interpreter::nort::{Clause, ClauseInst}; +use crate::interpreter::run::RunData; +use crate::utils::ddispatch::Responder; + +/// An atom that immediately decays to the result of the function when +/// normalized. Can be used to build infinite recursive datastructures from +/// Rust. +#[derive(Clone)] +pub struct Unstable(F); +impl R + Send + 'static, R: ToClause> Unstable { + /// Wrap a function in an Unstable + pub const fn new(f: F) -> Self { Self(f) } +} +impl Responder for Unstable {} +impl Debug for Unstable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Unstable").finish_non_exhaustive() + } +} +impl R + Send + 'static, R: ToClause> Atomic + for Unstable +{ + fn as_any(self: Box) -> Box { self } + fn as_any_ref(&self) -> &dyn std::any::Any { self } + fn apply_ref(&self, _: CallData) -> ExternResult { + panic!("This atom decays instantly") + } + fn run(self: Box, run: RunData) -> super::atom::AtomicResult { + let clause = self.0(run.clone()).to_clause(run.location.clone()); + AtomicReturn::run(clause, run) + } + fn redirect(&mut self) -> Option<&mut ClauseInst> { None } +} diff --git a/src/foreign/to_clause.rs b/src/foreign/to_clause.rs new file mode 100644 index 0000000..f363b38 --- /dev/null +++ b/src/foreign/to_clause.rs @@ -0,0 +1,43 @@ +use super::atom::Atomic; +use crate::interpreter::nort::{Clause, ClauseInst, Expr}; +use crate::location::CodeLocation; + +/// A trait for things that are infallibly convertible to [ClauseInst]. These +/// types can be returned by callbacks passed to the [super::xfn_1ary] family of +/// functions. +pub trait ToClause: Sized { + /// Convert this value to a [Clause]. If your value can only be directly + /// converted to a [ClauseInst], you can call `ClauseInst::to_clause` to + /// unwrap it if possible or fall back to [Clause::Identity]. + fn to_clause(self, location: CodeLocation) -> Clause; + + /// Convert the type to a [Clause]. + fn to_clsi(self, location: CodeLocation) -> ClauseInst { + ClauseInst::new(self.to_clause(location)) + } + + /// Convert to an expression via [ToClause]. + fn to_expr(self, location: CodeLocation) -> Expr { + Expr { clause: self.to_clsi(location.clone()), location } + } +} + +impl ToClause for T { + fn to_clause(self, _: CodeLocation) -> Clause { self.atom_cls() } +} +impl ToClause for Clause { + fn to_clause(self, _: CodeLocation) -> Clause { self } +} +impl ToClause for ClauseInst { + fn to_clause(self, _: CodeLocation) -> Clause { + self.into_cls() + } + fn to_clsi(self, _: CodeLocation) -> ClauseInst { self } +} +impl ToClause for Expr { + fn to_clause(self, location: CodeLocation) -> Clause { + self.clause.to_clause(location) + } + fn to_clsi(self, _: CodeLocation) -> ClauseInst { self.clause } + fn to_expr(self, _: CodeLocation) -> Expr { self } +} diff --git a/src/foreign/try_from_expr.rs b/src/foreign/try_from_expr.rs new file mode 100644 index 0000000..0874358 --- /dev/null +++ b/src/foreign/try_from_expr.rs @@ -0,0 +1,28 @@ +use super::error::ExternResult; +use crate::interpreter::nort::{ClauseInst, Expr}; +use crate::location::CodeLocation; + +/// Types automatically convertible from an [Expr]. Most notably, this is how +/// foreign functions request automatic argument downcasting. +pub trait TryFromExpr: Sized { + /// Match and clone the value out of an [Expr] + fn from_expr(expr: Expr) -> ExternResult; +} + +impl TryFromExpr for Expr { + fn from_expr(expr: Expr) -> ExternResult { Ok(expr) } +} + +impl TryFromExpr for ClauseInst { + fn from_expr(expr: Expr) -> ExternResult { Ok(expr.clause.clone()) } +} + +/// Request a value of a particular type and also return its location for +/// further error reporting +#[derive(Debug, Clone)] +pub struct WithLoc(pub CodeLocation, pub T); +impl TryFromExpr for WithLoc { + fn from_expr(expr: Expr) -> ExternResult { + Ok(Self(expr.location(), T::from_expr(expr)?)) + } +} diff --git a/src/gen/mod.rs b/src/gen/mod.rs new file mode 100644 index 0000000..ddff714 --- /dev/null +++ b/src/gen/mod.rs @@ -0,0 +1,7 @@ +//! Abstractions and primitives for defining Orchid code in compile-time Rust +//! constants. This is used both to generate glue code such as function call +//! expressions at runtime and to define completely static intrinsics and +//! constants accessible to usercode. +pub mod tpl; +pub mod traits; +pub mod tree; diff --git a/src/gen/tpl.rs b/src/gen/tpl.rs new file mode 100644 index 0000000..61169a9 --- /dev/null +++ b/src/gen/tpl.rs @@ -0,0 +1,80 @@ +//! Various elemental components to build expression trees that all implement +//! [GenClause]. + +use std::fmt::Debug; + +use super::traits::{GenClause, Generable}; +use crate::foreign::atom::{Atom, AtomGenerator, Atomic}; + +/// Atom, Embed a Rust value. See also [AnyAtom] +#[derive(Debug, Clone)] +pub struct V(pub A); +impl GenClause for V { + fn generate(&self, ctx: T::Ctx<'_>, _: &impl Fn() -> T) -> T { + T::atom(ctx, Atom::new(self.0.clone())) + } +} + +/// Atom, embed a Rust value of unspecified type. See also [V] +#[derive(Debug)] +pub struct AnyAtom(pub AtomGenerator); +impl GenClause for AnyAtom { + fn generate(&self, ctx: T::Ctx<'_>, _: &impl Fn() -> T) -> T { + T::atom(ctx, self.0.run()) + } +} + +/// Const, Reference a constant from the execution environment. Unlike Orchid +/// syntax, this doesn't include lambda arguments. For that, use [P] +#[derive(Debug, Clone)] +pub struct C(pub &'static str); +impl GenClause for C { + fn generate(&self, ctx: T::Ctx<'_>, _: &impl Fn() -> T) -> T { + T::constant(ctx, self.0.split("::")) + } +} + +/// Apply a function to a value provided by [L] +#[derive(Debug, Clone)] +pub struct A(pub F, pub X); +impl GenClause for A { + fn generate(&self, ctx: T::Ctx<'_>, p: &impl Fn() -> T) -> T { + T::apply(ctx, |gen| self.0.generate(gen, p), |gen| self.1.generate(gen, p)) + } +} + +/// Apply a function to two arguments +pub fn a2( + f: impl GenClause, + x: impl GenClause, + y: impl GenClause, +) -> impl GenClause { + A(A(f, x), y) +} + +/// Lambda expression. The argument can be referenced with [P] +#[derive(Debug, Clone)] +pub struct L(pub &'static str, pub B); +impl GenClause for L { + fn generate(&self, ctx: T::Ctx<'_>, p: &impl Fn() -> T) -> T { + T::lambda(ctx, self.0, |gen| self.1.generate(gen, p)) + } +} + +/// Parameter to a lambda expression +#[derive(Debug, Clone)] +pub struct P(pub &'static str); +impl GenClause for P { + fn generate(&self, ctx: T::Ctx<'_>, _: &impl Fn() -> T) -> T { + T::arg(ctx, self.0) + } +} + +/// Slot for an Orchid value to be specified during execution +#[derive(Debug, Clone)] +pub struct Slot; +impl GenClause for Slot { + fn generate(&self, _: T::Ctx<'_>, pop: &impl Fn() -> T) -> T { + pop() + } +} diff --git a/src/gen/traits.rs b/src/gen/traits.rs new file mode 100644 index 0000000..5d2b5c7 --- /dev/null +++ b/src/gen/traits.rs @@ -0,0 +1,74 @@ +//! Abstractions used to generate Orchid expressions + +use std::backtrace::Backtrace; +use std::cell::RefCell; +use std::collections::VecDeque; +use std::fmt::Debug; + +use crate::foreign::atom::Atom; + +/// Representations of the Orchid expression tree that can describe basic +/// language elements. +pub trait Generable: Sized { + /// Context information defined by parents. Generators just forward this. + type Ctx<'a>: Sized; + /// Wrap external data. + fn atom(ctx: Self::Ctx<'_>, a: Atom) -> Self; + /// Generate a reference to a constant + fn constant<'a>( + ctx: Self::Ctx<'_>, + name: impl IntoIterator, + ) -> Self; + /// Generate a function call given the function and its argument + fn apply( + ctx: Self::Ctx<'_>, + f: impl FnOnce(Self::Ctx<'_>) -> Self, + x: impl FnOnce(Self::Ctx<'_>) -> Self, + ) -> Self; + /// Generate a function. The argument name is only valid within the same + /// [Generable]. + fn lambda( + ctx: Self::Ctx<'_>, + name: &str, + body: impl FnOnce(Self::Ctx<'_>) -> Self, + ) -> Self; + /// Generate a reference to a function argument. The argument name is only + /// valid within the same [Generable]. + fn arg(ctx: Self::Ctx<'_>, name: &str) -> Self; +} + +/// Expression templates which can be instantiated in multiple representations +/// of Orchid. Expressions can be built from the elements defined in +/// [super::lit]. +/// +/// Do not depend on this trait, use [Gen] instead. Conversely, implement this +/// instead of [Gen]. +pub trait GenClause: Debug + Sized { + /// Enact the template at runtime to build a given type. + /// `pop` pops from the runtime template parameter list passed to the + /// generator. + /// + /// Do not call this, it's the backing operation of [Gen#template] + fn generate(&self, ctx: T::Ctx<'_>, pop: &impl Fn() -> T) -> T; +} + +/// Expression generators +/// +/// Do not implement this trait, it's the frontend for [GenClause]. Conversely, +/// do not consume [GenClause]. +pub trait Gen: Debug { + /// Create an instance of this template with some parameters + fn template(&self, ctx: T::Ctx<'_>, params: U) -> T; +} + +impl, G: GenClause> Gen for G { + fn template(&self, ctx: T::Ctx<'_>, params: I) -> T { + let values = RefCell::new(params.into_iter().collect::>()); + let t = self.generate(ctx, &|| { + values.borrow_mut().pop_front().expect("Not enough values provided") + }); + let leftover = values.borrow().len(); + assert_eq!(leftover, 0, "Too many values provided ({leftover} left) {}", Backtrace::force_capture()); + t + } +} diff --git a/src/gen/tree.rs b/src/gen/tree.rs new file mode 100644 index 0000000..0b372bc --- /dev/null +++ b/src/gen/tree.rs @@ -0,0 +1,81 @@ +//! Components to build in-memory module trees that in Orchid. These modules +//! can only contain constants and other modules. + +use std::fmt::Debug; + +use dyn_clone::{clone_box, DynClone}; +use trait_set::trait_set; + +use super::tpl; +use super::traits::{Gen, GenClause}; +use crate::foreign::atom::Atomic; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::nort::Expr; +use crate::location::CodeLocation; +use crate::tree::{ModEntry, ModMember, TreeConflict}; +use crate::utils::combine::Combine; + +trait_set! { + trait TreeLeaf = Gen + DynClone; +} + +/// A leaf in the [ConstTree] +#[derive(Debug)] +pub struct GenConst(Box); +impl GenConst { + fn new(data: impl GenClause + Clone + 'static) -> Self { + Self(Box::new(data)) + } + /// Instantiate template as [crate::interpreter::nort] + pub fn gen_nort(&self, location: CodeLocation) -> Expr { + self.0.template(nort_gen(location), []) + } +} +impl Clone for GenConst { + fn clone(&self) -> Self { Self(clone_box(&*self.0)) } +} + +/// Error condition when constant trees that define the the same constant are +/// merged. Produced during system loading if multiple modules define the +/// same constant +#[derive(Debug, Clone)] +pub struct ConflictingConsts; + +impl Combine for GenConst { + type Error = ConflictingConsts; + fn combine(self, _: Self) -> Result { + Err(ConflictingConsts) + } +} + +/// A lightweight module tree that can be built declaratively by hand to +/// describe libraries of external functions in Rust. It implements [Add] for +/// added convenience +pub type ConstTree = ModEntry; + +/// Describe a constant +#[must_use] +pub fn leaf(value: impl GenClause + Clone + 'static) -> ConstTree { + ModEntry { x: (), member: ModMember::Item(GenConst::new(value)) } +} + +/// Describe an [Atomic] +#[must_use] +pub fn atom_leaf(atom: impl Atomic + Clone + 'static) -> ConstTree { + leaf(tpl::V(atom)) +} + +/// Describe an [Atomic] which appears as an entry in a [ConstTree#tree] +/// +/// The unarray is used to trick rustfmt into breaking the atom into a block +/// without breaking this call into a block +#[must_use] +pub fn atom_ent>( + key: K, + [atom]: [impl Atomic + Clone + 'static; 1], +) -> (K, ConstTree) { + (key, atom_leaf(atom)) +} + +/// Errors produced duriung the merger of constant trees +pub type ConstCombineErr = TreeConflict; diff --git a/src/intermediate/ast_to_ir.rs b/src/intermediate/ast_to_ir.rs new file mode 100644 index 0000000..b53b81a --- /dev/null +++ b/src/intermediate/ast_to_ir.rs @@ -0,0 +1,142 @@ +//! Convert the preprocessed AST into IR + +use std::collections::VecDeque; +use std::rc::Rc; + +use substack::Substack; + +use super::ir; +use crate::error::{ProjectError, ProjectResult}; +use crate::location::{CodeLocation, SourceRange}; +use crate::name::Sym; +use crate::parse::parsed; +use crate::utils::unwrap_or::unwrap_or; + +trait IRErrorKind: Clone + Send + Sync + 'static { + const DESCR: &'static str; +} + +#[derive(Clone)] +struct IRError(SourceRange, Sym, T); +impl ProjectError for IRError { + const DESCRIPTION: &'static str = T::DESCR; + fn message(&self) -> String { format!("In {}, {}", self.1, T::DESCR) } + fn one_position(&self) -> CodeLocation { + CodeLocation::Source(self.0.clone()) + } +} + +#[derive(Clone)] +struct EmptyS; +impl IRErrorKind for EmptyS { + const DESCR: &'static str = + "`()` as a clause is meaningless in lambda calculus"; +} + +#[derive(Clone)] +struct BadGroup; +impl IRErrorKind for BadGroup { + const DESCR: &'static str = "Only `(...)` may be used after macros. \ + `[...]` and `{...}` left in the code are signs of incomplete macro execution"; +} + +#[derive(Clone)] +struct InvalidArg; +impl IRErrorKind for InvalidArg { + const DESCR: &'static str = "Argument names can only be Name nodes"; +} + +#[derive(Clone)] +struct PhLeak; +impl IRErrorKind for PhLeak { + const DESCR: &'static str = "Placeholders shouldn't even appear \ + in the code during macro execution, this is likely a compiler bug"; +} + +/// Try to convert an expression from AST format to typed lambda +pub fn ast_to_ir(expr: parsed::Expr, symbol: Sym) -> ProjectResult { + expr_rec(expr, Context::new(symbol)) +} + +#[derive(Clone)] +struct Context<'a> { + names: Substack<'a, Sym>, + symbol: Sym, +} + +impl<'a> Context<'a> { + #[must_use] + fn w_name<'b>(&'b self, name: Sym) -> Context<'b> + where 'a: 'b { + Context { names: self.names.push(name), symbol: self.symbol.clone() } + } +} +impl Context<'static> { + #[must_use] + fn new(symbol: Sym) -> Self { Self { names: Substack::Bottom, symbol } } +} + +/// Process an expression sequence +fn exprv_rec( + mut v: VecDeque, + ctx: Context<'_>, + location: SourceRange, +) -> ProjectResult { + let last = unwrap_or! {v.pop_back(); { + return Err(IRError(location, ctx.symbol, EmptyS).pack()); + }}; + let v_end = match v.back() { + None => return expr_rec(last, ctx), + Some(penultimate) => penultimate.range.range.end, + }; + let f = exprv_rec(v, ctx.clone(), location.map_range(|r| r.start..v_end))?; + let x = expr_rec(last, ctx)?; + let value = ir::Clause::Apply(Rc::new(f), Rc::new(x)); + Ok(ir::Expr { value, location: CodeLocation::Source(location) }) +} + +/// Process an expression +fn expr_rec( + parsed::Expr { value, range }: parsed::Expr, + ctx: Context, +) -> ProjectResult { + match value { + parsed::Clause::S(parsed::PType::Par, body) => { + return exprv_rec(body.to_vec().into(), ctx, range); + }, + parsed::Clause::S(..) => + return Err(IRError(range, ctx.symbol, BadGroup).pack()), + _ => (), + } + let value = match value { + parsed::Clause::Atom(a) => ir::Clause::Atom(a.clone()), + parsed::Clause::Lambda(arg, b) => { + let name = match &arg[..] { + [parsed::Expr { value: parsed::Clause::Name(name), .. }] => name, + [parsed::Expr { value: parsed::Clause::Placeh { .. }, .. }] => + return Err(IRError(range.clone(), ctx.symbol, PhLeak).pack()), + _ => return Err(IRError(range.clone(), ctx.symbol, InvalidArg).pack()), + }; + let body_ctx = ctx.w_name(name.clone()); + let body = exprv_rec(b.to_vec().into(), body_ctx, range.clone())?; + ir::Clause::Lambda(Rc::new(body)) + }, + parsed::Clause::Name(name) => { + let lvl_opt = (ctx.names.iter()) + .enumerate() + .find(|(_, n)| **n == name) + .map(|(lvl, _)| lvl); + match lvl_opt { + Some(lvl) => ir::Clause::LambdaArg(lvl), + None => ir::Clause::Constant(name.clone()), + } + }, + parsed::Clause::S(parsed::PType::Par, entries) => + exprv_rec(entries.to_vec().into(), ctx, range.clone())?.value, + parsed::Clause::S(..) => + return Err(IRError(range, ctx.symbol, BadGroup).pack()), + parsed::Clause::Placeh { .. } => + return Err(IRError(range, ctx.symbol, PhLeak).pack()), + }; + Ok(ir::Expr::new(value, range.clone())) +} diff --git a/src/representations/postmacro.rs b/src/intermediate/ir.rs similarity index 78% rename from src/representations/postmacro.rs rename to src/intermediate/ir.rs index f98df7f..258c965 100644 --- a/src/representations/postmacro.rs +++ b/src/intermediate/ir.rs @@ -1,10 +1,15 @@ +//! IR is an abstract representation of Orchid expressions that's impractical +//! for all purposes except converting to and from other representations. Future +//! innovations in the processing and execution of code will likely operate on +//! this representation. + use std::fmt::{Debug, Write}; use std::rc::Rc; -use super::location::Location; -use crate::foreign::{Atom, ExFn}; -use crate::utils::string_from_charset; -use crate::Sym; +use crate::foreign::atom::AtomGenerator; +use crate::location::{CodeLocation, SourceRange}; +use crate::name::Sym; +use crate::utils::string_from_charset::string_from_charset; /// Indicates whether either side needs to be wrapped. Syntax whose end is /// ambiguous on that side must use parentheses, or forward the flag @@ -14,10 +19,14 @@ struct Wrap(bool, bool); #[derive(Clone)] pub struct Expr { pub value: Clause, - pub location: Location, + pub location: CodeLocation, } impl Expr { + pub fn new(value: Clause, location: SourceRange) -> Self { + Self { value, location: CodeLocation::Source(location) } + } + fn deep_fmt( &self, f: &mut std::fmt::Formatter<'_>, @@ -42,10 +51,8 @@ pub enum Clause { Lambda(Rc), Constant(Sym), LambdaArg(usize), - /// An opaque function, eg. an effectful function employing CPS - ExternFn(ExFn), /// An opaque non-callable value, eg. a file handle - Atom(Atom), + Atom(AtomGenerator), } const ARGNAME_CHARSET: &str = "abcdefghijklmnopqrstuvwxyz"; @@ -79,7 +86,6 @@ impl Clause { ) -> std::fmt::Result { match self { Self::Atom(a) => write!(f, "{a:?}"), - Self::ExternFn(fun) => write!(f, "{fun:?}"), Self::Lambda(body) => parametric_fmt(f, depth, "\\", body, wr), Self::LambdaArg(skip) => { let lambda_depth = (depth - skip - 1).try_into().unwrap(); diff --git a/src/intermediate/ir_to_nort.rs b/src/intermediate/ir_to_nort.rs new file mode 100644 index 0000000..6150611 --- /dev/null +++ b/src/intermediate/ir_to_nort.rs @@ -0,0 +1,30 @@ +//! Convert IR to the interpreter's NORT representation + +use super::ir; +use crate::interpreter::nort; +use crate::interpreter::nort_builder::NortBuilder; + +fn expr(expr: &ir::Expr, ctx: NortBuilder<(), usize>) -> nort::Expr { + clause(&expr.value, ctx).to_expr(expr.location.clone()) +} + +fn clause(cls: &ir::Clause, ctx: NortBuilder<(), usize>) -> nort::Clause { + match cls { + ir::Clause::Constant(name) => nort::Clause::Constant(name.clone()), + ir::Clause::Atom(a) => nort::Clause::Atom(a.run()), + ir::Clause::LambdaArg(n) => { + ctx.arg_logic(n); + nort::Clause::LambdaArg + }, + ir::Clause::Apply(f, x) => ctx.apply_logic(|c| expr(f, c), |c| expr(x, c)), + ir::Clause::Lambda(body) => ctx.lambda_logic(&(), |c| expr(body, c)), + } +} + +pub fn ir_to_nort(expr: &ir::Expr) -> nort::Expr { + let c = NortBuilder::new(&|count| { + let mut count: usize = *count; + Box::new(move |()| count.checked_sub(1).map(|v| count = v).is_none()) + }); + nort::ClauseInst::new(clause(&expr.value, c)).to_expr(expr.location.clone()) +} diff --git a/src/intermediate/mod.rs b/src/intermediate/mod.rs new file mode 100644 index 0000000..0943548 --- /dev/null +++ b/src/intermediate/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod ast_to_ir; +pub(crate) mod ir; +pub(crate) mod ir_to_nort; diff --git a/src/interner/mod.rs b/src/interner/mod.rs deleted file mode 100644 index c04bbdf..0000000 --- a/src/interner/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! A type-agnostic interner -//! -//! Can be used to deduplicate various structures for fast equality comparisons. -//! The parser uses it to intern strings. -mod monotype; -mod multitype; -mod token; - -pub use monotype::TypedInterner; -pub use multitype::Interner; -pub use token::Tok; diff --git a/src/interner/monotype.rs b/src/interner/monotype.rs deleted file mode 100644 index 5c0d322..0000000 --- a/src/interner/monotype.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::borrow::Borrow; -use std::hash::{BuildHasher, Hash}; -use std::sync::{Arc, RwLock}; - -use hashbrown::HashMap; - -use super::token::Tok; - -/// An interner for any type that implements [Borrow]. This is inspired by -/// Lasso but much simpler, in part because not much can be known about the -/// type. -pub struct TypedInterner { - tokens: RwLock, Tok>>, -} -impl TypedInterner { - /// Create a fresh interner instance - #[must_use] - pub fn new() -> Arc { - Arc::new(Self { tokens: RwLock::new(HashMap::new()) }) - } - - /// Intern an object, returning a token - #[must_use] - pub fn i>( - self: &Arc, - q: &Q, - ) -> Tok - where - T: Borrow, - { - let mut tokens = self.tokens.write().unwrap(); - let hash = compute_hash(tokens.hasher(), q); - let raw_entry = tokens - .raw_entry_mut() - .from_hash(hash, |k| >::borrow(k) == q); - let kv = raw_entry.or_insert_with(|| { - let keyrc = Arc::new(q.to_owned()); - let token = Tok::::new(keyrc.clone(), Arc::downgrade(self)); - (keyrc, token) - }); - kv.1.clone() - } -} - -/// Helper function to compute hashes outside a hashmap -#[must_use] -fn compute_hash( - hash_builder: &impl BuildHasher, - key: &(impl Hash + ?Sized), -) -> u64 { - use core::hash::Hasher; - let mut state = hash_builder.build_hasher(); - key.hash(&mut state); - state.finish() -} diff --git a/src/interner/multitype.rs b/src/interner/multitype.rs deleted file mode 100644 index 38d7073..0000000 --- a/src/interner/multitype.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::any::{Any, TypeId}; -use std::borrow::Borrow; -use std::cell::{RefCell, RefMut}; -use std::hash::Hash; -use std::sync::Arc; - -use hashbrown::HashMap; - -use super::monotype::TypedInterner; -use super::token::Tok; - -/// A collection of interners based on their type. Allows to intern any object -/// that implements [ToOwned]. Objects of the same type are stored together in a -/// [TypedInterner]. -pub struct Interner { - interners: RefCell>>, -} -impl Interner { - /// Create a new interner - #[must_use] - pub fn new() -> Self { Self { interners: RefCell::new(HashMap::new()) } } - - /// Intern something - #[must_use] - pub fn i(&self, q: &Q) -> Tok - where - Q::Owned: 'static + Eq + Hash + Clone + Borrow + Send + Sync, - { - let mut interners = self.interners.borrow_mut(); - let interner = get_interner(&mut interners); - interner.i(q) - } - - /// Fully resolve a list of interned things. - #[must_use] - pub fn extern_all(s: &[Tok]) -> Vec { - s.iter().map(|t| (**t).clone()).collect() - } -} - -impl Default for Interner { - fn default() -> Self { Self::new() } -} - -/// Get or create an interner for a given type. -#[must_use] -fn get_interner( - interners: &mut RefMut>>, -) -> Arc> { - let boxed = interners - .raw_entry_mut() - .from_key(&TypeId::of::()) - .or_insert_with(|| (TypeId::of::(), TypedInterner::::new())) - .1 - .clone(); - boxed.downcast().expect("the typeid is supposed to protect from this") -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - pub fn test_string() { - let interner = Interner::new(); - let key1 = interner.i("foo"); - let key2 = interner.i(&"foo".to_string()); - assert_eq!(key1, key2) - } - - #[test] - pub fn test_slice() { - let interner = Interner::new(); - let key1 = interner.i(&vec![1, 2, 3]); - let key2 = interner.i(&[1, 2, 3][..]); - assert_eq!(key1, key2); - } - - // #[test] - #[allow(unused)] - pub fn test_str_slice() { - let interner = Interner::new(); - let key1 = - interner.i(&vec!["a".to_string(), "b".to_string(), "c".to_string()]); - let key2 = interner.i(&["a", "b", "c"][..]); - // assert_eq!(key1, key2); - } -} diff --git a/src/interner/token.rs b/src/interner/token.rs deleted file mode 100644 index 70b6374..0000000 --- a/src/interner/token.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::cmp::PartialEq; -use std::fmt::{Debug, Display}; -use std::hash::Hash; -use std::num::NonZeroUsize; -use std::ops::Deref; -use std::sync::{Arc, Weak}; - -use super::TypedInterner; - -/// A number representing an object of type `T` stored in some interner. It is a -/// logic error to compare tokens obtained from different interners, or to use a -/// token with an interner other than the one that created it, but this is -/// currently not enforced. -#[derive(Clone)] -pub struct Tok { - data: Arc, - interner: Weak>, -} -impl Tok { - /// Create a new token. Used exclusively by the interner - #[must_use] - pub(crate) fn new(data: Arc, interner: Weak>) -> Self { - Self { data, interner } - } - /// Take the ID number out of a token - #[must_use] - pub fn id(&self) -> NonZeroUsize { - ((self.data.as_ref() as *const T as usize).try_into()) - .expect("Pointer can always be cast to nonzero") - } - /// Cast into usize - #[must_use] - pub fn usize(&self) -> usize { self.id().into() } - /// - pub fn assert_comparable(&self, other: &Self) { - let iref = self.interner.as_ptr() as usize; - assert!( - iref == other.interner.as_ptr() as usize, - "Tokens must come from the same interner" - ); - } -} - -impl Tok>> { - /// Extern all elements of the vector in a new vector - pub fn extern_vec(&self) -> Vec { - self.iter().map(|t| (**t).clone()).collect() - } -} - -impl Deref for Tok { - type Target = T; - - fn deref(&self) -> &Self::Target { self.data.as_ref() } -} - -impl Debug for Tok { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Token({} -> {:?})", self.id(), self.data.as_ref()) - } -} - -impl Display for Tok { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", **self) - } -} - -impl Eq for Tok {} -impl PartialEq for Tok { - fn eq(&self, other: &Self) -> bool { - self.assert_comparable(other); - self.id() == other.id() - } -} - -impl Ord for Tok { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.assert_comparable(other); - self.id().cmp(&other.id()) - } -} -impl PartialOrd for Tok { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Hash for Tok { - fn hash(&self, state: &mut H) { - state.write_usize(self.usize()) - } -} diff --git a/src/interpreter/apply.rs b/src/interpreter/apply.rs index c593b39..b3ad50e 100644 --- a/src/interpreter/apply.rs +++ b/src/interpreter/apply.rs @@ -1,113 +1,188 @@ -use super::context::Context; -use super::error::RuntimeError; -use super::Return; -use crate::foreign::AtomicReturn; -use crate::representations::interpreted::{Clause, ExprInst}; -use crate::representations::PathSet; -use crate::utils::never::{unwrap_always, Always}; -use crate::utils::Side; +use std::collections::VecDeque; +use std::mem; + +use never::Never; + +use super::context::RunContext; +use super::error::RunError; +use super::nort::{Clause, ClauseInst, Expr}; +use super::path_set::{PathSet, Step}; +use super::run::run; +use crate::location::CodeLocation; + +/// Information about a function call presented to an external function +pub struct CallData<'a> { + /// Location of the function expression + pub location: CodeLocation, + /// The argument the function was called on. Functions are curried + pub arg: Expr, + /// Information relating to this interpreter run + pub ctx: RunContext<'a>, +} /// Process the clause at the end of the provided path. Note that paths always /// point to at least one target. Note also that this is not cached as a /// normalization step in the intermediate expressions. fn map_at( - path: &[Side], - source: ExprInst, - mapper: &mut impl FnMut(Clause) -> Result, -) -> Result { - source - .try_update(|value, _loc| { - // Pass right through lambdas - if let Clause::Lambda { args, body } = value { - return Ok(( - Clause::Lambda { args, body: map_at(path, body, mapper)? }, - (), - )); - } - // If the path ends here, process the next (non-lambda) node - let (head, tail) = if let Some(sf) = path.split_first() { - sf - } else { - return Ok((mapper(value)?, ())); + mut path: impl Iterator, + source: &Clause, + mapper: &mut impl FnMut(&Clause) -> Result, +) -> Result { + // Pass through some unambiguous wrapper clauses + match source { + Clause::Identity(alt) => return map_at(path, &alt.cls(), mapper), + Clause::Lambda { args, body: Expr { location: b_loc, clause } } => + return Ok(Clause::Lambda { + args: args.clone(), + body: Expr { + clause: map_at(path, &clause.cls(), mapper)?.to_inst(), + location: b_loc.clone(), + }, + }), + _ => (), + } + Ok(match (source, path.next()) { + (Clause::Lambda { .. } | Clause::Identity(_), _) => + unreachable!("Handled above"), + // If the path ends and this isn't a lambda, process it + (val, None) => mapper(val)?, + // If it's an Apply, execute the next step in the path + (Clause::Apply { f, x }, Some(head)) => { + let proc = |x: &Expr| { + Ok(map_at(path, &x.clause.cls(), mapper)?.to_expr(x.location())) }; - // If it's an Apply, execute the next step in the path - if let Clause::Apply { f, x } = value { - return Ok(( - match head { - Side::Left => Clause::Apply { f: map_at(tail, f, mapper)?, x }, - Side::Right => Clause::Apply { f, x: map_at(tail, x, mapper)? }, - }, - (), - )); + match head { + None => Clause::Apply { f: proc(f)?, x: x.clone() }, + Some(n) => { + let i = x.len() - n - 1; + let mut argv = x.clone(); + argv[i] = proc(&x[i])?; + Clause::Apply { f: f.clone(), x: argv } + }, } - panic!("Invalid path") - }) - .map(|p| p.0) + }, + (_, Some(_)) => panic!("Path leads into node that isn't Apply or Lambda"), + }) } /// Replace the [Clause::LambdaArg] placeholders at the ends of the [PathSet] /// with the value in the body. Note that a path may point to multiple /// placeholders. #[must_use] -fn substitute(paths: &PathSet, value: Clause, body: ExprInst) -> ExprInst { +fn substitute(paths: &PathSet, value: ClauseInst, body: &Clause) -> Clause { let PathSet { steps, next } = paths; - unwrap_always(map_at(steps, body, &mut |checkpoint| -> Always { - match (checkpoint, next) { - (Clause::Lambda { .. }, _) => unreachable!("Handled by map_at"), - (Clause::Apply { f, x }, Some((left, right))) => Ok(Clause::Apply { - f: substitute(left, value.clone(), f), - x: substitute(right, value.clone(), x), - }), - (Clause::LambdaArg, None) => Ok(value.clone()), - (_, None) => { - panic!("Substitution path ends in something other than LambdaArg") - }, - (_, Some(_)) => { - panic!("Substitution path leads into something other than Apply") + map_at(steps.iter().cloned(), body, &mut |chkpt| -> Result { + match (chkpt, next) { + (Clause::Lambda { .. } | Clause::Identity(_), _) => { + unreachable!("Handled by map_at") }, + (Clause::Apply { f, x }, Some(conts)) => { + let mut argv = x.clone(); + let f = match conts.get(&None) { + None => f.clone(), + Some(sp) => substitute(sp, value.clone(), &f.clause.cls()) + .to_expr(f.location()), + }; + for (i, old) in argv.iter_mut().rev().enumerate() { + if let Some(sp) = conts.get(&Some(i)) { + let tmp = substitute(sp, value.clone(), &old.clause.cls()); + *old = tmp.to_expr(old.location()); + } + } + Ok(Clause::Apply { f, x: argv }) + }, + (Clause::LambdaArg, None) => Ok(Clause::Identity(value.clone())), + (_, None) => panic!("Argument path must point to LambdaArg"), + (_, Some(_)) => panic!("Argument path can only fork at Apply"), } - })) + }) + .unwrap_or_else(|e| match e {}) +} + +pub(super) fn apply_as_atom( + f: Expr, + arg: Expr, + ctx: RunContext, +) -> Result { + let call = CallData { location: f.location(), arg, ctx }; + match f.clause.try_unwrap() { + Ok(clause) => match clause { + Clause::Atom(atom) => Ok(atom.apply(call)?), + _ => panic!("Not an atom"), + }, + Err(clsi) => match &*clsi.cls() { + Clause::Atom(atom) => Ok(atom.apply_ref(call)?), + _ => panic!("Not an atom"), + }, + } } /// Apply a function-like expression to a parameter. -pub fn apply( - f: ExprInst, - x: ExprInst, - ctx: Context, -) -> Result { - let (state, (gas, inert)) = f.try_update(|clause, loc| match clause { - // apply an ExternFn or an internal function - Clause::ExternFn(f) => { - let clause = f.apply(x, ctx.clone()).map_err(RuntimeError::Extern)?; - Ok((clause, (ctx.gas.map(|g| g - 1), false))) - }, - Clause::Lambda { args, body } => Ok(if let Some(args) = args { - let x_cls = x.expr_val().clause; - let result = substitute(&args, x_cls, body); - // cost of substitution - // XXX: should this be the number of occurrences instead? - (result.expr_val().clause, (ctx.gas.map(|x| x - 1), false)) - } else { - (body.expr_val().clause, (ctx.gas, false)) - }), - Clause::Constant(name) => - if let Some(sym) = ctx.symbols.get(&name) { - Ok((Clause::Apply { f: sym.clone(), x }, (ctx.gas, false))) - } else { - Err(RuntimeError::MissingSymbol(name.clone(), loc)) +pub(super) fn apply( + mut f: Expr, + mut argv: VecDeque, + mut ctx: RunContext, +) -> Result<(Option, Clause), RunError> { + // allow looping but break on the main path so that `continue` functions as a + // trampoline + loop { + if argv.is_empty() { + return Ok((ctx.gas, f.clause.into_cls())); + } else if ctx.gas == Some(0) { + return Ok((Some(0), Clause::Apply { f, x: argv })); + } + let mut f_cls = f.clause.cls_mut(); + match &mut *f_cls { + // apply an ExternFn or an internal function + Clause::Atom(_) => { + mem::drop(f_cls); + // take a step in expanding atom + let halt = run(f, ctx.clone())?; + ctx.gas = halt.gas; + if halt.inert && halt.state.clause.is_atom() { + let arg = argv.pop_front().expect("checked above"); + let loc = halt.state.location(); + f = apply_as_atom(halt.state, arg, ctx.clone())?.to_expr(loc) + } else { + f = halt.state + } }, - Clause::Atom(atom) => { - // take a step in expanding atom - let AtomicReturn { clause, gas, inert } = atom.run(ctx.clone())?; - Ok((Clause::Apply { f: clause.wrap(), x }, (gas, inert))) - }, - Clause::Apply { f: fun, x: arg } => { - // take a step in resolving pre-function - let ret = apply(fun, arg, ctx.clone())?; - let Return { state, inert, gas } = ret; - Ok((Clause::Apply { f: state, x }, (gas, inert))) - }, - _ => Err(RuntimeError::NonFunctionApplication(loc)), - })?; - Ok(Return { state, gas, inert }) + Clause::Lambda { args, body } => { + match args { + None => *f_cls = body.clause.clone().into_cls(), + Some(args) => { + let arg = argv.pop_front().expect("checked above").clause.clone(); + let cls = substitute(args, arg, &body.clause.cls()); + // cost of substitution + // XXX: should this be the number of occurrences instead? + ctx.use_gas(1); + mem::drop(f_cls); + f = cls.to_expr(f.location()); + }, + } + }, + Clause::Constant(name) => { + let name = name.clone(); + mem::drop(f_cls); + f = (ctx.symbols.get(&name).cloned()) + .ok_or_else(|| RunError::MissingSymbol(name, f.location()))?; + ctx.use_gas(1); + }, + Clause::Apply { f: fun, x } => { + for item in x.drain(..).rev() { + argv.push_front(item) + } + let tmp = fun.clone(); + mem::drop(f_cls); + f = tmp; + }, + Clause::Identity(f2) => { + let tmp = f2.clone(); + mem::drop(f_cls); + f.clause = tmp + }, + Clause::Bottom(bottom) => return Err(bottom.clone()), + Clause::LambdaArg => panic!("Leftover argument marker"), + } + } } diff --git a/src/interpreter/context.rs b/src/interpreter/context.rs index caf4c4e..094b2a5 100644 --- a/src/interpreter/context.rs +++ b/src/interpreter/context.rs @@ -1,32 +1,38 @@ use hashbrown::HashMap; -use crate::interner::Interner; -use crate::representations::interpreted::ExprInst; -use crate::Sym; +use super::nort::Expr; +use crate::name::Sym; /// All the data associated with an interpreter run #[derive(Clone)] -pub struct Context<'a> { +pub struct RunContext<'a> { /// Table used to resolve constants - pub symbols: &'a HashMap, - /// The interner used for strings internally, so external functions can - /// deduce referenced constant names on the fly - pub interner: &'a Interner, + pub symbols: &'a HashMap, /// The number of reduction steps the interpreter can take before returning pub gas: Option, } +impl<'a> RunContext<'a> { + /// Consume some gas if it is being counted + pub fn use_gas(&mut self, amount: usize) { + if let Some(g) = self.gas.as_mut() { + *g = g.saturating_sub(amount) + } + } + /// Gas is being counted and there is none left + pub fn no_gas(&self) -> bool { self.gas == Some(0) } +} /// All the data produced by an interpreter run #[derive(Clone)] -pub struct Return { +pub struct Halt { /// The new expression tree - pub state: ExprInst, + pub state: Expr, /// Leftover [Context::gas] if counted pub gas: Option, /// If true, the next run would not modify the expression pub inert: bool, } -impl Return { +impl Halt { /// Check if gas has run out. Returns false if gas is not being used pub fn preempted(&self) -> bool { self.gas.map_or(false, |g| g == 0) } /// Returns a general report of the return diff --git a/src/interpreter/error.rs b/src/interpreter/error.rs index 09cc840..e24de70 100644 --- a/src/interpreter/error.rs +++ b/src/interpreter/error.rs @@ -1,37 +1,33 @@ use std::fmt::{Debug, Display}; use std::sync::Arc; -use crate::foreign::ExternError; -use crate::{Location, Sym}; +use crate::foreign::error::ExternError; +use crate::location::CodeLocation; +use crate::name::Sym; + +use super::run::Interrupted; /// Problems in the process of execution #[derive(Debug, Clone)] -pub enum RuntimeError { +pub enum RunError { /// A Rust function encountered an error Extern(Arc), - /// Primitive applied as function - NonFunctionApplication(Location), /// Symbol not in context - MissingSymbol(Sym, Location), + MissingSymbol(Sym, CodeLocation), + /// Ran out of gas + Interrupted(Interrupted) } -impl From> for RuntimeError { +impl From> for RunError { fn from(value: Arc) -> Self { Self::Extern(value) } } -impl Display for RuntimeError { +impl Display for RunError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Extern(e) => write!(f, "Error in external function: {e}"), - Self::NonFunctionApplication(location) => { - write!(f, "Primitive applied as function at {}", location) - }, Self::MissingSymbol(sym, loc) => { - write!( - f, - "{}, called at {loc} is not loaded", - sym.extern_vec().join("::") - ) + write!(f, "{sym}, called at {loc} is not loaded") }, } } diff --git a/src/interpreter/gen_nort.rs b/src/interpreter/gen_nort.rs new file mode 100644 index 0000000..90963d9 --- /dev/null +++ b/src/interpreter/gen_nort.rs @@ -0,0 +1,126 @@ +//! Implementations of [Generable] for [super::nort] + +use intern_all::i; + +use super::nort_builder::NortBuilder; +use crate::foreign::atom::Atom; +use crate::foreign::to_clause::ToClause; +use crate::gen::traits::Generable; +use crate::interpreter::nort::{Clause, ClauseInst, Expr}; +use crate::location::CodeLocation; +use crate::name::Sym; + +/// Context data for instantiating templated expressions as [super::nort]. +/// Instances of this type are created via [nort_gen] +pub type NortGenCtx<'a> = (CodeLocation, NortBuilder<'a, str, str>); + +/// Create [NortGenCtx] instances to generate interpreted expressions +pub fn nort_gen<'a>(location: CodeLocation) -> NortGenCtx<'a> { + (location, NortBuilder::new(&|l| Box::new(move |r| l == r))) +} + +impl Generable for Expr { + type Ctx<'a> = NortGenCtx<'a>; + fn apply( + ctx: Self::Ctx<'_>, + f_cb: impl FnOnce(Self::Ctx<'_>) -> Self, + x_cb: impl FnOnce(Self::Ctx<'_>) -> Self, + ) -> Self { + (ctx + .1 + .apply_logic(|c| f_cb((ctx.0.clone(), c)), |c| x_cb((ctx.0.clone(), c)))) + .to_expr(ctx.0.clone()) + } + fn arg(ctx: Self::Ctx<'_>, name: &str) -> Self { + Clause::arg(ctx.clone(), name).to_expr(ctx.0.clone()) + } + fn atom(ctx: Self::Ctx<'_>, a: Atom) -> Self { + Clause::atom(ctx.clone(), a).to_expr(ctx.0.clone()) + } + fn constant<'a>( + ctx: Self::Ctx<'_>, + name: impl IntoIterator, + ) -> Self { + Clause::constant(ctx.clone(), name).to_expr(ctx.0.clone()) + } + fn lambda( + ctx: Self::Ctx<'_>, + name: &str, + body: impl FnOnce(Self::Ctx<'_>) -> Self, + ) -> Self { + (ctx.1.lambda_logic(name, |c| body((ctx.0.clone(), c)))) + .to_expr(ctx.0.clone()) + } +} + +impl Generable for ClauseInst { + type Ctx<'a> = NortGenCtx<'a>; + fn arg(ctx: Self::Ctx<'_>, name: &str) -> Self { + Clause::arg(ctx, name).to_inst() + } + fn atom(ctx: Self::Ctx<'_>, a: Atom) -> Self { + Clause::atom(ctx, a).to_inst() + } + fn constant<'a>( + ctx: Self::Ctx<'_>, + name: impl IntoIterator, + ) -> Self { + Clause::constant(ctx, name).to_inst() + } + fn lambda( + ctx: Self::Ctx<'_>, + name: &str, + body: impl FnOnce(Self::Ctx<'_>) -> Self, + ) -> Self { + (ctx + .1 + .lambda_logic(name, |c| body((ctx.0.clone(), c)).to_expr(ctx.0.clone()))) + .to_clsi(ctx.0.clone()) + } + fn apply( + ctx: Self::Ctx<'_>, + f: impl FnOnce(Self::Ctx<'_>) -> Self, + x: impl FnOnce(Self::Ctx<'_>) -> Self, + ) -> Self { + (ctx.1.apply_logic( + |c| f((ctx.0.clone(), c)).to_expr(ctx.0.clone()), + |c| x((ctx.0.clone(), c)).to_expr(ctx.0.clone()), + )) + .to_clsi(ctx.0.clone()) + } +} + +impl Generable for Clause { + type Ctx<'a> = NortGenCtx<'a>; + fn atom(_: Self::Ctx<'_>, a: Atom) -> Self { Clause::Atom(a) } + fn constant<'a>( + _: Self::Ctx<'_>, + name: impl IntoIterator, + ) -> Self { + let sym = Sym::new(name.into_iter().map(i)).expect("Empty constant"); + Clause::Constant(sym) + } + fn apply( + ctx: Self::Ctx<'_>, + f: impl FnOnce(Self::Ctx<'_>) -> Self, + x: impl FnOnce(Self::Ctx<'_>) -> Self, + ) -> Self { + ctx.1.apply_logic( + |c| f((ctx.0.clone(), c)).to_expr(ctx.0.clone()), + |c| x((ctx.0.clone(), c)).to_expr(ctx.0.clone()), + ) + } + fn arg(ctx: Self::Ctx<'_>, name: &str) -> Self { + ctx.1.arg_logic(name); + Clause::LambdaArg + } + fn lambda( + ctx: Self::Ctx<'_>, + name: &str, + body: impl FnOnce(Self::Ctx<'_>) -> Self, + ) -> Self { + ctx + .1 + .lambda_logic(name, |c| body((ctx.0.clone(), c)).to_expr(ctx.0.clone())) + } +} diff --git a/src/interpreter/handler.rs b/src/interpreter/handler.rs index 610bd74..62cae3a 100644 --- a/src/interpreter/handler.rs +++ b/src/interpreter/handler.rs @@ -1,16 +1,19 @@ use std::any::{Any, TypeId}; -use std::sync::Arc; use hashbrown::HashMap; use trait_set::trait_set; -use super::{run, Context, Return, RuntimeError}; -use crate::foreign::{Atom, Atomic, ExternError}; -use crate::interpreted::{Clause, Expr, ExprInst}; -use crate::utils::take_with_output; +use super::context::{Halt, RunContext}; +use super::error::RunError; +use super::nort::{Clause, Expr}; +use super::run::run; +use crate::foreign::atom::{Atom, Atomic}; +use crate::foreign::error::ExternResult; +use crate::foreign::to_clause::ToClause; +use crate::location::CodeLocation; trait_set! { - trait Handler = FnMut(Box) -> HandlerRes; + trait Handler = for<'a> FnMut(&'a dyn Any, CodeLocation) -> Expr; } /// A table of command handlers @@ -23,26 +26,37 @@ impl<'a> HandlerTable<'a> { #[must_use] pub fn new() -> Self { Self { handlers: HashMap::new() } } - /// Add a handler function to interpret a type of atom and decide what happens - /// next. This function can be impure. - pub fn register( + /// Add a handler function to interpret a command and select the continuation. + /// See [HandlerTable#with] for a declarative option. + pub fn register( &mut self, - mut f: impl FnMut(Box) -> HandlerRes + 'a, + mut f: impl for<'b> FnMut(&'b T) -> R + 'a, ) { - let cb = move |a: Box| f(a.downcast().expect("found by TypeId")); + let cb = move |a: &dyn Any, loc: CodeLocation| { + f(a.downcast_ref().expect("found by TypeId")).to_expr(loc) + }; let prev = self.handlers.insert(TypeId::of::(), Box::new(cb)); assert!(prev.is_none(), "A handler for this type is already registered"); } + /// Add a handler function to interpret a command and select the continuation. + /// See [HandlerTable#register] for a procedural option. + pub fn with( + mut self, + f: impl FnMut(&T) -> ExternResult + 'a, + ) -> Self { + self.register(f); + self + } + /// Find and execute the corresponding handler for this type pub fn dispatch( &mut self, - arg: Box, - ) -> Result> { - match self.handlers.get_mut(&arg.as_any_ref().type_id()) { - Some(f) => Ok(f(arg.as_any())), - None => Err(arg), - } + arg: &dyn Atomic, + loc: CodeLocation, + ) -> Option { + (self.handlers.get_mut(&arg.as_any_ref().type_id())) + .map(|f| f(arg.as_any_ref(), loc)) } /// Combine two non-overlapping handler sets @@ -56,33 +70,27 @@ impl<'a> HandlerTable<'a> { } } -/// Various possible outcomes of a [Handler] execution. Ok returns control to -/// the interpreter. The meaning of Err is decided by the value in it. -pub type HandlerRes = Result>; - /// [run] orchid code, executing any commands it returns using the specified /// [Handler]s. pub fn run_handler( - mut expr: ExprInst, + mut state: Expr, handlers: &mut HandlerTable, - mut ctx: Context, -) -> Result { + RunContext { mut gas, symbols }: RunContext, +) -> Result { loop { - let mut ret = run(expr, ctx.clone())?; - let quit = take_with_output(&mut ret.state, |exi| match exi.expr_val() { - Expr { clause: Clause::Atom(a), .. } => match handlers.dispatch(a.0) { - Err(b) => (Clause::Atom(Atom(b)).wrap(), Ok(true)), - Ok(e) => match e { - Ok(expr) => (expr, Ok(false)), - Err(e) => (Clause::Bottom.wrap(), Err(e)), - }, - }, - expr => (ExprInst::new(expr), Ok(true)), - })?; - if quit | ret.gas.map_or(false, |g| g == 0) { - return Ok(ret); + let inert; + Halt { gas, inert, state } = run(state, RunContext { gas, symbols })?; + let state_cls = state.clause.cls(); + if let Clause::Atom(Atom(a)) = &*state_cls { + if let Some(res) = handlers.dispatch(a.as_ref(), state.location()) { + drop(state_cls); + state = res; + continue; + } + } + if inert || gas == Some(0) { + drop(state_cls); + break Ok(Halt { gas, inert, state }); } - ctx.gas = ret.gas; - expr = ret.state; } } diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index b2dbedc..3350841 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -1,11 +1,10 @@ //! functions to interact with Orchid code -mod apply; -mod context; -mod error; -mod handler; -mod run; - -pub use context::{Context, Return, ReturnStatus}; -pub use error::RuntimeError; -pub use handler::{run_handler, HandlerRes, HandlerTable}; -pub use run::run; +pub mod apply; +pub mod context; +pub mod error; +pub mod gen_nort; +pub mod handler; +pub mod nort_builder; +pub mod nort; +pub(crate) mod path_set; +pub mod run; diff --git a/src/interpreter/nort.rs b/src/interpreter/nort.rs new file mode 100644 index 0000000..55ff40a --- /dev/null +++ b/src/interpreter/nort.rs @@ -0,0 +1,334 @@ +//! The NORT (Normal Order Referencing Tree) is the interpreter's runtime +//! representation of Orchid programs. +//! +//! It uses a locator tree to find bound variables in lambda functions, which +//! necessitates a normal reduction order because modifying the body by reducing +//! expressions would invalidate any locators in enclosing lambdas. +//! +//! Clauses are held in a mutable `Arc>`, so that after substitution +//! the instances of the argument remain linked and a reduction step applied to +//! any instance transforms all of them. +//! +//! To improve locality and make the tree less deep and locators shorter, +//! function calls store multiple arguments in a deque. + +use std::collections::VecDeque; +use std::fmt::{Debug, Display}; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, Mutex, TryLockError}; + +use itertools::Itertools; + +use super::error::RunError; +use super::path_set::PathSet; +use crate::foreign::atom::Atom; +#[allow(unused)] // for doc +use crate::foreign::atom::Atomic; +use crate::foreign::error::ExternResult; +use crate::foreign::try_from_expr::TryFromExpr; +use crate::location::CodeLocation; +use crate::name::Sym; +#[allow(unused)] // for doc +use crate::parse::parsed; +use crate::utils::ddispatch::request; +use crate::utils::take_with_output::take_with_output; + +/// Kinda like [AsMut] except it supports a guard +pub(crate) trait AsDerefMut { + fn as_deref_mut(&mut self) -> impl DerefMut + '_; +} + +/// An expression with metadata +#[derive(Clone)] +pub struct Expr { + /// The actual value + pub clause: ClauseInst, + /// Information about the code that produced this value + pub location: CodeLocation, +} +impl Expr { + /// Constructor + pub fn new(clause: ClauseInst, location: CodeLocation) -> Self { + Self { clause, location } + } + /// Obtain the location of the expression + pub fn location(&self) -> CodeLocation { self.location.clone() } + + /// Convert into any type that implements [TryFromExpr]. Calls to this + /// function are generated wherever a conversion is elided in an extern + /// function. + pub fn downcast(self) -> ExternResult { + let Expr { mut clause, location } = self; + loop { + let cls_deref = clause.cls(); + match &*cls_deref { + Clause::Identity(alt) => { + let temp = alt.clone(); + drop(cls_deref); + clause = temp; + }, + _ => { + drop(cls_deref); + return T::from_expr(Expr { clause, location }); + }, + }; + } + } + + /// Visit all expressions in the tree. The search can be exited early by + /// returning [Some] + /// + /// See also [parsed::Expr::search_all] + pub fn search_all( + &self, + predicate: &mut impl FnMut(&Self) -> Option, + ) -> Option { + if let Some(t) = predicate(self) { + return Some(t); + } + self.clause.inspect(|c| match c { + Clause::Identity(_alt) => unreachable!("Handled by inspect"), + Clause::Apply { f, x } => (f.search_all(predicate)) + .or_else(|| x.iter().find_map(|x| x.search_all(predicate))), + Clause::Lambda { body, .. } => body.search_all(predicate), + Clause::Constant(_) + | Clause::LambdaArg + | Clause::Atom(_) + | Clause::Bottom(_) => None, + }) + } +} + +impl Debug for Expr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}@{}", self.clause, self.location) + } +} + +impl Display for Expr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.clause) + } +} + +impl AsDerefMut for Expr { + fn as_deref_mut(&mut self) -> impl DerefMut + '_ { + self.clause.cls_mut() + } +} + +/// [ExprInst::with_literal] produces this marker unit to indicate that the +/// expression is not a literal +pub struct NotALiteral; + +/// A wrapper around expressions to handle their multiple occurences in +/// the tree together +#[derive(Clone)] +pub struct ClauseInst(pub Arc>); +impl ClauseInst { + /// Wrap a [Clause] in a shared container so that normalization steps are + /// applied to all references + #[must_use] + pub fn new(cls: Clause) -> Self { Self(Arc::new(Mutex::new(cls))) } + + /// Take the [Clause] out of this container if it's the last reference to it, + /// or return self. + pub fn try_unwrap(self) -> Result { + Arc::try_unwrap(self.0).map(|c| c.into_inner().unwrap()).map_err(Self) + } + + /// Read-only access to the shared clause instance + /// + /// # Panics + /// + /// if the clause is already borrowed in read-write mode + #[must_use] + pub fn cls(&self) -> impl Deref + '_ { + self.0.lock().unwrap() + } + + /// Read-Write access to the shared clause instance + /// + /// # Panics + /// + /// if the clause is already borrowed + #[must_use] + pub fn cls_mut(&self) -> impl DerefMut + '_ { + self.0.lock().unwrap() + } + + /// Call a normalization function on the expression. The expr is + /// updated with the new clause which affects all copies of it + /// across the tree. + pub fn try_normalize( + &self, + mapper: impl FnOnce(Clause) -> Result<(Clause, T), RunError>, + ) -> Result<(ClauseInst, T), RunError> { + enum Report { + Nested(ClauseInst, T), + Plain(T), + } + let ret = take_with_output(&mut *self.cls_mut(), |clause| match &clause { + Clause::Identity(alt) => match alt.try_normalize(mapper) { + Ok((nested, t)) => (clause, Ok(Report::Nested(nested, t))), + Err(e) => (Clause::Bottom(e.clone()), Err(e)), + }, + _ => match mapper(clause) { + Err(e) => (Clause::Bottom(e.clone()), Err(e)), + Ok((clause, t)) => (clause, Ok(Report::Plain(t))), + }, + })?; + Ok(match ret { + Report::Nested(nested, t) => (nested, t), + Report::Plain(t) => (self.clone(), t), + }) + } + + /// Call a predicate on the clause, returning whatever the + /// predicate returns. This is a convenience function for reaching + /// through the [Mutex]. The clause will never be [Clause::Identity]. + #[must_use] + pub fn inspect(&self, predicate: impl FnOnce(&Clause) -> T) -> T { + match &*self.cls() { + Clause::Identity(sub) => sub.inspect(predicate), + x => predicate(x), + } + } + + /// If this expression is an [Atomic], request an object of the given type. + /// If it's not an atomic, fail the request automatically. + #[must_use = "your request might not have succeeded"] + pub fn request(&self) -> Option { + match &*self.cls() { + Clause::Atom(a) => request(&*a.0), + Clause::Identity(alt) => alt.request(), + _ => None, + } + } + + /// Associate a location with this clause + pub fn to_expr(self, location: CodeLocation) -> Expr { + Expr { clause: self.clone(), location: location.clone() } + } + /// Check ahead-of-time if this clause contains an atom. Calls + /// [ClauseInst#cls] for read access. + /// + /// Since atoms cannot become normalizable, if this is true and previous + /// normalization failed, the atom is known to be in normal form. + pub fn is_atom(&self) -> bool { matches!(&*self.cls(), Clause::Atom(_)) } + + /// Tries to unwrap the [Arc]. If that fails, clones it field by field. + /// If it's a [Clause::Atom] which cannot be cloned, wraps it in a + /// [Clause::Identity]. + /// + /// Implementation of [crate::foreign::to_clause::ToClause::to_clause]. The + /// trait is more general so it requires a location which this one doesn't. + pub fn into_cls(self) -> Clause { + self.try_unwrap().unwrap_or_else(|clsi| match &*clsi.cls() { + Clause::Apply { f, x } => Clause::Apply { f: f.clone(), x: x.clone() }, + Clause::Atom(_) => Clause::Identity(clsi.clone()), + Clause::Bottom(e) => Clause::Bottom(e.clone()), + Clause::Constant(c) => Clause::Constant(c.clone()), + Clause::Identity(sub) => Clause::Identity(sub.clone()), + Clause::Lambda { args, body } => + Clause::Lambda { args: args.clone(), body: body.clone() }, + Clause::LambdaArg => Clause::LambdaArg, + }) + } +} + +impl Debug for ClauseInst { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0.try_lock() { + Ok(expr) => write!(f, "{expr:?}"), + Err(TryLockError::Poisoned(_)) => write!(f, ""), + Err(TryLockError::WouldBlock) => write!(f, ""), + } + } +} + +impl Display for ClauseInst { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0.try_lock() { + Ok(expr) => write!(f, "{expr}"), + Err(TryLockError::Poisoned(_)) => write!(f, ""), + Err(TryLockError::WouldBlock) => write!(f, ""), + } + } +} + +impl AsDerefMut for ClauseInst { + fn as_deref_mut(&mut self) -> impl DerefMut + '_ { + self.cls_mut() + } +} + +/// Distinct types of expressions recognized by the interpreter +#[derive(Debug)] +pub enum Clause { + /// An expression that causes an error + Bottom(RunError), + /// Indicates that this [ClauseInst] has the same value as the other + /// [ClauseInst]. This has two benefits; + /// + /// - [Clause] and therefore [Atomic] doesn't have to be [Clone] which saves + /// many synchronization primitives and reference counters in usercode + /// - it enforces on the type level that all copies are normalized together, + /// so accidental inefficiency in the interpreter is rarer. + /// + /// That being said, it's still arbitrary many indirections, so when possible + /// APIs should be usable with a [ClauseInst] directly. + Identity(ClauseInst), + /// An opaque non-callable value, eg. a file handle + Atom(Atom), + /// A function application + Apply { + /// Function to be applied + f: Expr, + /// Argument to be substituted in the function + x: VecDeque, + }, + /// A name to be looked up in the interpreter's symbol table + Constant(Sym), + /// A function + Lambda { + /// A collection of (zero or more) paths to placeholders belonging to this + /// function + args: Option, + /// The tree produced by this function, with placeholders where the + /// argument will go + body: Expr, + }, + /// A placeholder within a function that will be replaced upon application + LambdaArg, +} +impl Clause { + /// Wrap a clause in a refcounted lock + pub fn to_inst(self) -> ClauseInst { ClauseInst::new(self) } + /// Wrap a clause in an expression. + pub fn to_expr(self, location: CodeLocation) -> Expr { + self.to_inst().to_expr(location) + } +} + +impl Display for Clause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Clause::Atom(a) => write!(f, "{a:?}"), + Clause::Bottom(err) => write!(f, "bottom({err})"), + Clause::LambdaArg => write!(f, "arg"), + Clause::Apply { f: fun, x } => + write!(f, "({fun} {})", x.iter().join(" ")), + Clause::Lambda { args, body } => match args { + Some(path) => write!(f, "\\{path:?}.{body}"), + None => write!(f, "\\_.{body}"), + }, + Clause::Constant(t) => write!(f, "{t}"), + Clause::Identity(other) => write!(f, "({other})"), + } + } +} + +impl AsDerefMut for Clause { + fn as_deref_mut(&mut self) -> impl DerefMut + '_ { self } +} diff --git a/src/interpreter/nort_builder.rs b/src/interpreter/nort_builder.rs new file mode 100644 index 0000000..daadf19 --- /dev/null +++ b/src/interpreter/nort_builder.rs @@ -0,0 +1,149 @@ +use std::cell::RefCell; +use std::mem; + +use substack::Substack; + +use super::nort::{AsDerefMut, Clause, Expr}; +use super::path_set::PathSet; +use crate::utils::pure_seq::pushed; + +enum IntGenData<'a, T: ?Sized> { + Lambda(&'a T, &'a RefCell>), + /// Counts left steps within a chain of [Clause::Apply] for collapsing. + Apply(&'a RefCell), + /// Replaces [IntGenData::Apply] when stepping left into non-apply to record + /// a [None] [super::path_set::Step]. + AppF, + /// Replaces [IntGenData::Apply] when stepping right to freeze the value. + AppArg(usize), +} + +impl<'a, T: ?Sized> Copy for IntGenData<'a, T> {} +impl<'a, T: ?Sized> Clone for IntGenData<'a, T> { + fn clone(&self) -> Self { *self } +} + +struct ArgCollector(RefCell>); +impl ArgCollector { + pub fn new() -> Self { Self(RefCell::new(None)) } + pub fn into_path(self) -> Option { self.0.into_inner() } +} + +/// Strategy used to find the lambda corresponding to a given argument in the +/// stack. The function is called on the data associated with the argument, then +/// the callback it returns is called on every lambda ancestor's associated +/// data from closest to outermost ancestor. The first lambda where this +/// callback returns true is considered to own the argument. +pub type LambdaPicker<'a, T, U> = + &'a dyn for<'b> Fn(&'b U) -> Box bool + 'b>; + +/// Bundle of information passed down through recursive fnuctions to instantiate +/// runtime [Expr], [super::nort::ClauseInst] or [Clause]. +/// +/// The context used by [crate::gen::traits::Gen] to convert templates is which +/// includes this type is constructed with [super::gen_nort::nort_gen]. +pub struct NortBuilder<'a, T: ?Sized, U: ?Sized> { + stack: Substack<'a, IntGenData<'a, T>>, + lambda_picker: LambdaPicker<'a, T, U>, +} +impl<'a, T: ?Sized, U: ?Sized> NortBuilder<'a, T, U> { + /// Create a new recursive [super::nort] builder from a location that will be + pub fn new(lambda_picker: LambdaPicker<'a, T, U>) -> Self { + Self { stack: Substack::Bottom, lambda_picker } + } + /// [Substack::pop] and clone the location + fn pop<'b>(&'b self, count: usize) -> NortBuilder<'b, T, U> + where 'a: 'b { + let mut new = *self; + new.stack = *self.stack.pop(count); + new + } + /// [Substack::push] and clone the location + fn push<'b>(&'b self, data: IntGenData<'a, T>) -> NortBuilder<'b, T, U> + where 'a: 'b { + let mut new = *self; + new.stack = self.stack.push(data); + new + } + fn non_app_step(self, f: impl FnOnce(NortBuilder) -> V) -> V { + if let Some(IntGenData::Apply(_)) = self.stack.value() { + let prev = self.pop(1); + f(prev.push(IntGenData::AppF)) + } else { + f(self) + } + } + + /// Climb back through the stack and find a lambda associated with this + /// argument, then record the path taken from the lambda to this argument in + /// the lambda's mutable cell. + pub fn arg_logic(self, name: &'a U) { + let mut lambda_chk = (self.lambda_picker)(name); + self.non_app_step(|ctx| { + let opt = ctx.stack.rfold(None, |path, item| match item { + IntGenData::Apply(_) => panic!("This is removed after handling"), + IntGenData::Lambda(n, rc) => + lambda_chk(n).then(|| (vec![], *rc)).or(path), + IntGenData::AppArg(n) => path.map(|(p, rc)| (pushed(p, Some(*n)), rc)), + IntGenData::AppF => path.map(|(p, rc)| (pushed(p, None), rc)), + }); + let (path, slot) = opt.expect("Argument not wrapped in matching lambda"); + match &mut *slot.borrow_mut() { + slot @ None => *slot = Some(PathSet::end(path)), + Some(slot) => take_mut::take(slot, |p| p.overlay(PathSet::end(path))), + } + }) + } + + /// Push a stackframe corresponding to a lambda expression, build the body, + /// then record the path set collected by [NortBuilder::arg_logic] calls + /// within the body. + pub fn lambda_logic( + self, + name: &T, + body: impl FnOnce(NortBuilder) -> Expr, + ) -> Clause { + let coll = ArgCollector::new(); + let frame = IntGenData::Lambda(name, &coll.0); + let body = self.non_app_step(|ctx| body(ctx.push(frame))); + let args = coll.into_path(); + Clause::Lambda { args, body } + } + + /// Logic for collapsing Apply clauses. Different steps of the logic + /// communicate via mutable variables on the stack + pub fn apply_logic( + self, + f: impl FnOnce(NortBuilder) -> Expr, + x: impl FnOnce(NortBuilder) -> Expr, + ) -> Clause { + let mut fun: Expr; + let arg: Expr; + if let Some(IntGenData::Apply(rc)) = self.stack.value() { + // argument side commits backidx + arg = x(self.pop(1).push(IntGenData::AppArg(*rc.borrow()))); + // function side increments backidx + *rc.borrow_mut() += 1; + fun = f(self); + } else { + // function side starts from backidx 1 + fun = f(self.push(IntGenData::Apply(&RefCell::new(1)))); + // argument side commits 0 + arg = x(self.push(IntGenData::AppArg(0))); + }; + let mut cls_lk = fun.as_deref_mut(); + if let Clause::Apply { x, f: _ } = &mut *cls_lk { + x.push_back(arg); + mem::drop(cls_lk); + fun.clause.into_cls() + } else { + mem::drop(cls_lk); + Clause::Apply { f: fun, x: [arg].into() } + } + } +} + +impl<'a, T: ?Sized, U: ?Sized> Copy for NortBuilder<'a, T, U> {} +impl<'a, T: ?Sized, U: ?Sized> Clone for NortBuilder<'a, T, U> { + fn clone(&self) -> Self { *self } +} diff --git a/src/interpreter/path_set.rs b/src/interpreter/path_set.rs new file mode 100644 index 0000000..a642cda --- /dev/null +++ b/src/interpreter/path_set.rs @@ -0,0 +1,165 @@ +use std::collections::VecDeque; +use std::fmt; + +use hashbrown::HashMap; +use itertools::Itertools; + +use crate::utils::join::join_maps; + +/// A step into a [super::nort::Clause::Apply]. If [None], it steps to the +/// function. If [Some(n)], it steps to the `n`th _last_ argument. +pub type Step = Option; +fn print_step(step: Step) -> String { + if let Some(n) = step { format!("{n}>") } else { "f>".to_string() } +} + +/// A branching path selecting some placeholders (but at least one) in a Lambda +/// expression +#[derive(Clone)] +pub struct PathSet { + /// The single steps through [super::nort::Clause::Apply] + pub steps: VecDeque, + /// if Some, it splits at a [super::nort::Clause::Apply]. If None, it ends in + /// a [super::nort::Clause::LambdaArg] + pub next: Option>, +} + +impl PathSet { + /// Create a path set for more than one target + pub fn branch( + steps: impl IntoIterator, + conts: impl IntoIterator, + ) -> Self { + let conts = conts.into_iter().collect::>(); + assert!(1 < conts.len(), "Branching pathsets need multiple continuations"); + Self { steps: steps.into_iter().collect(), next: Some(conts) } + } + + /// Create a path set for one target + pub fn end(steps: impl IntoIterator) -> Self { + Self { steps: steps.into_iter().collect(), next: None } + } + + /// Create a path set that points to a slot that is a direct + /// child of the given lambda with no applications. In essence, this means + /// that this argument will be picked as the value of the expression after an + /// arbitrary amount of subsequent discarded parameters. + pub fn pick() -> Self { Self { steps: VecDeque::new(), next: None } } + + /// Merge two paths into one path that points to all targets of both. Only + /// works if both paths select leaf nodes of the same partial tree. + /// + /// # Panics + /// + /// if either path selects a node the other path dissects + pub fn overlay(self, other: Self) -> Self { + let (mut short, mut long) = match self.steps.len() < other.steps.len() { + true => (self, other), + false => (other, self), + }; + let short_len = short.steps.len(); + let long_len = long.steps.len(); + let match_len = (short.steps.iter()) + .zip(long.steps.iter()) + .take_while(|(a, b)| a == b) + .count(); + // fact: match_len <= short_len <= long_len + if short_len == match_len && match_len == long_len { + // implies match_len == short_len == long_len + match (short.next, long.next) { + (None, None) => Self::end(short.steps.iter().cloned()), + (Some(_), None) | (None, Some(_)) => { + panic!("One of these paths is faulty") + }, + (Some(s), Some(l)) => Self::branch( + short.steps.iter().cloned(), + join_maps(s, l, |_, l, r| l.overlay(r)), + ), + } + } else if short_len == match_len { + // implies match_len == short_len < long_len + // long.steps[0..match_len] is in steps + // long.steps[match_len] becomes the choice of branch below + // long.steps[match_len + 1..] is in tail + let mut conts = short.next.expect("One path ends inside the other"); + let tail_steps = long.steps.split_off(match_len + 1); + let tail = match long.next { + Some(n) => Self::branch(tail_steps, n), + None => Self::end(tail_steps), + }; + let branch = long.steps[match_len]; + let prev_c = conts.remove(&branch); + let new_c = if let Some(x) = prev_c { x.overlay(tail) } else { tail }; + conts.insert(branch, new_c); + Self::branch(short.steps, conts) + } else { + // implies match_len < short_len <= long_len + // steps[0..match_len] is in shared + // steps[match_len] become the branches below + // steps[match_len + 1..] is in new_long and new_short + let new_short_steps = short.steps.split_off(match_len + 1); + let short_last = short.steps.pop_back().expect("split at n + 1"); + let new_short = Self { next: short.next.clone(), steps: new_short_steps }; + let new_long_steps = long.steps.split_off(match_len + 1); + let new_long = Self { next: long.next.clone(), steps: new_long_steps }; + Self::branch(short.steps, [ + (short_last, new_short), + (long.steps[match_len], new_long), + ]) + } + } + + /// Prepend a step to a path. If it had previously started at a node that is + /// at the specified step within an Apply clause, it now starts at the Apply. + /// + /// This is only valid if the new Apply is **separate** from the previous + /// root. + pub fn prepend(&mut self, step: Step) { self.steps.push_front(step); } +} + +impl fmt::Display for PathSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let step_s = self.steps.iter().copied().map(print_step).join(""); + match &self.next { + Some(conts) => { + let opts = + conts.iter().map(|(h, t)| format!("{}{t}", print_step(*h))).join("|"); + write!(f, "{step_s}({opts})") + }, + None => write!(f, "{step_s}x"), + } + } +} + +impl fmt::Debug for PathSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PathSet({self})") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_combine() { + let ps1 = PathSet { next: None, steps: VecDeque::from([Some(2), None]) }; + let ps2 = PathSet { next: None, steps: VecDeque::from([Some(3), Some(1)]) }; + let sum = ps1.clone().overlay(ps2.clone()); + assert_eq!(format!("{sum}"), "(2>f>x|3>1>x)"); + } + + fn extend_scaffold() -> PathSet { + PathSet::branch([None, Some(1), None], [ + (None, PathSet::end([None, Some(1)])), + (Some(1), PathSet::end([None, Some(2)])), + ]) + } + + #[test] + fn test_extend_noclone() { + let mut ps = extend_scaffold(); + ps.prepend(Some(0)); + assert_eq!(format!("{ps}"), "0>f>1>f>(f>f>1|1>f>2)"); + } +} diff --git a/src/interpreter/run.rs b/src/interpreter/run.rs index 3463744..371cfe7 100644 --- a/src/interpreter/run.rs +++ b/src/interpreter/run.rs @@ -1,45 +1,101 @@ +use std::collections::VecDeque; + +use hashbrown::HashMap; + use super::apply::apply; -use super::context::{Context, Return}; -use super::error::RuntimeError; -use crate::foreign::AtomicReturn; -use crate::representations::interpreted::{Clause, ExprInst}; +use super::context::{Halt, RunContext}; +use super::error::RunError; +use super::nort::{Clause, Expr}; +use crate::foreign::atom::AtomicReturn; +use crate::foreign::error::ExternResult; +use crate::location::CodeLocation; +use crate::name::Sym; +use crate::utils::pure_seq::pushed; + +/// Information about a normalization run presented to an atom +#[derive(Clone)] +pub struct RunData<'a> { + /// Location of the atom + pub location: CodeLocation, + /// Information about the execution + pub ctx: RunContext<'a>, +} + +#[derive(Debug)] +pub struct Interrupted { + stack: Vec, +} +impl Interrupted { + pub fn resume(self, ctx: RunContext) -> Result { + run_stack(self.stack, ctx) + } +} /// Normalize an expression using beta reduction with memoization -pub fn run(expr: ExprInst, mut ctx: Context) -> Result { - let (state, (gas, inert)) = expr.try_normalize( - |mut cls, loc| -> Result<(Clause, _), RuntimeError> { - while ctx.gas.map(|g| g > 0).unwrap_or(true) { +pub fn run(mut expr: Expr, mut ctx: RunContext) -> Result { + run_stack(vec![expr], ctx) +} + +fn run_stack( + mut stack: Vec, + mut ctx: RunContext, +) -> Result { + let mut expr = stack.pop().expect("Empty stack"); + loop { + if ctx.no_gas() { + return Err(RunError::Interrupted(Interrupted { + stack: pushed(stack, expr), + })); + } + let (next_clsi, inert) = expr.clause.try_normalize(|mut cls| { + loop { + if ctx.no_gas() { + return Ok((cls, false)); + } match cls { + cls @ Clause::Identity(_) => return Ok((cls, false)), + // TODO: + // - unfuck nested loop + // - inline most of [apply] to eliminate recursion step Clause::Apply { f, x } => { - let res = apply(f, x, ctx.clone())?; - if res.inert { - return Ok((res.state.expr_val().clause, (res.gas, true))); + if x.is_empty() { + return Ok((f.clause.into_cls(), false)); } - ctx.gas = res.gas; - cls = res.state.expr().clause.clone(); + let (gas, clause) = apply(f, x, ctx.clone())?; + if ctx.gas.is_some() { + ctx.gas = gas; + } + cls = clause; }, Clause::Atom(data) => { - let AtomicReturn { clause, gas, inert } = data.run(ctx.clone())?; - if inert { - return Ok((clause, (gas, true))); + let run = RunData { ctx: ctx.clone(), location: expr.location() }; + let atomic_ret = data.run(run)?; + if ctx.gas.is_some() { + ctx.gas = atomic_ret.gas; } - ctx.gas = gas; - cls = clause; + if atomic_ret.inert { + return Ok((atomic_ret.clause, true)); + } + cls = atomic_ret.clause; }, Clause::Constant(c) => { let symval = (ctx.symbols.get(&c)).ok_or_else(|| { - RuntimeError::MissingSymbol(c.clone(), loc.clone()) + RunError::MissingSymbol(c.clone(), expr.location()) })?; ctx.gas = ctx.gas.map(|g| g - 1); // cost of lookup - cls = symval.expr().clause.clone(); + cls = Clause::Identity(symval.clause.clone()); }, // non-reducible - _ => return Ok((cls, (ctx.gas, true))), - } + c => return Ok((c, true)), + }; } - // out of gas - Ok((cls, (ctx.gas, false))) - }, - )?; - Ok(Return { state, gas, inert }) + })?; + expr.clause = next_clsi; + if inert { + match stack.pop() { + Some(e) => expr = e, + None => return Ok(Halt { state: expr, gas: ctx.gas, inert }), + } + } + } } diff --git a/src/lib.rs b/src/lib.rs index 7d93b60..e673273 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,27 +10,15 @@ pub mod error; pub mod facade; pub mod foreign; -pub mod interner; +pub mod gen; +pub mod intermediate; pub mod interpreter; +pub mod libs; +pub mod location; +pub mod name; pub mod parse; pub mod pipeline; -mod representations; pub mod rule; -pub mod systems; -mod utils; - -pub use interner::{Interner, Tok}; -pub use pipeline::file_loader::{mk_dir_cache, mk_embed_cache}; -pub use pipeline::parse_layer; -/// Element of VName and a common occurrence in the API -pub type Stok = Tok; -pub use representations::ast_to_interpreted::ast_to_interpreted; -pub use representations::project::{ - collect_consts, collect_rules, vname_to_sym_tree, ProjectTree, -}; -pub use representations::{ - ast, from_const_tree, interpreted, sourcefile, tree, ConstTree, Location, - NameLike, OrcString, PathSet, Sym, VName, -}; -pub use utils::substack::Substack; -pub use utils::{ddispatch, take_with_output, thread_pool, IdMap, Side}; +pub mod tree; +pub mod utils; +pub mod virt_fs; diff --git a/src/systems/asynch/async.orc b/src/libs/asynch/async.orc similarity index 100% rename from src/systems/asynch/async.orc rename to src/libs/asynch/async.orc diff --git a/src/utils/delete_cell.rs b/src/libs/asynch/delete_cell.rs similarity index 75% rename from src/utils/delete_cell.rs rename to src/libs/asynch/delete_cell.rs index 759c4c7..8c3effe 100644 --- a/src/utils/delete_cell.rs +++ b/src/libs/asynch/delete_cell.rs @@ -3,15 +3,10 @@ use std::sync::{Arc, Mutex}; pub struct DeleteCell(pub Arc>>); impl DeleteCell { pub fn new(t: T) -> Self { Self(Arc::new(Mutex::new(Some(t)))) } - pub fn take(&self) -> Option { self.0.lock().unwrap().take() } - - pub fn clone_out(&self) -> Option - where - T: Clone, - { - self.0.lock().unwrap().clone() - } +} +impl DeleteCell { + pub fn clone_out(&self) -> Option { self.0.lock().unwrap().clone() } } impl Clone for DeleteCell { fn clone(&self) -> Self { Self(self.0.clone()) } diff --git a/src/systems/asynch/mod.rs b/src/libs/asynch/mod.rs similarity index 80% rename from src/systems/asynch/mod.rs rename to src/libs/asynch/mod.rs index d94a886..9c2ba08 100644 --- a/src/systems/asynch/mod.rs +++ b/src/libs/asynch/mod.rs @@ -4,6 +4,6 @@ //! beyond being general Rust functions. //! It also exposes timers. -mod system; - -pub use system::{AsynchSystem, InfiniteBlock, MessagePort}; +pub mod poller; +pub mod system; +mod delete_cell; diff --git a/src/utils/poller.rs b/src/libs/asynch/poller.rs similarity index 82% rename from src/utils/poller.rs rename to src/libs/asynch/poller.rs index 673fd35..817bbd5 100644 --- a/src/utils/poller.rs +++ b/src/libs/asynch/poller.rs @@ -1,10 +1,12 @@ +//! Abstract implementation of the poller + use std::collections::BinaryHeap; use std::mem; use std::sync::mpsc::{channel, Receiver, RecvError, RecvTimeoutError, Sender}; use std::thread::sleep; use std::time::{Duration, Instant}; -use crate::utils::DeleteCell; +use super::delete_cell::DeleteCell; enum TimerKind { Once(DeleteCell), @@ -27,8 +29,8 @@ impl Clone for TimerKind { /// [Ord] implemenetation of this struct is reversed; it can be intuitively /// thought of as ordering by urgency. struct Timer { - pub expires: Instant, - pub kind: TimerKind, + expires: Instant, + kind: TimerKind, } impl Clone for Timer { fn clone(&self) -> Self { @@ -50,42 +52,55 @@ impl Ord for Timer { } } +/// Representation of a scheduled timer +#[derive(Clone)] +pub struct TimerHandle(DeleteCell); +impl TimerHandle { + /// Cancel the timer + pub fn cancel(self) { mem::drop(self.0.take()) } +} + +/// The abstract event poller implementation used by the standard asynch +/// subsystem. pub struct Poller { timers: BinaryHeap>, receiver: Receiver, } -impl Poller { +impl Poller { + /// Create an event poller and a [Sender] that can produce events on it. pub fn new() -> (Sender, Self) { let (sender, receiver) = channel(); let this = Self { receiver, timers: BinaryHeap::new() }; (sender, this) } + /// Set a single-fire timer pub fn set_timeout( &mut self, duration: Duration, data: TOnce, - ) -> impl Fn() + Clone { + ) -> TimerHandle { let data_cell = DeleteCell::new(data); self.timers.push(Timer { kind: TimerKind::Once(data_cell.clone()), expires: Instant::now() + duration, }); - move || mem::drop(data_cell.take()) + TimerHandle(data_cell) } + /// Set a recurring timer pub fn set_interval( &mut self, period: Duration, data: TRec, - ) -> impl Fn() + Send + Clone { + ) -> TimerHandle { let data_cell = DeleteCell::new(data); self.timers.push(Timer { expires: Instant::now() + period, kind: TimerKind::Recurring { period, data_cell: data_cell.clone() }, }); - move || mem::drop(data_cell.take()) + TimerHandle(data_cell) } /// Process a timer popped from the timers heap of this event loop. @@ -140,8 +155,12 @@ impl Poller { } } +/// Events produced by [Poller]. pub enum PollEvent { + /// An event was sent to the [Sender] associated with the [Poller]. Event(TEv), + /// A single-fire timer expired Once(TOnce), + /// A recurring event fired Recurring(TRec), } diff --git a/src/systems/asynch/system.rs b/src/libs/asynch/system.rs similarity index 63% rename from src/systems/asynch/system.rs rename to src/libs/asynch/system.rs index 9eff6be..437b7f6 100644 --- a/src/systems/asynch/system.rs +++ b/src/libs/asynch/system.rs @@ -1,3 +1,7 @@ +//! Object to pass to [crate::facade::loader::Loader::add_system] to enable the +//! I/O subsystem. Also many other systems depend on it, these take a mut ref to +//! register themselves. + use std::any::{type_name, Any, TypeId}; use std::cell::RefCell; use std::collections::VecDeque; @@ -11,17 +15,23 @@ use hashbrown::HashMap; use ordered_float::NotNan; use rust_embed::RustEmbed; -use crate::facade::{IntoSystem, System}; -use crate::foreign::cps_box::{init_cps, CPSBox}; -use crate::foreign::{xfn_2ary, Atomic, ExternError, InertAtomic, XfnResult}; -use crate::interpreted::{Clause, ExprInst}; -use crate::interpreter::HandlerTable; -use crate::pipeline::file_loader::embed_to_map; -use crate::systems::codegen::call; -use crate::systems::stl::Numeric; -use crate::utils::poller::{PollEvent, Poller}; -use crate::utils::unwrap_or; -use crate::{ConstTree, Interner}; +use super::poller::{PollEvent, Poller, TimerHandle}; +use crate::facade::system::{IntoSystem, System}; +use crate::foreign::atom::Atomic; +use crate::foreign::cps_box::CPSBox; +use crate::foreign::error::ExternError; +use crate::foreign::fn_bridge::constructors::xfn_2ary; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::gen::tpl; +use crate::gen::traits::Gen; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::handler::HandlerTable; +use crate::interpreter::nort::Expr; +use crate::libs::std::number::Numeric; +use crate::location::{CodeGenInfo, CodeLocation}; +use crate::utils::unwrap_or::unwrap_or; +use crate::virt_fs::{DeclTree, EmbeddedFS, PrefixFS, VirtFS}; #[derive(Debug, Clone)] struct Timer { @@ -29,28 +39,28 @@ struct Timer { delay: NotNan, } -pub fn set_timer(recurring: bool, delay: Numeric) -> XfnResult { - Ok(init_cps(2, Timer { recurring, delay: delay.as_float() })) +fn set_timer(rec: Inert, delay: Numeric) -> CPSBox { + CPSBox::new(2, Timer { recurring: rec.0, delay: delay.as_float() }) } #[derive(Clone)] struct CancelTimer(Arc>); impl CancelTimer { - pub fn new(f: impl Fn() + Send + 'static) -> Self { - Self(Arc::new(Mutex::new(f))) + pub fn new(canceller: TimerHandle) -> Self { + Self(Arc::new(Mutex::new(move || canceller.clone().cancel()))) } pub fn cancel(&self) { self.0.lock().unwrap()() } } impl Debug for CancelTimer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "opaque cancel operation") + f.debug_struct("CancelTimer").finish_non_exhaustive() } } #[derive(Clone, Debug)] struct Yield; -impl InertAtomic for Yield { - fn type_str() -> &'static str { "a yield command" } +impl InertPayload for Yield { + const TYPE_STR: &'static str = "asynch::yield"; } /// Error indicating a yield command when all event producers and timers had @@ -76,17 +86,24 @@ impl MessagePort { } } +fn gen() -> CodeGenInfo { CodeGenInfo::no_details("asynch") } + #[derive(RustEmbed)] -#[folder = "src/systems/asynch"] -#[prefix = "system/"] +#[folder = "src/libs/asynch"] #[include = "*.orc"] struct AsynchEmbed; -type AnyHandler<'a> = Box) -> Vec + 'a>; +fn code() -> DeclTree { + DeclTree::ns("system::async", [DeclTree::leaf( + PrefixFS::new(EmbeddedFS::new::(".orc", gen()), "", "io").rc(), + )]) +} + +type AnyHandler<'a> = Box) -> Vec + 'a>; /// Datastructures the asynch system will eventually be constructed from. pub struct AsynchSystem<'a> { - poller: Poller, ExprInst, ExprInst>, + poller: Poller, Expr, Expr>, sender: Sender>, handlers: HashMap>, } @@ -109,7 +126,7 @@ impl<'a> AsynchSystem<'a> { /// if the given type is already handled. pub fn register( &mut self, - mut f: impl FnMut(Box) -> Vec + 'a, + mut f: impl FnMut(Box) -> Vec + 'a, ) { let cb = move |a: Box| f(a.downcast().expect("keyed by TypeId")); let prev = self.handlers.insert(TypeId::of::(), Box::new(cb)); @@ -132,39 +149,40 @@ impl<'a> Default for AsynchSystem<'a> { } impl<'a> IntoSystem<'a> for AsynchSystem<'a> { - fn into_system(self, i: &Interner) -> System<'a> { + fn into_system(self) -> System<'a> { let Self { mut handlers, poller, .. } = self; let mut handler_table = HandlerTable::new(); let polly = Rc::new(RefCell::new(poller)); handler_table.register({ let polly = polly.clone(); - move |t: Box>| { + move |t: &CPSBox| { let mut polly = polly.borrow_mut(); - let (timeout, action, cont) = t.unpack2(); - let duration = Duration::from_secs_f64(*timeout.delay); - let cancel_timer = match timeout.recurring { + let (Timer { delay, recurring }, action, cont) = t.unpack2(); + let duration = Duration::from_secs_f64(**delay); + let cancel_timer = match *recurring { true => CancelTimer::new(polly.set_interval(duration, action)), false => CancelTimer::new(polly.set_timeout(duration, action)), }; - Ok(call(cont, [init_cps(1, cancel_timer).wrap()]).wrap()) + let tpl = tpl::A(tpl::Slot, tpl::V(CPSBox::new(1, cancel_timer))); + tpl.template(nort_gen(cont.location()), [cont]) } }); - handler_table.register(move |t: Box>| { + handler_table.register(move |t: &CPSBox| { let (command, cont) = t.unpack1(); command.cancel(); - Ok(cont) + cont }); handler_table.register({ let polly = polly.clone(); let mut microtasks = VecDeque::new(); - move |_: Box| { + move |_: &Inert| { if let Some(expr) = microtasks.pop_front() { return Ok(expr); } let mut polly = polly.borrow_mut(); loop { let next = unwrap_or!(polly.run(); - return Err(InfiniteBlock.into_extern()) + return Err(InfiniteBlock.rc()) ); match next { PollEvent::Once(expr) => return Ok(expr), @@ -179,7 +197,9 @@ impl<'a> IntoSystem<'a> for AsynchSystem<'a> { if !events.is_empty() { microtasks = VecDeque::from(events); // trampoline - return Ok(Yield.atom_exi()); + let loc = + CodeLocation::Gen(CodeGenInfo::no_details("system::asynch")); + return Ok(Inert(Yield).atom_expr(loc)); } }, } @@ -187,18 +207,14 @@ impl<'a> IntoSystem<'a> for AsynchSystem<'a> { } }); System { - name: vec!["system".to_string(), "asynch".to_string()], + name: "system::asynch", lexer_plugins: vec![], line_parsers: vec![], - constants: ConstTree::namespace( - [i.i("system"), i.i("async")], - ConstTree::tree([ - (i.i("set_timer"), ConstTree::xfn(xfn_2ary(set_timer))), - (i.i("yield"), ConstTree::atom(Yield)), - ]), - ) - .unwrap_tree(), - code: embed_to_map::(".orc", i), + constants: ConstTree::ns("system::async", [ConstTree::tree([ + ("set_timer", atom_leaf(xfn_2ary(set_timer))), + ("yield", atom_leaf(Inert(Yield))), + ])]), + code: code(), prelude: Vec::new(), handlers: handler_table, } diff --git a/src/libs/directfs/commands.rs b/src/libs/directfs/commands.rs new file mode 100644 index 0000000..933433e --- /dev/null +++ b/src/libs/directfs/commands.rs @@ -0,0 +1,201 @@ +use std::ffi::OsString; +use std::fs::File; +use std::path::{Path, PathBuf}; + +use super::osstring::os_string_lib; +use crate::facade::system::{IntoSystem, System}; +use crate::foreign::atom::Atomic; +use crate::foreign::cps_box::CPSBox; +use crate::foreign::error::ExternResult; +use crate::foreign::fn_bridge::constructors::{xfn_1ary, xfn_2ary}; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::foreign::process::Unstable; +use crate::foreign::to_clause::ToClause; +use crate::gen::tpl; +use crate::gen::traits::Gen; +use crate::gen::tree::{atom_ent, atom_leaf, ConstTree}; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::handler::HandlerTable; +use crate::interpreter::nort::{Clause, Expr}; +use crate::libs::io::instances::io_error_handler; +use crate::libs::io::{Sink, Source}; +use crate::libs::scheduler::system::{SeqScheduler, SharedHandle}; +use crate::libs::std::runtime_error::RuntimeError; +use crate::utils::combine::Combine; +use crate::virt_fs::DeclTree; + +#[derive(Debug, Clone)] +struct ReadFileCmd(OsString); +impl InertPayload for ReadFileCmd { + const TYPE_STR: &'static str = "readfile command"; +} + +#[derive(Debug, Clone)] +struct ReadDirCmd(OsString); +impl InertPayload for ReadDirCmd { + const TYPE_STR: &'static str = "readdir command"; +} + +#[derive(Debug, Clone)] +struct WriteFile { + name: OsString, + append: bool, +} +impl InertPayload for WriteFile { + const TYPE_STR: &'static str = "writefile command"; +} + +#[must_use] +fn read_file(sched: &SeqScheduler, cmd: &CPSBox) -> Expr { + let (ReadFileCmd(name), succ, fail, cont) = cmd.unpack3(); + let name = name.clone(); + let cancel = sched.run_orphan( + move |_| File::open(name), + |file, _| match file { + Err(e) => vec![io_error_handler(e, fail)], + Ok(f) => { + let source_handle = SharedHandle::wrap(Source::new(Box::new(f))); + let tpl = tpl::A(tpl::Slot, tpl::V(Inert(source_handle))); + vec![tpl.template(nort_gen(succ.location()), [succ])] + }, + }, + ); + let tpl = tpl::A(tpl::Slot, tpl::V(CPSBox::new(1, cancel))); + tpl.template(nort_gen(cont.location()), [cont]) +} + +#[must_use] +fn read_dir(sched: &SeqScheduler, cmd: &CPSBox) -> Expr { + let (ReadDirCmd(name), succ, fail, cont) = cmd.unpack3(); + let name = name.clone(); + let cancel = sched.run_orphan( + move |_| { + Path::new(&name) + .read_dir()? + .map(|r| r.and_then(|e| Ok((e.file_name(), e.file_type()?.is_dir())))) + .collect() + }, + |items: std::io::Result>, _| match items { + Err(e) => vec![io_error_handler(e, fail)], + Ok(os_namev) => { + let converted = (os_namev.into_iter()) + .map(|(n, d)| { + Ok(( + Inert(n).atom_expr(succ.location()), + Inert(d).atom_expr(succ.location()), + )) + }) + .collect::, Clause>>(); + match converted { + Err(e) => { + let e = e.to_expr(fail.location()); + let tpl = tpl::A(tpl::Slot, tpl::Slot); + vec![tpl.template(nort_gen(fail.location()), [fail, e])] + }, + Ok(names) => { + let names = names.to_expr(succ.location()); + let tpl = tpl::A(tpl::Slot, tpl::Slot); + vec![tpl.template(nort_gen(succ.location()), [succ, names])] + }, + } + }, + }, + ); + let tpl = tpl::A(tpl::Slot, tpl::V(CPSBox::new(1, cancel))); + tpl.template(nort_gen(cont.location()), [cont]) +} + +#[must_use] +fn write_file(sched: &SeqScheduler, cmd: &CPSBox) -> Expr { + let (cmd, succ, fail, cont) = cmd.unpack3(); + let cmd = cmd.clone(); + let cancel = sched.run_orphan( + move |_| File::options().write(true).append(cmd.append).open(&cmd.name), + |file, _| match file { + Err(e) => vec![io_error_handler(e, fail)], + Ok(f) => { + let sink_handle = SharedHandle::wrap(Box::new(f) as Sink); + let tpl = tpl::A(tpl::Slot, tpl::V(Inert(sink_handle))); + vec![tpl.template(nort_gen(succ.location()), [succ])] + }, + }, + ); + let tpl = tpl::A(tpl::Slot, tpl::V(CPSBox::new(1, cancel))); + tpl.template(nort_gen(cont.location()), [cont]) +} + +fn open_file_read_cmd(name: OsString) -> CPSBox { + CPSBox::new(3, ReadFileCmd(name)) +} + +fn read_dir_cmd(name: OsString) -> CPSBox { + CPSBox::new(3, ReadDirCmd(name)) +} + +fn open_file_write_cmd(name: OsString) -> CPSBox { + CPSBox::new(3, WriteFile { name, append: false }) +} + +fn open_file_append_cmd(name: OsString) -> CPSBox { + CPSBox::new(3, WriteFile { name, append: true }) +} + +fn join_paths(root: OsString, sub: OsString) -> OsString { + let mut path = PathBuf::from(root); + path.push(sub); + path.into_os_string() +} + +fn pop_path( + path: Inert, +) -> Option<(Inert, Inert)> { + let mut path = PathBuf::from(path.0); + let sub = path.file_name()?.to_owned(); + debug_assert!(path.pop(), "file_name above returned Some"); + Some((Inert(path.into_os_string()), Inert(sub))) +} + +/// A rudimentary system to read and write files. +#[derive(Clone)] +pub struct DirectFS { + scheduler: SeqScheduler, +} +impl DirectFS { + /// Create a new instance of the system. + pub fn new(scheduler: SeqScheduler) -> Self { Self { scheduler } } +} + +impl IntoSystem<'static> for DirectFS { + fn into_system(self) -> System<'static> { + let mut handlers = HandlerTable::new(); + let sched = self.scheduler.clone(); + handlers.register(move |cmd| read_file(&sched, cmd)); + let sched = self.scheduler.clone(); + handlers.register(move |cmd| read_dir(&sched, cmd)); + let sched = self.scheduler; + handlers.register(move |cmd| write_file(&sched, cmd)); + System { + name: "system::directfs", + code: DeclTree::empty(), + prelude: Vec::new(), + lexer_plugins: vec![], + line_parsers: vec![], + constants: ConstTree::ns("system::fs", [ConstTree::tree([ + ("read_file", atom_leaf(xfn_1ary(open_file_read_cmd))), + ("read_dir", atom_leaf(xfn_1ary(read_dir_cmd))), + ("write_file", atom_leaf(xfn_1ary(open_file_write_cmd))), + ("append_file", atom_leaf(xfn_1ary(open_file_append_cmd))), + ("join_paths", atom_leaf(xfn_2ary(join_paths))), + ("pop_path", atom_leaf(xfn_1ary(pop_path))), + atom_ent("cwd", [Unstable::new(|_| -> ExternResult<_> { + let path = std::env::current_dir() + .map_err(|e| RuntimeError::ext(e.to_string(), "reading CWD"))?; + Ok(Inert(path.into_os_string())) + })]), + ])]) + .combine(os_string_lib()) + .expect("os_string library and directfs conflict"), + handlers, + } + } +} diff --git a/src/systems/directfs/mod.rs b/src/libs/directfs/mod.rs similarity index 61% rename from src/systems/directfs/mod.rs rename to src/libs/directfs/mod.rs index 6cfa9aa..fd81be4 100644 --- a/src/systems/directfs/mod.rs +++ b/src/libs/directfs/mod.rs @@ -1,5 +1,8 @@ //! A rudimentary system exposing methods for Orchid to interact with the file //! system. All paths are strings. +//! +//! The system depends on [crate::libs::scheduler] for scheduling blocking I/O +//! on a separate thread. mod commands; mod osstring; diff --git a/src/libs/directfs/osstring.rs b/src/libs/directfs/osstring.rs new file mode 100644 index 0000000..3b4d753 --- /dev/null +++ b/src/libs/directfs/osstring.rs @@ -0,0 +1,44 @@ +use std::ffi::OsString; + +use crate::foreign::atom::Atomic; +use crate::foreign::error::ExternResult; +use crate::foreign::fn_bridge::constructors::xfn_1ary; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::foreign::to_clause::ToClause; +use crate::foreign::try_from_expr::TryFromExpr; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::nort::{Clause, Expr}; +use crate::libs::std::string::OrcString; +use crate::location::CodeLocation; + +impl InertPayload for OsString { + const TYPE_STR: &'static str = "OsString"; +} +impl TryFromExpr for OsString { + fn from_expr(exi: Expr) -> ExternResult { Ok(Inert::from_expr(exi)?.0) } +} +impl ToClause for OsString { + fn to_clause(self, _: CodeLocation) -> Clause { Inert(self).atom_cls() } +} + +pub fn os_to_string( + os: Inert, +) -> Result, Inert> { + os.0.into_string().map(|s| Inert(s.into())).map_err(Inert) +} + +pub fn string_to_os(str: Inert) -> Inert { + Inert(str.0.get_string().into()) +} + +pub fn os_print(os: Inert) -> Inert { + Inert(os.0.to_string_lossy().to_string().into()) +} + +pub fn os_string_lib() -> ConstTree { + ConstTree::tree([ + ("os_to_string", atom_leaf(xfn_1ary(os_to_string))), + ("string_to_os", atom_leaf(xfn_1ary(string_to_os))), + ("os_print", atom_leaf(xfn_1ary(os_print))), + ]) +} diff --git a/src/libs/io/bindings.rs b/src/libs/io/bindings.rs new file mode 100644 index 0000000..5f4f8d0 --- /dev/null +++ b/src/libs/io/bindings.rs @@ -0,0 +1,80 @@ +use super::flow::IOCmdHandlePack; +use super::instances::{BRead, ReadCmd, SRead, WriteCmd}; +use super::service::{Sink, Source}; +use crate::foreign::cps_box::CPSBox; +use crate::foreign::error::ExternResult; +use crate::foreign::fn_bridge::constructors::{xfn_1ary, xfn_2ary}; +use crate::foreign::inert::Inert; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::libs::scheduler::system::SharedHandle; +use crate::libs::std::binary::Binary; +use crate::libs::std::runtime_error::RuntimeError; +use crate::libs::std::string::OrcString; +use crate::utils::combine::Combine; + +pub type WriteHandle = Inert>; +pub type ReadHandle = Inert>; + +type ReadCmdPack = CPSBox>; +type WriteCmdPack = CPSBox>; + +pub fn read_string(Inert(handle): ReadHandle) -> ReadCmdPack { + let cmd = ReadCmd::RStr(SRead::All); + CPSBox::new(3, IOCmdHandlePack { handle, cmd }) +} +pub fn read_line(Inert(handle): ReadHandle) -> ReadCmdPack { + let cmd = ReadCmd::RStr(SRead::Line); + CPSBox::new(3, IOCmdHandlePack { handle, cmd }) +} +pub fn read_bin(Inert(handle): ReadHandle) -> ReadCmdPack { + let cmd = ReadCmd::RBytes(BRead::All); + CPSBox::new(3, IOCmdHandlePack { handle, cmd }) +} +pub fn read_bytes(Inert(handle): ReadHandle, n: Inert) -> ReadCmdPack { + let cmd = ReadCmd::RBytes(BRead::N(n.0)); + CPSBox::new(3, IOCmdHandlePack { cmd, handle }) +} +pub fn read_until( + Inert(handle): ReadHandle, + Inert(pattern): Inert, +) -> ExternResult { + let pattern = pattern.try_into().map_err(|_| { + let msg = format!("{pattern} doesn't fit into a byte"); + RuntimeError::ext(msg, "converting number to byte") + })?; + let cmd = ReadCmd::RBytes(BRead::Until(pattern)); + Ok(CPSBox::new(3, IOCmdHandlePack { handle, cmd })) +} +pub fn write_str( + Inert(handle): WriteHandle, + string: Inert, +) -> WriteCmdPack { + let cmd = WriteCmd::WStr(string.0.get_string()); + CPSBox::new(3, IOCmdHandlePack { handle, cmd }) +} +pub fn write_bin( + Inert(handle): WriteHandle, + bytes: Inert, +) -> WriteCmdPack { + CPSBox::new(3, IOCmdHandlePack { handle, cmd: WriteCmd::WBytes(bytes.0) }) +} +pub fn flush(Inert(handle): WriteHandle) -> WriteCmdPack { + CPSBox::new(3, IOCmdHandlePack { handle, cmd: WriteCmd::Flush }) +} + +pub fn io_bindings<'a>( + std_streams: impl IntoIterator, +) -> ConstTree { + ConstTree::ns("system::io", [ConstTree::tree([ + ("read_string", atom_leaf(xfn_1ary(read_string))), + ("read_line", atom_leaf(xfn_1ary(read_line))), + ("read_bin", atom_leaf(xfn_1ary(read_bin))), + ("read_n_bytes", atom_leaf(xfn_2ary(read_bytes))), + ("read_until", atom_leaf(xfn_2ary(read_until))), + ("write_str", atom_leaf(xfn_2ary(write_str))), + ("write_bin", atom_leaf(xfn_2ary(write_bin))), + ("flush", atom_leaf(xfn_1ary(flush))), + ]) + .combine(ConstTree::tree(std_streams)) + .expect("std_stream name clashing with io functions")]) +} diff --git a/src/systems/io/flow.rs b/src/libs/io/flow.rs similarity index 88% rename from src/systems/io/flow.rs rename to src/libs/io/flow.rs index 6a9b0cc..e4e41f0 100644 --- a/src/systems/io/flow.rs +++ b/src/libs/io/flow.rs @@ -1,7 +1,7 @@ use std::fmt::Display; -use crate::foreign::ExternError; -use crate::systems::scheduler::Canceller; +use crate::foreign::error::ExternError; +use crate::libs::scheduler::cancel_flag::CancelFlag; pub trait IOHandler { type Product; @@ -25,7 +25,7 @@ pub trait IOCmd: Send { fn execute( self, stream: &mut Self::Stream, - cancel: Canceller, + cancel: CancelFlag, ) -> Self::Result; } diff --git a/src/systems/io/instances.rs b/src/libs/io/instances.rs similarity index 61% rename from src/systems/io/instances.rs rename to src/libs/io/instances.rs index a55af99..600ddf4 100644 --- a/src/systems/io/instances.rs +++ b/src/libs/io/instances.rs @@ -1,36 +1,36 @@ -use std::io::{self, BufRead, BufReader, Read, Write}; +use std::io::{self, BufRead, Read, Write}; use std::sync::Arc; use super::flow::IOCmd; -use crate::foreign::Atomic; -use crate::interpreted::ExprInst; -use crate::systems::codegen::call; -use crate::systems::scheduler::{Canceller, SharedHandle}; -use crate::systems::stl::Binary; -use crate::OrcString; - -/// Any type that we can read controlled amounts of data from -pub type Source = BufReader>; -/// Any type that we can write data to -pub type Sink = Box; +use super::service::{Sink, Source}; +use crate::foreign::inert::Inert; +use crate::gen::tpl; +use crate::gen::traits::Gen; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::nort::Expr; +use crate::libs::scheduler::cancel_flag::CancelFlag; +use crate::libs::scheduler::system::SharedHandle; +use crate::libs::std::binary::Binary; +use crate::libs::std::string::OrcString; +use crate::location::{CodeGenInfo, CodeLocation}; /// String reading command #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum SRead { +pub(super) enum SRead { All, Line, } /// Binary reading command #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum BRead { +pub(super) enum BRead { All, N(usize), Until(u8), } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum ReadCmd { +pub(super) enum ReadCmd { RBytes(BRead), RStr(SRead), } @@ -45,7 +45,7 @@ impl IOCmd for ReadCmd { fn execute( self, stream: &mut Self::Stream, - _cancel: Canceller, + _cancel: CancelFlag, ) -> Self::Result { match self { Self::RBytes(bread) => { @@ -77,33 +77,34 @@ impl IOCmd for ReadCmd { } /// Reading command (string or binary) -pub enum ReadResult { +pub(super) enum ReadResult { RStr(SRead, io::Result), RBin(BRead, io::Result>), } impl ReadResult { - pub fn dispatch(self, succ: ExprInst, fail: ExprInst) -> Vec { - match self { - ReadResult::RBin(_, Err(e)) | ReadResult::RStr(_, Err(e)) => { - vec![call(fail, [wrap_io_error(e)]).wrap()] - }, - ReadResult::RBin(_, Ok(bytes)) => { - let arg = Binary(Arc::new(bytes)).atom_cls().wrap(); - vec![call(succ, [arg]).wrap()] - }, - ReadResult::RStr(_, Ok(text)) => { - vec![call(succ, [OrcString::from(text).atom_exi()]).wrap()] - }, - } + pub fn dispatch(self, succ: Expr, fail: Expr) -> Vec { + vec![match self { + ReadResult::RBin(_, Err(e)) | ReadResult::RStr(_, Err(e)) => + io_error_handler(e, fail), + ReadResult::RBin(_, Ok(bytes)) => + tpl::A(tpl::Slot, tpl::V(Inert(Binary(Arc::new(bytes))))) + .template(nort_gen(succ.location()), [succ]), + ReadResult::RStr(_, Ok(text)) => + tpl::A(tpl::Slot, tpl::V(Inert(OrcString::from(text)))) + .template(nort_gen(succ.location()), [succ]), + }] } } /// Function to convert [io::Error] to Orchid data -pub fn wrap_io_error(_e: io::Error) -> ExprInst { 0usize.atom_exi() } +pub(crate) fn io_error_handler(_e: io::Error, handler: Expr) -> Expr { + let ctx = nort_gen(CodeLocation::Gen(CodeGenInfo::no_details("io_error"))); + tpl::A(tpl::Slot, tpl::V(Inert(0usize))).template(ctx, [handler]) +} /// Writing command (string or binary) #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum WriteCmd { +pub(super) enum WriteCmd { WBytes(Binary), WStr(String), Flush, @@ -117,7 +118,7 @@ impl IOCmd for WriteCmd { fn execute( self, stream: &mut Self::Stream, - _cancel: Canceller, + _cancel: CancelFlag, ) -> Self::Result { let result = match &self { Self::Flush => stream.flush(), @@ -128,15 +129,13 @@ impl IOCmd for WriteCmd { } } -pub struct WriteResult { +pub(super) struct WriteResult { + #[allow(unused)] pub cmd: WriteCmd, pub result: io::Result<()>, } impl WriteResult { - pub fn dispatch(self, succ: ExprInst, fail: ExprInst) -> Vec { - match self.result { - Ok(_) => vec![succ], - Err(e) => vec![call(fail, vec![wrap_io_error(e)]).wrap()], - } + pub fn dispatch(self, succ: Expr, fail: Expr) -> Vec { + vec![self.result.map_or_else(|e| io_error_handler(e, fail), |()| succ)] } } diff --git a/src/systems/io/io.orc b/src/libs/io/io.orc similarity index 100% rename from src/systems/io/io.orc rename to src/libs/io/io.orc diff --git a/src/libs/io/mod.rs b/src/libs/io/mod.rs new file mode 100644 index 0000000..91294f5 --- /dev/null +++ b/src/libs/io/mod.rs @@ -0,0 +1,38 @@ +//! System that allows Orchid to interact with trait objects of Rust's `Writer` +//! and with `BufReader`s of `Reader` trait objects. +//! +//! You can pass standard streams during initialization, the stllib expects +//! `stdin`, `stdout` and `stderr`. This system depends on +//! [crate::libs::scheduler] to run blocking I/O operations off-thread, which in +//! turn depends on [crate::libs::asynch] to process results on the main thread, +//! and [crate::libs::std] for `std::panic`. +//! +//! ``` +//! use orchidlang::libs::asynch::system::AsynchSystem; +//! use orchidlang::libs::scheduler::system::SeqScheduler; +//! use orchidlang::libs::std::std_system::StdConfig; +//! use orchidlang::libs::io::{IOService, Stream}; +//! use orchidlang::facade::loader::Loader; +//! use std::io::BufReader; +//! +//! +//! let mut asynch = AsynchSystem::new(); +//! let scheduler = SeqScheduler::new(&mut asynch); +//! let std_streams = [ +//! ("stdin", Stream::Source(BufReader::new(Box::new(std::io::stdin())))), +//! ("stdout", Stream::Sink(Box::new(std::io::stdout()))), +//! ("stderr", Stream::Sink(Box::new(std::io::stderr()))), +//! ]; +//! let env = Loader::new() +//! .add_system(StdConfig { impure: false }) +//! .add_system(asynch) +//! .add_system(scheduler.clone()) +//! .add_system(IOService::new(scheduler.clone(), std_streams)); +//! ``` + +mod bindings; +mod flow; +pub(super) mod instances; +mod service; + +pub use service::{IOService, Sink, Source, Stream}; diff --git a/src/libs/io/service.rs b/src/libs/io/service.rs new file mode 100644 index 0000000..36ce43a --- /dev/null +++ b/src/libs/io/service.rs @@ -0,0 +1,138 @@ +//! Object to pass to [crate::facade::loader::Loader::add_system] to enable the +//! I/O subsystem + +use std::io::{BufReader, Read, Write}; + +use rust_embed::RustEmbed; +use trait_set::trait_set; + +use super::bindings::io_bindings; +use super::flow::{IOCmd, IOCmdHandlePack}; +use super::instances::{ReadCmd, WriteCmd}; +use crate::facade::system::{IntoSystem, System}; +use crate::foreign::cps_box::CPSBox; +use crate::foreign::inert::Inert; +use crate::gen::tpl; +use crate::gen::traits::Gen; +use crate::gen::tree::leaf; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::handler::HandlerTable; +use crate::libs::scheduler::system::{SeqScheduler, SharedHandle}; +use crate::location::CodeGenInfo; +use crate::name::VName; +use crate::pipeline::load_solution::Prelude; +use crate::virt_fs::{DeclTree, EmbeddedFS, PrefixFS, VirtFS}; + +/// Any type that we can read controlled amounts of data from +pub type Source = BufReader>; +/// Any type that we can write data to +pub type Sink = Box; + +/// A shared type for sinks and sources +pub enum Stream { + /// A Source, aka. a BufReader + Source(Source), + /// A Sink, aka. a Writer + Sink(Sink), +} + +trait_set! { + /// The table of default streams to be overlain on the I/O module, typicially + /// stdin, stdout, stderr. + pub(super) trait StreamTable<'a> = IntoIterator +} + +fn gen() -> CodeGenInfo { CodeGenInfo::no_details("system::io") } + +#[derive(RustEmbed)] +#[folder = "src/libs/io"] +#[include = "*.orc"] +struct IOEmbed; + +fn code() -> DeclTree { + DeclTree::ns("system::io", [DeclTree::leaf( + PrefixFS::new(EmbeddedFS::new::(".orc", gen()), "", "io").rc(), + )]) +} + +/// A streaming I/O service for interacting with Rust's [std::io::Write] and +/// [std::io::Read] traits. +pub struct IOService<'a, ST: IntoIterator> { + scheduler: SeqScheduler, + global_streams: ST, +} +impl<'a, ST: IntoIterator> IOService<'a, ST> { + /// Construct a new instance of the service + pub fn new(scheduler: SeqScheduler, global_streams: ST) -> Self { + Self { scheduler, global_streams } + } +} + +impl<'a, ST: IntoIterator> IntoSystem<'static> + for IOService<'a, ST> +{ + fn into_system(self) -> System<'static> { + let scheduler = self.scheduler.clone(); + let mut handlers = HandlerTable::new(); + handlers.register(move |cps: &CPSBox>| { + let (IOCmdHandlePack { cmd, handle }, succ, fail, cont) = cps.unpack3(); + let (cmd, fail1) = (*cmd, fail.clone()); + let result = scheduler.schedule( + handle.clone(), + move |mut stream, cancel| { + let ret = cmd.execute(&mut stream, cancel); + (stream, ret) + }, + move |stream, res, _cancel| (stream, res.dispatch(succ, fail1)), + |stream| (stream, Vec::new()), + ); + match result { + Ok(cancel) => tpl::A(tpl::Slot, tpl::V(CPSBox::new(1, cancel))) + .template(nort_gen(cont.location()), [cont]), + Err(e) => tpl::A(tpl::Slot, tpl::V(Inert(e))) + .template(nort_gen(fail.location()), [fail]), + } + }); + let scheduler = self.scheduler.clone(); + handlers.register(move |cps: &CPSBox>| { + let (IOCmdHandlePack { cmd, handle }, succ, fail, cont) = cps.unpack3(); + let (succ1, fail1, cmd) = (succ, fail.clone(), cmd.clone()); + let result = scheduler.schedule( + handle.clone(), + move |mut stream, cancel| { + let ret = cmd.execute(&mut stream, cancel); + (stream, ret) + }, + move |stream, res, _cancel| (stream, res.dispatch(succ1, fail1)), + |stream| (stream, Vec::new()), + ); + match result { + Ok(cancel) => tpl::A(tpl::Slot, tpl::V(CPSBox::new(1, cancel))) + .template(nort_gen(cont.location()), [cont]), + Err(e) => tpl::A(tpl::Slot, tpl::V(Inert(e))) + .template(nort_gen(fail.location()), [fail]), + } + }); + let streams = self.global_streams.into_iter().map(|(n, stream)| { + let handle = match stream { + Stream::Sink(sink) => leaf(tpl::V(Inert(SharedHandle::wrap(sink)))), + Stream::Source(source) => + leaf(tpl::V(Inert(SharedHandle::wrap(source)))), + }; + (n, handle) + }); + System { + handlers, + name: "system::io", + constants: io_bindings(streams), + code: code(), + prelude: vec![Prelude { + target: VName::literal("system::io::prelude"), + exclude: VName::literal("system::io"), + owner: gen(), + }], + lexer_plugins: vec![], + line_parsers: vec![], + } + } +} diff --git a/src/systems/mod.rs b/src/libs/mod.rs similarity index 82% rename from src/systems/mod.rs rename to src/libs/mod.rs index 7c8e3c0..2b98abb 100644 --- a/src/systems/mod.rs +++ b/src/libs/mod.rs @@ -1,9 +1,7 @@ //! Constants exposed to usercode by the interpreter pub mod asynch; -pub mod codegen; pub mod directfs; pub mod io; -pub mod scheduler; -pub mod stl; pub mod parse_custom_line; - +pub mod scheduler; +pub mod std; diff --git a/src/libs/parse_custom_line.rs b/src/libs/parse_custom_line.rs new file mode 100644 index 0000000..5697594 --- /dev/null +++ b/src/libs/parse_custom_line.rs @@ -0,0 +1,47 @@ +//! A helper for defining custom lines. See [custom_line] +use intern_all::Tok; + +use crate::error::ProjectResult; +use crate::location::SourceRange; +use crate::parse::errors::ParseErrorKind; +use crate::parse::frag::Frag; +use crate::parse::lexer::Lexeme; +use crate::parse::parse_plugin::ParsePluginReq; + +/// An exported line with a name for which the line parser denies exports +pub struct Unexportable(Lexeme); +impl ParseErrorKind for Unexportable { + const DESCRIPTION: &'static str = "this line type cannot be exported"; + fn message(&self) -> String { format!("{} cannot be exported", &self.0) } +} + +/// Parse a line identified by the specified leading keyword. Although not +/// required, plugins are encouraged to prefix their lines with a globally +/// unique keyword which makes or breaks their parsing, to avoid accidental +/// failure to recognize +pub fn custom_line<'a>( + tail: Frag<'a>, + keyword: Tok, + exportable: bool, + req: &dyn ParsePluginReq, +) -> Option, SourceRange)>> { + let line_loc = req.frag_loc(tail); + let (fst, tail) = req.pop(tail).ok()?; + let fst_name = req.expect_name(fst).ok()?; + let (exported, n_ent, tail) = if fst_name == keyword { + (false, fst, tail.trim()) + } else if fst_name.as_str() == "export" { + let (snd, tail) = req.pop(tail).ok()?; + req.expect(Lexeme::Name(keyword), snd).ok()?; + (true, snd, tail.trim()) + } else { + return None; + }; + Some(match exported && !exportable { + true => { + let err = Unexportable(n_ent.lexeme.clone()); + Err(err.pack(req.range_loc(n_ent.range.clone()))) + }, + false => Ok((exported, tail, line_loc)), + }) +} diff --git a/src/systems/scheduler/busy.rs b/src/libs/scheduler/busy.rs similarity index 76% rename from src/systems/scheduler/busy.rs rename to src/libs/scheduler/busy.rs index 6544988..1428f7a 100644 --- a/src/systems/scheduler/busy.rs +++ b/src/libs/scheduler/busy.rs @@ -1,31 +1,30 @@ use std::any::Any; use std::collections::VecDeque; -use super::Canceller; -use crate::interpreted::ExprInst; +use super::cancel_flag::CancelFlag; +use crate::interpreter::nort::Expr; pub type SyncResult = (T, Box); /// Output from handlers contains the resource being processed and any Orchid /// handlers executed as a result of the operation -pub type HandlerRes = (T, Vec); +pub type HandlerRes = (T, Vec); pub type SyncOperation = - Box SyncResult + Send>; -pub type SyncOpResultHandler = Box< - dyn FnOnce(T, Box, Canceller) -> (T, Vec) + Send, ->; + Box SyncResult + Send>; +pub type SyncOpResultHandler = + Box, CancelFlag) -> (T, Vec) + Send>; struct SyncQueueItem { - cancelled: Canceller, + cancelled: CancelFlag, operation: SyncOperation, handler: SyncOpResultHandler, - early_cancel: Box (T, Vec) + Send>, + early_cancel: Box (T, Vec) + Send>, } pub enum NextItemReportKind { Free(T), Next { instance: T, - cancelled: Canceller, + cancelled: CancelFlag, operation: SyncOperation, rest: BusyState, }, @@ -34,17 +33,17 @@ pub enum NextItemReportKind { pub struct NextItemReport { pub kind: NextItemReportKind, - pub events: Vec, + pub events: Vec, } -pub struct BusyState { +pub(super) struct BusyState { handler: SyncOpResultHandler, queue: VecDeque>, - seal: Option Vec + Send>>, + seal: Option Vec + Send>>, } impl BusyState { pub fn new( - handler: impl FnOnce(T, U, Canceller) -> HandlerRes + Send + 'static, + handler: impl FnOnce(T, U, CancelFlag) -> HandlerRes + Send + 'static, ) -> Self { BusyState { handler: Box::new(|t, payload, cancel| { @@ -62,14 +61,14 @@ impl BusyState { /// successfully enqueued and None if the queue is already sealed. pub fn enqueue( &mut self, - operation: impl FnOnce(T, Canceller) -> (T, U) + Send + 'static, - handler: impl FnOnce(T, U, Canceller) -> HandlerRes + Send + 'static, + operation: impl FnOnce(T, CancelFlag) -> (T, U) + Send + 'static, + handler: impl FnOnce(T, U, CancelFlag) -> HandlerRes + Send + 'static, early_cancel: impl FnOnce(T) -> HandlerRes + Send + 'static, - ) -> Option { + ) -> Option { if self.seal.is_some() { return None; } - let cancelled = Canceller::new(); + let cancelled = CancelFlag::new(); self.queue.push_back(SyncQueueItem { cancelled: cancelled.clone(), early_cancel: Box::new(early_cancel), @@ -87,7 +86,7 @@ impl BusyState { pub fn seal( &mut self, - recipient: impl FnOnce(T) -> Vec + Send + 'static, + recipient: impl FnOnce(T) -> Vec + Send + 'static, ) { assert!(self.seal.is_none(), "Already sealed"); self.seal = Some(Box::new(recipient)) @@ -99,7 +98,7 @@ impl BusyState { mut self, instance: T, result: Box, - cancelled: Canceller, + cancelled: CancelFlag, ) -> NextItemReport { let (mut instance, mut events) = (self.handler)(instance, result, cancelled); diff --git a/src/systems/scheduler/canceller.rs b/src/libs/scheduler/cancel_flag.rs similarity index 68% rename from src/systems/scheduler/canceller.rs rename to src/libs/scheduler/cancel_flag.rs index aad28be..9197f0d 100644 --- a/src/systems/scheduler/canceller.rs +++ b/src/libs/scheduler/cancel_flag.rs @@ -1,13 +1,15 @@ +//! Flag for cancelling scheduled operations + use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; /// A single-fire thread-safe boolean flag with relaxed ordering #[derive(Debug, Clone)] -pub struct Canceller(Arc); +pub struct CancelFlag(Arc); -impl Canceller { +impl CancelFlag { /// Create a new canceller - pub fn new() -> Self { Canceller(Arc::new(AtomicBool::new(false))) } + pub fn new() -> Self { CancelFlag(Arc::new(AtomicBool::new(false))) } /// Check whether the operation has been cancelled pub fn is_cancelled(&self) -> bool { self.0.load(Ordering::Relaxed) } @@ -16,6 +18,6 @@ impl Canceller { pub fn cancel(&self) { self.0.store(true, Ordering::Relaxed) } } -impl Default for Canceller { +impl Default for CancelFlag { fn default() -> Self { Self::new() } } diff --git a/src/utils/id_map.rs b/src/libs/scheduler/id_map.rs similarity index 74% rename from src/utils/id_map.rs rename to src/libs/scheduler/id_map.rs index f28dc54..80e8307 100644 --- a/src/utils/id_map.rs +++ b/src/libs/scheduler/id_map.rs @@ -16,9 +16,6 @@ impl IdMap { /// Create a new empty set pub fn new() -> Self { Self { next_id: 0, data: HashMap::new() } } - /// Obtain a reference to the underlying map for iteration - pub fn map(&self) -> &HashMap { &self.data } - /// Insert an element with a new ID and return the ID pub fn insert(&mut self, t: T) -> u64 { let id = self.next_id; @@ -28,14 +25,6 @@ impl IdMap { id } - /// Obtain a reference to the element with the given ID - pub fn get(&self, id: u64) -> Option<&T> { self.data.get(&id) } - - /// Obtain a mutable reference to the element with the given ID - pub fn get_mut(&mut self, id: u64) -> Option<&mut T> { - self.data.get_mut(&id) - } - /// Remove the element with the given ID from the set. The ID will not be /// reused. pub fn remove(&mut self, id: u64) -> Option { self.data.remove(&id) } @@ -56,6 +45,6 @@ mod test { let b = map.insert(2); assert_eq!(map.remove(a), Some(1)); assert_eq!(map.remove(a), None); - assert_eq!(map.get(b), Some(&2)); + assert_eq!(map.data.get(&b), Some(&2)); } } diff --git a/src/libs/scheduler/mod.rs b/src/libs/scheduler/mod.rs new file mode 100644 index 0000000..5b12124 --- /dev/null +++ b/src/libs/scheduler/mod.rs @@ -0,0 +1,8 @@ +//! A generic utility to sequence long blocking mutations that require a mutable +//! reference to a shared resource. + +mod busy; +pub mod cancel_flag; +mod id_map; +pub mod system; +pub mod thread_pool; diff --git a/src/systems/scheduler/system.rs b/src/libs/scheduler/system.rs similarity index 76% rename from src/systems/scheduler/system.rs rename to src/libs/scheduler/system.rs index e47cb2c..98ba09a 100644 --- a/src/systems/scheduler/system.rs +++ b/src/libs/scheduler/system.rs @@ -1,28 +1,47 @@ +//! Object to pass to [crate::facade::loader::Loader::add_system] to enable the +//! scheduling subsystem. Other systems also take clones as dependencies. +//! +//! ``` +//! use orchidlang::libs::asynch::system::AsynchSystem; +//! use orchidlang::libs::scheduler::system::SeqScheduler; +//! use orchidlang::libs::std::std_system::StdConfig; +//! use orchidlang::facade::loader::Loader; +//! +//! let mut asynch = AsynchSystem::new(); +//! let scheduler = SeqScheduler::new(&mut asynch); +//! let env = Loader::new() +//! .add_system(StdConfig { impure: false }) +//! .add_system(asynch) +//! .add_system(scheduler.clone()); +//! ``` + use std::any::{type_name, Any}; use std::cell::RefCell; use std::fmt::Debug; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use hashbrown::HashMap; -use itertools::Itertools; use trait_set::trait_set; -use super::busy::{BusyState, NextItemReportKind, SyncOperation}; -use super::{Canceller, HandlerRes}; -use crate::error::AssertionError; -use crate::facade::{IntoSystem, System}; -use crate::foreign::cps_box::{init_cps, CPSBox}; -use crate::foreign::{xfn_1ary, InertAtomic, XfnResult}; -use crate::interpreted::{Clause, ExprInst}; -use crate::interpreter::HandlerTable; -use crate::systems::asynch::{AsynchSystem, MessagePort}; +use super::busy::{BusyState, HandlerRes, NextItemReportKind, SyncOperation}; +use super::cancel_flag::CancelFlag; +use super::id_map::IdMap; +use super::thread_pool::ThreadPool; +use crate::facade::system::{IntoSystem, System}; +use crate::foreign::cps_box::CPSBox; +use crate::foreign::error::{AssertionError, ExternResult}; +use crate::foreign::fn_bridge::constructors::xfn_1ary; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::handler::HandlerTable; +use crate::interpreter::nort::Expr; +use crate::libs::asynch::system::{AsynchSystem, MessagePort}; use crate::utils::ddispatch::Request; -use crate::utils::thread_pool::ThreadPool; -use crate::utils::{take_with_output, unwrap_or, IdMap}; -use crate::{ConstTree, Location}; +use crate::utils::take_with_output::take_with_output; +use crate::utils::unwrap_or::unwrap_or; +use crate::virt_fs::DeclTree; -enum SharedResource { +pub(super) enum SharedResource { Free(T), Busy(BusyState), Taken, @@ -47,7 +66,7 @@ pub enum SharedState { /// A shared handle for a resource of type `T` that can be used with a /// [SeqScheduler] to execute mutating operations one by one in worker threads. -pub struct SharedHandle(Arc>>); +pub struct SharedHandle(pub(super) Arc>>); impl SharedHandle { /// Wrap a value to be accessible to a [SeqScheduler]. @@ -97,8 +116,8 @@ impl Debug for SharedHandle { .finish() } } -impl InertAtomic for SharedHandle { - fn type_str() -> &'static str { "a SharedHandle" } +impl InertPayload for SharedHandle { + const TYPE_STR: &'static str = "a SharedHandle"; fn respond(&self, mut request: Request) { request.serve_with(|| { let this = self.clone(); @@ -110,7 +129,7 @@ impl InertAtomic for SharedHandle { } #[derive(Clone)] -pub struct TakeCmd(pub Arc); +struct TakeCmd(pub Arc); impl Debug for TakeCmd { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "A command to drop a shared resource") @@ -121,27 +140,25 @@ impl Debug for TakeCmd { /// which is either already sealed or taken. #[derive(Debug, Clone)] pub struct SealedOrTaken; -impl InertAtomic for SealedOrTaken { - fn type_str() -> &'static str { - "a sealed-or-taken error for a shared resource" +impl InertPayload for SealedOrTaken { + const TYPE_STR: &'static str = "SealedOrTaken"; +} + +fn take_and_drop(x: Expr) -> ExternResult> { + match x.clause.request() { + Some(t) => Ok(CPSBox::::new(1, t)), + None => AssertionError::fail(x.location(), "SharedHandle", format!("{x}")), } } -pub fn take_and_drop(x: ExprInst) -> XfnResult { - match x.request() { - Some(t) => Ok(init_cps::(1, t)), - None => AssertionError::fail(Location::Unknown, "SharedHandle"), - } -} - -pub fn is_taken_error(x: ExprInst) -> XfnResult { - Ok(x.downcast::().is_ok()) +fn is_taken_error(x: Expr) -> Inert { + Inert(x.downcast::>().is_ok()) } trait_set! { /// The part of processing a blocking I/O task that cannot be done on a remote /// thread, eg. because it accesses other systems or Orchid code. - trait NonSendFn = FnOnce(Box, SeqScheduler) -> Vec; + trait NonSendFn = FnOnce(Box, SeqScheduler) -> Vec; } struct SyncReply { @@ -195,10 +212,10 @@ impl SeqScheduler { pub fn schedule( &self, handle: SharedHandle, - operation: impl FnOnce(T, Canceller) -> (T, U) + Send + 'static, - handler: impl FnOnce(T, U, Canceller) -> HandlerRes + Send + 'static, + operation: impl FnOnce(T, CancelFlag) -> (T, U) + Send + 'static, + handler: impl FnOnce(T, U, CancelFlag) -> HandlerRes + Send + 'static, early_cancel: impl FnOnce(T) -> HandlerRes + Send + 'static, - ) -> Result { + ) -> Result { take_with_output(&mut *handle.0.lock().unwrap(), { let handle = handle.clone(); |state| { @@ -211,7 +228,7 @@ impl SeqScheduler { } }, SharedResource::Free(t) => { - let cancelled = Canceller::new(); + let cancelled = CancelFlag::new(); drop(early_cancel); // cannot possibly be useful let op_erased: SyncOperation = Box::new(|t, c| { let (t, u) = operation(t, c); @@ -229,10 +246,10 @@ impl SeqScheduler { /// without queuing on any particular data. pub fn run_orphan( &self, - operation: impl FnOnce(Canceller) -> T + Send + 'static, - handler: impl FnOnce(T, Canceller) -> Vec + 'static, - ) -> Canceller { - let cancelled = Canceller::new(); + operation: impl FnOnce(CancelFlag) -> T + Send + 'static, + handler: impl FnOnce(T, CancelFlag) -> Vec + 'static, + ) -> CancelFlag { + let cancelled = CancelFlag::new(); let canc1 = cancelled.clone(); let opid = self.0.pending.borrow_mut().insert(Box::new( |data: Box, _| { @@ -252,8 +269,8 @@ impl SeqScheduler { pub fn seal( &self, handle: SharedHandle, - seal: impl FnOnce(T) -> Vec + Sync + Send + 'static, - ) -> Result, SealedOrTaken> { + seal: impl FnOnce(T) -> Vec + Sync + Send + 'static, + ) -> Result, SealedOrTaken> { take_with_output(&mut *handle.0.lock().unwrap(), |state| match state { SharedResource::Busy(mut b) if !b.is_sealed() => { b.seal(seal); @@ -274,7 +291,7 @@ impl SeqScheduler { &self, t: T, handle: SharedHandle, - cancelled: Canceller, + cancelled: CancelFlag, operation: SyncOperation, ) { // referenced by self until run, references handle @@ -315,33 +332,29 @@ impl SeqScheduler { } impl IntoSystem<'static> for SeqScheduler { - fn into_system(self, i: &crate::Interner) -> crate::facade::System<'static> { + fn into_system(self) -> System<'static> { let mut handlers = HandlerTable::new(); - handlers.register(|cmd: Box>| { + handlers.register(|cmd: &CPSBox| { let (canceller, cont) = cmd.unpack1(); canceller.cancel(); - Ok(cont) + cont }); - handlers.register(move |cmd: Box>| { + handlers.register(move |cmd: &CPSBox| { let (TakeCmd(cb), cont) = cmd.unpack1(); cb(self.clone()); - Ok(cont) + cont }); System { - name: ["system", "scheduler"].into_iter().map_into().collect(), + name: "system::scheduler", prelude: Vec::new(), - code: HashMap::new(), + code: DeclTree::empty(), handlers, lexer_plugins: vec![], line_parsers: vec![], - constants: ConstTree::namespace( - [i.i("system"), i.i("scheduler")], - ConstTree::tree([ - (i.i("is_taken_error"), ConstTree::xfn(xfn_1ary(is_taken_error))), - (i.i("take_and_drop"), ConstTree::xfn(xfn_1ary(take_and_drop))), - ]), - ) - .unwrap_tree(), + constants: ConstTree::ns("system::scheduler", [ConstTree::tree([ + ("is_taken_error", atom_leaf(xfn_1ary(is_taken_error))), + ("take_and_drop", atom_leaf(xfn_1ary(take_and_drop))), + ])]), } } } diff --git a/src/utils/thread_pool.rs b/src/libs/scheduler/thread_pool.rs similarity index 97% rename from src/utils/thread_pool.rs rename to src/libs/scheduler/thread_pool.rs index f360ead..f98cbca 100644 --- a/src/utils/thread_pool.rs +++ b/src/libs/scheduler/thread_pool.rs @@ -1,6 +1,8 @@ //! A thread pool for executing tasks in parallel, spawning threads as workload //! increases and terminating them as tasks finish. This is not terribly //! efficient, its main design goal is to parallelize blocking I/O calls. +//! +//! This is the abstract implementation of the scheduler. use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{sync_channel, SyncSender}; @@ -81,7 +83,7 @@ struct ThreadPoolData { /// arrive. To get rid of the last waiting thread, drop the thread pool. /// /// ``` -/// use orchidlang::thread_pool::{Task, ThreadPool}; +/// use orchidlang::libs::scheduler::thread_pool::{Task, ThreadPool}; /// /// struct MyTask(&'static str); /// impl Task for MyTask { diff --git a/src/systems/stl/arithmetic_error.rs b/src/libs/std/arithmetic_error.rs similarity index 89% rename from src/systems/stl/arithmetic_error.rs rename to src/libs/std/arithmetic_error.rs index 71348e3..89eb6b4 100644 --- a/src/systems/stl/arithmetic_error.rs +++ b/src/libs/std/arithmetic_error.rs @@ -1,6 +1,8 @@ +//! Error produced by numeric opperations + use std::fmt::Display; -use crate::foreign::ExternError; +use crate::foreign::error::ExternError; /// Various errors produced by arithmetic operations #[derive(Clone)] diff --git a/src/libs/std/binary.rs b/src/libs/std/binary.rs new file mode 100644 index 0000000..0da5532 --- /dev/null +++ b/src/libs/std/binary.rs @@ -0,0 +1,160 @@ +//! `std::binary` Operations on binary buffers. + +use std::fmt::Debug; +use std::ops::Deref; +use std::sync::Arc; + +use itertools::Itertools; + +use super::runtime_error::RuntimeError; +use crate::foreign::atom::Atomic; +use crate::foreign::error::ExternResult; +use crate::foreign::fn_bridge::constructors::{ + xfn_1ary, xfn_2ary, xfn_3ary, xfn_4ary, +}; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::nort::Clause; +use crate::utils::iter_find::iter_find; +use crate::utils::unwrap_or::unwrap_or; + +const INT_BYTES: usize = usize::BITS as usize / 8; + +/// A block of binary data +#[derive(Clone, Hash, PartialEq, Eq)] +pub struct Binary(pub Arc>); +impl InertPayload for Binary { + const TYPE_STR: &'static str = "a binary blob"; +} + +impl Deref for Binary { + type Target = Vec; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Debug for Binary { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut iter = self.0.iter().copied(); + f.write_str("Binary")?; + for mut chunk in iter.by_ref().take(32).chunks(4).into_iter() { + let a = chunk.next().expect("Chunks cannot be empty"); + let b = unwrap_or!(chunk.next(); return write!(f, "{a:02x}")); + let c = unwrap_or!(chunk.next(); return write!(f, "{a:02x}{b:02x}")); + let d = + unwrap_or!(chunk.next(); return write!(f, "{a:02x}{b:02x}{c:02x}")); + write!(f, "{a:02x}{b:02x}{c:02x}{d:02x}")? + } + if iter.next().is_some() { write!(f, "...") } else { Ok(()) } + } +} + +/// Append two binary data blocks +pub fn concatenate(a: Inert, b: Inert) -> Inert { + let data = (*a).iter().chain(b.0.0.iter()).copied().collect(); + Inert(Binary(Arc::new(data))) +} + +/// Extract a subsection of the binary data +pub fn slice( + s: Inert, + i: Inert, + len: Inert, +) -> ExternResult> { + if i.0 + len.0 < s.0.0.len() { + RuntimeError::fail( + "Byte index out of bounds".to_string(), + "indexing binary", + )? + } + Ok(Inert(Binary(Arc::new(s.0.0[i.0..i.0 + len.0].to_vec())))) +} + +/// Return the index where the first argument first contains the second, if any +pub fn find(haystack: Inert, needle: Inert) -> Option { + let found = iter_find(haystack.0.0.iter(), needle.0.0.iter()); + found.map(|i| Inert(i).atom_cls()) +} + +/// Split binary data block into two smaller blocks +pub fn split( + bin: Inert, + i: Inert, +) -> ExternResult<(Inert, Inert)> { + if bin.0.0.len() < i.0 { + RuntimeError::fail( + "Byte index out of bounds".to_string(), + "splitting binary", + )? + } + let (asl, bsl) = bin.0.0.split_at(i.0); + Ok(( + Inert(Binary(Arc::new(asl.to_vec()))), + Inert(Binary(Arc::new(bsl.to_vec()))), + )) +} + +/// Read a number from a binary blob +pub fn get_num( + buf: Inert, + loc: Inert, + size: Inert, + is_le: Inert, +) -> ExternResult> { + if buf.0.0.len() < (loc.0 + size.0) { + RuntimeError::fail( + "section out of range".to_string(), + "reading number from binary data", + )? + } + if INT_BYTES < size.0 { + RuntimeError::fail( + "more than std::bin::int_bytes bytes provided".to_string(), + "reading number from binary data", + )? + } + let mut data = [0u8; INT_BYTES]; + let section = &buf.0.0[loc.0..(loc.0 + size.0)]; + let num = if is_le.0 { + data[0..size.0].copy_from_slice(section); + usize::from_le_bytes(data) + } else { + data[INT_BYTES - size.0..].copy_from_slice(section); + usize::from_be_bytes(data) + }; + Ok(Inert(num)) +} + +/// Convert a number into a blob +pub fn from_num( + size: Inert, + is_le: Inert, + data: Inert, +) -> ExternResult> { + if INT_BYTES < size.0 { + RuntimeError::fail( + "more than std::bin::int_bytes bytes requested".to_string(), + "converting number to binary", + )? + } + let bytes = match is_le.0 { + true => data.0.to_le_bytes()[0..size.0].to_vec(), + false => data.0.to_be_bytes()[8 - size.0..].to_vec(), + }; + Ok(Inert(Binary(Arc::new(bytes)))) +} + +/// Detect the number of bytes in the blob +pub fn size(b: Inert) -> Inert { Inert(b.0.len()) } + +pub(super) fn bin_lib() -> ConstTree { + ConstTree::ns("std::binary", [ConstTree::tree([ + ("concat", atom_leaf(xfn_2ary(concatenate))), + ("slice", atom_leaf(xfn_3ary(slice))), + ("find", atom_leaf(xfn_2ary(find))), + ("split", atom_leaf(xfn_2ary(split))), + ("get_num", atom_leaf(xfn_4ary(get_num))), + ("from_num", atom_leaf(xfn_3ary(from_num))), + ("size", atom_leaf(xfn_1ary(size))), + ("int_bytes", atom_leaf(Inert(INT_BYTES))), + ])]) +} diff --git a/src/libs/std/bool.orc b/src/libs/std/bool.orc new file mode 100644 index 0000000..92c9a55 --- /dev/null +++ b/src/libs/std/bool.orc @@ -0,0 +1,46 @@ +import std::(pmatch, inspect) + +export ::(!=, ==) + +export const not := \bool. if bool then false else true +macro ...$a != ...$b =0x3p36=> (not (...$a == ...$b)) +macro ...$a == ...$b =0x3p36=> (equals (...$a) (...$b)) +export macro ...$a and ...$b =0x4p36=> (ifthenelse (...$a) (...$b) false) +export macro ...$a or ...$b =0x4p36=> (ifthenelse (...$a) true (...$b)) +export macro if ...$cond then ...$true else ...$false:1 =0x1p84=> ( + ifthenelse (...$cond) (...$true) (...$false) +) + +( + macro pmatch::request (== ...$other) + =0x1p230=> pmatch::response ( + if pmatch::value == (...$other) + then pmatch::pass + else pmatch::fail + ) + ( pmatch::no_binds ) +) + +( + macro pmatch::request (!= ...$other) + =0x1p230=> pmatch::response ( + if pmatch::value != (...$other) + then pmatch::pass + else pmatch::fail + ) + ( pmatch::no_binds ) +) + +( + macro pmatch::request (true) + =0x1p230=> pmatch::response + (if pmatch::value then pmatch::pass else pmatch::fail) + ( pmatch::no_binds ) +) + +( + macro pmatch::request (false) + =0x1p230=> pmatch::response + (if pmatch::value then pmatch::fail else pmatch::pass) + ( pmatch::no_binds ) +) diff --git a/src/libs/std/bool.rs b/src/libs/std/bool.rs new file mode 100644 index 0000000..53c3ca1 --- /dev/null +++ b/src/libs/std/bool.rs @@ -0,0 +1,53 @@ +use super::number::Numeric; +use super::string::OrcString; +use crate::foreign::error::{AssertionError, ExternResult}; +use crate::foreign::fn_bridge::constructors::{xfn_1ary, xfn_2ary}; +use crate::foreign::inert::Inert; +use crate::foreign::try_from_expr::WithLoc; +use crate::gen::tpl; +use crate::gen::traits::{Gen, GenClause}; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::nort_builder::NortBuilder; +use crate::interpreter::nort::Expr; + +const fn left() -> impl GenClause { tpl::L("l", tpl::L("_", tpl::P("l"))) } +const fn right() -> impl GenClause { tpl::L("_", tpl::L("r", tpl::P("r"))) } + +/// Takes a boolean and two branches, runs the first if the bool is true, the +/// second if it's false. +// Even though it's a ternary function, IfThenElse is implemented as an unary +// foreign function, as the rest of the logic can be defined in Orchid. +pub fn if_then_else(WithLoc(loc, b): WithLoc>) -> Expr { + let ctx = nort_gen(loc); + if b.0 { left().template(ctx, []) } else { right().template(ctx, []) } +} + +/// Compares the inner values if +/// +/// - both are string, +/// - both are bool, +/// - both are either uint or num +pub fn equals( + WithLoc(loc, a): WithLoc, + b: Expr, +) -> ExternResult> { + Ok(Inert(if let Ok(l) = a.clone().downcast::>() { + b.downcast::>().is_ok_and(|r| *l == *r) + } else if let Ok(l) = a.clone().downcast::>() { + b.downcast::>().is_ok_and(|r| *l == *r) + } else if let Some(l) = a.clause.request::() { + b.clause.request::().is_some_and(|r| l.as_float() == r.as_float()) + } else { + AssertionError::fail(loc, "string, bool or numeric", format!("{a}"))? + })) +} + +pub fn bool_lib() -> ConstTree { + ConstTree::ns("std::bool", [ConstTree::tree([ + ("ifthenelse", atom_leaf(xfn_1ary(if_then_else))), + ("equals", atom_leaf(xfn_2ary(equals))), + ("true", atom_leaf(Inert(true))), + ("false", atom_leaf(Inert(false))), + ])]) +} diff --git a/src/libs/std/conv.rs b/src/libs/std/conv.rs new file mode 100644 index 0000000..441103c --- /dev/null +++ b/src/libs/std/conv.rs @@ -0,0 +1,71 @@ +use once_cell::sync::Lazy; +use ordered_float::NotNan; + +use super::number::Numeric; +use super::protocol::{gen_resolv, Protocol}; +use super::string::OrcString; +use crate::foreign::atom::Atomic; +use crate::foreign::error::{AssertionError, ExternResult}; +use crate::foreign::fn_bridge::constructors::xfn_1ary; +use crate::foreign::inert::Inert; +use crate::foreign::try_from_expr::WithLoc; +use crate::gen::tpl; +use crate::gen::traits::Gen; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::nort_builder::NortBuilder; +use crate::interpreter::nort::{ClauseInst, Expr}; +use crate::parse::numeric::parse_num; + +pub static TO_STRING: Lazy = + Lazy::new(|| Protocol::new("to_string", [])); + +fn to_numeric(WithLoc(loc, a): WithLoc) -> ExternResult { + if let Some(n) = a.request::() { + return Ok(n); + } + if let Some(s) = a.request::() { + return parse_num(s.as_str()).map_err(|e| { + AssertionError::ext(loc, "number syntax", format!("{e:?}")) + }); + } + AssertionError::fail(loc, "string or number", format!("{a}")) +} + +/// parse a number. Accepts the same syntax Orchid does. +pub fn to_float(a: WithLoc) -> ExternResult>> { + to_numeric(a).map(|n| Inert(n.as_float())) +} + +/// Parse an unsigned integer. Accepts the same formats Orchid does. If the +/// input is a number, floors it. +pub fn to_uint(a: WithLoc) -> ExternResult> { + to_numeric(a).map(|n| match n { + Numeric::Float(f) => Inert(f.floor() as usize), + Numeric::Uint(i) => Inert(i), + }) +} + +/// Convert a literal to a string using Rust's conversions for floats, chars and +/// uints respectively +pub fn to_string(WithLoc(loc, a): WithLoc) -> Expr { + match a.clone().downcast::>() { + Ok(str) => str.atom_expr(loc), + Err(_) => match a.clause.request::() { + Some(str) => Inert(str).atom_expr(loc), + None => tpl::a2(gen_resolv("std::to_string"), tpl::Slot, tpl::Slot) + .template(nort_gen(loc), [a.clone(), a]), + }, + } +} + +pub fn conv_lib() -> ConstTree { + ConstTree::ns("std", [ConstTree::tree([ + TO_STRING.as_tree_ent([]), + ConstTree::tree_ent("conv", [ + ("to_float", atom_leaf(xfn_1ary(to_float))), + ("to_uint", atom_leaf(xfn_1ary(to_uint))), + ("to_string", atom_leaf(xfn_1ary(to_string))), + ]), + ])]) +} diff --git a/src/libs/std/cross_pipeline.rs b/src/libs/std/cross_pipeline.rs new file mode 100644 index 0000000..94fcc64 --- /dev/null +++ b/src/libs/std/cross_pipeline.rs @@ -0,0 +1,86 @@ +use std::collections::VecDeque; +use std::iter; +use std::rc::Rc; + +use crate::foreign::atom::Atomic; +use crate::foreign::fn_bridge::constructors::xfn_1ary; +use crate::foreign::process::Unstable; +use crate::foreign::to_clause::ToClause; +use crate::foreign::try_from_expr::TryFromExpr; +use crate::location::{CodeLocation, SourceRange}; +use crate::parse::parsed::{self, PType}; +use crate::utils::pure_seq::pushed; + +pub trait DeferredRuntimeCallback: + Fn(Vec<(T, U)>) -> R + Clone + Send + 'static +{ +} +impl) -> R + Clone + Send + 'static> + DeferredRuntimeCallback for F +{ +} + +/// Lazy-recursive function that takes the next value from the interpreter +/// and acts upon it +/// +/// # Panics +/// +/// If the list of remaining keys is empty +fn table_receiver_rec< + T: Clone + Send + 'static, + U: TryFromExpr + Clone + Send + 'static, + R: ToClause + 'static, +>( + range: SourceRange, + results: Vec<(T, U)>, + mut remaining_keys: VecDeque, + callback: impl DeferredRuntimeCallback, +) -> impl Atomic { + let t = remaining_keys.pop_front().expect("empty handled elsewhere"); + xfn_1ary(move |u: U| { + let results = pushed(results, (t, u)); + match remaining_keys.is_empty() { + true => callback(results).to_clause(CodeLocation::Source(range)), + false => + table_receiver_rec(range, results, remaining_keys, callback).atom_cls(), + } + }) +} + +fn table_receiver< + T: Clone + Send + 'static, + U: TryFromExpr + Clone + Send + 'static, + R: ToClause + 'static, +>( + range: SourceRange, + keys: VecDeque, + callback: impl DeferredRuntimeCallback, +) -> parsed::Clause { + if keys.is_empty() { + Unstable::new(move |_| callback(Vec::new())).ast_cls() + } else { + Unstable::new(move |_| { + table_receiver_rec(range, Vec::new(), keys, callback).atom_cls() + }) + .ast_cls() + } +} + +/// Defers the execution of the callback to runtime, allowing it to depend on +/// the result of Otchid expressions. +pub fn defer_to_runtime< + T: Clone + Send + 'static, + U: TryFromExpr + Clone + Send + 'static, + R: ToClause + 'static, +>( + range: SourceRange, + pairs: impl IntoIterator)>, + callback: impl DeferredRuntimeCallback, +) -> parsed::Clause { + let (keys, ast_values) = + pairs.into_iter().unzip::<_, _, VecDeque<_>, Vec<_>>(); + let items = iter::once(table_receiver(range.clone(), keys, callback)).chain( + ast_values.into_iter().map(|v| parsed::Clause::S(PType::Par, Rc::new(v))), + ); + parsed::Clause::s('(', items, range) +} diff --git a/src/libs/std/exit_status.rs b/src/libs/std/exit_status.rs new file mode 100644 index 0000000..d2b4e3f --- /dev/null +++ b/src/libs/std/exit_status.rs @@ -0,0 +1,41 @@ +//! `std::exit_status` Exit status of a program or effectful subprogram. +//! +//! There is no support for custom codes, and the success/failure state can be +//! inspected. + +use std::process::ExitCode; + +use crate::foreign::fn_bridge::constructors::xfn_1ary; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::gen::tree::{atom_leaf, ConstTree}; + +/// An Orchid equivalent to Rust's binary exit status model +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ExitStatus { + /// unix exit code 0 + Success, + /// unix exit code 1 + Failure, +} +impl ExitStatus { + /// Convert to Rust-land [ExitCode] + pub fn code(self) -> ExitCode { + match self { + Self::Success => ExitCode::SUCCESS, + Self::Failure => ExitCode::FAILURE, + } + } +} + +impl InertPayload for ExitStatus { + const TYPE_STR: &'static str = "ExitStatus"; +} + +pub(super) fn exit_status_lib() -> ConstTree { + let is_success = |es: Inert| Inert(es.0 == ExitStatus::Success); + ConstTree::ns("std::exit_status", [ConstTree::tree([ + ("success", atom_leaf(Inert(ExitStatus::Success))), + ("failure", atom_leaf(Inert(ExitStatus::Failure))), + ("is_success", atom_leaf(xfn_1ary(is_success))), + ])]) +} diff --git a/src/systems/stl/functional.orc b/src/libs/std/functional.orc similarity index 95% rename from src/systems/stl/functional.orc rename to src/libs/std/functional.orc index a9b4d91..c68ef0a 100644 --- a/src/systems/stl/functional.orc +++ b/src/libs/std/functional.orc @@ -1,7 +1,6 @@ import super::known::* -import super::match::* +import super::pmatch::* import super::macro -import super::match::(match, =>) --[ Do nothing. Especially useful as a passive cps operation ]-- export const identity := \x.x diff --git a/src/libs/std/inspect.rs b/src/libs/std/inspect.rs new file mode 100644 index 0000000..f547300 --- /dev/null +++ b/src/libs/std/inspect.rs @@ -0,0 +1,39 @@ +use std::fmt::Debug; + +use crate::foreign::atom::{Atomic, AtomicResult, AtomicReturn}; +use crate::foreign::error::ExternResult; +use crate::foreign::fn_bridge::constructors::xfn_1ary; +use crate::foreign::to_clause::ToClause; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::apply::CallData; +use crate::interpreter::nort::{Clause, ClauseInst, Expr}; +use crate::interpreter::run::RunData; +use crate::utils::ddispatch::Responder; + +#[derive(Debug, Clone)] +struct Inspect; +impl Responder for Inspect {} +impl Atomic for Inspect { + fn as_any(self: Box) -> Box { self } + fn as_any_ref(&self) -> &dyn std::any::Any { self } + fn redirect(&mut self) -> Option<&mut ClauseInst> { None } + fn run(self: Box, run: RunData) -> AtomicResult { + AtomicReturn::inert(*self, run.ctx) + } + fn apply_ref(&self, call: CallData) -> ExternResult { + eprintln!("{}", call.arg); + Ok(call.arg.to_clause(call.location)) + } +} + +fn tee(x: Expr) -> Expr { + eprintln!("{x}"); + x +} + +pub fn inspect_lib() -> ConstTree { + ConstTree::ns("std", [ConstTree::tree([ + ("inspect", atom_leaf(Inspect)), + ("tee", atom_leaf(xfn_1ary(tee))), + ])]) +} diff --git a/src/systems/stl/known.orc b/src/libs/std/known.orc similarity index 100% rename from src/systems/stl/known.orc rename to src/libs/std/known.orc diff --git a/src/libs/std/list.orc b/src/libs/std/list.orc new file mode 100644 index 0000000..c7ac8c7 --- /dev/null +++ b/src/libs/std/list.orc @@ -0,0 +1,137 @@ +import super::(option, tuple, tuple::t, panic, pmatch, pmatch::=>, macro, tee) +import super::(functional::*, procedural::*) +import super::(loop::*, bool::*, known::*, number::*) + +as_type list () + +export const cons := \hd. \tl. wrap (option::some t[hd, unwrap tl]) +export const end := wrap option::none +export const pop := \list. \default. \f. ( + pmatch::match (unwrap list) { + option::none => default; + option::some t[hd, tl] => f hd (wrap tl); + } +) + +-- Operators + +--[ Fold each element into an accumulator using an `acc -> el -> acc`. #eager ]-- +export const fold := \list. \acc. \f. ( + loop_over (list, acc) { + cps head, list = pop list acc; + let acc = f acc head; + } +) + +--[ Fold each element into an accumulator in reverse order. #eager-notail ]-- +export const rfold := \list. \acc. \f. ( + recursive r (list) + pop list acc \head. \tail. + f (r tail) head +) + +--[ Reverse a list. #eager ]-- +export const reverse := \list. fold list end \tl. \hd. cons hd tl + +--[ Fold each element into a shared element with an `el -> el -> el`. #eager-notail ]-- +export const reduce := \list. \f. do{ + cps head, list = pop list option::none; + option::some $ fold list head f +} + +--[ + Return a new list that contains only the elements from the input list + for which the function returns true. #lazy +]-- +export const filter := \list. \f. ( + pop list end \head. \tail. + if (f head) + then cons head (filter tail f) + else filter tail f +) + +--[ Transform each element of the list with an `el -> any`. #lazy ]-- +export const map := \list. \f. ( + recursive r (list) + pop list end \head. \tail. + cons (f head) (r tail) +) + +--[ Skip `n` elements from the list and return the tail. #lazy ]-- +export const skip := \foo. \n. ( + loop_over (foo, n) { + cps _head, foo = if n <= 0 + then return foo + else pop foo end; + let n = n - 1; + } +) + +--[ Return `n` elements from the list and discard the rest. #lazy ]-- +export const take := \list. \n. ( + recursive r (list, n) + if n == 0 + then end + else pop list end \head. \tail. + cons head $ r tail $ n - 1 +) + +--[ Return the `n`th element from the list. #eager ]-- +export const get := \list. \n. ( + loop_over (list, n) { + cps head, list = pop list option::none; + cps if n == 0 + then return (option::some head) + else identity; + let n = n - 1; + } +) + +--[ Map every element to a pair of the index and the original element. #lazy ]-- +export const enumerate := \list. ( + recursive r (list, n = 0) + pop list end \head. \tail. + cons t[n, head] $ r tail $ n + 1 +) + +--[ + Turn a list of CPS commands into a sequence. This is achieved by calling every + element on the return value of the next element with the tail passed to it. + The continuation is passed to the very last argument. #lazy +]-- +export const chain := \list. \cont. loop_over (list) { + cps head, list = pop list cont; + cps head; +} + +macro new[..$items] =0x2p84=> mk_list macro::comma_list (..$items) + +macro mk_list ( macro::list_item $item $tail ) =0x1p254=> (cons $item mk_list $tail) +macro mk_list macro::list_end =0x1p254=> end + +export ::(new) + +( macro pmatch::request (cons $head $tail) + =0x1p230=> await_subpatterns + (pmatch::request ($head)) + (pmatch::request ($tail)) +) +( macro await_subpatterns + (pmatch::response $h_expr ( $h_binds )) + (pmatch::response $t_expr ( $t_binds )) + =0x1p230=> pmatch::response ( + pop + pmatch::value + pmatch::fail + \head. \tail. ( + (\pmatch::pass. (\pmatch::value. $h_expr) head) + (pmatch::take_binds $h_binds ( + (\pmatch::pass. (\pmatch::value. $t_expr) tail) + (pmatch::take_binds $t_binds ( + pmatch::give_binds pmatch::chain_binds $h_binds $t_binds pmatch::pass + )) + )) + ) + ) + (pmatch::chain_binds $h_binds $t_binds) +) diff --git a/src/systems/stl/loop.orc b/src/libs/std/loop.orc similarity index 99% rename from src/systems/stl/loop.orc rename to src/libs/std/loop.orc index 3d168de..0cb8f20 100644 --- a/src/systems/stl/loop.orc +++ b/src/libs/std/loop.orc @@ -37,7 +37,6 @@ macro parse_binds (...$item) =0x1p250=> ( () ) - -- while loop export macro statement ( while ..$condition (..$binds) { diff --git a/src/systems/stl/macro.orc b/src/libs/std/macro.orc similarity index 100% rename from src/systems/stl/macro.orc rename to src/libs/std/macro.orc diff --git a/src/libs/std/map.orc b/src/libs/std/map.orc new file mode 100644 index 0000000..461fceb --- /dev/null +++ b/src/libs/std/map.orc @@ -0,0 +1,94 @@ +import super::(bool::*, functional::*, known::*, loop::*, procedural::*, string::*) +import super::(panic, pmatch, macro, option, list, tuple, to_string, conv, pmatch::[=>]) + +as_type map ( + impl to_string := \map. "map[" ++ ( + unwrap map + |> list::map ( + (tuple::t[k, v]) => conv::to_string k ++ " = " ++ conv::to_string v + ) + |> list::reduce (\l. \r. l ++ ", " ++ r) + |> option::fallback "" + ) ++ "]" +) + +--[ Constructors ]-- + +const empty := wrap list::end +const add := \m. \k. \v. wrap ( + list::cons + tuple::t[k, v] + (unwrap m) +) + +--[ List constructor ]-- + +export ::new +macro new[..$items] =0x2p84=> mk_map macro::comma_list (..$items) + +macro mk_map macro::list_end =0x1p254=> empty +( macro mk_map ( macro::list_item ( ...$key = ...$value:1 ) $tail ) + =0x1p254=> ( set mk_map $tail (...$key) (...$value) ) +) + +--[ Queries ]-- + +-- return the last occurrence of a key if exists +export const get := \m. \key. ( + loop_over (m=unwrap m) { + cps record, m = list::pop m option::none; + cps if tuple::pick record 0 == key + then return $ option::some $ tuple::pick record 1 + else identity; + } +) + +--[ Commands ]-- + +-- remove one occurrence of a key +export const del := \m. \k. wrap ( + recursive r (m=unwrap m) + list::pop m list::end \head. \tail. + if tuple::pick head 0 == k then tail + else list::cons head $ r tail +) + +-- replace at most one occurrence of a key +export const set := \m. \k. \v. m |> del k |> add k v + +export ::having +( macro pmatch::request (having [..$items]) + =0x1p230=> having_pattern ( + pattern_walker + macro::comma_list ( ..$items ) + ) +) +( macro having_pattern ( tail_result $expr ( $binds ) ) + =0x1p254=> pmatch::response $expr ( $binds ) +) +( macro pattern_walker macro::list_end + =0x1p254=> tail_result pmatch::pass ( pmatch::no_binds ) +) +( macro pattern_walker ( macro::list_item ( ...$key = ...$value:1 ) $tail ) + =0x1p254=> await_pattern ( ...$key ) + ( pmatch::request (...$value) ) + ( pattern_walker $tail ) +) +( macro await_pattern $key + ( pmatch::response $expr ( $binds ) ) + ( tail_result $t_expr ( $t_binds ) ) + =0x1p254=> tail_result ( + option::handle (get pmatch::value $key) + pmatch::fail + \value. (\pmatch::pass. (\pmatch::value. $expr) value) ( + pmatch::take_binds $binds ( + (\pmatch::pass. $t_expr) ( + pmatch::take_binds $t_binds ( + pmatch::give_binds pmatch::chain_binds $binds $t_binds pmatch::pass + ) + ) + ) + ) + ) + ( pmatch::chain_binds $binds $t_binds ) +) diff --git a/src/libs/std/mod.rs b/src/libs/std/mod.rs new file mode 100644 index 0000000..0d89ffe --- /dev/null +++ b/src/libs/std/mod.rs @@ -0,0 +1,18 @@ +//! Basic types and their functions, frequently used tools with no environmental +//! dependencies. +pub mod arithmetic_error; +pub mod binary; +mod bool; +mod conv; +mod cross_pipeline; +pub mod exit_status; +mod inspect; +pub mod number; +mod panic; +mod protocol; +pub mod reflect; +pub mod runtime_error; +mod state; +pub mod std_system; +pub mod string; +pub mod tuple; diff --git a/src/systems/stl/number.orc b/src/libs/std/number.orc similarity index 100% rename from src/systems/stl/number.orc rename to src/libs/std/number.orc diff --git a/src/systems/stl/number.rs b/src/libs/std/number.rs similarity index 54% rename from src/systems/stl/number.rs rename to src/libs/std/number.rs index d1bc92b..0d428e0 100644 --- a/src/systems/stl/number.rs +++ b/src/libs/std/number.rs @@ -1,13 +1,17 @@ +//! `std::number` Numeric operations. + use ordered_float::NotNan; -use super::ArithmeticError; -use crate::error::AssertionError; -use crate::foreign::{xfn_2ary, Atomic, ExternError, ToClause, XfnResult}; -use crate::interpreted::TryFromExprInst; -use crate::representations::interpreted::{Clause, ExprInst}; -use crate::{ConstTree, Interner, Location}; - -// region: Numeric, type to handle floats and uints together +use super::arithmetic_error::ArithmeticError; +use crate::foreign::atom::Atomic; +use crate::foreign::error::{AssertionError, ExternError, ExternResult}; +use crate::foreign::fn_bridge::constructors::xfn_2ary; +use crate::foreign::inert::Inert; +use crate::foreign::to_clause::ToClause; +use crate::foreign::try_from_expr::TryFromExpr; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::nort::{Clause, Expr}; +use crate::location::CodeLocation; /// A number, either floating point or unsigned int, visible to Orchid. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -37,42 +41,41 @@ impl Numeric { } /// Wrap a f64 in a Numeric - pub fn new(value: f64) -> XfnResult { - if value.is_finite() { - NotNan::new(value) - .map(Self::Float) - .map_err(|_| ArithmeticError::NaN.into_extern()) - } else { - Err(ArithmeticError::Infinity.into_extern()) + pub fn new(value: f64) -> ExternResult { + match value.is_finite() { + false => Err(ArithmeticError::Infinity.rc()), + true => match NotNan::new(value) { + Ok(f) => Ok(Self::Float(f)), + Err(_) => Err(ArithmeticError::NaN.rc()), + }, } } } -impl TryFromExprInst for Numeric { - fn from_exi(exi: ExprInst) -> XfnResult { - (exi.request()) - .ok_or_else(|| AssertionError::ext(Location::Unknown, "a numeric value")) +impl TryFromExpr for Numeric { + fn from_expr(exi: Expr) -> ExternResult { + (exi.clause.request()).ok_or_else(|| { + AssertionError::ext(exi.location(), "a numeric value", format!("{exi}")) + }) } } impl ToClause for Numeric { - fn to_clause(self) -> Clause { + fn to_clause(self, _: CodeLocation) -> Clause { match self { - Numeric::Uint(i) => i.atom_cls(), - Numeric::Float(n) => n.atom_cls(), + Numeric::Uint(i) => Inert(i).atom_cls(), + Numeric::Float(n) => Inert(n).atom_cls(), } } } -// endregion - /// Add two numbers. If they're both uint, the output is uint. If either is /// number, the output is number. -pub fn add(a: Numeric, b: Numeric) -> XfnResult { +pub fn add(a: Numeric, b: Numeric) -> ExternResult { match (a, b) { (Numeric::Uint(a), Numeric::Uint(b)) => a .checked_add(b) .map(Numeric::Uint) - .ok_or_else(|| ArithmeticError::Overflow.into_extern()), + .ok_or_else(|| ArithmeticError::Overflow.rc()), (Numeric::Float(a), Numeric::Float(b)) => Numeric::new(*(a + b)), (Numeric::Float(a), Numeric::Uint(b)) | (Numeric::Uint(b), Numeric::Float(a)) => Numeric::new(*a + b as f64), @@ -80,7 +83,7 @@ pub fn add(a: Numeric, b: Numeric) -> XfnResult { } /// Subtract a number from another. Always returns Number. -pub fn subtract(a: Numeric, b: Numeric) -> XfnResult { +pub fn subtract(a: Numeric, b: Numeric) -> ExternResult { match (a, b) { (Numeric::Uint(a), Numeric::Uint(b)) => Numeric::new(a as f64 - b as f64), (Numeric::Float(a), Numeric::Float(b)) => Numeric::new(*(a - b)), @@ -91,12 +94,12 @@ pub fn subtract(a: Numeric, b: Numeric) -> XfnResult { /// Multiply two numbers. If they're both uint, the output is uint. If either /// is number, the output is number. -pub fn multiply(a: Numeric, b: Numeric) -> XfnResult { +pub fn multiply(a: Numeric, b: Numeric) -> ExternResult { match (a, b) { (Numeric::Uint(a), Numeric::Uint(b)) => a .checked_mul(b) .map(Numeric::Uint) - .ok_or_else(|| ArithmeticError::Overflow.into_extern()), + .ok_or_else(|| ArithmeticError::Overflow.rc()), (Numeric::Float(a), Numeric::Float(b)) => Numeric::new(*(a * b)), (Numeric::Uint(a), Numeric::Float(b)) | (Numeric::Float(b), Numeric::Uint(a)) => Numeric::new(a as f64 * *b), @@ -104,48 +107,44 @@ pub fn multiply(a: Numeric, b: Numeric) -> XfnResult { } /// Divide a number by another. Always returns Number. -pub fn divide(a: Numeric, b: Numeric) -> XfnResult { +pub fn divide(a: Numeric, b: Numeric) -> ExternResult { let a: f64 = a.as_f64(); let b: f64 = b.as_f64(); if b == 0.0 { - return Err(ArithmeticError::DivByZero.into_extern()); + return Err(ArithmeticError::DivByZero.rc()); } Numeric::new(a / b) } /// Take the remainder of two numbers. If they're both uint, the output is /// uint. If either is number, the output is number. -pub fn remainder(a: Numeric, b: Numeric) -> XfnResult { +pub fn remainder(a: Numeric, b: Numeric) -> ExternResult { match (a, b) { (Numeric::Uint(a), Numeric::Uint(b)) => a .checked_rem(b) .map(Numeric::Uint) - .ok_or_else(|| ArithmeticError::DivByZero.into_extern()), + .ok_or_else(|| ArithmeticError::DivByZero.rc()), (Numeric::Float(a), Numeric::Float(b)) => Numeric::new(*(a % b)), (Numeric::Uint(a), Numeric::Float(b)) => Numeric::new(a as f64 % *b), (Numeric::Float(a), Numeric::Uint(b)) => Numeric::new(*a % b as f64), } } -pub fn less_than(a: Numeric, b: Numeric) -> XfnResult { - Ok(match (a, b) { - (Numeric::Uint(a), Numeric::Uint(b)) => a < b, - (a, b) => a.as_f64() < b.as_f64(), - }) +/// Tries to use integer comparison, casts to float otherwise +pub fn less_than(a: Numeric, b: Numeric) -> Inert { + match (a, b) { + (Numeric::Uint(a), Numeric::Uint(b)) => Inert(a < b), + (a, b) => Inert(a.as_f64() < b.as_f64()), + } } -// endregion - -pub fn num(i: &Interner) -> ConstTree { - ConstTree::tree([( - i.i("number"), - ConstTree::tree([ - (i.i("add"), ConstTree::xfn(xfn_2ary(add))), - (i.i("subtract"), ConstTree::xfn(xfn_2ary(subtract))), - (i.i("multiply"), ConstTree::xfn(xfn_2ary(multiply))), - (i.i("divide"), ConstTree::xfn(xfn_2ary(divide))), - (i.i("remainder"), ConstTree::xfn(xfn_2ary(remainder))), - (i.i("less_than"), ConstTree::xfn(xfn_2ary(less_than))), - ]), - )]) +pub(super) fn num_lib() -> ConstTree { + ConstTree::ns("std::number", [ConstTree::tree([ + ("add", atom_leaf(xfn_2ary(add))), + ("subtract", atom_leaf(xfn_2ary(subtract))), + ("multiply", atom_leaf(xfn_2ary(multiply))), + ("divide", atom_leaf(xfn_2ary(divide))), + ("remainder", atom_leaf(xfn_2ary(remainder))), + ("less_than", atom_leaf(xfn_2ary(less_than))), + ])]) } diff --git a/src/libs/std/option.orc b/src/libs/std/option.orc new file mode 100644 index 0000000..482c7a2 --- /dev/null +++ b/src/libs/std/option.orc @@ -0,0 +1,42 @@ +import std::(panic, pmatch, to_string, conv) +import std::(functional::*, string::*) + +as_type option ( + impl to_string := \opt. ( + handle opt "none" \x. "some(" ++ conv::to_string x ++ ")" + ) +) + +export const some := \v. wrap \d. \f. f v +export const none := wrap \d. \f. d + +export const handle := \t. \d. \f. (unwrap t) d f + +export const map := \option. \f. handle option none \x. some $ f x +export const fallback := \option. \fallback. handle option fallback \data. data +export const flatten := \option. handle option none \opt. wrap $ unwrap opt -- assert type +export const flatmap := \option. \f. handle option none \opt. wrap $ unwrap $ f opt -- assert return +export const assume := \option. handle option (panic "value expected") \x.x + +( + macro pmatch::request ( none ) + =0x1p230=> pmatch::response ( + handle pmatch::value + pmatch::pass + \_. pmatch::fail + ) ( pmatch::no_binds ) +) + +( + macro pmatch::request ( some ...$value ) + =0x1p230=> await_some_subpattern ( pmatch::request (...$value) ) +) + +( + macro await_some_subpattern ( pmatch::response $expr ( $binds ) ) + =0x1p254=> pmatch::response ( + handle pmatch::value + pmatch::fail + \pmatch::value. $expr + ) ( $binds ) +) diff --git a/src/systems/stl/panic.rs b/src/libs/std/panic.rs similarity index 55% rename from src/systems/stl/panic.rs rename to src/libs/std/panic.rs index dfec15c..c50826f 100644 --- a/src/systems/stl/panic.rs +++ b/src/libs/std/panic.rs @@ -1,9 +1,13 @@ use std::fmt::Display; use std::sync::Arc; -use crate::foreign::{xfn_1ary, ExternError, XfnResult}; -use crate::interpreted::Clause; -use crate::{ConstTree, Interner, OrcString}; +use never::Never; + +use super::string::OrcString; +use crate::foreign::error::{ExternError, ExternResult}; +use crate::foreign::fn_bridge::constructors::xfn_1ary; +use crate::foreign::inert::Inert; +use crate::gen::tree::{atom_leaf, ConstTree}; /// An unrecoverable error in Orchid land. Because Orchid is lazy, this only /// invalidates expressions that reference the one that generated it. @@ -19,11 +23,11 @@ impl Display for OrchidPanic { impl ExternError for OrchidPanic {} /// Takes a message, returns an [ExternError] unconditionally. -pub fn orc_panic(msg: OrcString) -> XfnResult { +pub fn orc_panic(msg: Inert) -> ExternResult { // any return value would work, but Clause is the simplest - Err(OrchidPanic(Arc::new(msg.get_string())).into_extern()) + Err(OrchidPanic(Arc::new(msg.0.get_string())).rc()) } -pub fn panic(i: &Interner) -> ConstTree { - ConstTree::tree([(i.i("panic"), ConstTree::xfn(xfn_1ary(orc_panic)))]) +pub fn panic_lib() -> ConstTree { + ConstTree::ns("std::panic", [atom_leaf(xfn_1ary(orc_panic))]) } diff --git a/src/systems/stl/match.orc b/src/libs/std/pmatch.orc similarity index 100% rename from src/systems/stl/match.orc rename to src/libs/std/pmatch.orc diff --git a/src/systems/stl/prelude.orc b/src/libs/std/prelude.orc similarity index 89% rename from src/systems/stl/prelude.orc rename to src/libs/std/prelude.orc index 6b0d33b..1df70c8 100644 --- a/src/systems/stl/prelude.orc +++ b/src/libs/std/prelude.orc @@ -7,10 +7,10 @@ export ::([== !=], if, then, else, true, false, and, or, not) import std::functional::* export ::([$ |>], identity, pass, pass2, return) import std::procedural::* -export ::(do, let, cps, [;]) +export ::(do, let, cps) import std::tuple::t export ::(t) -import std::match::(match, [=>]) +import std::pmatch::(match, [=>]) export ::(match, [=>]) import std::tuple import std::list diff --git a/src/systems/stl/procedural.orc b/src/libs/std/procedural.orc similarity index 50% rename from src/systems/stl/procedural.orc rename to src/libs/std/procedural.orc index a9626df..5dad872 100644 --- a/src/systems/stl/procedural.orc +++ b/src/libs/std/procedural.orc @@ -1,29 +1,36 @@ -import super::match::=> +import super::pmatch::=> import super::known::* +export ::(do, statement, [;]) + -- remove duplicate ;-s -export macro do { +macro do { ...$statement ; ; ...$rest:1 } =0x3p130=> do { ...$statement ; ...$rest } -- modular operation block that returns a value -export macro do { +macro do { ...$statement ; ...$rest:1 } =0x2p130=> statement (...$statement) (do { ...$rest }) -export macro do { ...$return } =0x1p130=> (...$return) --- modular operation block that returns a CPS function -export macro do cps { ...$body } =0x1p130=> \cont. do { ...$body ; cont } +macro do { ...$return } =0x1p130=> (...$return) -export macro statement (let $_name = ...$value) (...$next) =0x2p230=> ( +export ::let + +macro statement (let $_name = ...$value) (...$next) =0x2p230=> ( ( \$_name. ...$next) (...$value) ) -export macro statement (let ...$pattern = ...$value:1) (...$next) =0x1p230=> ( +macro statement (let ...$pattern = ...$value:1) (...$next) =0x1p230=> ( ( (...$pattern) => (...$next) ) (...$value) ) -export macro statement (cps ...$names = ...$operation:1) (...$next) =0x2p230=> ( + +export ::cps + +-- modular operation block that returns a CPS function +macro do cps { ...$body } =0x1p130=> \cont. do { ...$body ; cont } +macro statement (cps ...$names = ...$operation:1) (...$next) =0x2p230=> ( (...$operation) ( (...$names) => ...$next ) ) -export macro statement (cps ...$operation) (...$next) =0x1p230=> ( +macro statement (cps ...$operation) (...$next) =0x1p230=> ( (...$operation) (...$next) ) diff --git a/src/libs/std/protocol.rs b/src/libs/std/protocol.rs new file mode 100644 index 0000000..ffa7c29 --- /dev/null +++ b/src/libs/std/protocol.rs @@ -0,0 +1,447 @@ +use std::fmt::Debug; +use std::iter; +use std::sync::Arc; + +use const_format::formatcp; +use hashbrown::HashMap; +use intern_all::{i, Tok}; +use itertools::Itertools; + +use super::cross_pipeline::defer_to_runtime; +use super::reflect::{refer_seq, RefEqual}; +use super::runtime_error::RuntimeError; +use crate::error::ProjectResult; +use crate::foreign::atom::Atomic; +use crate::foreign::error::ExternResult; +use crate::foreign::fn_bridge::constructors::xfn_2ary; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::foreign::process::Unstable; +use crate::foreign::to_clause::ToClause; +use crate::gen::tpl; +use crate::gen::traits::GenClause; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::nort as int; +use crate::interpreter::nort::ClauseInst; +use crate::libs::parse_custom_line::custom_line; +use crate::location::SourceRange; +use crate::name::{Sym, VName}; +use crate::parse::frag::Frag; +use crate::parse::lexer::Lexeme; +use crate::parse::parse_plugin::{ParseLinePlugin, ParsePluginReq}; +use crate::parse::parsed::{ + self, Constant, Member, MemberKind, ModuleBlock, PType, SourceLine, + SourceLineKind, +}; +use crate::utils::ddispatch::Request; +use crate::utils::pure_seq::pushed; + +pub struct TypeData { + pub id: RefEqual, + pub display_name: Tok, + pub impls: HashMap, +} + +/// Key for type data. The value is either [Inert] or [Inert] +const TYPE_KEY: &str = "__type_data__"; + +#[derive(Clone)] +pub struct Protocol(pub Arc); +impl Protocol { + const ID_KEY: &'static str = "__protocol_id__"; + + pub fn new_id( + id: RefEqual, + display_name: Tok, + impls: impl IntoIterator, + ) -> Self { + let impls = impls.into_iter().collect(); + Protocol(Arc::new(TypeData { id, display_name, impls })) + } + + pub fn new( + display_name: &'static str, + impls: impl IntoIterator, + ) -> Self { + Self::new_id(RefEqual::new(), i(display_name), impls) + } + + pub fn id(&self) -> RefEqual { self.0.id.clone() } + + pub fn as_tree_ent<'a>( + &'a self, + rest: impl IntoIterator, + ) -> (&str, ConstTree) { + ConstTree::tree_ent( + self.0.display_name.as_str(), + rest.into_iter().chain([ + (Self::ID_KEY, atom_leaf(Inert(self.id()))), + (TYPE_KEY, atom_leaf(Inert(self.clone()))), + ]), + ) + } +} +impl Debug for Protocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(&self.0.display_name).field(&self.0.id.id()).finish() + } +} +impl InertPayload for Protocol { + const TYPE_STR: &'static str = "Protocol"; +} + +#[derive(Clone)] +pub struct Tag(pub Arc); +impl Tag { + const ID_KEY: &'static str = "__type_id__"; + + pub fn new_id( + id: RefEqual, + display_name: Tok, + impls: impl IntoIterator, + ) -> Self { + let impls = impls.into_iter().collect(); + Self(Arc::new(TypeData { id, display_name, impls })) + } + + pub fn new( + display_name: &'static str, + impls: impl IntoIterator, + ) -> Self { + Self::new_id(RefEqual::new(), i(display_name), impls) + } + + pub fn id(&self) -> RefEqual { self.0.id.clone() } + + pub fn as_tree_ent<'a>( + &'a self, + rest: impl IntoIterator, + ) -> (&str, ConstTree) { + ConstTree::tree_ent( + self.0.display_name.as_str(), + rest.into_iter().chain([ + (Self::ID_KEY, atom_leaf(Inert(self.id()))), + (TYPE_KEY, atom_leaf(Inert(self.clone()))), + ]), + ) + } +} +impl Debug for Tag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(&self.0.display_name).field(&self.0.id.id()).finish() + } +} +impl InertPayload for Tag { + const TYPE_STR: &'static str = "Tag"; + fn strict_eq(&self, other: &Self) -> bool { self.0.id == other.0.id } +} + +#[derive(Clone)] +pub struct Tagged { + pub tag: Tag, + pub value: int::Expr, +} +impl Debug for Tagged { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Tagged").field(&self.tag).field(&self.value).finish() + } +} +impl InertPayload for Tagged { + const TYPE_STR: &'static str = "Tagged"; + fn respond(&self, mut request: Request) { + request.serve_with(|| self.tag.clone()) + } +} + +fn parse_impl( + tail: Frag, + req: &dyn ParsePluginReq, +) -> Option> { + custom_line(tail, i("impl"), false, req).map(|res| { + let (_, tail, _) = res?; + let (name, tail) = req.parse_nsname(tail)?; + let (walrus, tail) = req.pop(tail.trim())?; + req.expect(Lexeme::Walrus, walrus)?; + let (body, empty) = req.parse_exprv(tail, None)?; + req.expect_empty(empty)?; + let value = req.vec_to_single(tail.fallback, body)?; + Ok((name, value)) + }) +} + +struct Impl { + target: Sym, + value: parsed::Expr, +} + +fn extract_impls( + tail: Frag, + req: &dyn ParsePluginReq, + range: SourceRange, + typeid_name: Tok, +) -> ProjectResult<(Vec, Vec)> { + let mut lines = Vec::new(); + let mut impls = Vec::new(); // name1, value1, name2, value2, etc... + for line in req.split_lines(tail) { + match parse_impl(line, req) { + Some(result) => { + let (name, value) = result?; + let target = + Sym::new(name.suffix([typeid_name.clone()])).unwrap(); + impls.push(Impl { target, value }); + }, + None => lines.extend( + (req.parse_line(line)?.into_iter()).map(|k| k.wrap(range.clone())), + ), + } + } + Ok((lines, impls)) +} + +trait WrapImpl: Clone + Send + Sync + 'static { + type R: ToClause; + fn wrap(&self, data: Arc) -> Self::R; +} +impl) -> R + Clone + Send + Sync + 'static> + WrapImpl for F +{ + type R = R; + fn wrap(&self, data: Arc) -> Self::R { self(data) } +} + +struct ImplsProfile<'a, W: WrapImpl> { + wrap: W, + own_id: Tok, + other_id: Tok, + prelude: &'a str, +} + +fn parse_body_with_impls( + display_name: Tok, + body: Frag, + req: &dyn ParsePluginReq, + range: SourceRange, + profile: &ImplsProfile<'static, W>, +) -> ProjectResult> { + let id = RefEqual::new(); + let (lines, impls) = + extract_impls(body, req, range.clone(), profile.other_id.clone())?; + + Ok( + req + .parse_entries(profile.prelude, range.clone()) + .into_iter() + .chain( + [ + (profile.own_id.clone(), Inert(id.clone()).ast_cls()), + ( + i(TYPE_KEY), + defer_to_runtime( + range.clone(), + impls.into_iter().flat_map({ + let line_loc = range.clone(); + move |Impl { target, value }| { + [ + parsed::Clause::Name(target).into_expr(line_loc.clone()), + value, + ] + .map(|e| ((), vec![e])) + } + }), + { + let display_name = display_name.clone(); + let wrap = profile.wrap.clone(); + move |pairs: Vec<((), int::Expr)>| -> ExternResult<_> { + let mut impls = HashMap::new(); + debug_assert_eq!(pairs.len() % 2, 0, "key-value pairs"); + let mut nvnvnv = pairs.into_iter().map(|t| t.1); + while let Some((name, value)) = nvnvnv.next_tuple() { + let key = name.downcast::>()?; + impls.insert(key.0, value); + } + let id = id.clone(); + let display_name = display_name.clone(); + Ok(wrap.wrap(Arc::new(TypeData { id, display_name, impls }))) + } + }, + ), + ), + ] + .map(|(name, value)| { + let value = parsed::Expr { value, range: range.clone() }; + MemberKind::Constant(Constant { name, value }) + .to_line(true, range.clone()) + }), + ) + .chain(lines) + .collect(), + ) +} + +fn protocol_impls_profile() -> ImplsProfile<'static, impl WrapImpl> { + ImplsProfile { + wrap: |t| Inert(Protocol(t)), + own_id: i(Protocol::ID_KEY), + other_id: i(Tag::ID_KEY), + prelude: formatcp!( + "import std::protocol + const resolve := protocol::resolve {TYPE_KEY} + const get_impl := protocol::get_impl {TYPE_KEY}" + ), + } +} + +fn type_impls_profile() -> ImplsProfile<'static, impl WrapImpl> { + ImplsProfile { + wrap: |t| Inert(Tag(t)), + own_id: i(Tag::ID_KEY), + other_id: i(Protocol::ID_KEY), + prelude: formatcp!( + "import std::protocol + const unwrap := protocol::unwrap {TYPE_KEY} + const wrap := protocol::wrap {TYPE_KEY}" + ), + } +} + +struct ProtocolParser; +impl ParseLinePlugin for ProtocolParser { + fn parse( + &self, + req: &dyn ParsePluginReq, + ) -> Option>> { + custom_line(req.frag(), i("protocol"), true, req).map(|res| { + let (exported, tail, line_loc) = res?; + let (name, tail) = req.pop(tail)?; + let name = req.expect_name(name)?; + let tail = req.expect_block(tail, PType::Par)?; + let profile = protocol_impls_profile(); + let body = + parse_body_with_impls(name.clone(), tail, req, line_loc, &profile)?; + let kind = MemberKind::Module(ModuleBlock { name, body }); + Ok(vec![SourceLineKind::Member(Member { exported, kind })]) + }) + } +} + +struct TypeParser; +impl ParseLinePlugin for TypeParser { + fn parse( + &self, + req: &dyn ParsePluginReq, + ) -> Option>> { + custom_line(req.frag(), i("type"), true, req).map(|res| { + let (exported, tail, line_loc) = res?; + let (name, tail) = req.pop(tail)?; + let name = req.expect_name(name)?; + let tail = req.expect_block(tail, PType::Par)?; + let profile = type_impls_profile(); + let body = + parse_body_with_impls(name.clone(), tail, req, line_loc, &profile)?; + let kind = MemberKind::Module(ModuleBlock { name, body }); + Ok(vec![SourceLineKind::Member(Member { exported, kind })]) + }) + } +} + +struct AsProtocolParser; +impl ParseLinePlugin for AsProtocolParser { + fn parse( + &self, + req: &dyn ParsePluginReq, + ) -> Option>> { + custom_line(req.frag(), i("as_protocol"), false, req).map(|res| { + let (_, tail, line_loc) = res?; + let (name, tail) = req.pop(tail)?; + let name = req.expect_name(name)?; + let body = req.expect_block(tail, PType::Par)?; + let profile = protocol_impls_profile(); + parse_body_with_impls(name, body, req, line_loc, &profile) + .map(|v| v.into_iter().map(|e| e.kind).collect()) + }) + } +} + +struct AsTypeParser; +impl ParseLinePlugin for AsTypeParser { + fn parse( + &self, + req: &dyn ParsePluginReq, + ) -> Option>> { + custom_line(req.frag(), i("as_type"), false, req).map(|res| { + let (_, tail, line_loc) = res?; + let (name, tail) = req.pop(tail)?; + let name = req.expect_name(name)?; + let body = req.expect_block(tail, PType::Par)?; + let profile = type_impls_profile(); + parse_body_with_impls(name, body, req, line_loc, &profile) + .map(|v| v.into_iter().map(|e| e.kind).collect()) + }) + } +} + +pub fn parsers() -> Vec> { + vec![ + Box::new(ProtocolParser), + Box::new(TypeParser), + Box::new(AsTypeParser), + Box::new(AsProtocolParser), + ] +} + +pub fn unwrap( + tag: Inert, + tagged: Inert, +) -> ExternResult { + if tagged.tag.strict_eq(&tag) { + return Ok(tagged.value.clone()); + } + let msg = format!("expected {:?} but got {:?}", tag, tagged.tag); + RuntimeError::fail(msg, "unwrapping type-tagged value") +} + +pub fn wrap(tag: Inert, value: int::Expr) -> Inert { + Inert(Tagged { tag: tag.0, value }) +} + +pub fn resolve( + protocol: Inert, + value: ClauseInst, +) -> ExternResult { + let tag = value.request::().ok_or_else(|| { + let msg = format!("{value} is not type-tagged"); + RuntimeError::ext(msg, "resolving protocol impl") + })?; + get_impl(protocol, Inert(tag)) +} + +pub fn get_impl( + Inert(proto): Inert, + Inert(tag): Inert, +) -> ExternResult { + if let Some(implem) = proto.0.impls.get(&tag.0.id) { + Ok(implem.clone()) + } else if let Some(implem) = tag.0.impls.get(&proto.0.id) { + Ok(implem.clone()) + } else { + let message = format!("{tag:?} doesn't implement {proto:?}"); + RuntimeError::fail(message, "dispatching protocol") + } +} + +pub const fn gen_resolv(name: &'static str) -> impl GenClause { + tpl::A( + tpl::C("std::protocol::resolve"), + tpl::V(Unstable::new(move |_| { + refer_seq(name.split("::").chain(iter::once(TYPE_KEY))) + })), + ) +} + +pub fn protocol_lib() -> ConstTree { + ConstTree::ns("std::protocol", [ConstTree::tree([ + ("unwrap", atom_leaf(xfn_2ary(unwrap))), + ("wrap", atom_leaf(xfn_2ary(wrap))), + ("get_impl", atom_leaf(xfn_2ary(get_impl))), + ("resolve", atom_leaf(xfn_2ary(resolve))), + ])]) +} diff --git a/src/libs/std/reflect.rs b/src/libs/std/reflect.rs new file mode 100644 index 0000000..1bcc6c0 --- /dev/null +++ b/src/libs/std/reflect.rs @@ -0,0 +1,77 @@ +//! `std::reflect` Abstraction-breaking operations for dynamically constructing +//! [Clause::Constant] references. + +use std::cmp; +use std::fmt::Debug; +use std::hash::Hash; +use std::sync::atomic::{self, AtomicUsize}; + +use intern_all::i; + +use crate::foreign::fn_bridge::constructors::{xfn_1ary, xfn_2ary}; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::gen::tree::{atom_ent, ConstTree}; +use crate::interpreter::nort::Clause; +use crate::name::Sym; + +impl InertPayload for Sym { + const TYPE_STR: &'static str = "SymbolName"; +} + +/// Generate a constant reference at runtime. Referencing a nonexistent constant +/// is a runtime error. +pub fn refer_seq(name: impl IntoIterator) -> Clause { + Clause::Constant(Sym::new(name.into_iter().map(i)).expect("Empty name")) +} + +/// Generate a constant reference at runtime. Referencing a nonexistent constant +/// is a runtime error. +pub fn refer(name: &'static str) -> Clause { refer_seq(name.split("::")) } + +static COUNTER: AtomicUsize = AtomicUsize::new(0); + +/// A struct that equals its own copies and only its own copies +#[derive(Clone)] +pub struct RefEqual(usize); +impl RefEqual { + /// Create a new [RefEqual] which is initially completely unique + #[allow(clippy::new_without_default)] // new has semantic meaning + pub fn new() -> Self { Self(COUNTER.fetch_add(1, atomic::Ordering::Relaxed)) } + /// Return the unique identifier of this [RefEqual] and its copies + pub fn id(&self) -> usize { self.0 } +} +impl Debug for RefEqual { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("RefEqual").field(&self.id()).finish() + } +} +impl InertPayload for RefEqual { + const TYPE_STR: &'static str = "RefEqual"; + fn strict_eq(&self, other: &Self) -> bool { self == other } +} +impl Eq for RefEqual {} +impl PartialEq for RefEqual { + fn eq(&self, other: &Self) -> bool { self.id() == other.id() } +} +impl Ord for RefEqual { + fn cmp(&self, other: &Self) -> cmp::Ordering { self.id().cmp(&other.id()) } +} +impl PartialOrd for RefEqual { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Hash for RefEqual { + fn hash(&self, state: &mut H) { self.id().hash(state) } +} + +pub(super) fn reflect_lib() -> ConstTree { + ConstTree::ns("std::reflect", [ConstTree::tree([ + atom_ent("auto_tuple_test", [xfn_1ary(|_: Inert| { + (Inert(1usize), Inert(2usize), Inert(3usize)) + })]), + atom_ent("ref_equal", [xfn_2ary( + |l: Inert, r: Inert| Inert(l.0.id() == r.0.id()), + )]), + ])]) +} diff --git a/src/libs/std/result.orc b/src/libs/std/result.orc new file mode 100644 index 0000000..53b0685 --- /dev/null +++ b/src/libs/std/result.orc @@ -0,0 +1,12 @@ +import std::panic + +as_type result () + +export const ok := \v. wrap \fe. \fv. fv v +export const err := \e. wrap \fe. \fv. fe e + +export const map := \result. \fv. unwrap result err fv +export const map_err := \result. \fe. unwrap result fe ok +export const flatten := \result. unwrap result err \res. wrap (unwrap res) +export const and_then := \result. \f. unwrap result err \v. f v +export const assume := \result. unwrap result (\e. panic "value expected") \v.v diff --git a/src/error/runtime_error.rs b/src/libs/std/runtime_error.rs similarity index 58% rename from src/error/runtime_error.rs rename to src/libs/std/runtime_error.rs index 9e5a835..f3d4e03 100644 --- a/src/error/runtime_error.rs +++ b/src/libs/std/runtime_error.rs @@ -1,7 +1,10 @@ +//! Errors thrown by the standard library in lieu of in-language error handling +//! for runtime errors such as missing files. + use std::fmt::Display; use std::sync::Arc; -use crate::foreign::{ExternError, XfnResult}; +use crate::foreign::error::{ExternError, ExternResult}; /// Some external event prevented the operation from succeeding #[derive(Clone)] @@ -13,16 +16,13 @@ pub struct RuntimeError { impl RuntimeError { /// Construct, upcast and wrap in a Result that never succeeds for easy /// short-circuiting - pub fn fail(message: String, operation: &'static str) -> XfnResult { - Err(Self { message, operation }.into_extern()) + pub fn fail(message: String, operation: &'static str) -> ExternResult { + Err(Self { message, operation }.rc()) } /// Construct and upcast to [ExternError] - pub fn ext( - message: String, - operation: &'static str, - ) -> Arc { - Self { message, operation }.into_extern() + pub fn ext(message: String, operation: &'static str) -> Arc { + Self { message, operation }.rc() } } diff --git a/src/libs/std/state.rs b/src/libs/std/state.rs new file mode 100644 index 0000000..7afdf91 --- /dev/null +++ b/src/libs/std/state.rs @@ -0,0 +1,84 @@ +use std::sync::{Arc, Mutex}; + +use crate::foreign::fn_bridge::constructors::{xfn_2ary, xfn_3ary}; +use crate::foreign::fn_bridge::Thunk; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::gen::tpl; +use crate::gen::traits::Gen; +use crate::gen::tree::{atom_leaf, ConstTree}; +use crate::interpreter::gen_nort::nort_gen; +use crate::interpreter::nort_builder::NortBuilder; +use crate::interpreter::handler::HandlerTable; +use crate::interpreter::nort::Expr; + +#[derive(Debug, Clone)] +pub struct State(Arc>); +impl InertPayload for State { + const TYPE_STR: &'static str = "State"; +} + +#[derive(Debug, Clone)] +struct NewStateCmd(Expr, Expr); +impl InertPayload for NewStateCmd { + const TYPE_STR: &'static str = "NewStateCmd"; + fn strict_eq(&self, _: &Self) -> bool { true } +} +#[derive(Debug, Clone)] +struct SetStateCmd(State, Expr, Expr); +impl InertPayload for SetStateCmd { + const TYPE_STR: &'static str = "SetStateCmd"; +} + +#[derive(Debug, Clone)] +struct GetStateCmd(State, Expr); +impl InertPayload for GetStateCmd { + const TYPE_STR: &'static str = "GetStateCmd"; +} + +fn new_state(default: Thunk, cont: Thunk) -> Inert { + Inert(NewStateCmd(default.0, cont.0)) +} + +fn get_state(s: Inert, cont: Thunk) -> Inert { + Inert(GetStateCmd(s.0, cont.0)) +} + +fn set_state(s: Inert, value: Thunk, cont: Thunk) -> Inert { + Inert(SetStateCmd(s.0, value.0, cont.0)) +} + +fn new_state_handler(cmd: &Inert) -> Expr { + let Inert(NewStateCmd(default, handler)) = cmd; + let state = State(Arc::new(Mutex::new(default.clone()))); + let tpl = tpl::A(tpl::Slot, tpl::V(Inert(state))); + tpl.template(nort_gen(handler.location()), [handler.clone()]) +} + +fn set_state_handler(cmd: &Inert) -> Expr { + let Inert(SetStateCmd(state, value, handler)) = cmd; + *state.0.lock().unwrap() = value.clone(); + handler.clone() +} + +fn get_state_handler(cmd: &Inert) -> Expr { + let Inert(GetStateCmd(state, handler)) = cmd; + let val = state.0.lock().unwrap().clone(); + let tpl = tpl::A(tpl::Slot, tpl::Slot); + tpl.template(nort_gen(handler.location()), [handler.clone(), val]) +} + +pub fn state_handlers() -> HandlerTable<'static> { + let mut handlers = HandlerTable::new(); + handlers.register(new_state_handler); + handlers.register(get_state_handler); + handlers.register(set_state_handler); + handlers +} + +pub fn state_lib() -> ConstTree { + ConstTree::ns("std::state", [ConstTree::tree([ + ("new_state", atom_leaf(xfn_2ary(new_state))), + ("get_state", atom_leaf(xfn_2ary(get_state))), + ("set_state", atom_leaf(xfn_3ary(set_state))), + ])]) +} diff --git a/src/libs/std/std_system.rs b/src/libs/std/std_system.rs new file mode 100644 index 0000000..bb16105 --- /dev/null +++ b/src/libs/std/std_system.rs @@ -0,0 +1,86 @@ +//! Add the standard library's constants and mcacros to an Orchid environment + +#![allow(non_upper_case_globals)] + +use rust_embed::RustEmbed; + +use super::binary::bin_lib; +use super::bool::bool_lib; +use super::conv::conv_lib; +use super::exit_status::exit_status_lib; +use super::inspect::inspect_lib; +use super::number::num_lib; +use super::panic::panic_lib; +use super::protocol::{parsers, protocol_lib}; +use super::reflect::reflect_lib; +use super::state::{state_handlers, state_lib}; +use super::string::str_lib; +use super::tuple::tuple_lib; +use crate::facade::system::{IntoSystem, System}; +use crate::gen::tree::{ConstCombineErr, ConstTree}; +use crate::location::CodeGenInfo; +use crate::name::VName; +use crate::pipeline::load_solution::Prelude; +use crate::tree::ModEntry; +use crate::utils::combine::Combine; +use crate::virt_fs::{EmbeddedFS, VirtFS}; + +#[derive(RustEmbed)] +#[folder = "src/libs/std"] +#[include = "*.orc"] +struct StdEmbed; + +/// Feature flags for the STL. +#[derive(Default)] +pub struct StdConfig { + /// Whether impure functions (such as io::debug) are allowed. An embedder + /// would typically disable this flag + pub impure: bool, +} +impl StdConfig { + fn stdlib(&self) -> Result { + let pure_tree = tuple_lib() + .combine(bin_lib())? + .combine(bool_lib())? + .combine(conv_lib())? + .combine(exit_status_lib())? + .combine(num_lib())? + .combine(panic_lib())? + .combine(protocol_lib())? + .combine(reflect_lib())? + .combine(state_lib())? + .combine(str_lib())?; + // panic!( + // "{:?}", + // pure_tree + // .unwrap_mod_ref() + // .walk1_ref(&[], &[i("std"), i("protocol")], |_| true) + // .map(|p| p.0) + // ); + if !self.impure { + return Ok(pure_tree); + } + pure_tree.combine(inspect_lib()) + } +} + +impl IntoSystem<'static> for StdConfig { + fn into_system(self) -> System<'static> { + let gen = CodeGenInfo::no_details("std"); + System { + name: "stdlib", + constants: self.stdlib().expect("stdlib tree is malformed"), + code: ModEntry::ns("std", [ModEntry::leaf( + EmbeddedFS::new::(".orc", gen.clone()).rc(), + )]), + prelude: vec![Prelude { + target: VName::literal("std::prelude"), + exclude: VName::literal("std"), + owner: gen.clone(), + }], + handlers: state_handlers(), + lexer_plugins: vec![], + line_parsers: parsers(), + } + } +} diff --git a/src/systems/stl/string.orc b/src/libs/std/string.orc similarity index 75% rename from src/systems/stl/string.orc rename to src/libs/std/string.orc index 7d244a9..bd8be5a 100644 --- a/src/systems/stl/string.orc +++ b/src/libs/std/string.orc @@ -1,4 +1,4 @@ -import super::(procedural::*, bool::*, panic) +import super::(procedural::*, bool::*, panic, inspect, known::*) export macro ...$a ++ ...$b =0x4p36=> (concat (...$a) (...$b)) diff --git a/src/libs/std/string.rs b/src/libs/std/string.rs new file mode 100644 index 0000000..9ef46af --- /dev/null +++ b/src/libs/std/string.rs @@ -0,0 +1,170 @@ +//! `std::string` String processing + +use std::fmt::Debug; +use std::hash::Hash; +use std::ops::Deref; +use std::sync::Arc; + +use intern_all::{i, Tok}; +use unicode_segmentation::UnicodeSegmentation; + +use super::runtime_error::RuntimeError; +use crate::foreign::atom::Atomic; +use crate::foreign::error::ExternResult; +use crate::foreign::fn_bridge::constructors::{xfn_1ary, xfn_2ary, xfn_3ary}; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::foreign::to_clause::ToClause; +use crate::foreign::try_from_expr::TryFromExpr; +use crate::gen::tree::{atom_ent, ConstTree}; +use crate::interpreter::nort::{Clause, Expr}; +use crate::location::CodeLocation; +use crate::utils::iter_find::iter_find; + +/// An Orchid string which may or may not be interned +#[derive(Clone, Eq)] +pub enum OrcString { + /// An interned string. Equality-conpared by reference. + Interned(Tok), + /// An uninterned bare string. Equality-compared by character + Runtime(Arc), +} + +impl Debug for OrcString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Runtime(s) => write!(f, "r\"{s}\""), + Self::Interned(t) => write!(f, "i\"{t}\""), + } + } +} + +impl OrcString { + /// Intern the contained string + pub fn intern(&mut self) { + if let Self::Runtime(t) = self { + *self = Self::Interned(i(t.as_str())) + } + } + /// Clone out the plain Rust [String] + #[must_use] + pub fn get_string(self) -> String { + match self { + Self::Interned(s) => s.as_str().to_owned(), + Self::Runtime(rc) => + Arc::try_unwrap(rc).unwrap_or_else(|rc| (*rc).clone()), + } + } +} + +impl Deref for OrcString { + type Target = String; + + fn deref(&self) -> &Self::Target { + match self { + Self::Interned(t) => t, + Self::Runtime(r) => r, + } + } +} + +impl Hash for OrcString { + fn hash(&self, state: &mut H) { + self.as_str().hash(state) + } +} + +impl From for OrcString { + fn from(value: String) -> Self { Self::Runtime(Arc::new(value)) } +} + +impl From> for OrcString { + fn from(value: Tok) -> Self { Self::Interned(value) } +} + +impl PartialEq for OrcString { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Interned(t1), Self::Interned(t2)) => t1 == t2, + _ => **self == **other, + } + } +} + +impl InertPayload for OrcString { + const TYPE_STR: &'static str = "OrcString"; + fn strict_eq(&self, other: &Self) -> bool { self == other } +} + +impl ToClause for String { + fn to_clause(self, _: CodeLocation) -> Clause { + Inert(OrcString::from(self)).atom_cls() + } +} + +impl TryFromExpr for String { + fn from_expr(exi: Expr) -> ExternResult { + Ok(exi.downcast::>()?.0.get_string()) + } +} + +pub(super) fn str_lib() -> ConstTree { + ConstTree::ns("std::string", [ConstTree::tree([ + atom_ent("slice", [xfn_3ary( + |s: Inert, i: Inert, len: Inert| { + let graphs = s.0.as_str().graphemes(true); + if i.0 == 0 { + return Ok(graphs.take(len.0).collect::()); + } + let mut prefix = graphs.skip(i.0 - 1); + if prefix.next().is_none() { + return Err(RuntimeError::ext( + "Character index out of bounds".to_string(), + "indexing string", + )); + } + let mut count = 0; + let ret = (prefix.take(len.0)) + .map(|x| { + count += 1; + x + }) + .collect::(); + if count == len.0 { + Ok(ret) + } else { + RuntimeError::fail( + "Character index out of bounds".to_string(), + "indexing string", + ) + } + }, + )]), + atom_ent("concat", [xfn_2ary(|a: String, b: Inert| { + a + b.0.as_str() + })]), + atom_ent("find", [xfn_2ary( + |haystack: Inert, needle: Inert| { + let haystack_graphs = haystack.0.as_str().graphemes(true); + iter_find(haystack_graphs, needle.0.as_str().graphemes(true)).map(Inert) + }, + )]), + atom_ent("split", [xfn_2ary( + |s: String, i: Inert| -> (String, String) { + let mut graphs = s.as_str().graphemes(true); + (graphs.by_ref().take(i.0).collect(), graphs.collect()) + }, + )]), + atom_ent("len", [xfn_1ary(|s: Inert| { + Inert(s.0.graphemes(true).count()) + })]), + atom_ent("size", [xfn_1ary(|s: Inert| { + Inert(s.0.as_bytes().len()) + })]), + atom_ent("intern", [xfn_1ary(|s: Inert| { + Inert(match s.0 { + OrcString::Runtime(s) => OrcString::Interned(i(&*s)), + x => x, + }) + })]), + ])]) +} diff --git a/src/libs/std/tuple.orc b/src/libs/std/tuple.orc new file mode 100644 index 0000000..f3ea26a --- /dev/null +++ b/src/libs/std/tuple.orc @@ -0,0 +1,77 @@ +import super::(known::*, bool::*, number::*, string::*, functional::*) +import super::loop::recursive +import super::(to_string, pmatch, macro, panic, conv, list, option) + +-- referenced in the impl table in Rust +const to_string_impl := \t. "tuple[" ++ ( + to_list t + |> list::map conv::to_string + |> list::reduce (\l. \r. l ++ ", " ++ r) + |> option::fallback "" +) ++ "]" + +export const to_list := \t. ( + recursive r (n=length t, l=list::end) + if n == 0 then l + else r (n - 1) (list::cons (pick t $ conv::to_uint $ n - 1) l) +) + +macro gen_tuple $tup macro::list_end =0x1p254=> $tup +macro gen_tuple $tup ( macro::list_item $item $tail ) =0x1p254=> (gen_tuple (push $tup $item) $tail) +export macro new ( $list ) =0x1p84=> (gen_tuple empty $list) + +macro t[..$items] =0x2p84=> ( new ( macro::comma_list (..$items) ) ) + +export ::(t, size) + +--[ + request l -> tuple_pattern pattern_walker l + pattern_walker end -> pattern_result + pattern_walker h ++ t -> pattern_await ( request h ) ( pattern_walker t ) + pattern_await response pattern_result -> pattern_result + tuple_pattern pattern_result -> response +]-- + +( macro pmatch::request ( t[ ..$items ] ) + =0x1p230=> tuple_pattern + ( macro::length macro::comma_list ( ..$items ) ) + ( + pattern_walker + (0) -- index of next item + macro::comma_list ( ..$items ) -- leftover items + ) +) +( macro tuple_pattern $length ( pattern_result $expr ( $binds ) ) + =0x1p254=> pmatch::response ( + if length pmatch::value == $length + then $expr + else pmatch::fail + ) ( $binds ) +) +( macro pattern_walker $length macro::list_end + =0x1p254=> pattern_result pmatch::pass ( pmatch::no_binds ) +) +( macro pattern_walker (...$length) ( macro::list_item $next $tail ) + =0x1p254=> pattern_await + (...$length) + ( pmatch::request $next ) + ( pattern_walker (...$length + 1) $tail ) +) +( macro pattern_await $length + ( pmatch::response $expr ( $binds ) ) + ( pattern_result $tail_expr ( $tail_binds ) ) + =0x1p254=> + pattern_result + ( + (\pmatch::pass. (\pmatch::value. $expr) (pick pmatch::value $length)) ( + pmatch::take_binds $binds ( + (\pmatch::pass. $tail_expr) ( pmatch::take_binds $tail_binds ( + pmatch::give_binds + pmatch::chain_binds $binds $tail_binds + pmatch::pass + )) + ) + ) + ) + ( pmatch::chain_binds $binds $tail_binds ) +) diff --git a/src/libs/std/tuple.rs b/src/libs/std/tuple.rs new file mode 100644 index 0000000..6e5d708 --- /dev/null +++ b/src/libs/std/tuple.rs @@ -0,0 +1,72 @@ +//! `std::tuple` A vector-based sequence for storing short sequences. + +use std::fmt::Debug; +use std::sync::Arc; + +use once_cell::sync::Lazy; + +use super::conv::TO_STRING; +use super::protocol::Tag; +use super::reflect::refer; +use crate::foreign::error::{AssertionError, ExternResult}; +use crate::foreign::fn_bridge::constructors::{xfn_1ary, xfn_2ary}; +use crate::foreign::fn_bridge::Thunk; +use crate::foreign::inert::{Inert, InertPayload}; +use crate::foreign::try_from_expr::WithLoc; +use crate::gen::tpl; +use crate::gen::tree::{atom_leaf, leaf, ConstTree}; +use crate::interpreter::nort::Expr; +use crate::location::{CodeGenInfo, CodeLocation}; +use crate::utils::ddispatch::Request; +use crate::utils::pure_seq::pushed; + +static TUPLE_TAG: Lazy = Lazy::new(|| { + let location = + CodeLocation::Gen(CodeGenInfo::no_details("stdlib::tuple::tag")); + Tag::new("tuple", [( + TO_STRING.id(), + refer("std::tuple::to_string_impl").to_expr(location), + )]) +}); + +/// A short contiquous random access sequence of Orchid values. +#[derive(Clone)] +pub struct Tuple(pub Arc>); +impl InertPayload for Tuple { + const TYPE_STR: &'static str = "tuple"; + fn respond(&self, mut request: Request) { + request.serve_with(|| TUPLE_TAG.clone()) + } +} +impl Debug for Tuple { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Tuple")?; + f.debug_list().entries(self.0.iter()).finish() + } +} + +fn length(tuple: Inert) -> Inert { Inert(tuple.0.0.len()) } + +fn pick( + WithLoc(loc, tuple): WithLoc>, + idx: Inert, +) -> ExternResult { + (tuple.0.0.get(idx.0).cloned()).ok_or_else(|| { + let msg = format!("{} <= {idx}", tuple.0.0.len()); + AssertionError::ext(loc, "Tuple index out of bounds", msg) + }) +} + +fn push(Inert(tuple): Inert, item: Thunk) -> Inert { + let items = Arc::try_unwrap(tuple.0).unwrap_or_else(|a| (*a).clone()); + Inert(Tuple(Arc::new(pushed(items, item.0)))) +} + +pub(super) fn tuple_lib() -> ConstTree { + ConstTree::ns("std", [ConstTree::tree([TUPLE_TAG.as_tree_ent([ + ("empty", leaf(tpl::V(Inert(Tuple(Arc::new(Vec::new())))))), + ("length", atom_leaf(xfn_1ary(length))), + ("pick", atom_leaf(xfn_2ary(pick))), + ("push", atom_leaf(xfn_2ary(push))), + ])])]) +} diff --git a/src/location.rs b/src/location.rs new file mode 100644 index 0000000..b7a44bf --- /dev/null +++ b/src/location.rs @@ -0,0 +1,131 @@ +use std::fmt::{Debug, Display}; +use std::hash::Hash; +use std::ops::Range; +use std::sync::Arc; + +use itertools::Itertools; + +use crate::name::VPath; + +/// A full source code unit, such as a source file +#[derive(Clone, Eq)] +pub struct SourceCode { + /// Location the source code was loaded from in the virtual tree + pub path: Arc, + /// Raw source code string + pub source: Arc, +} +impl PartialEq for SourceCode { + fn eq(&self, other: &Self) -> bool { self.path == other.path } +} +impl Hash for SourceCode { + fn hash(&self, state: &mut H) { self.path.hash(state) } +} +impl Debug for SourceCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CodeInfo({self})") + } +} +impl Display for SourceCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.orc", self.path.str_iter().join("/")) + } +} + +/// Exact source code location. Includes where the code was loaded from, what +/// the original source code was, and a byte range. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SourceRange { + /// Source code + pub code: SourceCode, + /// Byte range + pub range: Range, +} +impl SourceRange { + /// Transform the numeric byte range + pub fn map_range( + &self, + map: impl FnOnce(Range) -> Range, + ) -> Self { + Self { code: self.code.clone(), range: map(self.range.clone()) } + } +} +impl Debug for SourceRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CodeRange({} {:?})", self.code, self.range) + } +} +impl Display for SourceRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { code, range } = self; + let (sl, sc) = pos2lc(code.source.as_str(), range.start); + let (el, ec) = pos2lc(code.source.as_str(), range.end); + write!(f, "{code} {sl}:{sc}")?; + if el == sl { + if sc + 1 == ec { Ok(()) } else { write!(f, "..{ec}") } + } else { + write!(f, "..{el}:{ec}") + } + } +} + +/// Information about a code generator attached to the generated code +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct CodeGenInfo { + /// formatted like a Rust namespace + pub generator: &'static str, + /// Unformatted user message with relevant circumstances and parameters + pub details: Arc, +} +impl CodeGenInfo { + /// A codegen marker with no user message and parameters + pub fn no_details(generator: &'static str) -> Self { + Self { generator, details: Arc::new(String::new()) } + } + /// A codegen marker with a user message or parameters + pub fn details(generator: &'static str, details: impl AsRef) -> Self { + Self { generator, details: Arc::new(details.as_ref().to_string()) } + } +} +impl Debug for CodeGenInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CodeGenInfo({self})") + } +} +impl Display for CodeGenInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "generated by {}", self.generator)?; + if !self.details.is_empty() { + write!(f, ", details: {}", self.details) + } else { + write!(f, ".") + } + } +} + +/// A location for error reporting. In the context of an error, identifies a +/// sequence of suspect characters or the reason the code was generated for +/// generated code. Meaningful within the context of a project. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum CodeLocation { + /// Character sequence + Source(SourceRange), + /// Generated construct + Gen(CodeGenInfo), +} + +impl Display for CodeLocation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Gen(info) => write!(f, "{info}"), + Self::Source(cr) => write!(f, "{cr}"), + } + } +} + +#[must_use] +fn pos2lc(s: &str, i: usize) -> (usize, usize) { + s.chars().take(i).fold((1, 1), |(line, col), char| { + if char == '\n' { (line + 1, 1) } else { (line, col + 1) } + }) +} diff --git a/src/name.rs b/src/name.rs new file mode 100644 index 0000000..7ff39c9 --- /dev/null +++ b/src/name.rs @@ -0,0 +1,334 @@ +use std::borrow::Borrow; +use std::fmt::{Debug, Display}; +use std::hash::Hash; +use std::num::NonZeroUsize; +use std::ops::Index; +use std::vec; + +use intern_all::{i, Tok}; +use itertools::Itertools; + +use crate::utils::boxed_iter::BoxedIter; + +/// A borrowed name fragment which can be empty. See [VPath] for the owned +/// variant. +pub struct PathSlice<'a>(pub &'a [Tok]); +impl<'a> PathSlice<'a> { + /// Convert to an owned name fragment + pub fn to_vpath(&self) -> VPath { VPath(self.0.to_vec()) } + /// Iterate over the segments + pub fn str_iter(&self) -> impl Iterator { + Box::new(self.0.iter().map(|s| s.as_str())) + } +} +impl<'a> Debug for PathSlice<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "VName({self})") + } +} +impl<'a> Display for PathSlice<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.str_iter().join("::")) + } +} +impl<'a> Borrow<[Tok]> for PathSlice<'a> { + fn borrow(&self) -> &[Tok] { self.0 } +} +impl<'a, T> Index for PathSlice<'a> +where [Tok]: Index +{ + type Output = <[Tok] as Index>::Output; + + fn index(&self, index: T) -> &Self::Output { &self.0[index] } +} + +/// A token path which may be empty. [VName] is the non-empty, +/// [PathSlice] is the borrowed version +#[derive(Clone, Default, Hash, PartialEq, Eq)] +pub struct VPath(pub Vec>); +impl VPath { + /// Collect segments into a vector + pub fn new(items: impl IntoIterator>) -> Self { + Self(items.into_iter().collect()) + } + /// Prepend some tokens to the path + pub fn prefix(self, items: impl IntoIterator>) -> Self { + Self(items.into_iter().chain(self.0).collect()) + } + /// Append some tokens to the path + pub fn suffix(self, items: impl IntoIterator>) -> Self { + Self(self.0.into_iter().chain(items).collect()) + } + /// Partition the string by `::` namespace separators + pub fn parse(s: &str) -> Self { + Self(if s.is_empty() { vec![] } else { s.split("::").map(i).collect() }) + } + /// Walk over the segments + pub fn str_iter(&self) -> impl Iterator { + Box::new(self.0.iter().map(|s| s.as_str())) + } + /// Try to convert into non-empty version + pub fn into_name(self) -> Result { VName::new(self.0) } + /// Add a token to the path. Since now we know that it can't be empty, turn it + /// into a name. + pub fn as_prefix_of(self, name: Tok) -> VName { + VName(self.into_iter().chain([name]).collect()) + } + /// Add a token to the beginning of the. Since now we know that it can't be + /// empty, turn it into a name. + pub fn as_suffix_of(self, name: Tok) -> VName { + VName([name].into_iter().chain(self).collect()) + } +} +impl Debug for VPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "VName({self})") + } +} +impl Display for VPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.str_iter().join("::")) + } +} +impl FromIterator> for VPath { + fn from_iter>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} +impl IntoIterator for VPath { + type Item = Tok; + type IntoIter = vec::IntoIter; + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } +} +impl Borrow<[Tok]> for VPath { + fn borrow(&self) -> &[Tok] { self.0.borrow() } +} + +impl Index for VPath +where Vec>: Index +{ + type Output = > as Index>::Output; + + fn index(&self, index: T) -> &Self::Output { &self.0[index] } +} + +/// A mutable representation of a namespaced identifier of at least one segment. +/// +/// These names may be relative or otherwise partially processed. +/// +/// See also [Sym] for the immutable representation, and [VPath] for possibly +/// empty values +#[derive(Clone, Hash, PartialEq, Eq)] +pub struct VName(Vec>); +impl VName { + /// Assert that the sequence isn't empty and wrap it in [VName] to represent + /// this invariant + pub fn new( + items: impl IntoIterator>, + ) -> Result { + let data: Vec<_> = items.into_iter().collect(); + if data.is_empty() { Err(EmptyNameError) } else { Ok(Self(data)) } + } + /// Unwrap the enclosed vector + pub fn into_vec(self) -> Vec> { self.0 } + /// Get a reference to the enclosed vector + pub fn vec(&self) -> &Vec> { &self.0 } + /// Mutable access to the underlying vector. To ensure correct results, this + /// must never be empty. + pub fn vec_mut(&mut self) -> &mut Vec> { &mut self.0 } + /// Intern the name and return a [Sym] + pub fn to_sym(&self) -> Sym { Sym(i(&self.0)) } + /// like Slice's split_first, but non-optional and tokens are cheap to clone + pub fn split_first(&self) -> (Tok, &[Tok]) { + let (h, t) = self.0.split_first().expect("VName can never be empty"); + (h.clone(), t) + } + /// like Slice's split_last, but non-optional and tokens are cheap to clone + pub fn split_last(&self) -> (Tok, &[Tok]) { + let (f, b) = self.0.split_last().expect("VName can never be empty"); + (f.clone(), b) + } + /// If this name has only one segment, return it + pub fn as_root(&self) -> Option> { + self.0.iter().exactly_one().ok().cloned() + } + /// Prepend the segments to this name + #[must_use = "This is a pure function"] + pub fn prefix(self, items: impl IntoIterator>) -> Self { + Self(items.into_iter().chain(self.0).collect()) + } + /// Append the segments to this name + #[must_use = "This is a pure function"] + pub fn suffix(self, items: impl IntoIterator>) -> Self { + Self(self.0.into_iter().chain(items).collect()) + } + /// Read a `::` separated namespaced name + pub fn parse(s: &str) -> Result { + Self::new(VPath::parse(s)) + } + /// Read a name from a string literal which can be known not to be empty + pub fn literal(s: &'static str) -> Self { + Self::parse(s).expect("name literal should not be empty") + } + /// Find the longest shared prefix of this name and another sequence + pub fn coprefix(&self, other: &[Tok]) -> &[Tok] { + &self.0 + [0..self.0.iter().zip(other.iter()).take_while(|(l, r)| l == r).count()] + } + /// Obtain an iterator over the segments of the name + pub fn iter(&self) -> impl Iterator> + '_ { + self.0.iter().cloned() + } + + /// Convert to [PathSlice] + pub fn as_path_slice(&self) -> PathSlice { PathSlice(&self[..]) } +} +impl Debug for VName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "VName({self})") + } +} +impl Display for VName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.str_iter().join("::")) + } +} +impl IntoIterator for VName { + type Item = Tok; + type IntoIter = vec::IntoIter; + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } +} +impl Index for VName +where Vec>: Index +{ + type Output = > as Index>::Output; + + fn index(&self, index: T) -> &Self::Output { &self.0[index] } +} +impl Borrow<[Tok]> for VName { + fn borrow(&self) -> &[Tok] { self.0.borrow() } +} + +/// Error produced when a non-empty name [VName] or [Sym] is constructed with an +/// empty sequence +#[derive(Debug, Copy, Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct EmptyNameError; +impl TryFrom<&[Tok]> for VName { + type Error = EmptyNameError; + fn try_from(value: &[Tok]) -> Result { + Self::new(value.iter().cloned()) + } +} + +/// An interned representation of a namespaced identifier. +/// +/// These names are always absolute. +/// +/// See also [VName] +#[derive(Clone, Hash, PartialEq, Eq)] +pub struct Sym(Tok>>); +impl Sym { + /// Assert that the sequence isn't empty, intern it and wrap it in a [Sym] to + /// represent this invariant + pub fn new( + v: impl IntoIterator>, + ) -> Result { + let items = v.into_iter().collect::>(); + Self::from_tok(i(&items)) + } + + /// Read a `::` separated namespaced name. + pub fn parse(s: &str) -> Result { + Ok(Sym(i(&VName::parse(s)?.into_vec()))) + } + + /// Parse a string and panic if it's not empty + pub fn literal(s: &'static str) -> Self { + Self::parse(s).expect("name literal should not be empty") + } + + /// Assert that a token isn't empty, and wrap it in a [Sym] + pub fn from_tok(t: Tok>>) -> Result { + if t.is_empty() { Err(EmptyNameError) } else { Ok(Self(t)) } + } + /// Grab the interner token + pub fn tok(&self) -> Tok>> { self.0.clone() } + /// Get a number unique to this name suitable for arbitrary ordering. + pub fn id(&self) -> NonZeroUsize { self.0.id() } + /// Get an iterator over the tokens in this name + pub fn iter(&self) -> impl Iterator> + '_ { + self.0.iter().cloned() + } + + /// Like Slice's split_last, except this slice is never empty + pub fn split_last(&self) -> (Tok, PathSlice) { + let (foot, torso) = self.0.split_last().expect("Sym never empty"); + (foot.clone(), PathSlice(torso)) + } + + /// Like Slice's split_first, except this slice is never empty + pub fn split_first(&self) -> (Tok, PathSlice) { + let (head, tail) = self.0.split_first().expect("Sym never empty"); + (head.clone(), PathSlice(tail)) + } + + /// Extern the sym for editing + pub fn to_vname(&self) -> VName { VName(self[..].to_vec()) } + + /// Convert to [PathSlice] + pub fn as_path_slice(&self) -> PathSlice { PathSlice(&self[..]) } +} +impl Debug for Sym { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Sym{self})") + } +} +impl Display for Sym { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.str_iter().join("::")) + } +} +impl Index for Sym +where Vec>: Index +{ + type Output = > as Index>::Output; + + fn index(&self, index: T) -> &Self::Output { &(&*self.0)[index] } +} +impl Borrow<[Tok]> for Sym { + fn borrow(&self) -> &[Tok] { self.0.borrow() } +} + +/// An abstraction over tokenized vs non-tokenized names so that they can be +/// handled together in datastructures. The names can never be empty +#[allow(clippy::len_without_is_empty)] // never empty +pub trait NameLike: 'static + Clone + Eq + Hash + Debug + Display { + /// Fully resolve the name for printing + #[must_use] + fn to_strv(&self) -> Vec { + self.str_iter().map(str::to_owned).collect() + } + /// Format the name as an approximate filename + fn as_src_path(&self) -> String { + format!("{}.orc", self.str_iter().join("/")) + } + /// Return the number of segments in the name + fn len(&self) -> NonZeroUsize { + NonZeroUsize::try_from(self.str_iter().count()) + .expect("NameLike never empty") + } + /// Fully resolve the name for printing + fn str_iter(&self) -> BoxedIter<'_, &str>; +} + +impl NameLike for Sym { + fn str_iter(&self) -> BoxedIter<'_, &str> { + Box::new(self.0.iter().map(|s| s.as_str())) + } +} + +impl NameLike for VName { + fn str_iter(&self) -> BoxedIter<'_, &str> { + Box::new(self.0.iter().map(|s| s.as_str())) + } +} diff --git a/src/parse/context.rs b/src/parse/context.rs index 8a7029f..76001cb 100644 --- a/src/parse/context.rs +++ b/src/parse/context.rs @@ -1,176 +1,127 @@ +//! Definition and implementations of the parsing context, which is used + use std::ops::Range; use std::sync::Arc; -use super::stream::Stream; -use crate::error::ProjectResult; -use crate::foreign::Atom; -use crate::interner::Interner; -use crate::sourcefile::FileEntryKind; -use crate::{Location, VName}; +use super::lex_plugin::LexerPlugin; +use super::parse_plugin::ParseLinePlugin; +use crate::location::{SourceCode, SourceRange}; +use crate::name::VPath; +use crate::utils::boxed_iter::{box_empty, BoxedIter}; +use crate::utils::sequence::Sequence; /// Trait enclosing all context features /// -/// Hiding type parameters in associated types allows for simpler -/// parser definitions -pub trait Context { - /// Get the path to the current file +/// The main implementation is [ParsingContext] +pub trait ParseCtx { + /// Get an object describing the file this source code comes from #[must_use] - fn file(&self) -> Arc; - /// Get a reference to the interner - #[must_use] - fn interner(&self) -> &Interner; - /// Get a reference to the full source text for position math and to build - /// [Location]s. - #[must_use] - fn source(&self) -> Arc; + fn code_info(&self) -> SourceCode; /// Get the list of all lexer plugins #[must_use] - fn lexers(&self) -> &[&dyn LexerPlugin]; + fn lexers(&self) -> BoxedIter<'_, &dyn LexerPlugin>; /// Get the list of all parser plugins #[must_use] - fn line_parsers(&self) -> &[&dyn LineParser]; + fn line_parsers(&self) -> BoxedIter<'_, &dyn ParseLinePlugin>; /// Find our position in the text given the text we've yet to parse #[must_use] fn pos(&self, tail: &str) -> usize { self.source().len() - tail.len() } /// Generate a location given the length of a token and the unparsed text /// after it. See also [Context::range_loc] if the maths gets complex. #[must_use] - fn location(&self, len: usize, tail: &str) -> Location { - match self.pos(tail).checked_sub(len) { - Some(start) => self.range_loc(start..self.pos(tail)), + fn range(&self, len: usize, tl: &str) -> Range { + match self.pos(tl).checked_sub(len) { + Some(start) => start..self.pos(tl), None => { - let tl = tail.len(); - panic!("len={len} greater than tail.len()={tl}; tail={tail:?}") + panic!("len={len} greater than tail.len()={}; tail={tl:?}", tl.len()) }, } } - /// Generate a location given a range in the source file. The location can be - /// computed with [Context::pos]. See also [Context::location]. + /// Create a contextful location for error reporting #[must_use] - fn range_loc(&self, range: Range) -> Location { - Location::Range { file: self.file(), range, source: self.source() } + fn code_range(&self, len: usize, tl: &str) -> SourceRange { + self.range_loc(&self.range(len, tl)) } + /// Create a contentful location from a range directly. + #[must_use] + fn range_loc(&self, range: &Range) -> SourceRange { + SourceRange { code: self.code_info(), range: range.clone() } + } + /// Get a reference to the full source text. This should not be used for + /// position math. + #[must_use] + fn source(&self) -> Arc { self.code_info().source.clone() } } -impl Context for &C { - fn file(&self) -> Arc { (*self).file() } - fn interner(&self) -> &Interner { (*self).interner() } - fn lexers(&self) -> &[&dyn LexerPlugin] { (*self).lexers() } - fn line_parsers(&self) -> &[&dyn LineParser] { (*self).line_parsers() } - fn location(&self, len: usize, tail: &str) -> Location { - (*self).location(len, tail) +impl<'a, C: ParseCtx + 'a + ?Sized> ParseCtx for &'a C { + fn lexers(&self) -> BoxedIter<'_, &dyn LexerPlugin> { (*self).lexers() } + fn line_parsers(&self) -> BoxedIter<'_, &dyn ParseLinePlugin> { + (*self).line_parsers() } fn pos(&self, tail: &str) -> usize { (*self).pos(tail) } - fn range_loc(&self, range: Range) -> Location { - (*self).range_loc(range) - } + fn code_info(&self) -> SourceCode { (*self).code_info() } fn source(&self) -> Arc { (*self).source() } -} - -/// Return value of a lexer plugin; the parsed data and the remaining string -pub type LexerPluginOut<'a> = Option>; -/// Return value of a line parser; the meaningful lines derived from this parser -pub type LineParserOut = Option>>; - -/// A plugin callback that reads a custom lexeme. -pub trait LexerPlugin: - for<'a> Fn(&'a str, &dyn Context) -> LexerPluginOut<'a> + Sync + Send -{ -} -impl LexerPlugin for F where - F: for<'a> Fn(&'a str, &dyn Context) -> LexerPluginOut<'a> + Sync + Send -{ -} - -/// A plugin callback that parses a custom file entry -pub trait LineParser: - for<'a> Fn(Stream<'_>, &'a (dyn Context + 'a)) -> LineParserOut - + Sync - + Send -{ -} -impl LineParser for F where - F: for<'a> Fn(Stream<'_>, &'a (dyn Context + 'a)) -> LineParserOut - + Sync - + Send -{ + fn range(&self, l: usize, t: &str) -> Range { (*self).range(l, t) } } /// Struct implementing context -/// -/// Hiding type parameters in associated types allows for simpler -/// parser definitions -pub struct ParsingContext<'a> { - interner: &'a Interner, - file_path: Arc, - source: Arc, - lexers: &'a [&'a dyn LexerPlugin], - line_parsers: &'a [&'a dyn LineParser], +#[derive(Clone)] +pub struct ParseCtxImpl<'a> { + /// File to be parsed; where it belongs in the tree and its text + pub code: SourceCode, + /// Lexer plugins for parsing custom literals + pub lexers: Sequence<'a, &'a (dyn LexerPlugin + 'a)>, + /// Parser plugins for parsing custom line structures + pub line_parsers: Sequence<'a, &'a dyn ParseLinePlugin>, } - -impl<'a> ParsingContext<'a> { - /// Create a new parsing context - pub fn new( - interner: &'a Interner, - file_path: Arc, - source: Arc, - lexers: &'a [&'a dyn LexerPlugin], - line_parsers: &'a [&'a dyn LineParser], - ) -> Self { - Self { interner, file_path, source, lexers, line_parsers } +impl<'a> ParseCtx for ParseCtxImpl<'a> { + // Rust doesn't realize that this lifetime is covariant + #[allow(clippy::map_identity)] + fn lexers(&self) -> BoxedIter<'_, &dyn LexerPlugin> { + Box::new(self.lexers.iter().map(|r| r)) } -} - -impl<'a> Clone for ParsingContext<'a> { - fn clone(&self) -> Self { - Self { - interner: self.interner, - file_path: self.file_path.clone(), - source: self.source.clone(), - lexers: self.lexers, - line_parsers: self.line_parsers, - } + #[allow(clippy::map_identity)] + fn line_parsers(&self) -> BoxedIter<'_, &dyn ParseLinePlugin> { + Box::new(self.line_parsers.iter().map(|r| r)) } + fn code_info(&self) -> SourceCode { self.code.clone() } } -impl Context for ParsingContext<'_> { - fn interner(&self) -> &Interner { self.interner } - fn file(&self) -> Arc { self.file_path.clone() } - fn source(&self) -> Arc { self.source.clone() } - fn lexers(&self) -> &[&dyn LexerPlugin] { self.lexers } - fn line_parsers(&self) -> &[&dyn LineParser] { self.line_parsers } -} - -pub struct MockContext<'a>(pub &'a Interner); -impl<'a> Context for MockContext<'a> { - // these are doing something - fn interner(&self) -> &Interner { self.0 } +/// Context instance for testing +pub struct MockContext; +impl ParseCtx for MockContext { fn pos(&self, tail: &str) -> usize { usize::MAX / 2 - tail.len() } // these are expendable - fn file(&self) -> Arc { Arc::new(Vec::new()) } - fn lexers(&self) -> &[&dyn LexerPlugin] { &[] } - fn line_parsers(&self) -> &[&dyn LineParser] { &[] } - fn location(&self, _: usize, _: &str) -> Location { Location::Unknown } - fn range_loc(&self, _: Range) -> Location { Location::Unknown } - fn source(&self) -> Arc { Arc::new(String::new()) } + fn code_info(&self) -> SourceCode { + SourceCode { + path: Arc::new(VPath(vec![])), + source: Arc::new(String::new()), + } + } + fn lexers(&self) -> BoxedIter<'_, &dyn LexerPlugin> { box_empty() } + fn line_parsers(&self) -> BoxedIter<'_, &dyn ParseLinePlugin> { box_empty() } } -pub struct FlatLocContext<'a, C: Context + ?Sized> { +/// Context that assigns the same location to every subset of the source code. +/// Its main use case is to process source code that was dynamically generated +/// in response to some user code. +pub struct FlatLocContext<'a, C: ParseCtx + ?Sized> { sub: &'a C, - location: &'a Location, + range: &'a SourceRange, } -impl<'a, C: Context + ?Sized> FlatLocContext<'a, C> { - pub fn new(sub: &'a C, location: &'a Location) -> Self { - Self { sub, location } +impl<'a, C: ParseCtx + ?Sized> FlatLocContext<'a, C> { + /// Create a new context that will use the same provided range for every + /// parsed token + pub fn new(sub: &'a C, range: &'a SourceRange) -> Self { Self { sub, range } } +} +impl<'a, C: ParseCtx + ?Sized> ParseCtx for FlatLocContext<'a, C> { + fn pos(&self, _: &str) -> usize { 0 } + fn lexers(&self) -> BoxedIter<'_, &dyn LexerPlugin> { self.sub.lexers() } + fn line_parsers(&self) -> BoxedIter<'_, &dyn ParseLinePlugin> { + self.sub.line_parsers() + } + fn code_info(&self) -> SourceCode { self.range.code.clone() } + fn range(&self, _: usize, _: &str) -> Range { + self.range.range.clone() } } -impl<'a, C: Context + ?Sized> Context for FlatLocContext<'a, C> { - fn interner(&self) -> &Interner { self.sub.interner() } - fn pos(&self, _: &str) -> usize { 0 } - fn file(&self) -> Arc { self.sub.file() } - fn lexers(&self) -> &[&dyn LexerPlugin] { self.sub.lexers() } - fn line_parsers(&self) -> &[&dyn LineParser] { self.sub.line_parsers() } - fn source(&self) -> Arc { self.sub.source() } - fn location(&self, _: usize, _: &str) -> Location { self.location.clone() } - fn range_loc(&self, _: Range) -> Location { self.location.clone() } -} diff --git a/src/parse/errors.rs b/src/parse/errors.rs index 0918681..bdea4a3 100644 --- a/src/parse/errors.rs +++ b/src/parse/errors.rs @@ -1,100 +1,96 @@ //! Errors produced by the parser. Plugins are encouraged to reuse these where //! applicable. -use std::rc::Rc; - +use intern_all::Tok; use itertools::Itertools; -use super::{Entry, Lexeme, Stream}; -use crate::ast::PType; -use crate::error::{ProjectError, ProjectResult}; -use crate::{Location, Tok}; +use super::context::ParseCtx; +use super::frag::Frag; +use super::lexer::{Entry, Lexeme}; +use crate::error::{ProjectError, ProjectErrorObj, ProjectResult}; +use crate::location::{CodeLocation, SourceRange}; +use crate::parse::parsed::PType; -/// A line does not begin with an identifying keyword -#[derive(Debug)] -pub struct LineNeedsPrefix { - /// Erroneous line starter - pub entry: Entry, -} -impl ProjectError for LineNeedsPrefix { - fn description(&self) -> &str { "This linetype requires a prefix" } - fn one_position(&self) -> Location { self.entry.location() } - fn message(&self) -> String { - format!("{} cannot appear at the beginning of a line", self.entry) +/// Parse error information without a location. Location data is added by the +/// parser. +pub trait ParseErrorKind: Sized + Send + Sync + 'static { + /// A general description of the error condition + const DESCRIPTION: &'static str; + /// A specific description of the error with concrete text sections + fn message(&self) -> String { Self::DESCRIPTION.to_string() } + /// Convert this error to a type-erased [ProjectError] to be handled together + /// with other Orchid errors. + fn pack(self, range: SourceRange) -> ProjectErrorObj { + ParseError { kind: self, range }.pack() } } -/// The line ends abruptly -#[derive(Debug)] -pub struct UnexpectedEOL { - /// Last entry before EOL - pub entry: Entry, +struct ParseError { + pub range: SourceRange, + pub kind: T, } -impl ProjectError for UnexpectedEOL { - fn description(&self) -> &str { "The line ended abruptly" } - fn one_position(&self) -> Location { self.entry.location() } +impl ProjectError for ParseError { + const DESCRIPTION: &'static str = T::DESCRIPTION; + fn one_position(&self) -> CodeLocation { + CodeLocation::Source(self.range.clone()) + } + fn message(&self) -> String { self.kind.message() } +} + +/// A line does not begin with an identifying keyword. Raised on the first token +pub(super) struct LineNeedsPrefix(pub Lexeme); +impl ParseErrorKind for LineNeedsPrefix { + const DESCRIPTION: &'static str = "This linetype requires a prefix"; fn message(&self) -> String { - "The line ends unexpectedly here. In Orchid, all line breaks outside \ - parentheses start a new declaration" + format!("{} cannot appear at the beginning of a line", self.0) + } +} + +/// The line ends abruptly. Raised on the last token +pub(super) struct UnexpectedEOL(pub Lexeme); +impl ParseErrorKind for UnexpectedEOL { + const DESCRIPTION: &'static str = "The line ended abruptly"; + fn message(&self) -> String { + "In Orchid, all line breaks outside parentheses start a new declaration" .to_string() } } -/// The line should have ended -pub struct ExpectedEOL { - /// Location of the last valid or first excessive token - pub location: Location, -} -impl ProjectError for ExpectedEOL { - fn description(&self) -> &str { "Expected the end of the line" } - fn one_position(&self) -> Location { self.location.clone() } +/// The line should have ended. Raised on last valid or first excess token +pub(super) struct ExpectedEOL; +impl ParseErrorKind for ExpectedEOL { + const DESCRIPTION: &'static str = "Expected the end of the line"; } -/// A name was expected -#[derive(Debug)] -pub struct ExpectedName { - /// Non-name entry - pub entry: Entry, +/// A name was expected. +pub(super) struct ExpectedName(pub Lexeme); +impl ParseErrorKind for ExpectedName { + const DESCRIPTION: &'static str = "A name was expected"; + fn message(&self) -> String { format!("Expected a name, found {}", self.0) } } -impl ExpectedName { - /// If the entry is a name, return its text. If it's not, produce this error. - pub fn expect(entry: &Entry) -> Result, Rc> { - match &entry.lexeme { - Lexeme::Name(n) => Ok(n.clone()), - _ => Err(Self { entry: entry.clone() }.rc()), - } - } -} -impl ProjectError for ExpectedName { - fn description(&self) -> &str { "A name was expected" } - fn one_position(&self) -> Location { self.entry.location() } - fn message(&self) -> String { - format!("Expected a name, found {}", self.entry) + +/// Unwrap a name or operator. +pub(super) fn expect_name( + Entry { lexeme, range }: &Entry, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult> { + match lexeme { + Lexeme::Name(n) => Ok(n.clone()), + lex => Err(ExpectedName(lex.clone()).pack(ctx.range_loc(range))), } } /// A specific lexeme was expected -#[derive()] -pub struct Expected { +pub(super) struct Expected { /// The lexemes that would have been acceptable pub expected: Vec, /// Whether a name would also have been acceptable (multiname) pub or_name: bool, /// What was actually found - pub found: Entry, + pub found: Lexeme, } -impl Expected { - /// Assert that the entry contains exactly the specified lexeme - pub fn expect(l: Lexeme, e: &Entry) -> Result<(), Rc> { - if e.lexeme.strict_eq(&l) { - return Ok(()); - } - Err(Self { expected: vec![l], or_name: false, found: e.clone() }.rc()) - } -} -impl ProjectError for Expected { - fn description(&self) -> &str { "A concrete token was expected" } - fn one_position(&self) -> Location { self.found.location() } +impl ParseErrorKind for Expected { + const DESCRIPTION: &'static str = "A concrete token was expected"; fn message(&self) -> String { let list = match &self.expected[..] { &[] => return "Unsatisfiable expectation".to_string(), @@ -108,167 +104,152 @@ impl ProjectError for Expected { format!("Expected {list}{or_name} but found {}", self.found) } } +/// Assert that the entry contains exactly the specified lexeme +pub(super) fn expect( + l: Lexeme, + e: &Entry, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult<()> { + if e.lexeme.strict_eq(&l) { + return Ok(()); + } + let found = e.lexeme.clone(); + let kind = Expected { expected: vec![l], or_name: false, found }; + Err(kind.pack(ctx.range_loc(&e.range))) +} /// A token reserved for future use was found in the code -pub struct ReservedToken { - /// The offending token - pub entry: Entry, -} -impl ProjectError for ReservedToken { - fn description(&self) -> &str { "Syntax reserved for future use" } - fn one_position(&self) -> Location { self.entry.location() } - fn message(&self) -> String { format!("{} is a reserved token", self.entry) } +pub(super) struct ReservedToken(pub Lexeme); +impl ParseErrorKind for ReservedToken { + const DESCRIPTION: &'static str = "Syntax reserved for future use"; + fn message(&self) -> String { format!("{} is a reserved token", self.0) } } /// A token was found where it doesn't belong -pub struct BadTokenInRegion { +pub(super) struct BadTokenInRegion { /// What was found - pub entry: Entry, + pub lexeme: Lexeme, /// Human-readable name of the region where it should not appear pub region: &'static str, } -impl ProjectError for BadTokenInRegion { - fn description(&self) -> &str { "An unexpected token was found" } - fn one_position(&self) -> Location { self.entry.location() } +impl ParseErrorKind for BadTokenInRegion { + const DESCRIPTION: &'static str = "An unexpected token was found"; fn message(&self) -> String { - format!("{} cannot appear in {}", self.entry, self.region) + format!("{} cannot appear in {}", self.lexeme, self.region) } } -/// A specific lexeme was searched but not found -pub struct NotFound { - /// Human-readable description of what was searched - pub expected: &'static str, - /// Area covered by the search - pub location: Location, -} -impl ProjectError for NotFound { - fn description(&self) -> &str { "A specific lexeme was expected" } - fn one_position(&self) -> Location { self.location.clone() } - fn message(&self) -> String { format!("{} was expected", self.expected) } +/// Some construct was searched but not found. +pub(super) struct NotFound(pub &'static str); +impl ParseErrorKind for NotFound { + const DESCRIPTION: &'static str = "A specific lexeme was expected"; + fn message(&self) -> String { format!("{} was expected", self.0) } } /// :: found on its own somewhere other than a general export -pub struct LeadingNS(pub Location); -impl ProjectError for LeadingNS { - fn description(&self) -> &str { ":: can only follow a name token" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct LeadingNS; +impl ParseErrorKind for LeadingNS { + const DESCRIPTION: &'static str = ":: can only follow a name token"; } /// Parens don't pair up -pub struct MisalignedParen(pub Entry); -impl ProjectError for MisalignedParen { - fn description(&self) -> &str { "(), [] and {} must always pair up" } - fn one_position(&self) -> Location { self.0.location() } +pub(super) struct MisalignedParen(pub Lexeme); +impl ParseErrorKind for MisalignedParen { + const DESCRIPTION: &'static str = "(), [] and {} must always pair up"; fn message(&self) -> String { format!("This {} has no pair", self.0) } } /// Export line contains a complex name -pub struct NamespacedExport(pub Location); -impl ProjectError for NamespacedExport { - fn description(&self) -> &str { "Only local names may be exported" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct NamespacedExport; +impl ParseErrorKind for NamespacedExport { + const DESCRIPTION: &'static str = "Only local names may be exported"; } /// Export line contains * -pub struct GlobExport(pub Location); -impl ProjectError for GlobExport { - fn description(&self) -> &str { "Globstars are not allowed in exports" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct GlobExport; +impl ParseErrorKind for GlobExport { + const DESCRIPTION: &'static str = "Globstars are not allowed in exports"; } /// String literal never ends -pub struct NoStringEnd(pub Location); -impl ProjectError for NoStringEnd { - fn description(&self) -> &str { "A string literal was not closed with `\"`" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct NoStringEnd; +impl ParseErrorKind for NoStringEnd { + const DESCRIPTION: &'static str = "A string literal was not closed with `\"`"; } /// Comment never ends -pub struct NoCommentEnd(pub Location); -impl ProjectError for NoCommentEnd { - fn description(&self) -> &str { "a comment was not closed with `]--`" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct NoCommentEnd; +impl ParseErrorKind for NoCommentEnd { + const DESCRIPTION: &'static str = "a comment was not closed with `]--`"; } /// A placeholder's priority is a floating point number -pub struct FloatPlacehPrio(pub Location); -impl ProjectError for FloatPlacehPrio { - fn description(&self) -> &str { - "a placeholder priority has a decimal point or a negative exponent" - } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct FloatPlacehPrio; +impl ParseErrorKind for FloatPlacehPrio { + const DESCRIPTION: &'static str = + "a placeholder priority has a decimal point or a negative exponent"; } /// A number literal decodes to NaN -pub struct NaNLiteral(pub Location); -impl ProjectError for NaNLiteral { - fn description(&self) -> &str { "float literal decoded to NaN" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct NaNLiteral; +impl ParseErrorKind for NaNLiteral { + const DESCRIPTION: &'static str = "float literal decoded to NaN"; } /// A sequence of digits in a number literal overflows [usize]. -pub struct LiteralOverflow(pub Location); -impl ProjectError for LiteralOverflow { - fn description(&self) -> &str { - "number literal described number greater than usize::MAX" - } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct LiteralOverflow; +impl ParseErrorKind for LiteralOverflow { + const DESCRIPTION: &'static str = + "number literal described number greater than usize::MAX"; } /// A digit was expected but something else was found -pub struct ExpectedDigit(pub Location); -impl ProjectError for ExpectedDigit { - fn description(&self) -> &str { "expected a digit" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct ExpectedDigit; +impl ParseErrorKind for ExpectedDigit { + const DESCRIPTION: &'static str = "expected a digit"; } /// A unicode escape sequence contains something other than a hex digit -pub struct NotHex(pub Location); -impl ProjectError for NotHex { - fn description(&self) -> &str { "Expected a hex digit" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct NotHex; +impl ParseErrorKind for NotHex { + const DESCRIPTION: &'static str = "Expected a hex digit"; } /// A unicode escape sequence contains a number that isn't a unicode code point. -pub struct BadCodePoint(pub Location); -impl ProjectError for BadCodePoint { - fn description(&self) -> &str { - "\\uXXXX escape sequence does not describe valid code point" - } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct BadCodePoint; +impl ParseErrorKind for BadCodePoint { + const DESCRIPTION: &'static str = + "\\uXXXX escape sequence does not describe valid code point"; } /// An unrecognized escape sequence occurred in a string. -pub struct BadEscapeSequence(pub Location); -impl ProjectError for BadEscapeSequence { - fn description(&self) -> &str { "Unrecognized escape sequence" } - fn one_position(&self) -> Location { self.0.clone() } +pub(super) struct BadEscapeSequence; +impl ParseErrorKind for BadEscapeSequence { + const DESCRIPTION: &'static str = "Unrecognized escape sequence"; } /// Expected a parenthesized block at the end of the line -pub struct ExpectedBlock(pub Location); -impl ExpectedBlock { - /// Remove two parentheses from the ends of the cursor - pub fn expect(tail: Stream, typ: PType) -> ProjectResult { - let (lp, tail) = tail.trim().pop()?; - Expected::expect(Lexeme::LP(typ), lp)?; - let (rp, tail) = tail.pop_back()?; - Expected::expect(Lexeme::RP(typ), rp)?; - Ok(tail.trim()) - } +pub(super) struct ExpectedBlock; +impl ParseErrorKind for ExpectedBlock { + const DESCRIPTION: &'static str = "Expected a parenthesized block"; } -impl ProjectError for ExpectedBlock { - fn description(&self) -> &str { "Expected a parenthesized block" } - fn one_position(&self) -> Location { self.0.clone() } +/// Remove two parentheses from the ends of the cursor +pub(super) fn expect_block<'a>( + tail: Frag<'a>, + typ: PType, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult> { + let (lp, tail) = tail.trim().pop(ctx)?; + expect(Lexeme::LP(typ), lp, ctx)?; + let (rp, tail) = tail.pop_back(ctx)?; + expect(Lexeme::RP(typ), rp, ctx)?; + Ok(tail.trim()) } /// A namespaced name was expected but a glob pattern or a branching multiname /// was found. -pub struct ExpectedSingleName(pub Location); -impl ProjectError for ExpectedSingleName { - fn one_position(&self) -> Location { self.0.clone() } - fn description(&self) -> &str { - "expected a single name, no wildcards, no branches" - } +pub(super) struct ExpectedSingleName; +impl ParseErrorKind for ExpectedSingleName { + const DESCRIPTION: &'static str = + "expected a single name, no wildcards, no branches"; } diff --git a/src/parse/facade.rs b/src/parse/facade.rs index 9d9b04a..f0e85bd 100644 --- a/src/parse/facade.rs +++ b/src/parse/facade.rs @@ -1,46 +1,38 @@ -use super::context::{Context, FlatLocContext}; +//! Entrypoints to the parser that combine lexing and parsing + +use super::context::{FlatLocContext, ParseCtx}; +use super::frag::Frag; use super::lexer::lex; -use super::sourcefile::{parse_exprv, parse_module_body, vec_to_single}; -use super::stream::Stream; -use super::{parse_line, split_lines}; -use crate::ast::Expr; +use super::sourcefile::parse_module_body; use crate::error::ProjectResult; -use crate::representations::sourcefile::FileEntry; -use crate::{Location, VName}; +use crate::location::SourceRange; +use crate::parse::parsed::SourceLine; +use crate::parse::sourcefile::{parse_line, split_lines}; /// Parse a file -pub fn parse_file(ctx: impl Context) -> ProjectResult> { - let tokens = lex(vec![], ctx.source().as_str(), &ctx)?; +pub fn parse_file(ctx: &impl ParseCtx) -> ProjectResult> { + let tokens = lex(vec![], ctx.source().as_str(), ctx, |_| false)?.tokens; if tokens.is_empty() { Ok(Vec::new()) } else { - parse_module_body(Stream::from_slice(&tokens), &ctx) + parse_module_body(Frag::from_slice(&tokens), ctx) } } -/// Parse a ready-made expression -pub fn parse_expr( - ctx: &impl Context, - text: &'static str, - location: Location, -) -> ProjectResult> { - let ctx = FlatLocContext::new(ctx, &location); - let tokens = lex(vec![], text, &ctx)?; - let items = parse_exprv(Stream::from_slice(&tokens), None, &ctx)?.0; - vec_to_single(tokens.first().expect("source must not be empty"), items) -} - -/// Parse a ready-made line +/// Parse a statically defined line sequence +/// +/// # Panics +/// +/// On any parse error, which is why it only accepts a string literal pub fn parse_entries( - ctx: &(impl Context + ?Sized), + ctx: &dyn ParseCtx, text: &'static str, - location: Location, -) -> ProjectResult> { - let ctx = FlatLocContext::new(ctx, &location); - let tokens = lex(vec![], text, &ctx)?; - let entries = split_lines(Stream::from_slice(&tokens)) + range: SourceRange, +) -> Vec { + let ctx = FlatLocContext::new(ctx, &range); + let res = lex(vec![], text, &ctx, |_| false).expect("pre-specified source"); + split_lines(Frag::from_slice(&res.tokens), &ctx) .flat_map(|tokens| parse_line(tokens, &ctx).expect("pre-specified source")) - .map(|kind| kind.wrap(location.clone())) - .collect(); - Ok(entries) + .map(|kind| kind.wrap(range.clone())) + .collect() } diff --git a/src/parse/frag.rs b/src/parse/frag.rs new file mode 100644 index 0000000..45bb816 --- /dev/null +++ b/src/parse/frag.rs @@ -0,0 +1,151 @@ +//! The [Frag] is the main input datastructure of parsers. Beyond the slice of +//! tokens, it contains a fallback value that can be used for error reporting if +//! the fragment is empty. + +use std::ops::Range; + +use super::context::ParseCtx; +use super::errors::{ExpectedEOL, NotFound, ParseErrorKind, UnexpectedEOL}; +use super::lexer::{Entry, Lexeme}; +use crate::error::ProjectResult; + +/// Represents a slice which may or may not contain items, and a fallback entry +/// used for error reporting whenever the errant fragment is empty. +#[must_use = "fragment of code should not be discarded implicitly"] +#[derive(Clone, Copy)] +pub struct Frag<'a> { + /// Entry to place in errors if the fragment contains no tokens + pub fallback: &'a Entry, + /// Tokens to parse + pub data: &'a [Entry], +} +impl<'a> Frag<'a> { + /// Create a new fragment + pub fn new(fallback: &'a Entry, data: &'a [Entry]) -> Self { + Self { fallback, data } + } + + /// Remove comments and line breaks from both ends of the text + pub fn trim(self) -> Self { + let Self { data, fallback } = self; + let front = data.iter().take_while(|e| e.is_filler()).count(); + let (_, right) = data.split_at(front); + let back = right.iter().rev().take_while(|e| e.is_filler()).count(); + let (data, _) = right.split_at(right.len() - back); + Self { fallback, data } + } + + /// Discard the first entry + pub fn step(self, ctx: &(impl ParseCtx + ?Sized)) -> ProjectResult { + let Self { data, fallback: Entry { lexeme, range } } = self; + match data.split_first() { + Some((fallback, data)) => Ok(Frag { data, fallback }), + None => Err(UnexpectedEOL(lexeme.clone()).pack(ctx.range_loc(range))), + } + } + + /// Get the first entry + pub fn pop( + self, + ctx: &(impl ParseCtx + ?Sized), + ) -> ProjectResult<(&'a Entry, Self)> { + Ok((self.get(0, ctx)?, self.step(ctx)?)) + } + + /// Retrieve an index from a slice or raise an [UnexpectedEOL]. + pub fn get( + self, + idx: usize, + ctx: &(impl ParseCtx + ?Sized), + ) -> ProjectResult<&'a Entry> { + self.data.get(idx).ok_or_else(|| { + let entry = self.data.last().unwrap_or(self.fallback).clone(); + UnexpectedEOL(entry.lexeme).pack(ctx.range_loc(&entry.range)) + }) + } + + /// Area covered by this fragment + #[must_use] + pub fn range(self) -> Range { + self.data.first().map_or_else( + || self.fallback.range.clone(), + |f| f.range.start..self.data.last().unwrap().range.end, + ) + } + + /// Find a given token, split the fragment there and read some value from the + /// separator. See also [fragment::find] + pub fn find_map( + self, + msg: &'static str, + ctx: &(impl ParseCtx + ?Sized), + mut f: impl FnMut(&'a Lexeme) -> Option, + ) -> ProjectResult<(Self, T, Self)> { + let Self { data, fallback } = self; + let (dot_idx, output) = skip_parenthesized(data.iter()) + .find_map(|(i, e)| f(&e.lexeme).map(|t| (i, t))) + .ok_or_else(|| NotFound(msg).pack(ctx.range_loc(&self.range())))?; + let (left, not_left) = data.split_at(dot_idx); + let (middle_ent, right) = not_left.split_first().unwrap(); + Ok((Self::new(fallback, left), output, Self::new(middle_ent, right))) + } + + /// Split the fragment at a token and return just the two sides. + /// See also [fragment::find_map]. + pub fn find( + self, + descr: &'static str, + ctx: &(impl ParseCtx + ?Sized), + mut f: impl FnMut(&Lexeme) -> bool, + ) -> ProjectResult<(Self, Self)> { + let (l, _, r) = self.find_map(descr, ctx, |l| Some(l).filter(|l| f(l)))?; + Ok((l, r)) + } + + /// Remove the last item from the fragment + pub fn pop_back( + self, + ctx: &(impl ParseCtx + ?Sized), + ) -> ProjectResult<(&'a Entry, Self)> { + let Self { data, fallback } = self; + let (last, data) = (data.split_last()).ok_or_else(|| { + UnexpectedEOL(fallback.lexeme.clone()) + .pack(ctx.range_loc(&fallback.range)) + })?; + Ok((last, Self { fallback, data })) + } + + /// # Panics + /// + /// If the slice is empty + pub fn from_slice(data: &'a [Entry]) -> Self { + let fallback = + (data.first()).expect("Empty slice cannot be converted into a parseable"); + Self { data, fallback } + } + + /// Assert that the fragment is empty. + pub fn expect_empty( + self, + ctx: &(impl ParseCtx + ?Sized), + ) -> ProjectResult<()> { + match self.data.first() { + Some(x) => Err(ExpectedEOL.pack(ctx.range_loc(&x.range))), + None => Ok(()), + } + } +} + +fn skip_parenthesized<'a>( + it: impl Iterator, +) -> impl Iterator { + let mut paren_lvl = 1; + it.enumerate().filter(move |(_, e)| { + match e.lexeme { + Lexeme::LP(_) => paren_lvl += 1, + Lexeme::RP(_) => paren_lvl -= 1, + _ => (), + } + paren_lvl <= 1 + }) +} diff --git a/src/parse/lex_plugin.rs b/src/parse/lex_plugin.rs new file mode 100644 index 0000000..051607e --- /dev/null +++ b/src/parse/lex_plugin.rs @@ -0,0 +1,50 @@ +//! Abstractions for dynamic extensions to the lexer to parse custom literals + +use super::context::ParseCtx; +use super::lexer::{lex, LexRes}; +use crate::error::ProjectResult; + +/// Data passed to the recursive sub-lexer +pub struct LexPluginRecur<'a, 'b> { + /// Text to tokenize + pub tail: &'a str, + /// Callback that will be called between lexemes on the leftover text. + /// When it returns true, the lexer exits and leaves the remaining text for + /// you. + pub exit: &'b dyn for<'c> Fn(&'c str) -> bool, +} + +/// Data and actions available to a lexer plugin +pub trait LexPluginReq<'a> { + /// Text to tokenize + fn tail(&self) -> &'a str; + /// [ParseCtx] instance for calculating locations and such + fn ctx(&self) -> &dyn ParseCtx; + /// Start a child lexer that calls back between lexemes and exits on your + /// command. You can combine this with custom atoms to create holes for + /// expressions in your literals like the template strings of most languages + /// other than Rust. + fn recurse(&self, req: LexPluginRecur<'a, '_>) -> ProjectResult>; +} + +/// External plugin that parses a literal into recognized Orchid lexemes, most +/// likely atoms. +pub trait LexerPlugin: Send + Sync { + /// Run the lexer + fn lex<'a>( + &self, + req: &'_ dyn LexPluginReq<'a>, + ) -> Option>>; +} + +pub(super) struct LexPlugReqImpl<'a, 'b, TCtx: ParseCtx> { + pub tail: &'a str, + pub ctx: &'b TCtx, +} +impl<'a, 'b, TCtx: ParseCtx> LexPluginReq<'a> for LexPlugReqImpl<'a, 'b, TCtx> { + fn tail(&self) -> &'a str { self.tail } + fn ctx(&self) -> &dyn ParseCtx { self.ctx } + fn recurse(&self, req: LexPluginRecur<'a, '_>) -> ProjectResult> { + lex(Vec::new(), req.tail, self.ctx, |s| (req.exit)(s)) + } +} diff --git a/src/parse/lexer.rs b/src/parse/lexer.rs index fbf9a87..b2cb066 100644 --- a/src/parse/lexer.rs +++ b/src/parse/lexer.rs @@ -1,32 +1,37 @@ +//! Convert source text into a sequence of tokens. Newlines and comments are +//! included, but spacing is converted into numerical ranges on the elements. +//! +//! Literals lose their syntax form here and are handled in an abstract +//! representation hence + use std::fmt::Display; use std::ops::Range; use std::sync::Arc; +use intern_all::{i, Tok}; use itertools::Itertools; use ordered_float::NotNan; -use super::context::Context; +use super::context::ParseCtx; use super::errors::{FloatPlacehPrio, NoCommentEnd}; +use super::lex_plugin::LexerPlugin; use super::numeric::{numstart, parse_num, print_nat16}; -use super::LexerPlugin; -use crate::ast::{PHClass, PType, Placeholder}; -use crate::error::{ProjectError, ProjectResult}; -use crate::foreign::Atom; -use crate::interner::Tok; -use crate::parse::numeric::{lex_numeric, numchar}; -use crate::parse::string::lex_string; -use crate::systems::stl::Numeric; -use crate::utils::pure_seq::next; -use crate::utils::unwrap_or; -use crate::{Location, VName}; +use super::string::StringLexer; +use crate::error::ProjectResult; +use crate::foreign::atom::AtomGenerator; +use crate::libs::std::number::Numeric; +use crate::parse::errors::ParseErrorKind; +use crate::parse::lex_plugin::LexPlugReqImpl; +use crate::parse::numeric::{numchar, NumericLexer}; +use crate::parse::parsed::{PHClass, PType, Placeholder}; /// A lexeme and the location where it was found #[derive(Clone, Debug)] pub struct Entry { /// the lexeme pub lexeme: Lexeme, - /// the location. Always a range - pub location: Location, + /// the range in bytes + pub range: Range, } impl Entry { /// Checks if the lexeme is a comment or line break @@ -35,25 +40,7 @@ impl Entry { matches!(self.lexeme, Lexeme::Comment(_) | Lexeme::BR) } - /// Get location - #[must_use] - pub fn location(&self) -> Location { self.location.clone() } - - /// Get range from location - #[must_use] - pub fn range(&self) -> Range { - self.location.range().expect("An Entry can only have a known location") - } - - /// Get file path from location - #[must_use] - pub fn file(&self) -> Arc { - self.location.file().expect("An Entry can only have a range location") - } - - fn new(location: Location, lexeme: Lexeme) -> Self { - Self { lexeme, location } - } + fn new(range: Range, lexeme: Lexeme) -> Self { Self { lexeme, range } } } impl Display for Entry { @@ -62,15 +49,11 @@ impl Display for Entry { } } -impl AsRef for Entry { - fn as_ref(&self) -> &Location { &self.location } -} - /// A unit of syntax #[derive(Clone, Debug)] pub enum Lexeme { /// Atoms parsed by plugins - Atom(Atom), + Atom(AtomGenerator), /// Keyword or name Name(Tok), /// Macro operator `=`number`=>` @@ -127,7 +110,8 @@ impl Lexeme { (Self::BS, Self::BS) => true, (Self::NS, Self::NS) | (Self::Type, Self::Type) => true, (Self::Walrus, Self::Walrus) => true, - (Self::Atom(a1), Self::Atom(a2)) => a1.0.strict_eq(&a2.0), + (Self::Atom(a1), Self::Atom(a2)) => + a1.run().0.parser_eq(a2.run().0.as_any_ref()), (Self::Comment(c1), Self::Comment(c2)) => c1 == c2, (Self::LP(p1), Self::LP(p2)) | (Self::RP(p1), Self::RP(p2)) => p1 == p2, (Self::Name(n1), Self::Name(n2)) => n1 == n2, @@ -137,6 +121,14 @@ impl Lexeme { } } +/// Data returned from the lexer +pub struct LexRes<'a> { + /// Leftover text. If the bail callback never returned true, this is empty + pub tail: &'a str, + /// Lexemes extracted from the text + pub tokens: Vec, +} + /// Neatly format source code #[allow(unused)] pub fn format(lexed: &[Entry]) -> String { lexed.iter().join(" ") } @@ -175,65 +167,70 @@ fn lit_table() -> impl IntoIterator { ] } -static BUILTIN_ATOMS: &[&dyn LexerPlugin] = &[&lex_string, &lex_numeric]; +static BUILTIN_ATOMS: &[&dyn LexerPlugin] = &[&NumericLexer, &StringLexer]; -pub fn lex( +/// Convert source code to a flat list of tokens. The bail callback will be +/// called between lexemes. When it returns true, the remaining text is +/// returned without processing. +pub fn lex<'a>( mut tokens: Vec, - mut data: &str, - ctx: &impl Context, -) -> ProjectResult> { + mut data: &'a str, + ctx: &'_ impl ParseCtx, + bail: impl Fn(&str) -> bool, +) -> ProjectResult> { let mut prev_len = data.len() + 1; 'tail: loop { + if bail(data) { + return Ok(LexRes { tokens, tail: data }); + } if prev_len == data.len() { panic!("got stuck at {data:?}, parsed {:?}", tokens.last().unwrap()); } prev_len = data.len(); data = data.trim_start_matches(|c: char| c.is_whitespace() && c != '\n'); - let (head, _) = match next(data.chars()) { - Some((h, t)) => (h, t.as_str()), - None => return Ok(tokens), + let mut chars = data.chars(); + let head = match chars.next() { + None => return Ok(LexRes { tokens, tail: data }), + Some(h) => h, }; - for lexer in ctx.lexers().iter().chain(BUILTIN_ATOMS.iter()) { - if let Some(res) = lexer(data, ctx) { - let (atom, tail) = res?; + let req = LexPlugReqImpl { tail: data, ctx }; + for lexer in ctx.lexers().chain(BUILTIN_ATOMS.iter().copied()) { + if let Some(res) = lexer.lex(&req) { + let LexRes { tail, tokens: mut new_tokens } = res?; if tail.len() == data.len() { panic!("lexer plugin consumed 0 characters") } - let loc = ctx.location(data.len() - tail.len(), tail); - tokens.push(Entry::new(loc, Lexeme::Atom(atom))); + tokens.append(&mut new_tokens); data = tail; continue 'tail; } } for (prefix, lexeme) in lit_table() { if let Some(tail) = data.strip_prefix(prefix) { - tokens - .push(Entry::new(ctx.location(prefix.len(), tail), lexeme.clone())); + tokens.push(Entry::new(ctx.range(prefix.len(), tail), lexeme.clone())); data = tail; continue 'tail; } } if let Some(tail) = data.strip_prefix(',') { - let lexeme = Lexeme::Name(ctx.interner().i(",")); - tokens.push(Entry::new(ctx.location(1, tail), lexeme)); + let lexeme = Lexeme::Name(i(",")); + tokens.push(Entry::new(ctx.range(1, tail), lexeme)); data = tail; continue 'tail; } if let Some(tail) = data.strip_prefix("--[") { let (note, tail) = (tail.split_once("]--")) - .ok_or_else(|| NoCommentEnd(ctx.location(tail.len(), "")).rc())?; + .ok_or_else(|| NoCommentEnd.pack(ctx.code_range(tail.len(), "")))?; let lexeme = Lexeme::Comment(Arc::new(note.to_string())); - let location = ctx.location(note.len() + 3, tail); - tokens.push(Entry::new(location, lexeme)); + tokens.push(Entry::new(ctx.range(note.len() + 3, tail), lexeme)); data = tail; continue 'tail; } if let Some(tail) = data.strip_prefix("--") { let (note, tail) = split_filter(tail, |c| c != '\n'); let lexeme = Lexeme::Comment(Arc::new(note.to_string())); - let location = ctx.location(note.len(), tail); - tokens.push(Entry::new(location, lexeme)); + tokens.push(Entry::new(ctx.range(note.len(), tail), lexeme)); data = tail; continue 'tail; } @@ -241,13 +238,10 @@ pub fn lex( if tail.chars().next().map_or(false, numstart) { let (num, post_num) = split_filter(tail, numchar); if let Some(tail) = post_num.strip_prefix("=>") { - let lexeme = Lexeme::Arrow( - parse_num(num) - .map_err(|e| e.into_proj(num.len(), post_num, ctx))? - .as_float(), - ); - let location = ctx.location(num.len() + 3, tail); - tokens.push(Entry::new(location, lexeme)); + let prio = parse_num(num) + .map_err(|e| e.into_proj(num.len(), post_num, ctx))?; + let lexeme = Lexeme::Arrow(prio.as_float()); + tokens.push(Entry::new(ctx.range(num.len() + 3, tail), lexeme)); data = tail; continue 'tail; } @@ -259,11 +253,9 @@ pub fn lex( tail.strip_prefix('_').map_or((false, tail), |t| (true, t)); let (name, tail) = split_filter(tail, namechar); if !name.is_empty() { - let name = ctx.interner().i(name); - let location = ctx.location(name.len() + 1, tail); let class = if nameonly { PHClass::Name } else { PHClass::Scalar }; - let lexeme = Lexeme::Placeh(Placeholder { name, class }); - tokens.push(Entry::new(location, lexeme)); + let lexeme = Lexeme::Placeh(Placeholder { name: i(name), class }); + tokens.push(Entry::new(ctx.range(name.len() + 1, tail), lexeme)); data = tail; continue 'tail; } @@ -281,20 +273,19 @@ pub fn lex( .map(|(num_str, tail)| { parse_num(num_str) .map_err(|e| e.into_proj(num_str.len(), tail, ctx)) - .and_then(|num| { - Ok(unwrap_or!(num => Numeric::Uint; { - let location = ctx.location(num_str.len(), tail); - return Err(FloatPlacehPrio(location).rc()) - })) + .and_then(|num| match num { + Numeric::Uint(usize) => Ok(usize), + Numeric::Float(_) => Err( + FloatPlacehPrio.pack(ctx.code_range(num_str.len(), tail)), + ), }) .map(|p| (p, num_str.len() + 1, tail)) }) .unwrap_or(Ok((0, 0, tail)))?; let byte_len = if nonzero { 4 } else { 3 } + priolen + name.len(); - let name = ctx.interner().i(name); let class = PHClass::Vec { nonzero, prio }; - let lexeme = Lexeme::Placeh(Placeholder { name, class }); - tokens.push(Entry::new(ctx.location(byte_len, tail), lexeme)); + let lexeme = Lexeme::Placeh(Placeholder { name: i(name), class }); + tokens.push(Entry::new(ctx.range(byte_len, tail), lexeme)); data = tail; continue 'tail; } @@ -303,8 +294,8 @@ pub fn lex( if namestart(head) { let (name, tail) = split_filter(data, namechar); if !name.is_empty() { - let lexeme = Lexeme::Name(ctx.interner().i(name)); - tokens.push(Entry::new(ctx.location(name.len(), tail), lexeme)); + let lexeme = Lexeme::Name(i(name)); + tokens.push(Entry::new(ctx.range(name.len(), tail), lexeme)); data = tail; continue 'tail; } @@ -312,8 +303,8 @@ pub fn lex( if opchar(head) { let (name, tail) = split_filter(data, opchar); if !name.is_empty() { - let lexeme = Lexeme::Name(ctx.interner().i(name)); - tokens.push(Entry::new(ctx.location(name.len(), tail), lexeme)); + let lexeme = Lexeme::Name(i(name)); + tokens.push(Entry::new(ctx.range(name.len(), tail), lexeme)); data = tail; continue 'tail; } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index ea0374f..040da49 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,29 +1,13 @@ -//! Types for interacting with the Orchid parser, and parts of the parser -//! plugins can use to match the language's behaviour on certain tasks -mod context; +//! Parser, and abstractions for interacting with it from language extensions +pub mod context; pub mod errors; -mod facade; -mod lexer; -mod multiname; -mod numeric; +pub mod facade; +pub mod frag; +pub mod lex_plugin; +pub mod lexer; +pub mod multiname; +pub mod numeric; +pub mod parse_plugin; +pub mod parsed; mod sourcefile; -mod stream; -mod string; - -pub use context::{ - Context, LexerPlugin, LexerPluginOut, LineParser, LineParserOut, - ParsingContext, -}; -pub use facade::{parse_entries, parse_expr, parse_file}; -pub use lexer::{namechar, namestart, opchar, split_filter, Entry, Lexeme}; -pub use multiname::parse_multiname; -pub use numeric::{ - lex_numeric, numchar, numstart, parse_num, print_nat16, NumError, - NumErrorKind, -}; -pub use sourcefile::{ - expr_slice_location, parse_const, parse_exprv, parse_line, parse_module, - parse_module_body, parse_rule, split_lines, vec_to_single, parse_nsname -}; -pub use stream::Stream; -pub use string::{lex_string, parse_string, StringError, StringErrorKind}; +pub mod string; diff --git a/src/parse/multiname.rs b/src/parse/multiname.rs index 5230131..22e9267 100644 --- a/src/parse/multiname.rs +++ b/src/parse/multiname.rs @@ -1,30 +1,34 @@ -use std::collections::VecDeque; +//! Parse the tree-like name sets used to represent imports -use super::context::Context; -use super::errors::Expected; -use super::stream::Stream; -use super::Lexeme; -use crate::ast::PType; -use crate::error::{ProjectError, ProjectResult}; -use crate::sourcefile::Import; -use crate::utils::boxed_iter::{box_chain, box_once}; -use crate::utils::BoxedIter; -use crate::{Location, Tok}; +use std::collections::VecDeque; +use std::ops::Range; + +use intern_all::{i, Tok}; + +use super::context::ParseCtx; +use super::errors::{Expected, ParseErrorKind}; +use super::frag::Frag; +use super::lexer::{Entry, Lexeme}; +use crate::error::ProjectResult; +use crate::location::SourceRange; +use crate::name::VPath; +use crate::parse::parsed::{Import, PType}; +use crate::utils::boxed_iter::{box_chain, box_once, BoxedIter}; struct Subresult { glob: bool, deque: VecDeque>, - location: Location, + range: Range, } impl Subresult { #[must_use] - fn new_glob(location: Location) -> Self { - Self { glob: true, deque: VecDeque::new(), location } + fn new_glob(range: &Range) -> Self { + Self { glob: true, deque: VecDeque::new(), range: range.clone() } } #[must_use] - fn new_named(name: Tok, location: Location) -> Self { - Self { location, glob: false, deque: VecDeque::from([name]) } + fn new_named(name: Tok, range: &Range) -> Self { + Self { glob: false, deque: VecDeque::from([name]), range: range.clone() } } #[must_use] @@ -34,62 +38,60 @@ impl Subresult { } #[must_use] - fn finalize(self) -> Import { - let Self { mut deque, glob, location } = self; + fn finalize(self, ctx: &(impl ParseCtx + ?Sized)) -> Import { + let Self { mut deque, glob, range } = self; debug_assert!(glob || !deque.is_empty(), "The constructors forbid this"); let name = if glob { None } else { deque.pop_back() }; - Import { name, location, path: deque.into() } + let range = ctx.range_loc(&range); + Import { name, range, path: VPath(deque.into()) } } } fn parse_multiname_branch<'a>( - cursor: Stream<'a>, - ctx: &(impl Context + ?Sized), -) -> ProjectResult<(BoxedIter<'a, Subresult>, Stream<'a>)> { - let comma = ctx.interner().i(","); + cursor: Frag<'a>, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult<(BoxedIter<'a, Subresult>, Frag<'a>)> { + let comma = i(","); let (subnames, cursor) = parse_multiname_rec(cursor, ctx)?; - let (delim, cursor) = cursor.trim().pop()?; - match &delim.lexeme { + let (Entry { lexeme, range }, cursor) = cursor.trim().pop(ctx)?; + match &lexeme { + Lexeme::RP(PType::Par) => Ok((subnames, cursor)), Lexeme::Name(n) if n == &comma => { let (tail, cont) = parse_multiname_branch(cursor, ctx)?; Ok((box_chain!(subnames, tail), cont)) }, - Lexeme::RP(PType::Par) => Ok((subnames, cursor)), - _ => Err( - Expected { - expected: vec![Lexeme::Name(comma), Lexeme::RP(PType::Par)], - or_name: false, - found: delim.clone(), - } - .rc(), - ), + _ => { + let expected = vec![Lexeme::Name(comma), Lexeme::RP(PType::Par)]; + let err = Expected { expected, or_name: false, found: lexeme.clone() }; + Err(err.pack(SourceRange { range: range.clone(), code: ctx.code_info() })) + }, } } fn parse_multiname_rec<'a>( - curosr: Stream<'a>, - ctx: &(impl Context + ?Sized), -) -> ProjectResult<(BoxedIter<'a, Subresult>, Stream<'a>)> { - let star = ctx.interner().i("*"); - let comma = ctx.interner().i(","); - let (head, mut cursor) = curosr.trim().pop()?; + cursor: Frag<'a>, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult<(BoxedIter<'a, Subresult>, Frag<'a>)> { + let star = i("*"); + let comma = i(","); + let (head, mut cursor) = cursor.trim().pop(ctx)?; match &head.lexeme { Lexeme::LP(PType::Par) => parse_multiname_branch(cursor, ctx), Lexeme::LP(PType::Sqr) => { let mut names = Vec::new(); loop { - let head; - (head, cursor) = cursor.trim().pop()?; - match &head.lexeme { - Lexeme::Name(n) => names.push((n, head.location())), + let (Entry { lexeme, range }, tail) = cursor.trim().pop(ctx)?; + cursor = tail; + match lexeme { + Lexeme::Name(n) => names.push((n.clone(), range)), Lexeme::RP(PType::Sqr) => break, _ => { let err = Expected { expected: vec![Lexeme::RP(PType::Sqr)], or_name: true, - found: head.clone(), + found: head.lexeme.clone(), }; - return Err(err.rc()); + return Err(err.pack(ctx.range_loc(range))); }, } } @@ -101,39 +103,37 @@ fn parse_multiname_rec<'a>( )) }, Lexeme::Name(n) if *n == star => - Ok((box_once(Subresult::new_glob(head.location())), cursor)), + Ok((box_once(Subresult::new_glob(&head.range)), cursor)), Lexeme::Name(n) if ![comma, star].contains(n) => { let cursor = cursor.trim(); - if cursor.get(0).map_or(false, |e| e.lexeme.strict_eq(&Lexeme::NS)) { - let cursor = cursor.step()?; + if cursor.get(0, ctx).map_or(false, |e| e.lexeme.strict_eq(&Lexeme::NS)) { + let cursor = cursor.step(ctx)?; let (out, cursor) = parse_multiname_rec(cursor, ctx)?; let out = Box::new(out.map(|sr| sr.push_front(n.clone()))); Ok((out, cursor)) } else { - Ok((box_once(Subresult::new_named(n.clone(), head.location())), cursor)) + Ok((box_once(Subresult::new_named(n.clone(), &head.range)), cursor)) } }, - _ => Err( - Expected { - expected: vec![Lexeme::LP(PType::Par)], - or_name: true, - found: head.clone(), - } - .rc(), - ), + _ => { + let expected = vec![Lexeme::LP(PType::Par)]; + let err = + Expected { expected, or_name: true, found: head.lexeme.clone() }; + Err(err.pack(ctx.range_loc(&head.range))) + }, } } /// Parse a tree that describes several names. The tree can be -/// +/// /// - name (except `,` or `*`) /// - name (except `,` or `*`) `::` tree /// - `(` tree `,` tree ... `)` /// - `*` (wildcard) /// - `[` name name ... `]` (including `,` or `*`). -/// +/// /// Examples of valid syntax: -/// +/// /// ```txt /// foo /// foo::bar::baz @@ -141,9 +141,9 @@ fn parse_multiname_rec<'a>( /// foo::bar::[baz quz * +] /// ``` pub fn parse_multiname<'a>( - cursor: Stream<'a>, - ctx: &(impl Context + ?Sized), -) -> ProjectResult<(Vec, Stream<'a>)> { + cursor: Frag<'a>, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult<(Vec, Frag<'a>)> { let (output, cont) = parse_multiname_rec(cursor, ctx)?; - Ok((output.map(|sr| sr.finalize()).collect(), cont)) + Ok((output.map(|sr| sr.finalize(ctx)).collect(), cont)) } diff --git a/src/parse/numeric.rs b/src/parse/numeric.rs index a008209..db3643f 100644 --- a/src/parse/numeric.rs +++ b/src/parse/numeric.rs @@ -1,17 +1,23 @@ +//! Parse a float or integer. These functions are also used for the macro +//! priority numbers + use std::num::IntErrorKind; use std::ops::Range; -use std::rc::Rc; use ordered_float::NotNan; -use super::context::Context; +use super::context::ParseCtx; +use super::errors::{ + ExpectedDigit, LiteralOverflow, NaNLiteral, ParseErrorKind, +}; +use super::lex_plugin::LexPluginReq; #[allow(unused)] // for doc -use super::context::LexerPlugin; -use super::errors::{ExpectedDigit, LiteralOverflow, NaNLiteral}; -use super::lexer::split_filter; -use crate::error::{ProjectError, ProjectResult}; -use crate::foreign::Atom; -use crate::systems::stl::Numeric; +use super::lex_plugin::LexerPlugin; +use super::lexer::{split_filter, Entry, LexRes, Lexeme}; +use crate::error::{ProjectErrorObj, ProjectResult}; +use crate::foreign::atom::AtomGenerator; +use crate::foreign::inert::Inert; +use crate::libs::std::number::Numeric; /// Rasons why [parse_num] might fail. See [NumError]. #[derive(Debug, Clone, PartialEq, Eq)] @@ -48,14 +54,14 @@ impl NumError { self, len: usize, tail: &str, - ctx: &(impl Context + ?Sized), - ) -> Rc { + ctx: &(impl ParseCtx + ?Sized), + ) -> ProjectErrorObj { let start = ctx.source().len() - tail.len() - len + self.range.start; - let location = ctx.range_loc(start..start + self.range.len()); + let location = ctx.range_loc(&(start..start + self.range.len())); match self.kind { - NumErrorKind::NaN => NaNLiteral(location).rc(), - NumErrorKind::InvalidDigit => ExpectedDigit(location).rc(), - NumErrorKind::Overflow => LiteralOverflow(location).rc(), + NumErrorKind::NaN => NaNLiteral.pack(location), + NumErrorKind::InvalidDigit => ExpectedDigit.pack(location), + NumErrorKind::Overflow => LiteralOverflow.pack(location), } } } @@ -93,7 +99,7 @@ pub fn parse_num(string: &str) -> Result { Some((whole, part)) => { let whole_n = int_parse(whole, radix, pos)? as f64; let part_n = int_parse(part, radix, pos + whole.len() + 1)? as f64; - let real_val = whole_n + (part_n / radix.pow(part.len() as u32) as f64); + let real_val = whole_n + (part_n / (radix as f64).powi(part.len() as i32)); let f = real_val * (radix as f64).powi(exponent); Ok(Numeric::Float(NotNan::new(f).expect("None of the inputs are NaN"))) }, @@ -112,25 +118,49 @@ pub fn numchar(c: char) -> bool { c.is_alphanumeric() | "._-".contains(c) } /// Filter for characters that can start numbers pub fn numstart(c: char) -> bool { c.is_ascii_digit() } +/// Print a number as a base-16 floating point literal +#[must_use] +pub fn print_nat16(num: NotNan) -> String { + if *num == 0.0 { + return "0x0".to_string(); + } else if num.is_infinite() { + return match num.is_sign_positive() { + true => "Infinity".to_string(), + false => "-Infinity".to_string(), + }; + } else if num.is_nan() { + return "NaN".to_string(); + } + let exp = num.log(16.0).floor(); + let man = *num / 16_f64.powf(exp); + format!("0x{man}p{exp:.0}") +} + /// [LexerPlugin] for a number literal -pub fn lex_numeric<'a>( - data: &'a str, - ctx: &dyn Context, -) -> Option> { - data.chars().next().filter(|c| numstart(*c)).map(|_| { - let (num_str, tail) = split_filter(data, numchar); - match parse_num(num_str) { - Ok(Numeric::Float(f)) => Ok((Atom::new(f), tail)), - Ok(Numeric::Uint(i)) => Ok((Atom::new(i), tail)), - Err(e) => Err(e.into_proj(num_str.len(), tail, ctx)), - } - }) +pub struct NumericLexer; +impl LexerPlugin for NumericLexer { + fn lex<'b>( + &self, + req: &'_ dyn LexPluginReq<'b>, + ) -> Option>> { + req.tail().chars().next().filter(|c| numstart(*c)).map(|_| { + let (num_str, tail) = split_filter(req.tail(), numchar); + let ag = match parse_num(num_str) { + Ok(Numeric::Float(f)) => AtomGenerator::cloner(Inert(f)), + Ok(Numeric::Uint(i)) => AtomGenerator::cloner(Inert(i)), + Err(e) => return Err(e.into_proj(num_str.len(), tail, req.ctx())), + }; + let range = req.ctx().range(num_str.len(), tail); + let entry = Entry { lexeme: Lexeme::Atom(ag), range }; + Ok(LexRes { tail, tokens: vec![entry] }) + }) + } } #[cfg(test)] mod test { + use crate::libs::std::number::Numeric; use crate::parse::numeric::parse_num; - use crate::systems::stl::Numeric; #[test] fn just_ints() { @@ -153,21 +183,3 @@ mod test { test("0x2.5p3", (0x25 * 0x100) as f64); } } - -/// Print a number as a base-16 floating point literal -#[must_use] -pub fn print_nat16(num: NotNan) -> String { - if *num == 0.0 { - return "0x0".to_string(); - } else if num.is_infinite() { - return match num.is_sign_positive() { - true => "Infinity".to_string(), - false => "-Infinity".to_string(), - }; - } else if num.is_nan() { - return "NaN".to_string(); - } - let exp = num.log(16.0).floor(); - let man = *num / 16_f64.powf(exp); - format!("0x{man}p{exp:.0}") -} diff --git a/src/parse/parse_plugin.rs b/src/parse/parse_plugin.rs new file mode 100644 index 0000000..004cdab --- /dev/null +++ b/src/parse/parse_plugin.rs @@ -0,0 +1,172 @@ +//! Abstractions for dynamic extensions to the parser that act across entries. +//! Macros are the primary syntax extension mechanism, but they only operate +//! within a constant and can't interfere with name resolution. + +use std::ops::Range; + +use intern_all::Tok; + +use super::context::ParseCtx; +use super::errors::{expect, expect_block, expect_name}; +use super::facade::parse_entries; +use super::frag::Frag; +use super::lexer::{Entry, Lexeme}; +use super::parsed::{ + Constant, Expr, ModuleBlock, PType, Rule, SourceLine, SourceLineKind, +}; +use super::sourcefile::{ + parse_const, parse_exprv, parse_line, parse_module, parse_module_body, + parse_nsname, parse_rule, split_lines, vec_to_single, +}; +use crate::error::ProjectResult; +use crate::location::SourceRange; +use crate::name::VName; +use crate::utils::boxed_iter::BoxedIter; + +/// Information and actions exposed to [ParseLinePlugin]. A plugin should never +/// import and call the parser directly because it might be executed in a +/// different version of the parser. +pub trait ParsePluginReq<'t> { + // ################ Frag and ParseCtx ################ + + /// The token sequence this parser must parse + fn frag(&self) -> Frag; + /// Get the location of a fragment + fn frag_loc(&self, f: Frag) -> SourceRange; + /// Convert a numeric byte range into a location + fn range_loc(&self, r: Range) -> SourceRange; + /// Remove the first token of the fragment + fn pop<'a>(&self, f: Frag<'a>) -> ProjectResult<(&'a Entry, Frag<'a>)>; + /// Remove the last element of the fragment + fn pop_back<'a>(&self, f: Frag<'a>) -> ProjectResult<(&'a Entry, Frag<'a>)>; + + // ################ Parser states ################ + + /// Split up the lines in a fragment. The fragment must outlive the iterator + /// and the request itself must outlive both + fn split_lines<'a: 'b, 'b>(&'b self, f: Frag<'a>) -> BoxedIter<'b, Frag<'a>> + where 't: 'b + 'a; + /// Parse a sequence of source lines separated by line breaks + fn parse_module_body(&self, frag: Frag) -> ProjectResult>; + /// Parse a single source line. This returns a vector because plugins can + /// convert a single line into multiple entries + fn parse_line(&self, frag: Frag) -> ProjectResult>; + /// Parse a macro rule ` =prio=> ` + fn parse_rule(&self, frag: Frag) -> ProjectResult; + /// Parse a constant declaration ` := ` + fn parse_const(&self, frag: Frag) -> ProjectResult; + /// Parse a namespaced name `name::name` + fn parse_nsname<'a>(&self, f: Frag<'a>) -> ProjectResult<(VName, Frag<'a>)>; + /// Parse a module declaration. ` ( )` + fn parse_module(&self, frag: Frag) -> ProjectResult; + /// Parse a sequence of expressions. In principle, it never makes sense to + /// parse a single expression because it could always be a macro invocation. + fn parse_exprv<'a>( + &self, + f: Frag<'a>, + p: Option, + ) -> ProjectResult<(Vec, Frag<'a>)>; + /// Parse a prepared string of code + fn parse_entries(&self, t: &'static str, r: SourceRange) -> Vec; + /// Convert a sequence of expressions to a single one by parenthesization if + /// necessary + fn vec_to_single( + &self, + fallback: &Entry, + v: Vec, + ) -> ProjectResult; + + // ################ Assertions ################ + + /// Unwrap a single name token or raise an error + fn expect_name(&self, entry: &Entry) -> ProjectResult>; + /// Assert that the entry contains exactly the specified lexeme + fn expect(&self, l: Lexeme, e: &Entry) -> ProjectResult<()>; + /// Remove two parentheses from the ends of the cursor + fn expect_block<'a>(&self, f: Frag<'a>, p: PType) -> ProjectResult>; + /// Ensure that the fragment is empty + fn expect_empty(&self, f: Frag) -> ProjectResult<()>; +} + +/// External plugin that parses an unrecognized source line into lines of +/// recognized types +pub trait ParseLinePlugin: Sync + Send { + /// Attempt to parse a line. Returns [None] if the line isn't recognized, + /// [Some][Err] if it's recognized but incorrect. + fn parse( + &self, + req: &dyn ParsePluginReq, + ) -> Option>>; +} + +/// Implementation of [ParsePluginReq] exposing sub-parsers and data to the +/// plugin via dynamic dispatch +pub struct ParsePlugReqImpl<'a, TCtx: ParseCtx + ?Sized> { + /// Fragment of text to be parsed by the plugin + pub frag: Frag<'a>, + /// Context for recursive commands and to expose to the plugin + pub ctx: &'a TCtx, +} +impl<'ty, TCtx: ParseCtx + ?Sized> ParsePluginReq<'ty> + for ParsePlugReqImpl<'ty, TCtx> +{ + fn frag(&self) -> Frag { self.frag } + fn frag_loc(&self, f: Frag) -> SourceRange { self.range_loc(f.range()) } + fn range_loc(&self, r: Range) -> SourceRange { self.ctx.range_loc(&r) } + fn pop<'a>(&self, f: Frag<'a>) -> ProjectResult<(&'a Entry, Frag<'a>)> { + f.pop(self.ctx) + } + fn pop_back<'a>(&self, f: Frag<'a>) -> ProjectResult<(&'a Entry, Frag<'a>)> { + f.pop_back(self.ctx) + } + fn split_lines<'a: 'b, 'b>(&'b self, f: Frag<'a>) -> BoxedIter<'b, Frag<'a>> + where + 'ty: 'b, + 'ty: 'a, + { + Box::new(split_lines(f, self.ctx)) + } + fn parse_module_body(&self, f: Frag) -> ProjectResult> { + parse_module_body(f, self.ctx) + } + fn parse_line(&self, f: Frag) -> ProjectResult> { + parse_line(f, self.ctx) + } + fn parse_rule(&self, f: Frag) -> ProjectResult { + parse_rule(f, self.ctx) + } + fn parse_const(&self, f: Frag) -> ProjectResult { + parse_const(f, self.ctx) + } + fn parse_nsname<'a>(&self, f: Frag<'a>) -> ProjectResult<(VName, Frag<'a>)> { + parse_nsname(f, self.ctx) + } + fn parse_module(&self, f: Frag) -> ProjectResult { + parse_module(f, self.ctx) + } + fn parse_exprv<'a>( + &self, + f: Frag<'a>, + p: Option, + ) -> ProjectResult<(Vec, Frag<'a>)> { + parse_exprv(f, p, self.ctx) + } + fn parse_entries(&self, s: &'static str, r: SourceRange) -> Vec { + parse_entries(&self.ctx, s, r) + } + fn vec_to_single(&self, fb: &Entry, v: Vec) -> ProjectResult { + vec_to_single(fb, v, self.ctx) + } + fn expect_name(&self, e: &Entry) -> ProjectResult> { + expect_name(e, self.ctx) + } + fn expect(&self, l: Lexeme, e: &Entry) -> ProjectResult<()> { + expect(l, e, self.ctx) + } + fn expect_block<'a>(&self, f: Frag<'a>, t: PType) -> ProjectResult> { + expect_block(f, t, self.ctx) + } + fn expect_empty(&self, f: Frag) -> ProjectResult<()> { + f.expect_empty(self.ctx) + } +} diff --git a/src/parse/parsed.rs b/src/parse/parsed.rs new file mode 100644 index 0000000..2da0827 --- /dev/null +++ b/src/parse/parsed.rs @@ -0,0 +1,554 @@ +//! Datastructures representing the units of macro execution +//! +//! These structures are produced by the pipeline, processed by the macro +//! executor, and then converted to other usable formats. + +use std::fmt::Display; +use std::hash::Hash; +use std::rc::Rc; + +use hashbrown::HashSet; +use intern_all::Tok; +use itertools::Itertools; +use ordered_float::NotNan; + +use crate::foreign::atom::AtomGenerator; +#[allow(unused)] // for doc +use crate::interpreter::nort; +use crate::location::SourceRange; +use crate::name::{Sym, VName, VPath}; +use crate::parse::numeric::print_nat16; + +/// A [Clause] with associated metadata +#[derive(Clone, Debug)] +pub struct Expr { + /// The actual value + pub value: Clause, + /// Information about the code that produced this value + pub range: SourceRange, +} + +impl Expr { + /// Process all names with the given mapper. + /// Return a new object if anything was processed + #[must_use] + pub fn map_names( + &self, + pred: &mut impl FnMut(Sym) -> Option, + ) -> Option { + (self.value.map_names(pred)) + .map(|value| Self { value, range: self.range.clone() }) + } + + /// Visit all expressions in the tree. The search can be exited early by + /// returning [Some] + /// + /// See also [crate::interpreter::nort::Expr::search_all] + pub fn search_all( + &self, + f: &mut impl FnMut(&Self) -> Option, + ) -> Option { + f(self).or_else(|| self.value.search_all(f)) + } +} + +/// Visit all expression sequences including this sequence itself. +pub fn search_all_slcs( + this: &[Expr], + f: &mut impl FnMut(&[Expr]) -> Option, +) -> Option { + f(this).or_else(|| this.iter().find_map(|expr| expr.value.search_all_slcs(f))) +} + +impl Expr { + /// Add the specified prefix to every Name + #[must_use] + pub fn prefix( + &self, + prefix: &[Tok], + except: &impl Fn(Tok) -> bool, + ) -> Self { + Self { value: self.value.prefix(prefix, except), range: self.range.clone() } + } +} + +impl Display for Expr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} + +/// Various types of placeholders +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum PHClass { + /// Matches multiple tokens, lambdas or parenthesized groups + Vec { + /// If true, must match at least one clause + nonzero: bool, + /// Greediness in the allocation of tokens + prio: usize, + }, + /// Matches exactly one token, lambda or parenthesized group + Scalar, + /// Matches exactly one name + Name, +} + +/// Properties of a placeholder that matches unknown tokens in macros +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Placeholder { + /// Identifier to pair placeholders in the pattern and template + pub name: Tok, + /// The nature of the token set matched by this placeholder + pub class: PHClass, +} + +impl Display for Placeholder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = &self.name; + match self.class { + PHClass::Scalar => write!(f, "${name}"), + PHClass::Name => write!(f, "$_{name}"), + PHClass::Vec { nonzero, prio } => { + if nonzero { write!(f, "...") } else { write!(f, "..") }?; + write!(f, "${name}:{prio}") + }, + } + } +} + +/// Different types of brackets supported by Orchid +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum PType { + /// () + Par, + /// [] + Sqr, + /// {} + Curl, +} +impl PType { + /// Left paren character for this paren type + pub fn l(self) -> char { + match self { + PType::Curl => '{', + PType::Par => '(', + PType::Sqr => '[', + } + } + + /// Right paren character for this paren type + pub fn r(self) -> char { + match self { + PType::Curl => '}', + PType::Par => ')', + PType::Sqr => ']', + } + } +} + +/// An S-expression as read from a source file +#[derive(Debug, Clone)] +pub enum Clause { + /// An opaque non-callable value, eg. a file handle + Atom(AtomGenerator), + /// A c-style name or an operator, eg. `+`, `i`, `foo::bar` + Name(Sym), + /// A parenthesized expression + /// eg. `(print out "hello")`, `[1, 2, 3]`, `{Some(t) => t}` + S(PType, Rc>), + /// A function expression, eg. `\x. x + 1` + Lambda(Rc>, Rc>), + /// A placeholder for macros, eg. `$name`, `...$body`, `...$lhs:1` + Placeh(Placeholder), +} + +impl Clause { + /// Extract the expressions from an auto, lambda or S + #[must_use] + pub fn body(&self) -> Option>> { + match self { + Self::Lambda(_, body) | Self::S(_, body) => Some(body.clone()), + _ => None, + } + } + + /// Convert with identical meaning + #[must_use] + pub fn into_expr(self, range: SourceRange) -> Expr { + if let Self::S(PType::Par, body) = &self { + if let [wrapped] = &body[..] { + return wrapped.clone(); + } + } + Expr { value: self, range } + } + + /// Convert with identical meaning + #[must_use] + pub fn from_exprs(exprs: &[Expr]) -> Option { + match exprs { + [] => None, + [only] => Some(only.value.clone()), + _ => Some(Self::S(PType::Par, Rc::new(exprs.to_vec()))), + } + } + + /// Convert with identical meaning + #[must_use] + pub fn from_exprv(exprv: &Rc>) -> Option { + if exprv.len() < 2 { + Self::from_exprs(exprv) + } else { + Some(Self::S(PType::Par, exprv.clone())) + } + } + + /// Collect all names that appear in this expression. + /// NOTICE: this isn't the total set of unbound names, it's mostly useful to + /// make weak statements for optimization. + #[must_use] + pub fn collect_names(&self) -> HashSet { + if let Self::Name(n) = self { + return HashSet::from([n.clone()]); + } + let mut glossary = HashSet::new(); + let result = self.search_all(&mut |e| { + if let Clause::Name(n) = &e.value { + glossary.insert(n.clone()); + } + None::<()> + }); + assert!(result.is_none(), "Callback never returns Some"); + glossary + } + + /// Process all names with the given mapper. + /// Return a new object if anything was processed + #[must_use] + pub fn map_names( + &self, + pred: &mut impl FnMut(Sym) -> Option, + ) -> Option { + match self { + Clause::Atom(_) | Clause::Placeh(_) => None, + Clause::Name(name) => pred(name.clone()).map(Clause::Name), + Clause::S(c, body) => { + let mut any_some = false; + let new_body = body + .iter() + .map(|e| { + let val = e.map_names(pred); + any_some |= val.is_some(); + val.unwrap_or_else(|| e.clone()) + }) + .collect(); + if any_some { Some(Clause::S(*c, Rc::new(new_body))) } else { None } + }, + Clause::Lambda(arg, body) => { + let mut any_some = false; + let new_arg = (arg.iter()) + .map(|e| { + let val = e.map_names(pred); + any_some |= val.is_some(); + val.unwrap_or_else(|| e.clone()) + }) + .collect(); + let new_body = (body.iter()) + .map(|e| { + let val = e.map_names(pred); + any_some |= val.is_some(); + val.unwrap_or_else(|| e.clone()) + }) + .collect(); + if any_some { + Some(Clause::Lambda(Rc::new(new_arg), Rc::new(new_body))) + } else { + None + } + }, + } + } + + /// Pair of [Expr::search_all] + pub fn search_all( + &self, + f: &mut impl FnMut(&Expr) -> Option, + ) -> Option { + match self { + Clause::Lambda(arg, body) => + arg.iter().chain(body.iter()).find_map(|expr| expr.search_all(f)), + Clause::Name(_) | Clause::Atom(_) | Clause::Placeh(_) => None, + Clause::S(_, body) => body.iter().find_map(|expr| expr.search_all(f)), + } + } + + /// Pair of [Expr::search_all_slcs] + pub fn search_all_slcs( + &self, + f: &mut impl FnMut(&[Expr]) -> Option, + ) -> Option { + match self { + Clause::Lambda(arg, body) => + search_all_slcs(arg, f).or_else(|| search_all_slcs(body, f)), + Clause::Name(_) | Clause::Atom(_) | Clause::Placeh(_) => None, + Clause::S(_, body) => search_all_slcs(body, f), + } + } + + /// Generate a parenthesized expression sequence + pub fn s( + delimiter: char, + body: impl IntoIterator, + range: SourceRange, + ) -> Self { + let ptype = match delimiter { + '(' => PType::Par, + '[' => PType::Sqr, + '{' => PType::Curl, + _ => panic!("not an opening paren"), + }; + let body = body.into_iter().map(|it| it.into_expr(range.clone())).collect(); + Self::S(ptype, Rc::new(body)) + } +} + +impl Clause { + /// Add the specified prefix to every Name + #[must_use] + pub fn prefix( + &self, + prefix: &[Tok], + except: &impl Fn(Tok) -> bool, + ) -> Self { + self + .map_names(&mut |name| match except(name[0].clone()) { + true => None, + false => { + let prefixed = + prefix.iter().chain(name[..].iter()).cloned().collect::>(); + Some(Sym::from_tok(name.tok().interner().i(&prefixed)).unwrap()) + }, + }) + .unwrap_or_else(|| self.clone()) + } +} + +impl Display for Clause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Atom(a) => write!(f, "{a:?}"), + Self::Name(name) => write!(f, "{}", name), + Self::S(t, items) => { + let body = items.iter().join(" "); + write!(f, "{}{body}{}", t.l(), t.r()) + }, + Self::Lambda(arg, body) => { + let args = arg.iter().join(" "); + let bodys = body.iter().join(" "); + write!(f, "\\{args}.{bodys}") + }, + Self::Placeh(ph) => ph.fmt(f), + } + } +} + +/// A substitution rule as loaded from source +#[derive(Debug, Clone)] +pub struct Rule { + /// Expressions on the left side of the arrow + pub pattern: Vec, + /// Priority number written inside the arrow + pub prio: NotNan, + /// Expressions on the right side of the arrow + pub template: Vec, +} + +impl Display for Rule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "rule {} ={}=> {}", + self.pattern.iter().join(" "), + print_nat16(self.prio), + self.template.iter().join(" ") + ) + } +} + +/// A named constant +#[derive(Debug, Clone)] +pub struct Constant { + /// Used to reference the constant + pub name: Tok, + /// The constant value inserted where the name is found + pub value: Expr, +} + +impl Display for Constant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "const {} := {}", *self.name, self.value) + } +} + +/// An import pointing at another module, either specifying the symbol to be +/// imported or importing all available symbols with a globstar (*) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Import { + /// Import path, a sequence of module names. Can either start with + /// + /// - `self` to reference the current module + /// - any number of `super` to reference the parent module of the implied + /// `self` + /// - a root name + pub path: VPath, + /// If name is None, this is a wildcard import + pub name: Option>, + /// Location of the final name segment, which uniquely identifies this name + pub range: SourceRange, +} +impl Import { + /// Constructor + pub fn new( + path: impl IntoIterator>, + name: Option>, + range: SourceRange, + ) -> Self { + let path = VPath(path.into_iter().collect()); + assert!(name.is_some() || !path.0.is_empty(), "import * not allowed"); + Self { range, name, path } + } + + /// Get the preload target space for this import - the prefix below + /// which all files should be included in the compilation + /// + /// Returns the path if this is a glob import, or the path plus the + /// name if this is a specific import + #[must_use] + pub fn nonglob_path(&self) -> VName { + VName::new(self.path.0.iter().chain(&self.name).cloned()) + .expect("Everything import (`import *`) not allowed") + } +} + +impl Display for Import { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.name { + None => write!(f, "{}::*", self.path), + Some(n) => write!(f, "{}::{}", self.path, n), + } + } +} + +/// A namespace block +#[derive(Debug, Clone)] +pub struct ModuleBlock { + /// Name prefixed to all names in the block + pub name: Tok, + /// Prefixed entries + pub body: Vec, +} + +impl Display for ModuleBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bodys = self.body.iter().map(|e| e.to_string()).join("\n"); + write!(f, "module {} {{\n{}\n}}", self.name, bodys) + } +} + +/// see [Member] +#[derive(Debug, Clone)] +pub enum MemberKind { + /// A substitution rule. Rules apply even when they're not in scope, if the + /// absolute names are present eg. because they're produced by other rules + Rule(Rule), + /// A constant (or function) associated with a name + Constant(Constant), + /// A prefixed set of other entries + Module(ModuleBlock), +} +impl MemberKind { + /// Convert to [SourceLine] + pub fn to_line(self, exported: bool, range: SourceRange) -> SourceLine { + SourceLineKind::Member(Member { exported, kind: self }).wrap(range) + } +} + +impl Display for MemberKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Constant(c) => c.fmt(f), + Self::Module(m) => m.fmt(f), + Self::Rule(r) => r.fmt(f), + } + } +} + +/// Things that may be prefixed with an export +/// see [MemberKind] +#[derive(Debug, Clone)] +pub struct Member { + /// Various members + pub kind: MemberKind, + /// Whether this member is exported or not + pub exported: bool, +} + +impl Display for Member { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self { exported: true, kind } => write!(f, "export {kind}"), + Self { exported: false, kind } => write!(f, "{kind}"), + } + } +} + +/// See [SourceLine] +#[derive(Debug, Clone)] +pub enum SourceLineKind { + /// Imports one or all names in a module + Import(Vec), + /// Comments are kept here in case dev tooling wants to parse documentation + Comment(String), + /// An element with visibility information + Member(Member), + /// A list of tokens exported explicitly. This can also create new exported + /// tokens that the local module doesn't actually define a role for + Export(Vec<(Tok, SourceRange)>), +} +impl SourceLineKind { + /// Wrap with no location + pub fn wrap(self, range: SourceRange) -> SourceLine { + SourceLine { kind: self, range } + } +} + +impl Display for SourceLineKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Comment(s) => write!(f, "--[{s}]--"), + Self::Export(s) => { + write!(f, "export ::({})", s.iter().map(|t| &**t.0).join(", ")) + }, + Self::Member(member) => write!(f, "{member}"), + Self::Import(i) => { + write!(f, "import ({})", i.iter().map(|i| i.to_string()).join(", ")) + }, + } + } +} + +/// Anything the parser might encounter in a file. See [SourceLineKind] +#[derive(Debug, Clone)] +pub struct SourceLine { + /// What we encountered + pub kind: SourceLineKind, + /// Where we encountered it. + pub range: SourceRange, +} + +impl Display for SourceLine { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.kind.fmt(f) + } +} diff --git a/src/parse/sourcefile.rs b/src/parse/sourcefile.rs index a7fd1e2..8207d78 100644 --- a/src/parse/sourcefile.rs +++ b/src/parse/sourcefile.rs @@ -1,28 +1,31 @@ use std::iter; use std::rc::Rc; +use intern_all::i; use itertools::Itertools; -use super::context::Context; +use super::context::ParseCtx; use super::errors::{ - BadTokenInRegion, Expected, ExpectedBlock, ExpectedName, ExpectedSingleName, - GlobExport, LeadingNS, MisalignedParen, NamespacedExport, ReservedToken, - UnexpectedEOL, + expect, expect_block, expect_name, BadTokenInRegion, ExpectedSingleName, + GlobExport, LeadingNS, MisalignedParen, NamespacedExport, ParseErrorKind, + ReservedToken, UnexpectedEOL, }; -use super::lexer::Lexeme; +use super::frag::Frag; +use super::lexer::{Entry, Lexeme}; use super::multiname::parse_multiname; -use super::stream::Stream; -use super::Entry; -use crate::ast::{Clause, Constant, Expr, PType, Rule}; -use crate::error::{ProjectError, ProjectResult}; -use crate::representations::location::Location; -use crate::representations::sourcefile::{FileEntry, MemberKind, ModuleBlock}; -use crate::representations::VName; -use crate::sourcefile::{FileEntryKind, Import, Member}; -use crate::utils::pure_seq::pushed; +use super::parse_plugin::ParsePlugReqImpl; +use crate::error::ProjectResult; +use crate::name::VName; +use crate::parse::parsed::{ + Clause, Constant, Expr, Import, Member, MemberKind, ModuleBlock, PType, Rule, + SourceLine, SourceLineKind, +}; -/// Split the stream at each line break outside parentheses -pub fn split_lines(module: Stream<'_>) -> impl Iterator> { +/// Split the fragment at each line break outside parentheses +pub fn split_lines<'a>( + module: Frag<'a>, + ctx: &'a (impl ParseCtx + ?Sized), +) -> impl Iterator> { let mut source = module.data.iter().enumerate(); let mut fallback = module.fallback; let mut last_slice = 0; @@ -38,7 +41,7 @@ pub fn split_lines(module: Stream<'_>) -> impl Iterator> { last_slice = i + 1; let cur_prev = fallback; fallback = &module.data[i]; - return Some(Stream::new(cur_prev, &module.data[begin..i])); + return Some(Frag::new(cur_prev, &module.data[begin..i])); }, _ => (), } @@ -46,15 +49,15 @@ pub fn split_lines(module: Stream<'_>) -> impl Iterator> { // Include last line even without trailing newline if !finished { finished = true; - return Some(Stream::new(fallback, &module.data[last_slice..])); + return Some(Frag::new(fallback, &module.data[last_slice..])); } None }) - .map(Stream::trim) + .map(Frag::trim) .map(|s| { - s.pop() + s.pop(ctx) .and_then(|(first, inner)| { - let (last, inner) = inner.pop_back()?; + let (last, inner) = inner.pop_back(ctx)?; match (&first.lexeme, &last.lexeme) { (Lexeme::LP(PType::Par), Lexeme::RP(PType::Par)) => Ok(inner.trim()), _ => Ok(s), @@ -67,123 +70,128 @@ pub fn split_lines(module: Stream<'_>) -> impl Iterator> { /// Parse linebreak-separated entries pub fn parse_module_body( - cursor: Stream<'_>, - ctx: &(impl Context + ?Sized), -) -> ProjectResult> { - split_lines(cursor) - .map(|l| { - parse_line(l, ctx).map(move |kinds| { - kinds - .into_iter() - .map(move |kind| FileEntry { locations: vec![l.location()], kind }) - }) - }) - .flatten_ok() - .collect() + cursor: Frag<'_>, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult> { + let mut lines = Vec::new(); + for l in split_lines(cursor, ctx) { + let kinds = parse_line(l, ctx)?; + let r = ctx.range_loc(&l.range()); + lines.extend( + kinds.into_iter().map(|kind| SourceLine { range: r.clone(), kind }), + ); + } + Ok(lines) } /// Parse a single, possibly exported entry pub fn parse_line( - cursor: Stream<'_>, - ctx: &(impl Context + ?Sized), -) -> ProjectResult> { + cursor: Frag<'_>, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult> { + let req = ParsePlugReqImpl { ctx, frag: cursor }; for line_parser in ctx.line_parsers() { - if let Some(result) = line_parser(cursor, &ctx) { + if let Some(result) = line_parser.parse(&req) { return result; } } - match &cursor.get(0)?.lexeme { - Lexeme::BR | Lexeme::Comment(_) => parse_line(cursor.step()?, ctx), + let head = cursor.get(0, ctx)?; + match &head.lexeme { + Lexeme::Comment(cmt) => + cmt.strip_prefix('|').and_then(|c| c.strip_suffix('|')).map_or_else( + || parse_line(cursor.step(ctx)?, ctx), + |cmt| Ok(vec![SourceLineKind::Comment(cmt.to_string())]), + ), + Lexeme::BR => parse_line(cursor.step(ctx)?, ctx), Lexeme::Name(n) if **n == "export" => - parse_export_line(cursor.step()?, ctx).map(|k| vec![k]), - Lexeme::Name(n) if ["const", "macro", "module"].contains(&n.as_str()) => - Ok(vec![FileEntryKind::Member(Member { - kind: parse_member(cursor, ctx)?, - exported: false, - })]), - Lexeme::Name(n) if **n == "import" => { - let (imports, cont) = parse_multiname(cursor.step()?, ctx)?; - cont.expect_empty()?; - Ok(vec![FileEntryKind::Import(imports)]) + parse_export_line(cursor.step(ctx)?, ctx).map(|k| vec![k]), + Lexeme::Name(n) if ["const", "macro", "module"].contains(&n.as_str()) => { + let member = Member { exported: false, kind: parse_member(cursor, ctx)? }; + Ok(vec![SourceLineKind::Member(member)]) }, - _ => { - let err = BadTokenInRegion { - entry: cursor.get(0)?.clone(), - region: "start of line", - }; - Err(err.rc()) + Lexeme::Name(n) if **n == "import" => { + let (imports, cont) = parse_multiname(cursor.step(ctx)?, ctx)?; + cont.expect_empty(ctx)?; + Ok(vec![SourceLineKind::Import(imports)]) + }, + lexeme => { + let lexeme = lexeme.clone(); + Err( + BadTokenInRegion { lexeme, region: "start of line" } + .pack(ctx.range_loc(&head.range)), + ) }, } } fn parse_export_line( - cursor: Stream<'_>, - ctx: &(impl Context + ?Sized), -) -> ProjectResult { + cursor: Frag<'_>, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult { let cursor = cursor.trim(); - match &cursor.get(0)?.lexeme { + let head = cursor.get(0, ctx)?; + match &head.lexeme { Lexeme::NS => { - let (names, cont) = parse_multiname(cursor.step()?, ctx)?; - cont.expect_empty()?; + let (names, cont) = parse_multiname(cursor.step(ctx)?, ctx)?; + cont.expect_empty(ctx)?; let names = (names.into_iter()) - .map(|Import { name, path, location }| match (name, &path[..]) { - (Some(n), []) => Ok((n, location)), - (None, _) => Err(GlobExport(location).rc()), - _ => Err(NamespacedExport(location).rc()), + .map(|Import { name, path, range }| match (name, &path[..]) { + (Some(n), []) => Ok((n, range)), + (None, _) => Err(GlobExport.pack(range)), + _ => Err(NamespacedExport.pack(range)), }) .collect::, _>>()?; - Ok(FileEntryKind::Export(names)) + Ok(SourceLineKind::Export(names)) }, Lexeme::Name(n) if ["const", "macro", "module"].contains(&n.as_str()) => - Ok(FileEntryKind::Member(Member { + Ok(SourceLineKind::Member(Member { kind: parse_member(cursor, ctx)?, exported: true, })), - _ => { - let err = BadTokenInRegion { - entry: cursor.get(0)?.clone(), - region: "exported line", - }; - Err(err.rc()) + lexeme => { + let lexeme = lexeme.clone(); + let err = BadTokenInRegion { lexeme, region: "exported line" }; + Err(err.pack(ctx.range_loc(&head.range))) }, } } fn parse_member( - cursor: Stream<'_>, - ctx: &(impl Context + ?Sized), + cursor: Frag<'_>, + ctx: &(impl ParseCtx + ?Sized), ) -> ProjectResult { - let (typemark, cursor) = cursor.trim().pop()?; + let (typemark, cursor) = cursor.trim().pop(ctx)?; match &typemark.lexeme { Lexeme::Name(n) if **n == "const" => { let constant = parse_const(cursor, ctx)?; Ok(MemberKind::Constant(constant)) }, Lexeme::Name(n) if **n == "macro" => { - let rule = parse_rule(cursor, &ctx)?; + let rule = parse_rule(cursor, ctx)?; Ok(MemberKind::Rule(rule)) }, Lexeme::Name(n) if **n == "module" => { let module = parse_module(cursor, ctx)?; Ok(MemberKind::Module(module)) }, - _ => { - let err = - BadTokenInRegion { entry: typemark.clone(), region: "member type" }; - Err(err.rc()) + lexeme => { + let lexeme = lexeme.clone(); + let err = BadTokenInRegion { lexeme, region: "member type" }; + Err(err.pack(ctx.range_loc(&typemark.range))) }, } } /// Parse a macro rule pub fn parse_rule( - cursor: Stream<'_>, - ctx: &impl Context, -) -> ProjectResult> { - let (pattern, prio, template) = cursor.find_map("arrow", |a| match a { - Lexeme::Arrow(p) => Some(*p), - _ => None, - })?; + cursor: Frag<'_>, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult { + let (pattern, prio, template) = + cursor.find_map("arrow", ctx, |a| match a { + Lexeme::Arrow(p) => Some(*p), + _ => None, + })?; let (pattern, _) = parse_exprv(pattern, None, ctx)?; let (template, _) = parse_exprv(template, None, ctx)?; Ok(Rule { pattern, prio, template }) @@ -191,138 +199,138 @@ pub fn parse_rule( /// Parse a constant declaration pub fn parse_const( - cursor: Stream<'_>, - ctx: &(impl Context + ?Sized), + cursor: Frag<'_>, + ctx: &(impl ParseCtx + ?Sized), ) -> ProjectResult { - let (name_ent, cursor) = cursor.trim().pop()?; - let name = ExpectedName::expect(name_ent)?; - let (walrus_ent, cursor) = cursor.trim().pop()?; - Expected::expect(Lexeme::Walrus, walrus_ent)?; + let (name_ent, cursor) = cursor.trim().pop(ctx)?; + let name = expect_name(name_ent, ctx)?; + let (walrus_ent, cursor) = cursor.trim().pop(ctx)?; + expect(Lexeme::Walrus, walrus_ent, ctx)?; let (body, _) = parse_exprv(cursor, None, ctx)?; - Ok(Constant { name, value: vec_to_single(walrus_ent, body)? }) + Ok(Constant { name, value: vec_to_single(walrus_ent, body, ctx)? }) } /// Parse a namespaced name. TODO: use this for modules pub fn parse_nsname<'a>( - cursor: Stream<'a>, - ctx: &(impl Context + ?Sized), -) -> ProjectResult<(VName, Stream<'a>)> { + cursor: Frag<'a>, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult<(VName, Frag<'a>)> { let (name, tail) = parse_multiname(cursor, ctx)?; - let name = match name.into_iter().exactly_one() { - Ok(Import { name: Some(name), path, .. }) => pushed(path, name), - _ => { - let loc = cursor.data[0].location().to(tail.data[0].location()); - return Err(ExpectedSingleName(loc).rc()); + match name.into_iter().exactly_one() { + Ok(Import { name: Some(name), path, .. }) => + Ok((VName::new([name]).unwrap().prefix(path), tail)), + Err(_) | Ok(Import { name: None, .. }) => { + let range = cursor.data[0].range.start..tail.data[0].range.end; + Err(ExpectedSingleName.pack(ctx.range_loc(&range))) }, - }; - Ok((name, tail)) + } } /// Parse a submodule declaration pub fn parse_module( - cursor: Stream<'_>, - ctx: &(impl Context + ?Sized), + cursor: Frag<'_>, + ctx: &(impl ParseCtx + ?Sized), ) -> ProjectResult { - let (name_ent, cursor) = cursor.trim().pop()?; - let name = ExpectedName::expect(name_ent)?; - let body = ExpectedBlock::expect(cursor, PType::Par)?; + let (name_ent, cursor) = cursor.trim().pop(ctx)?; + let name = expect_name(name_ent, ctx)?; + let body = expect_block(cursor, PType::Par, ctx)?; Ok(ModuleBlock { name, body: parse_module_body(body, ctx)? }) } /// Parse a sequence of expressions pub fn parse_exprv<'a>( - mut cursor: Stream<'a>, + mut cursor: Frag<'a>, paren: Option, - ctx: &(impl Context + ?Sized), -) -> ProjectResult<(Vec>, Stream<'a>)> { + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult<(Vec, Frag<'a>)> { let mut output = Vec::new(); cursor = cursor.trim(); - while let Ok(current) = cursor.get(0) { + while let Ok(current) = cursor.get(0, ctx) { match ¤t.lexeme { Lexeme::BR | Lexeme::Comment(_) => unreachable!("Fillers skipped"), - Lexeme::At | Lexeme::Type => - return Err(ReservedToken { entry: current.clone() }.rc()), + Lexeme::At | Lexeme::Type => { + let err = ReservedToken(current.lexeme.clone()); + return Err(err.pack(ctx.range_loc(¤t.range))); + }, Lexeme::Atom(a) => { let value = Clause::Atom(a.clone()); - output.push(Expr { value, location: current.location() }); - cursor = cursor.step()?; + output.push(Expr { value, range: ctx.range_loc(¤t.range) }); + cursor = cursor.step(ctx)?; }, Lexeme::Placeh(ph) => { output.push(Expr { value: Clause::Placeh(ph.clone()), - location: current.location(), + range: ctx.range_loc(¤t.range), }); - cursor = cursor.step()?; + cursor = cursor.step(ctx)?; }, Lexeme::Name(n) => { - let location = cursor.location(); - let mut fullname: VName = vec![n.clone()]; - while cursor.get(1).map_or(false, |e| e.lexeme.strict_eq(&Lexeme::NS)) { - fullname.push(ExpectedName::expect(cursor.get(2)?)?); - cursor = cursor.step()?.step()?; + let range = ctx.range_loc(&cursor.range()); + let mut fullname = VName::new([n.clone()]).unwrap(); + while cursor.get(1, ctx).is_ok_and(|e| e.lexeme.strict_eq(&Lexeme::NS)) + { + fullname = fullname.suffix([expect_name(cursor.get(2, ctx)?, ctx)?]); + cursor = cursor.step(ctx)?.step(ctx)?; } - output.push(Expr { value: Clause::Name(fullname), location }); - cursor = cursor.step()?; + let clause = Clause::Name(fullname.to_sym()); + output.push(Expr { value: clause, range }); + cursor = cursor.step(ctx)?; }, - Lexeme::NS => return Err(LeadingNS(current.location()).rc()), - Lexeme::RP(c) => - return if Some(*c) == paren { - Ok((output, cursor.step()?)) - } else { - Err(MisalignedParen(cursor.get(0)?.clone()).rc()) + Lexeme::NS => return Err(LeadingNS.pack(ctx.range_loc(¤t.range))), + Lexeme::RP(c) => match paren { + Some(exp_c) if exp_c == *c => return Ok((output, cursor.step(ctx)?)), + _ => { + let err = MisalignedParen(current.lexeme.clone()); + return Err(err.pack(ctx.range_loc(¤t.range))); }, + }, Lexeme::LP(c) => { - let (result, leftover) = parse_exprv(cursor.step()?, Some(*c), ctx)?; - output.push(Expr { - value: Clause::S(*c, Rc::new(result)), - location: cursor.get(0)?.location().to(leftover.fallback.location()), - }); + let (result, leftover) = parse_exprv(cursor.step(ctx)?, Some(*c), ctx)?; + let range = current.range.start..leftover.fallback.range.end; + let value = Clause::S(*c, Rc::new(result)); + output.push(Expr { value, range: ctx.range_loc(&range) }); cursor = leftover; }, Lexeme::BS => { - let dot = ctx.interner().i("."); - let (arg, body) = (cursor.step())? - .find("A '.'", |l| l.strict_eq(&Lexeme::Name(dot.clone())))?; + let dot = i("."); + let (arg, body) = (cursor.step(ctx))? + .find("A '.'", ctx, |l| l.strict_eq(&Lexeme::Name(dot.clone())))?; let (arg, _) = parse_exprv(arg, None, ctx)?; let (body, leftover) = parse_exprv(body, paren, ctx)?; output.push(Expr { - location: cursor.location(), + range: ctx.range_loc(&cursor.range()), value: Clause::Lambda(Rc::new(arg), Rc::new(body)), }); return Ok((output, leftover)); }, - _ => { - let err = BadTokenInRegion { - entry: cursor.get(0)?.clone(), - region: "expression", - }; - return Err(err.rc()); + lexeme => { + let lexeme = lexeme.clone(); + let err = BadTokenInRegion { lexeme, region: "expression" }; + return Err(err.pack(ctx.range_loc(¤t.range))); }, } cursor = cursor.trim(); } - Ok((output, Stream::new(cursor.fallback, &[]))) + Ok((output, Frag::new(cursor.fallback, &[]))) } /// Wrap an expression list in parentheses if necessary pub fn vec_to_single( fallback: &Entry, - v: Vec>, -) -> ProjectResult> { + v: Vec, + ctx: &(impl ParseCtx + ?Sized), +) -> ProjectResult { match v.len() { - 0 => Err(UnexpectedEOL { entry: fallback.clone() }.rc()), + 0 => { + let err = UnexpectedEOL(fallback.lexeme.clone()); + Err(err.pack(ctx.range_loc(&fallback.range))) + }, 1 => Ok(v.into_iter().exactly_one().unwrap()), - _ => Ok(Expr { - location: expr_slice_location(&v), - value: Clause::S(PType::Par, Rc::new(v)), - }), + _ => { + let f_range = &v.first().unwrap().range; + let l_range = &v.last().unwrap().range; + let range = f_range.map_range(|r| r.start..l_range.range.end); + Ok(Expr { range, value: Clause::S(PType::Par, Rc::new(v)) }) + }, } } - -/// Return the location of a sequence of consecutive expressions -#[must_use] -pub fn expr_slice_location(v: &[impl AsRef]) -> Location { - v.first() - .map(|l| l.as_ref().clone().to(v.last().unwrap().as_ref().clone())) - .unwrap_or(Location::Unknown) -} diff --git a/src/parse/stream.rs b/src/parse/stream.rs deleted file mode 100644 index 68a6c73..0000000 --- a/src/parse/stream.rs +++ /dev/null @@ -1,128 +0,0 @@ -use super::errors::{ExpectedEOL, NotFound, UnexpectedEOL}; -use super::{Entry, Lexeme}; -use crate::error::{ProjectError, ProjectResult}; -use crate::Location; - -/// Represents a slice which may or may not contain items, and a fallback entry -/// used for error reporting whenever the errant stream is empty. -#[must_use = "streams represent segments of code that must be parsed"] -#[derive(Clone, Copy)] -pub struct Stream<'a> { - /// Entry to place in errors if the stream contains no tokens - pub fallback: &'a Entry, - /// Tokens to parse - pub data: &'a [Entry], -} -impl<'a> Stream<'a> { - /// Create a new stream - pub fn new(fallback: &'a Entry, data: &'a [Entry]) -> Self { - Self { fallback, data } - } - - /// Remove comments and line breaks from both ends of the text - pub fn trim(self) -> Self { - let Self { data, fallback } = self; - let front = data.iter().take_while(|e| e.is_filler()).count(); - let (_, right) = data.split_at(front); - let back = right.iter().rev().take_while(|e| e.is_filler()).count(); - let (data, _) = right.split_at(right.len() - back); - Self { fallback, data } - } - - /// Discard the first entry - pub fn step(self) -> ProjectResult { - let (fallback, data) = (self.data.split_first()) - .ok_or_else(|| UnexpectedEOL { entry: self.fallback.clone() }.rc())?; - Ok(Stream { data, fallback }) - } - - /// Get the first entry - pub fn pop(self) -> ProjectResult<(&'a Entry, Stream<'a>)> { - Ok((self.get(0)?, self.step()?)) - } - - /// Retrieve an index from a slice or raise an [UnexpectedEOL]. - pub fn get(self, idx: usize) -> ProjectResult<&'a Entry> { - self.data.get(idx).ok_or_else(|| { - let entry = self.data.last().unwrap_or(self.fallback).clone(); - UnexpectedEOL { entry }.rc() - }) - } - - /// Area covered by this stream - #[must_use] - pub fn location(self) -> Location { - self.data.first().map_or_else( - || self.fallback.location(), - |f| f.location().to(self.data.last().unwrap().location()), - ) - } - - /// Find a given token, split the stream there and read some value from the - /// separator. See also [Stream::find] - pub fn find_map( - self, - expected: &'static str, - mut f: impl FnMut(&'a Lexeme) -> Option, - ) -> ProjectResult<(Self, T, Self)> { - let Self { data, fallback } = self; - let (dot_idx, output) = skip_parenthesized(data.iter()) - .find_map(|(i, e)| f(&e.lexeme).map(|t| (i, t))) - .ok_or_else(|| NotFound { expected, location: self.location() }.rc())?; - let (left, not_left) = data.split_at(dot_idx); - let (middle_ent, right) = not_left.split_first().unwrap(); - Ok((Self::new(fallback, left), output, Self::new(middle_ent, right))) - } - - /// Split the stream at a token and return just the two sides. - /// See also [Stream::find_map]. - pub fn find( - self, - expected: &'static str, - mut f: impl FnMut(&Lexeme) -> bool, - ) -> ProjectResult<(Self, Self)> { - let (left, _, right) = - self.find_map(expected, |l| if f(l) { Some(()) } else { None })?; - Ok((left, right)) - } - - /// Remove the last item from the stream - pub fn pop_back(self) -> ProjectResult<(&'a Entry, Self)> { - let Self { data, fallback } = self; - let (last, data) = (data.split_last()) - .ok_or_else(|| UnexpectedEOL { entry: fallback.clone() }.rc())?; - Ok((last, Self { fallback, data })) - } - - /// # Panics - /// - /// If the slice is empty - pub fn from_slice(data: &'a [Entry]) -> Self { - let fallback = - (data.first()).expect("Empty slice cannot be converted into a parseable"); - Self { data, fallback } - } - - /// Assert that the stream is empty. - pub fn expect_empty(self) -> ProjectResult<()> { - if let Some(x) = self.data.first() { - Err(ExpectedEOL { location: x.location() }.rc()) - } else { - Ok(()) - } - } -} - -pub fn skip_parenthesized<'a>( - it: impl Iterator, -) -> impl Iterator { - let mut paren_lvl = 1; - it.enumerate().filter(move |(_, e)| { - match e.lexeme { - Lexeme::LP(_) => paren_lvl += 1, - Lexeme::RP(_) => paren_lvl -= 1, - _ => (), - } - paren_lvl <= 1 - }) -} diff --git a/src/parse/string.rs b/src/parse/string.rs index 499de6d..c609139 100644 --- a/src/parse/string.rs +++ b/src/parse/string.rs @@ -1,12 +1,18 @@ +//! Parser for a string literal + +use intern_all::i; use itertools::Itertools; -use super::context::Context; +use super::errors::{ + BadCodePoint, BadEscapeSequence, NoStringEnd, NotHex, ParseErrorKind, +}; #[allow(unused)] // for doc -use super::context::LexerPlugin; -use super::errors::{BadCodePoint, BadEscapeSequence, NoStringEnd, NotHex}; -use crate::error::{ProjectError, ProjectResult}; -use crate::foreign::Atom; -use crate::OrcString; +use super::lex_plugin::LexerPlugin; +use super::lexer::{Entry, LexRes, Lexeme}; +use crate::error::ProjectResult; +use crate::foreign::atom::AtomGenerator; +use crate::foreign::inert::Inert; +use crate::libs::std::string::OrcString; /// Reasons why [parse_string] might fail. See [StringError] pub enum StringErrorKind { @@ -76,56 +82,71 @@ pub fn parse_string(str: &str) -> Result { } /// [LexerPlugin] for a string literal. -pub fn lex_string<'a>( - data: &'a str, - ctx: &dyn Context, -) -> Option> { - data.strip_prefix('"').map(|data| { - let mut leftover = data; - return loop { - let (inside, outside) = (leftover.split_once('"')) - .ok_or_else(|| NoStringEnd(ctx.location(data.len(), "")).rc())?; - let backslashes = inside.chars().rev().take_while(|c| *c == '\\').count(); - if backslashes % 2 == 0 { - // cut form tail to recoup what string_content doesn't have - let (string_data, tail) = data.split_at(data.len() - outside.len() - 1); - let tail = &tail[1..]; // push the tail past the end quote - let string = parse_string(string_data).map_err(|e| { - let start = ctx.pos(data) + e.pos; - let location = ctx.range_loc(start..start + 1); - match e.kind { - StringErrorKind::NotHex => NotHex(location).rc(), - StringErrorKind::BadCodePoint => BadCodePoint(location).rc(), - StringErrorKind::BadEscSeq => BadEscapeSequence(location).rc(), - } - })?; - let tok = ctx.interner().i(&string); - break Ok((Atom::new(OrcString::from(tok)), tail)); - } else { - leftover = outside; - } - }; - }) +pub struct StringLexer; +impl LexerPlugin for StringLexer { + fn lex<'b>( + &self, + req: &'_ dyn super::lex_plugin::LexPluginReq<'b>, + ) -> Option>> { + req.tail().strip_prefix('"').map(|data| { + let mut leftover = data; + return loop { + let (inside, outside) = + (leftover.split_once('"')).ok_or_else(|| { + NoStringEnd.pack(req.ctx().code_range(data.len(), "")) + })?; + let backslashes = + inside.chars().rev().take_while(|c| *c == '\\').count(); + if backslashes % 2 == 0 { + // cut form tail to recoup what string_content doesn't have + let (string_data, tail) = + data.split_at(data.len() - outside.len() - 1); + let tail = &tail[1..]; // push the tail past the end quote + let string = parse_string(string_data).map_err(|e| { + let start = req.ctx().pos(data) + e.pos; + let location = req.ctx().range_loc(&(start..start + 1)); + match e.kind { + StringErrorKind::NotHex => NotHex.pack(location), + StringErrorKind::BadCodePoint => BadCodePoint.pack(location), + StringErrorKind::BadEscSeq => BadEscapeSequence.pack(location), + } + })?; + let output = Inert(OrcString::from(i(&string))); + let ag = AtomGenerator::cloner(output); + let range = req.ctx().range(string_data.len(), tail); + let entry = Entry { lexeme: Lexeme::Atom(ag), range }; + break Ok(LexRes { tokens: vec![entry], tail }); + } else { + leftover = outside; + } + }; + }) + } } -// TODO: rewrite the tree building pipeline step to load files #[cfg(test)] mod test { - use super::lex_string; + use super::StringLexer; + use crate::foreign::inert::Inert; + use crate::libs::std::string::OrcString; use crate::parse::context::MockContext; - use crate::{Interner, OrcString}; + use crate::parse::lex_plugin::{LexPlugReqImpl, LexerPlugin}; + use crate::parse::lexer::{Entry, Lexeme}; #[test] fn plain_string() { let source = r#""hello world!" - says the programmer"#; - let i = Interner::new(); - let (data, tail) = lex_string(source, &MockContext(&i)) + let req = LexPlugReqImpl { ctx: &MockContext, tail: source }; + let res = (StringLexer.lex(&req)) .expect("the snippet starts with a quote") .expect("it contains a valid string"); - assert_eq!( - data.try_downcast::().unwrap().as_str(), - "hello world!" - ); - assert_eq!(tail, " - says the programmer"); + let ag = match &res.tokens[..] { + [Entry { lexeme: Lexeme::Atom(ag), .. }] => ag, + _ => panic!("Expected a single atom"), + }; + let atom = + ag.run().try_downcast::>().expect("Lexed to inert"); + assert_eq!(atom.0.as_str(), "hello world!"); + assert_eq!(res.tail, " - says the programmer"); } } diff --git a/src/pipeline/dealias/mod.rs b/src/pipeline/dealias/mod.rs index b006b5f..d4c99f8 100644 --- a/src/pipeline/dealias/mod.rs +++ b/src/pipeline/dealias/mod.rs @@ -1,4 +1,2 @@ -mod resolve_aliases; +pub mod resolve_aliases; mod walk_with_links; - -pub use resolve_aliases::resolve_aliases; diff --git a/src/pipeline/dealias/resolve_aliases.rs b/src/pipeline/dealias/resolve_aliases.rs index 15dffbd..f77137b 100644 --- a/src/pipeline/dealias/resolve_aliases.rs +++ b/src/pipeline/dealias/resolve_aliases.rs @@ -1,95 +1,174 @@ use std::iter; +use hashbrown::HashMap; +use intern_all::Tok; use itertools::Itertools; use super::walk_with_links::walk_with_links; -use crate::ast::{Expr, Rule}; -use crate::representations::project::{ - ItemKind, ProjectExt, ProjectItem, ProjectMod, +use crate::error::{ErrorPosition, ProjectError, ProjectResult}; +use crate::location::CodeLocation; +use crate::name::{Sym, VPath}; +use crate::parse::parsed::Expr; +use crate::pipeline::project::{ + ItemKind, ProjItem, ProjRule, ProjXEnt, ProjXMod, ProjectMod, SourceModule, }; use crate::tree::{ModEntry, ModMember, Module}; -use crate::utils::pure_seq::pushed; -use crate::{Interner, ProjectTree, Tok, VName}; +use crate::utils::pure_seq::with_pushed; -#[must_use] -fn resolve_aliases_rec( - root: &ProjectMod, - module: &ProjectMod, - updated: &impl Fn(&[Tok]) -> bool, - is_root: bool, -) -> ProjectMod { - if !is_root && !updated(&module.extra.path) { - return module.clone(); +#[derive(Clone)] +struct NotFound { + location: CodeLocation, + last_stop: VPath, + bad_step: Tok, +} +impl ProjectError for NotFound { + const DESCRIPTION: &'static str = "A path pointed out of the tree"; + fn message(&self) -> String { + format!("{} doesn't contain {}", self.last_stop, self.bad_step) } - let process_expr = |expr: &Expr| { - expr - .map_names(&|n| { - let full_name = (module.extra.path.iter()).chain(n.iter()).cloned(); - match walk_with_links(root, full_name, false) { - Ok(rep) => Some(rep.abs_path), - Err(e) => { - let leftovers = e.tail.collect::>(); - if !leftovers.is_empty() { - let full_name = (module.extra.path.iter()) - .chain(n.iter()) - .cloned() - .collect::>(); - let _ = walk_with_links(root, full_name.iter().cloned(), true); - panic!( - "Invalid path {} while resolving {} should have been noticed \ - earlier", - (e.abs_path.into_iter()) - .chain(iter::once(e.name)) - .chain(leftovers.into_iter()) - .join("::"), - Interner::extern_all(&full_name).join("::"), - ); - } - Some(pushed(e.abs_path, e.name)) - }, + fn one_position(&self) -> CodeLocation { self.location.clone() } +} + +struct NameErrors(Vec); +impl ProjectError for NameErrors { + const DESCRIPTION: &'static str = "Some symbols were missing"; + fn positions(&self) -> impl IntoIterator { + self.0.clone().into_iter().map(|nf| ErrorPosition { + location: nf.one_position(), + message: Some(nf.message()), + }) + } +} + +fn resolve_name( + name: Sym, + root: &ProjectMod, + path: &[Tok], + location: CodeLocation, + env: &Module, +) -> Result, NotFound> { + let full_name = path.iter().chain(&name[..]).cloned().collect_vec(); + match walk_with_links(root, full_name.clone().into_iter()) { + Ok(rep) => Ok(Some(rep.abs_path.to_sym())), + Err(mut e) => match e.tail.next() { + // If it got stuck on the very last step, allow it through for + // now in case it is a binding. If the name doesn't get bound, by + // macros it will be raised at the postmacro check. + None => Ok(Some(e.consumed_path().to_sym())), + Some(step) => { + // If there's more, rebuild the last full path after redirects and + // try to resolve it on the env tree. The env tree doesn't contain + // redirects so a plain tree walk is enough. + let fallback_path = (e.abs_path.iter()) + .chain(iter::once(&e.name)) + .cloned() + .chain(iter::once(step)) + .chain(e.tail) + .collect_vec(); + let valid_in_env = env.walk1_ref(&[], &fallback_path, |_| true).is_ok(); + match valid_in_env { + false => Err(NotFound { + location, + last_stop: VPath(e.abs_path), + bad_step: e.name, + }), + true => Ok(e.aliased.then(|| { + Sym::new(fallback_path).expect("Not empty by construction") + })), } - }) - .unwrap_or_else(|| expr.clone()) - }; - Module { - extra: ProjectExt { - path: module.extra.path.clone(), - file: module.extra.file.clone(), - imports_from: module.extra.imports_from.clone(), - rules: (module.extra.rules.iter()) - .map(|Rule { pattern, prio, template }| Rule { - pattern: pattern.iter().map(process_expr).collect(), - template: template.iter().map(process_expr).collect(), - prio: *prio, - }) - .collect(), + }, }, - entries: module - .entries - .iter() - .map(|(k, v)| { - (k.clone(), ModEntry { - exported: v.exported, + } +} + +fn process_expr( + expr: &Expr, + root: &ProjectMod, + path: &[Tok], + errors: &mut Vec, + env: &Module, +) -> Expr { + let location = CodeLocation::Source(expr.range.clone()); + expr + .map_names(&mut |n| { + resolve_name(n, root, path, location.clone(), env).unwrap_or_else(|e| { + errors.push(e); + None + }) + }) + .unwrap_or_else(|| expr.clone()) +} + +fn resolve_aliases_rec( + root: &ProjectMod, + path: &mut Vec>, + module: &ProjectMod, + env: &Module, +) -> ProjectResult { + let mut errors = Vec::new(); + let module = Module { + x: ProjXMod { + src: module.x.src.as_ref().map(|s| SourceModule { + range: s.range.clone(), + rules: (s.rules.iter()) + .map(|ProjRule { pattern, prio, template, comments }| ProjRule { + pattern: (pattern.iter()) + .map(|e| process_expr(e, root, path, &mut errors, env)) + .collect(), + template: (template.iter()) + .map(|e| process_expr(e, root, path, &mut errors, env)) + .collect(), + comments: comments.clone(), + prio: *prio, + }) + .collect(), + }), + }, + entries: (module.entries.iter()) + .map(|(k, v)| -> ProjectResult<_> { + Ok((k.clone(), ModEntry { + x: ProjXEnt { + exported: v.x.exported, + comments: v.x.comments.clone(), + locations: v.x.locations.clone(), + }, member: match &v.member { - ModMember::Sub(module) => - ModMember::Sub(resolve_aliases_rec(root, module, updated, false)), - ModMember::Item(item) => ModMember::Item(ProjectItem { + ModMember::Sub(module) => { + let m = with_pushed(path, k.clone(), |p| { + resolve_aliases_rec(root, p, module, env) + }); + ModMember::Sub(m.1?) + }, + ModMember::Item(item) => ModMember::Item(ProjItem { kind: match &item.kind { - ItemKind::Const(value) => ItemKind::Const(process_expr(value)), - other => other.clone(), + ItemKind::Const(v) => { + let v = process_expr(v, root, path, &mut errors, env); + ItemKind::Const(v) + }, + ItemKind::Alias(n) => { + let location = (v.x.locations.first().cloned()) + .expect("Aliases are never created without source code"); + // this is an absolute path so we set the path to empty + match resolve_name(n.clone(), root, &[], location, env) { + Ok(Some(n)) => ItemKind::Alias(n), + Ok(None) => ItemKind::Alias(n.clone()), + Err(e) => return Err(e.pack()), + } + }, + _ => item.kind.clone(), }, }), }, - }) + })) }) - .collect(), - } + .collect::, _>>()?, + }; + errors.is_empty().then_some(module).ok_or_else(|| NameErrors(errors).pack()) } -#[must_use] pub fn resolve_aliases( - project: ProjectTree, - updated: &impl Fn(&[Tok]) -> bool, -) -> ProjectTree { - ProjectTree(resolve_aliases_rec(&project.0, &project.0, updated, true)) + project: ProjectMod, + env: &Module, +) -> ProjectResult { + resolve_aliases_rec(&project, &mut Vec::new(), &project, env) } diff --git a/src/pipeline/dealias/walk_with_links.rs b/src/pipeline/dealias/walk_with_links.rs index 6a56332..ec72188 100644 --- a/src/pipeline/dealias/walk_with_links.rs +++ b/src/pipeline/dealias/walk_with_links.rs @@ -1,27 +1,20 @@ -#[allow(unused)] // for doc -use crate::representations::project::ProjectEntry; -use crate::representations::project::{ItemKind, ProjectItem, ProjectMod}; +use intern_all::Tok; + +use crate::name::{VName, VPath}; +use crate::pipeline::project::{ItemKind, ProjectMemberRef, ProjectMod}; use crate::tree::ModMember; -use crate::utils::{unwrap_or, BoxedIter}; -use crate::{Interner, NameLike, Tok, VName}; +use crate::utils::boxed_iter::BoxedIter; +use crate::utils::unwrap_or::unwrap_or; -/// The destination of a linked walk. [ProjectEntry] cannot be used for this -/// purpose because it might be the project root. -pub enum Target<'a, N: NameLike> { - Mod(&'a ProjectMod), - Leaf(&'a ProjectItem), -} - -#[must_use = "this is the sole product of this function"] -pub struct WalkReport<'a, N: NameLike> { - pub target: Target<'a, N>, +pub struct WalkReport<'a> { + pub target: ProjectMemberRef<'a>, pub abs_path: VName, pub aliased: bool, } pub struct LinkWalkError<'a> { /// The last known valid path - pub abs_path: VName, + pub abs_path: Vec>, /// The name that wasn't found pub name: Tok, /// Leftover steps @@ -29,29 +22,29 @@ pub struct LinkWalkError<'a> { /// Whether an alias was ever encountered pub aliased: bool, } +impl<'a> LinkWalkError<'a> { + pub fn consumed_path(self) -> VName { + VPath::new(self.abs_path).as_prefix_of(self.name) + } +} -fn walk_with_links_rec<'a, 'b, N: NameLike>( - mut abs_path: VName, - root: &'a ProjectMod, - cur: &'a ProjectMod, - prev_tgt: Target<'a, N>, +fn walk_with_links_rec<'a, 'b>( + mut abs_path: Vec>, + root: &'a ProjectMod, + cur: &'a ProjectMod, + prev_tgt: ProjectMemberRef<'a>, aliased: bool, mut path: impl Iterator> + 'b, - l: bool, -) -> Result, LinkWalkError<'b>> { - let name = unwrap_or! {path.next(); +) -> Result, LinkWalkError<'b>> { + let name = match path.next() { + Some(name) => name, // ends on this module - return Ok(WalkReport{ target: prev_tgt, abs_path, aliased }) + None => { + let abs_path = VName::new(abs_path).expect("Aliases are never empty"); + return Ok(WalkReport { target: prev_tgt, abs_path, aliased }); + }, }; - if l { - eprintln!( - "Resolving {} in {}", - name, - Interner::extern_all(&abs_path).join("::") - ) - } let entry = unwrap_or! {cur.entries.get(&name); { - // panic!("No entry {name} on {}", Interner::extern_all(&cur.extra.path).join("::")); // leads into a missing branch return Err(LinkWalkError{ abs_path, aliased, name, tail: Box::new(path) }) }}; @@ -59,24 +52,19 @@ fn walk_with_links_rec<'a, 'b, N: NameLike>( ModMember::Sub(m) => { // leads into submodule abs_path.push(name); - walk_with_links_rec(abs_path, root, m, Target::Mod(m), aliased, path, l) + let tgt = ProjectMemberRef::Mod(m); + walk_with_links_rec(abs_path, root, m, tgt, aliased, path) }, ModMember::Item(item) => match &item.kind { ItemKind::Alias(alias) => { // leads into alias (reset acc, cur, cur_entry) - if l { - eprintln!( - "{} points to {}", - Interner::extern_all(&abs_path).join("::"), - Interner::extern_all(alias).join("::") - ) - } - abs_path.clone_from(alias); + abs_path.clear(); + abs_path.extend_from_slice(&alias[..]); abs_path.extend(path); let path_acc = Vec::with_capacity(abs_path.len()); let new_path = abs_path.into_iter(); - let tgt = Target::Mod(root); - walk_with_links_rec(path_acc, root, root, tgt, true, new_path, l) + let tgt = ProjectMemberRef::Mod(root); + walk_with_links_rec(path_acc, root, root, tgt, true, new_path) }, ItemKind::Const(_) | ItemKind::None => { abs_path.push(name); @@ -88,7 +76,8 @@ fn walk_with_links_rec<'a, 'b, N: NameLike>( }, None => { // ends on leaf - let target = Target::Leaf(item); + let target = ProjectMemberRef::Item(item); + let abs_path = VName::new(abs_path).expect("pushed just above"); Ok(WalkReport { target, abs_path, aliased }) }, } @@ -100,24 +89,16 @@ fn walk_with_links_rec<'a, 'b, N: NameLike>( /// Execute a walk down the tree, following aliases. /// If the path ends on an alias, that alias is also resolved. /// If the path leads out of the tree, the shortest failing path is returned -pub fn walk_with_links<'a, N: NameLike>( - root: &ProjectMod, +pub fn walk_with_links<'a>( + root: &ProjectMod, path: impl Iterator> + 'a, - l: bool, -) -> Result, LinkWalkError<'a>> { +) -> Result, LinkWalkError<'a>> { let path_acc = path.size_hint().1.map_or_else(Vec::new, Vec::with_capacity); - let mut result = walk_with_links_rec( - path_acc, - root, - root, - Target::Mod(root), - false, - path, - l, - ); + let tgt = ProjectMemberRef::Mod(root); + let mut result = walk_with_links_rec(path_acc, root, root, tgt, false, path); // cut off excess preallocated space within normal vector growth policy let abs_path = match &mut result { - Ok(rep) => &mut rep.abs_path, + Ok(rep) => rep.abs_path.vec_mut(), Err(err) => &mut err.abs_path, }; abs_path.shrink_to(abs_path.len().next_power_of_two()); diff --git a/src/pipeline/file_loader.rs b/src/pipeline/file_loader.rs deleted file mode 100644 index 9e7e857..0000000 --- a/src/pipeline/file_loader.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Source loader callback definition and builtin implementations -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::{fs, io}; - -use hashbrown::{HashMap, HashSet}; -use rust_embed::RustEmbed; - -use crate::error::{ProjectError, ProjectResult}; -#[allow(unused)] // for doc -use crate::facade::System; -use crate::interner::Interner; -use crate::utils::Cache; -use crate::{Location, Stok, Tok, VName}; - -/// All the data available about a failed source load call -#[derive(Debug)] -pub struct FileLoadingError { - file: io::Error, - dir: io::Error, - path: VName, -} -impl ProjectError for FileLoadingError { - fn description(&self) -> &str { - "Neither a file nor a directory could be read from the requested path" - } - fn one_position(&self) -> crate::Location { - Location::File(Arc::new(self.path.clone())) - } - fn message(&self) -> String { - format!("File: {}\nDirectory: {}", self.file, self.dir) - } -} - -/// Represents the result of loading code from a string-tree form such -/// as the file system. -#[derive(Clone, PartialEq, Eq, Hash)] -pub enum Loaded { - /// Conceptually equivalent to a sourcefile - Code(Arc), - /// Conceptually equivalent to the list of *.orc files in a folder, without - /// the extension - Collection(Arc>), -} -impl Loaded { - /// Is the loaded item source code (not a collection)? - pub fn is_code(&self) -> bool { matches!(self, Loaded::Code(_)) } -} - -/// Returned by any source loading callback -pub type IOResult = ProjectResult; - -/// Load a file from a path expressed in Rust strings, but relative to -/// a root expressed as an OS Path. -pub fn load_file(root: &Path, path: &[Tok]) -> IOResult { - let full_path = path.iter().fold(root.to_owned(), |p, t| p.join(t.as_str())); - let file_path = full_path.with_extension("orc"); - let file_error = match fs::read_to_string(file_path) { - Ok(string) => return Ok(Loaded::Code(Arc::new(string))), - Err(err) => err, - }; - let dir = match fs::read_dir(&full_path) { - Ok(dir) => dir, - Err(dir_error) => - return Err( - FileLoadingError { - file: file_error, - dir: dir_error, - path: path.to_vec(), - } - .rc(), - ), - }; - let names = dir - .filter_map(Result::ok) - .filter_map(|ent| { - let fname = ent.file_name().into_string().ok()?; - let ftyp = ent.metadata().ok()?.file_type(); - Some(if ftyp.is_dir() { - fname - } else { - fname.strip_suffix(".or")?.to_string() - }) - }) - .collect(); - Ok(Loaded::Collection(Arc::new(names))) -} - -/// Generates a cached file loader for a directory -#[must_use] -pub fn mk_dir_cache(root: PathBuf) -> Cache<'static, VName, IOResult> { - Cache::new(move |vname: VName, _this| load_file(&root, &vname)) -} - -/// Load a file from the specified path from an embed table -/// -/// # Panics -/// -/// if the `RustEmbed` includes files that do not end in `ext` -pub fn load_embed(path: &str, ext: &str) -> IOResult { - let file_path = path.to_string() + ext; - if let Some(file) = T::get(&file_path) { - let s = - String::from_utf8(file.data.to_vec()).expect("Embed must be valid UTF-8"); - Ok(Loaded::Code(Arc::new(s))) - } else { - let entries = T::iter() - .map(|c| c.to_string()) - .filter_map(|path: String| { - let item_prefix = path.to_string() + "/"; - path.strip_prefix(&item_prefix).map(|subpath| { - let item_name = subpath - .split_inclusive('/') - .next() - .expect("Exact match excluded earlier"); - item_name - .strip_suffix('/') // subdirectory - .or_else(|| item_name.strip_suffix(ext)) // file - .expect("embed should be filtered to extension") - .to_string() - }) - }) - .collect::>(); - Ok(Loaded::Collection(Arc::new(entries))) - } -} - -/// Generates a cached file loader for a [RustEmbed] -#[must_use] -pub fn mk_embed_cache( - ext: &str, -) -> Cache<'_, Vec, IOResult> { - Cache::new(move |vname: VName, _this| -> IOResult { - let path = Interner::extern_all(&vname).join("/"); - load_embed::(&path, ext) - }) -} - -/// Load all files from an embed and convert them into a map usable in a -/// [System] -#[must_use] -pub fn embed_to_map( - suffix: &str, - i: &Interner, -) -> HashMap, Loaded> { - let mut files = HashMap::new(); - let mut dirs = HashMap::new(); - for path in T::iter() { - let vpath = path - .strip_suffix(suffix) - .expect("the embed must be filtered for suffix") - .split('/') - .map(|s| s.to_string()) - .collect::>(); - let tokvpath = vpath.iter().map(|segment| i.i(segment)).collect::>(); - let data = T::get(&path).expect("path from iterator").data; - let text = - String::from_utf8(data.to_vec()).expect("code embeds must be utf-8"); - files.insert(tokvpath.clone(), text); - for (lvl, subname) in vpath.iter().enumerate() { - let dirname = tokvpath.split_at(lvl).0; - let (_, entries) = (dirs.raw_entry_mut().from_key(dirname)) - .or_insert_with(|| (dirname.to_vec(), HashSet::new())); - entries.get_or_insert_with(subname, Clone::clone); - } - } - (files.into_iter()) - .map(|(k, s)| (k, Loaded::Code(Arc::new(s)))) - .chain((dirs.into_iter()).map(|(k, entv)| { - (k, Loaded::Collection(Arc::new(entv.into_iter().collect()))) - })) - .collect() -} diff --git a/src/pipeline/import_abs_path.rs b/src/pipeline/import_abs_path.rs deleted file mode 100644 index df1bd2f..0000000 --- a/src/pipeline/import_abs_path.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::error::ProjectResult; -use crate::interner::{Interner, Tok}; -use crate::representations::sourcefile::absolute_path; -use crate::utils::substack::Substack; -use crate::{Location, VName}; - -pub fn import_abs_path( - src_path: &[Tok], - mod_stack: Substack>, - import_path: &[Tok], - i: &Interner, - location: &Location, -) -> ProjectResult { - // path of module within file - let mod_pathv = mod_stack.iter().rev_vec_clone(); - // path of module within compilation - let abs_pathv = - (src_path.iter()).chain(mod_pathv.iter()).cloned().collect::>(); - // preload-target path relative to module - // preload-target path within compilation - absolute_path(&abs_pathv, import_path, i, location) -} diff --git a/src/pipeline/load_solution.rs b/src/pipeline/load_solution.rs new file mode 100644 index 0000000..a30fa21 --- /dev/null +++ b/src/pipeline/load_solution.rs @@ -0,0 +1,239 @@ +//! Load an Orchid project by starting from one or more entry points and +//! following the imports + +use std::collections::VecDeque; +use std::sync::Arc; + +use hashbrown::{HashMap, HashSet}; +use intern_all::{sweep_t, Tok}; + +use super::dealias::resolve_aliases::resolve_aliases; +use super::process_source::{process_ns, resolve_globs, GlobImports}; +use super::project::{ItemKind, ProjItem, ProjXEnt, ProjectMod, ProjectTree}; +use crate::error::{ + bundle_location, ErrorPosition, ProjectError, ProjectResult, +}; +use crate::location::{CodeGenInfo, CodeLocation, SourceCode, SourceRange}; +use crate::name::{PathSlice, Sym, VName, VPath}; +use crate::parse::context::ParseCtxImpl; +use crate::parse::facade::parse_file; +use crate::parse::lex_plugin::LexerPlugin; +use crate::parse::parse_plugin::ParseLinePlugin; +use crate::tree::{ModEntry, ModMember, Module}; +use crate::utils::combine::Combine; +use crate::utils::sequence::Sequence; +use crate::virt_fs::{Loaded, VirtFS}; + +// apply layer: +// 1. build trees +// Question: what if a file is not found? +// - raising an error would risk failing on a std module +// - moving on could obscure very simple errors +// can we get rid of layers and show system sources alongside user sources? +// what would break? Can we break it? +// the project moves into a prefix, imports are either super:: or user:: +// custom support for root:: specifier +// virtual file tree is back on +// systems get free reign on their subtree, less jank +// would also solve some weird accidental private member aliasing issues + +/// Split off the longest prefix accepted by the validator +fn split_max_prefix<'a, T>( + path: &'a [T], + is_valid: &impl Fn(&[T]) -> bool, +) -> Option<(&'a [T], &'a [T])> { + (0..=path.len()) + .rev() + .map(|i| path.split_at(i)) + .find(|(file, _)| is_valid(file)) +} + +/// Represents a prelude / implicit import requested by a library. +/// A prelude extends any module with a glob import from the target module +/// unless its path begins with exclude. +#[derive(Debug, Clone)] +pub struct Prelude { + /// Path the glob imports will point to + pub target: VName, + /// subtree to exclude (typically the region the prelude collates items from) + pub exclude: VName, + /// Location data attached to the aliases + pub owner: CodeGenInfo, +} + +/// Hooks and extensions to the source loading process +#[derive(Clone)] +pub struct SolutionContext<'a> { + /// Callbacks from the lexer to support literals of custom datatypes + pub lexer_plugins: Sequence<'a, &'a (dyn LexerPlugin + 'a)>, + /// Callbacks from the parser to support custom module tree elements + pub line_parsers: Sequence<'a, &'a (dyn ParseLinePlugin + 'a)>, + /// Lines prepended to various modules to import "global" values + pub preludes: Sequence<'a, &'a Prelude>, +} +impl<'a> SolutionContext<'a> { + /// Derive context for the parser + pub fn parsing(&self, code: SourceCode) -> ParseCtxImpl<'a> { + ParseCtxImpl { + code, + lexers: self.lexer_plugins.clone(), + line_parsers: self.line_parsers.clone(), + } + } +} + +/// Load source files from a source tree and parse them starting from the +/// specified targets and following imports. An in-memory environment tree is +/// used to allow imports from modules that are defined by other loading steps +/// and later merged into this source code. +pub fn load_solution( + ctx: SolutionContext, + targets: impl IntoIterator, + env: &Module, + fs: &impl VirtFS, +) -> ProjectResult { + let mut target_queue = VecDeque::<(Sym, CodeLocation)>::new(); + target_queue.extend(targets.into_iter()); + target_queue.extend( + (ctx.preludes.iter()) + .map(|p| (p.target.to_sym(), CodeLocation::Gen(p.owner.clone()))), + ); + let mut known_files = HashSet::new(); + let mut tree_acc: ProjectMod = Module::wrap([]); + let mut glob_acc: GlobImports = Module::wrap([]); + while let Some((target, referrer)) = target_queue.pop_front() { + let path_parts = split_max_prefix(&target[..], &|p| { + fs.read(PathSlice(p)).map(|l| l.is_code()).unwrap_or(false) + }); + if let Some((filename, _)) = path_parts { + if known_files.contains(filename) { + continue; + } + known_files.insert(filename.to_vec()); + let path = VPath(filename.to_vec()); + let loaded = fs + .read(PathSlice(filename)) + .map_err(|e| bundle_location(&referrer, &*e))?; + let code = match loaded { + Loaded::Collection(_) => + return Err(UnexpectedDirectory { path }.pack()), + Loaded::Code(source) => SourceCode { source, path: Arc::new(path) }, + }; + let full_range = + SourceRange { range: 0..code.source.len(), code: code.clone() }; + let lines = parse_file(&ctx.parsing(code.clone()))?; + let report = process_ns(code.path, lines, full_range)?; + target_queue.extend( + (report.external_references.into_iter()) + .map(|(k, v)| (k, CodeLocation::Source(v))), + ); + if !report.comments.is_empty() && filename.is_empty() { + todo!("panic - module op comments on root are lost") + } + let mut comments = Some(report.comments); + let mut module = report.module; + let mut glob = report.glob_imports; + for i in (0..filename.len()).rev() { + // i over valid indices of filename + let key = filename[i].clone(); // last segment + let comments = comments.take().into_iter().flatten().collect(); + glob = + Module::wrap([(key.clone(), ModEntry::wrap(ModMember::Sub(glob)))]); + module = Module::wrap([(key, ModEntry { + member: ModMember::Sub(module), + x: ProjXEnt { comments, ..Default::default() }, + })]); + } + glob_acc = (glob_acc.combine(glob)) + .expect("source code loaded for two nested paths"); + tree_acc = (tree_acc.combine(module)) + .expect("source code loaded for two nested paths"); + } else { + known_files.insert(target[..].to_vec()); + // If the path is not within a file, load it as directory + match fs.read(target.as_path_slice()) { + Ok(Loaded::Collection(c)) => target_queue + .extend(c.iter().map(|e| (Sym::parse(e).unwrap(), referrer.clone()))), + Ok(Loaded::Code(_)) => unreachable!("Should have split to self and []"), + // Ignore error if the path is walkable in the const tree + Err(_) if env.walk1_ref(&[], &target[..], |_| true).is_ok() => (), + Err(e) => return Err(bundle_location(&referrer, &*e)), + } + } + } + let mut contention = HashMap::new(); + resolve_globs( + VPath(vec![]), + glob_acc, + ctx.preludes.clone(), + &mut tree_acc, + env, + &mut contention, + )?; + let ret = resolve_aliases(tree_acc, env)?; + for ((glob, original), locations) in contention { + let (glob_val, _) = ret + .walk1_ref(&[], &glob[..], |_| true) + .expect("Should've emerged in dealias"); + let (original_val, _) = ret + .walk1_ref(&[], &original[..], |_| true) + .expect("Should've emerged in dealias"); + let glob_real = match &glob_val.member { + ModMember::Item(ProjItem { kind: ItemKind::Alias(glob_tgt) }) => glob_tgt, + _ => &glob, + }; + let original_real = match &original_val.member { + ModMember::Item(ProjItem { kind: ItemKind::Alias(orig_tgt) }) => orig_tgt, + _ => &original, + }; + if glob_real != original_real { + let real = original_real.clone(); + let glob_real = glob_real.clone(); + let err = ConflictingGlobs { real, glob_real, original, glob, locations }; + return Err(err.pack()); + } + } + sweep_t::(); + sweep_t::>>(); + Ok(ProjectTree(ret)) +} + +/// Produced when a stage that deals specifically with code encounters +/// a path that refers to a directory +#[derive(Debug)] +struct UnexpectedDirectory { + /// Path to the offending collection + pub path: VPath, +} +impl ProjectError for UnexpectedDirectory { + const DESCRIPTION: &'static str = "A stage that deals specifically with code \ + encountered a path that refers to a directory"; + fn message(&self) -> String { + format!("{} was expected to be a file", self.path) + } + fn positions(&self) -> impl IntoIterator { [] } +} + +#[derive(Debug)] +struct ConflictingGlobs { + original: Sym, + real: Sym, + glob: Sym, + glob_real: Sym, + locations: Vec, +} +impl ProjectError for ConflictingGlobs { + const DESCRIPTION: &'static str = + "A symbol from a glob import conflicts with an existing name"; + fn message(&self) -> String { + let Self { glob, glob_real, original, real, .. } = self; + format!( + "glob import included {glob} which resolved to {glob_real}. \ + This conflicts with {original} which resolved to {real}" + ) + } + fn positions(&self) -> impl IntoIterator { + (self.locations.iter()) + .map(|l| ErrorPosition { location: l.clone(), message: None }) + } +} diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index c31393b..e6244ff 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -1,10 +1,6 @@ -//! Loading Orchid modules from source +//! Loading Orchid projects from source mod dealias; -pub mod file_loader; -mod import_abs_path; -mod parse_layer; -mod project_tree; -mod source_loader; -// mod tree_loader; - -pub use parse_layer::parse_layer; +pub mod load_solution; +mod path; +mod process_source; +pub mod project; diff --git a/src/pipeline/parse_layer.rs b/src/pipeline/parse_layer.rs deleted file mode 100644 index def9090..0000000 --- a/src/pipeline/parse_layer.rs +++ /dev/null @@ -1,44 +0,0 @@ -use super::dealias::resolve_aliases; -use super::file_loader::IOResult; -use super::{project_tree, source_loader}; -use crate::error::ProjectResult; -use crate::interner::{Interner, Tok}; -use crate::parse::{LexerPlugin, LineParser}; -use crate::representations::sourcefile::FileEntry; -use crate::representations::VName; -use crate::utils::never; -use crate::ProjectTree; - -/// Using an IO callback, produce a project tree that includes the given -/// target symbols or files if they're defined. -/// -/// The environment accessible to the loaded source can be specified with -/// a pre-existing tree which will be merged with the loaded data, and a -/// prelude which will be prepended to each individual file. Since the -/// prelude gets compiled with each file, normally it should be a glob -/// import pointing to a module in the environment. -pub fn parse_layer<'a>( - targets: impl Iterator]>, - loader: &impl Fn(&[Tok], &[Tok]) -> IOResult, - environment: &'a ProjectTree, - prelude: &[FileEntry], - lexer_plugins: &[&dyn LexerPlugin], - line_parsers: &[&dyn LineParser], - i: &Interner, -) -> ProjectResult> { - let sl_ctx = - source_loader::Context { prelude, i, lexer_plugins, line_parsers }; - let (preparsed, source) = - source_loader::load_source(targets, sl_ctx, loader, &|path| { - environment.0.walk_ref(&[], path, false).is_ok() - })?; - let tree = - project_tree::rebuild_tree(&source, preparsed, environment, prelude, i)?; - let sum = ProjectTree(never::unwrap_always( - environment.0.clone().overlay(tree.0.clone()), - )); - let resolvd = - resolve_aliases(sum, &|path| tree.0.walk1_ref(&[], path, false).is_ok()); - // Addition among modules favours the left hand side. - Ok(resolvd) -} diff --git a/src/pipeline/path.rs b/src/pipeline/path.rs new file mode 100644 index 0000000..a0ffee2 --- /dev/null +++ b/src/pipeline/path.rs @@ -0,0 +1,82 @@ +use intern_all::{i, Tok}; + +use crate::error::{ProjectError, ProjectResult}; +use crate::location::CodeLocation; +use crate::name::VName; + +/// Turn a relative (import) path into an absolute path. +/// If the import path is empty, the return value is also empty. +/// +/// # Errors +/// +/// if the relative path contains as many or more `super` segments than the +/// length of the absolute path. +pub(super) fn absolute_path( + abs_location: &[Tok], + rel_path: &[Tok], + location: CodeLocation, +) -> ProjectResult { + match absolute_path_rec(abs_location, rel_path) { + Some(v) => VName::new(v).map_err(|_| ImportAll { location }.pack()), + None => { + let path = rel_path.try_into().expect("At least one super"); + Err(TooManySupers { path, location }.pack()) + }, + } +} + +#[must_use = "this could be None which means that there are too many supers"] +fn absolute_path_rec( + mut abs_location: &[Tok], + mut rel_path: &[Tok], +) -> Option>> { + let mut relative = false; + if rel_path.first().cloned() == Some(i("self")) { + relative = true; + rel_path = rel_path.split_first().expect("checked above").1; + } else { + while rel_path.first().cloned() == Some(i("super")) { + match abs_location.split_last() { + Some((_, torso)) => abs_location = torso, + None => return None, + }; + rel_path = rel_path.split_first().expect("checked above").1; + relative = true; + } + } + match relative { + true => Some(abs_location.iter().chain(rel_path).cloned().collect()), + false => Some(rel_path.to_vec()), + } +} + +/// Error produced when an import path starts with more `super` segments +/// than the current module's absolute path +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct TooManySupers { + /// The offending import path + pub path: VName, + /// The faulty import statement + pub location: CodeLocation, +} +impl ProjectError for TooManySupers { + const DESCRIPTION: &'static str = "an import path starts with more \ + `super` segments than the current module's absolute path"; + fn message(&self) -> String { + format!("path {} contains too many `super` steps.", self.path) + } + + fn one_position(&self) -> CodeLocation { self.location.clone() } +} + +/// Error produced for the statement `import *` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct ImportAll { + /// The file containing the offending import + pub location: CodeLocation, +} +impl ProjectError for ImportAll { + const DESCRIPTION: &'static str = "a top-level glob import was used"; + fn message(&self) -> String { format!("{} imports *", self.location) } + fn one_position(&self) -> CodeLocation { self.location.clone() } +} diff --git a/src/pipeline/process_source.rs b/src/pipeline/process_source.rs new file mode 100644 index 0000000..0fa144c --- /dev/null +++ b/src/pipeline/process_source.rs @@ -0,0 +1,358 @@ +use std::mem; +use std::sync::Arc; + +use hashbrown::HashMap; +use intern_all::Tok; +use itertools::Itertools; +use never::Never; + +use super::load_solution::Prelude; +use super::path::absolute_path; +use super::project::{ + ItemKind, ProjItem, ProjRule, ProjXEnt, ProjXMod, ProjectEntry, ProjectMod, + SourceModule, +}; +use crate::error::{ + ErrorPosition, ProjectError, ProjectErrorObj, ProjectResult, +}; +use crate::location::{CodeLocation, SourceRange}; +use crate::name::{Sym, VName, VPath}; +use crate::parse::parsed::{ + Constant, Member, MemberKind, ModuleBlock, Rule, SourceLine, SourceLineKind, +}; +use crate::tree::{ModEntry, ModMember, Module, WalkError}; +use crate::utils::combine::Combine; +use crate::utils::get_or::get_or_make; +use crate::utils::pure_seq::pushed_ref; +use crate::utils::sequence::Sequence; +use crate::utils::unwrap_or::unwrap_or; + +// Problem: import normalization +// +// Imports aren't explicitly present in the tree we're currently producing. +// Named imports can be placed in Aliases, but glob imports should +// not be included in the Project Tree. A separate Glob Import Tree +// should be produced, which preferably utilizes the existing [Module] +// tree. Then a postprocessing step can use the GIT to both look up the exports +// and write them back into &mut PT. + +/// This tree contains the absolute path of glob imports. + +#[derive(Debug, Clone)] +pub(super) struct GlobImpReport { + pub target: VName, + pub location: SourceRange, +} + +#[derive(Debug, Clone, Default)] +pub(super) struct GlobImpXMod(pub Vec); +impl Combine for GlobImpXMod { + type Error = Never; + fn combine(self, other: Self) -> Result { + Ok(GlobImpXMod(self.0.into_iter().chain(other.0).collect())) + } +} + +pub(super) type GlobImports = Module; + +pub(super) struct FileReport { + /// Absolute path of values outside the file + pub external_references: HashMap, + pub comments: Vec>, + pub module: ProjectMod, + pub glob_imports: GlobImports, +} + +fn default_entry() -> ProjectEntry { + ProjectEntry { + member: ModMember::Item(ProjItem::default()), + x: ProjXEnt { comments: vec![], locations: vec![], exported: false }, + } +} + +pub(super) fn process_ns( + path: Arc, + lines: Vec, + ns_location: SourceRange, +) -> ProjectResult { + let mut file_comments = Vec::new(); + let mut new_comments = Vec::new(); + let mut entries = HashMap::new(); + let mut external_references = HashMap::new(); + let mut rules = Vec::new(); + let wrap = Module::wrap([]); + let mut glob_imports: GlobImports = wrap; + for SourceLine { kind: line_kind, range } in lines { + match line_kind { + SourceLineKind::Comment(comment) => new_comments.push(Arc::new(comment)), + SourceLineKind::Export(names) => { + let comments = (names.len() == 1).then(|| mem::take(&mut new_comments)); + for (name, name_location) in names { + let entry = get_or_make(&mut entries, &name, default_entry); + let location = CodeLocation::Source(name_location); + if entry.x.exported { + let err = MultipleExports { + path: (*path).clone().as_prefix_of(name).to_sym(), + locations: pushed_ref(&entry.x.locations, location), + }; + return Err(err.pack()); + } + entry.x.exported = true; + entry.x.comments.extend(comments.iter().flatten().cloned()); + entry.x.locations.push(location); + } + }, + SourceLineKind::Import(imports) => { + file_comments.append(&mut new_comments); + for import in imports { + let nonglob_path = import.nonglob_path(); + let location = CodeLocation::Source(range.clone()); + let abs = absolute_path(&path[..], &nonglob_path[..], location)?; + if !abs[..].starts_with(&path[..]) { + external_references.insert(abs.to_sym(), import.range.clone()); + } + match import.name { + None => (glob_imports.x.0) + .push(GlobImpReport { target: abs, location: import.range }), + Some(key) => { + let entry = get_or_make(&mut entries, &key, default_entry); + entry.x.locations.push(CodeLocation::Source(import.range)); + match &mut entry.member { + ModMember::Item(ProjItem { kind: old @ ItemKind::None }) => + *old = ItemKind::Alias(abs.to_sym()), + _ => { + let err = MultipleDefinitions { + path: (*path).clone().as_prefix_of(key).to_sym(), + locations: entry.x.locations.clone(), + }; + return Err(err.pack()); + }, + } + }, + } + } + }, + SourceLineKind::Member(Member { exported, kind }) => match kind { + MemberKind::Constant(Constant { name, value }) => { + let entry = get_or_make(&mut entries, &name, default_entry); + entry.x.locations.push(CodeLocation::Source(range)); + match &mut entry.member { + ModMember::Item(ProjItem { kind: old @ ItemKind::None }) => + *old = ItemKind::Const(value), + _ => { + let err = MultipleDefinitions { + path: (*path).clone().as_prefix_of(name).to_sym(), + locations: entry.x.locations.clone(), + }; + return Err(err.pack()); + }, + } + entry.x.exported |= exported; + entry.x.comments.append(&mut new_comments); + }, + MemberKind::Rule(Rule { pattern, prio, template }) => { + let prule = + ProjRule { pattern, prio, template, comments: new_comments }; + new_comments = Vec::new(); + for name in prule.collect_root_names() { + let entry = get_or_make(&mut entries, &name, default_entry); + entry.x.locations.push(CodeLocation::Source(range.clone())); + if entry.x.exported && exported { + let err = MultipleExports { + path: (*path).clone().as_prefix_of(name).to_sym(), + locations: entry.x.locations.clone(), + }; + return Err(err.pack()); + } + entry.x.exported |= exported; + } + rules.push(prule); + }, + MemberKind::Module(ModuleBlock { name, body }) => { + let entry = get_or_make(&mut entries, &name, default_entry); + entry.x.locations.push(CodeLocation::Source(range.clone())); + if !matches!( + entry.member, + ModMember::Item(ProjItem { kind: ItemKind::None }) + ) { + let err = MultipleDefinitions { + path: (*path).clone().as_prefix_of(name).to_sym(), + locations: entry.x.locations.clone(), + }; + return Err(err.pack()); + } + if entry.x.exported && exported { + let err = MultipleExports { + path: (*path).clone().as_prefix_of(name).to_sym(), + locations: entry.x.locations.clone(), + }; + return Err(err.pack()); + } + let subpath = Arc::new(VPath(pushed_ref(&path.0, name.clone()))); + let report = process_ns(subpath, body, range)?; + entry.x.comments.append(&mut new_comments); + entry.x.comments.extend(report.comments); + entry.x.exported |= exported; + entry.member = ModMember::Sub(report.module); + // record new external references + external_references.extend( + (report.external_references.into_iter()) + .filter(|(r, _)| !r[..].starts_with(&path.0)), + ); + // add glob_imports subtree to own tree + glob_imports.entries.insert(name, ModEntry { + x: (), + member: ModMember::Sub(report.glob_imports), + }); + }, + }, + } + } + Ok(FileReport { + external_references, + comments: file_comments, + glob_imports, + module: Module { + entries, + x: ProjXMod { src: Some(SourceModule { range: ns_location, rules }) }, + }, + }) +} + +fn walk_at_path( + e: WalkError, + root: &ProjectMod, + path: &[Tok], +) -> ProjectErrorObj { + let submod = (root.walk_ref(&[], path, |_| true)) + .expect("Invalid source path in walk error populator"); + let src = + submod.x.src.as_ref().expect("Import cannot appear in implied module"); + e.at(&CodeLocation::Source(src.range.clone())) +} + +/// Resolve the glob tree separately produced by [process_ns] by looking up the +/// keys of the referenced module and creating an [ItemKind::Alias] for each of +/// them. Supports a prelude table which is applied to each module, and an +/// environment whose keys are combined with those from within the [ProjectMod]. +pub fn resolve_globs( + globtree_prefix: VPath, + globtree: GlobImports, + preludes: Sequence<&Prelude>, + project_root: &mut ProjectMod, + env: &Module, + contention: &mut HashMap<(Sym, Sym), Vec>, +) -> ProjectResult<()> { + // All glob imports in this module + let all = (globtree.x.0.into_iter()) + .map(|gir| (gir.target, CodeLocation::Source(gir.location))) + .chain( + preludes + .iter() + .filter(|&pre| !globtree_prefix.0.starts_with(&pre.exclude[..])) + .map(|Prelude { target, owner, .. }| { + (target.clone(), CodeLocation::Gen(owner.clone())) + }), + ); + for (target, location) in all { + let (tgt, parent) = project_root + .inner_walk(&globtree_prefix.0, &target[..], |e| e.x.exported) + .map_err(|e| walk_at_path(e, project_root, &globtree_prefix.0))?; + match &tgt.member { + ModMember::Item(_) => { + use crate::tree::ErrKind::NotModule; + let options = Sequence::new(|| parent.keys(|e| e.x.exported)); + let e = WalkError::last(&target[..], NotModule, options); + return Err(walk_at_path(e, project_root, &globtree_prefix.0)); + }, + ModMember::Sub(module) => { + // All public keys in this module and, if walkable, the environment. + let pub_keys = (env.walk_ref(&[], &target[..], |_| true).into_iter()) + .flat_map(|m| m.keys(|_| true)) + .chain(module.keys(|e| e.x.exported)) + .collect_vec(); + // Reference to the module to be modified + let mut_mod = + globtree_prefix.0.iter().fold(&mut *project_root, |m, k| { + let entry = m.entries.get_mut(k).expect("this is a source path"); + unwrap_or!(&mut entry.member => ModMember::Sub; { + panic!("This is also a source path") + }) + }); + // Walk errors for the environment are suppressed because leaf-node + // conflicts will emerge when merging modules, and walking off the tree + // is valid. + for key in pub_keys { + let entry = get_or_make(&mut mut_mod.entries, &key, default_entry); + entry.x.locations.push(location.clone()); + let alias_tgt = target.clone().suffix([key.clone()]).to_sym(); + match &mut entry.member { + ModMember::Item(ProjItem { kind: kref @ ItemKind::None }) => + *kref = ItemKind::Alias(alias_tgt), + ModMember::Item(ProjItem { kind: ItemKind::Alias(prev_alias) }) => + if prev_alias != &alias_tgt { + let local_name = + globtree_prefix.clone().as_prefix_of(key.clone()).to_sym(); + let locs = pushed_ref(&entry.x.locations, location.clone()); + contention.insert((alias_tgt, local_name), locs); + }, + _ => { + let err = MultipleDefinitions { + locations: entry.x.locations.clone(), + path: globtree_prefix.as_prefix_of(key).to_sym(), + }; + return Err(err.pack()); + }, + } + } + }, + } + } + for (key, entry) in globtree.entries { + match entry.member { + ModMember::Item(n) => match n {}, + ModMember::Sub(module) => { + let subpath = VPath(pushed_ref(&globtree_prefix.0, key)); + resolve_globs( + subpath, + module, + preludes.clone(), + project_root, + env, + contention, + )?; + }, + } + } + Ok(()) +} + +struct MultipleExports { + path: Sym, + locations: Vec, +} +impl ProjectError for MultipleExports { + const DESCRIPTION: &'static str = "A symbol was exported in multiple places"; + fn message(&self) -> String { + format!("{} exported multiple times", self.path) + } + fn positions(&self) -> impl IntoIterator { + (self.locations.iter()) + .map(|l| ErrorPosition { location: l.clone(), message: None }) + } +} + +pub(super) struct MultipleDefinitions { + pub(super) path: Sym, + pub(super) locations: Vec, +} +impl ProjectError for MultipleDefinitions { + const DESCRIPTION: &'static str = "Symbol defined twice"; + fn message(&self) -> String { + format!("{} refers to multiple conflicting items", self.path) + } + fn positions(&self) -> impl IntoIterator { + (self.locations.iter()) + .map(|l| ErrorPosition { location: l.clone(), message: None }) + } +} diff --git a/src/pipeline/project.rs b/src/pipeline/project.rs new file mode 100644 index 0000000..fa55658 --- /dev/null +++ b/src/pipeline/project.rs @@ -0,0 +1,271 @@ +//! Datastructures used to define an Orchid project + +use std::fmt::Display; +use std::sync::Arc; + +use hashbrown::{HashMap, HashSet}; +use intern_all::Tok; +use itertools::Itertools; +use never::Never; +use ordered_float::NotNan; +use substack::Substack; + +use crate::location::{CodeLocation, SourceRange}; +use crate::name::Sym; +use crate::parse::numeric::print_nat16; +use crate::parse::parsed::{Clause, Expr}; +use crate::tree::{ModEntry, ModMember, Module, ModMemberRef}; +use crate::utils::combine::Combine; +use crate::utils::unwrap_or::unwrap_or; + +/// Different elements that can appear in a module other than submodules +#[derive(Debug, Clone)] +pub enum ItemKind { + /// An imported symbol or module. The value is the absolute path of + /// the symbol that should be used instead of this one. + Alias(Sym), + /// This name is only used in macros + None, + /// This name has a value associated with it + Const(Expr), +} + +impl Default for ItemKind { + fn default() -> Self { Self::None } +} + +/// Element in a module +#[derive(Debug, Clone)] +pub struct ProjItem { + /// The nature of the element + pub kind: ItemKind, +} + +impl Default for ProjItem { + fn default() -> Self { Self { kind: ItemKind::None } } +} + +impl Display for ProjItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.kind { + ItemKind::None => write!(f, "keyword"), + ItemKind::Const(c) => write!(f, "constant {c}"), + ItemKind::Alias(alias) => write!(f, "alias to {alias}"), + } + } +} + +impl Combine for ProjItem { + type Error = Never; + fn combine(self, _: Self) -> Result { + unimplemented!("Only implied project modules can be merged, not items") + } +} + +/// A substitution rule as stored in the tree +#[derive(Debug, Clone)] +pub struct ProjRule { + /// Tree fragment in the source code that activates this rule + pub pattern: Vec, + /// Influences the order in which rules are checked + pub prio: NotNan, + /// Tree fragment generated by this rule + pub template: Vec, + /// Comments associated with this rule + pub comments: Vec>, +} + +impl ProjRule { + /// Namespace all tokens in the rule + #[must_use] + pub fn prefix( + self, + prefix: &[Tok], + except: &impl Fn(Tok) -> bool, + ) -> Self { + let Self { comments, prio, mut pattern, mut template } = self; + (pattern.iter_mut()) + .chain(template.iter_mut()) + .for_each(|e| *e = e.prefix(prefix, except)); + Self { prio, comments, pattern, template } + } + + /// Return a list of all names that don't contain a namespace separator `::`. + /// These are exported when the rule is exported + #[must_use] + pub fn collect_root_names(&self) -> HashSet> { + let mut names = HashSet::new(); + for e in self.pattern.iter() { + e.search_all(&mut |e| { + if let Clause::Name(ns_name) = &e.value { + names.extend(ns_name[..].iter().exactly_one().ok().cloned()) + } + None::<()> + }); + } + names + } +} + +impl Display for ProjRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}rule {} ={}=> {}", + self.comments.iter().map(|s| format!("--[{s}]--\n")).join(""), + self.pattern.iter().join(" "), + print_nat16(self.prio), + self.template.iter().join(" ") + ) + } +} + +/// Information about a module that is defined in a source file +#[derive(Clone, Debug)] +pub struct SourceModule { + /// All rules defined in this module, exported or not + pub rules: Vec, + /// Location of this module. + pub range: SourceRange, +} + +/// Additional data about a loaded module beyond the list of constants and +/// submodules +#[derive(Clone, Debug, Default)] +pub struct ProjXMod { + /// Details only available for a module loaded from a source file + pub src: Option, +} + +impl Combine for ProjXMod { + type Error = Never; + fn combine(self, other: Self) -> Result { + match (self.src, other.src) { + (None, None) => Ok(Self { src: None }), + (..) => panic!("Only implied modules can be merged"), + } + } +} + +/// Information about a module entry +#[derive(Clone, Debug)] +pub struct ProjXEnt { + /// All comments appearing above the item or submodule + pub comments: Vec>, + /// Whether the member is visible to modules other than the parent + pub exported: bool, + /// Location of this item + pub locations: Vec, +} +impl Default for ProjXEnt { + fn default() -> Self { + Self { comments: vec![], exported: true, locations: vec![] } + } +} +impl ProjXEnt { + /// Implied modules can be merged easily. It's difficult to detect whether + /// a module is implied so we just assert that it doesn't have an associated + /// source location + pub fn is_default(&self) -> bool { self.locations.is_empty() } +} +impl Combine for ProjXEnt { + type Error = MergingFiles; + fn combine(self, other: Self) -> Result { + (self.is_default() && other.is_default()) + .then_some(self) + .ok_or(MergingFiles) + } +} + +/// Error produced when a module defined in a file has an alternate definition +/// in another file or as a folder. This is explicitly banned because +/// implementing a good version of it would require undue complexity +#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)] +pub struct MergingFiles; + +impl Display for ProjXMod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut dbg = f.debug_struct("ProjectExt"); + match &self.src { + None => dbg.finish_non_exhaustive(), + Some(SourceModule { rules, range }) => + dbg.field("rules", &rules).field("range", &range).finish(), + } + } +} + +/// A child to a [ProjectMod] +pub type ProjectEntry = ModEntry; +/// A node in the tree describing the project +pub type ProjectMod = Module; +pub type ProjectMemberRef<'a> = ModMemberRef<'a, ProjItem, ProjXMod, ProjXEnt>; + +fn collect_rules_rec(bag: &mut Vec, module: &ProjectMod) { + bag.extend(module.x.src.iter().flat_map(|s| &s.rules).cloned()); + for item in module.entries.values() { + if let ModMember::Sub(module) = &item.member { + collect_rules_rec(bag, module); + } + } +} +fn collect_consts_rec( + path: Substack>, + bag: &mut HashMap, + module: &ProjectMod, +) { + for (key, entry) in module.entries.iter() { + match &entry.member { + ModMember::Item(it) => + if let ItemKind::Const(expr) = &it.kind { + let name = path.push(key.clone()).iter().unreverse(); + let name = Sym::new(name).expect("pushed above"); + let location = + entry.x.locations.first().cloned().unwrap_or_else(|| { + panic!("{name} is a const in source, yet it has no location") + }); + let range = unwrap_or!(location => CodeLocation::Source; { + panic!("{name} is a const in source, yet its \ + location is generated") + }); + bag.insert(name, ConstReport { + comments: entry.x.comments.clone(), + value: expr.clone(), + range, + }); + }, + ModMember::Sub(module) => + collect_consts_rec(path.push(key.clone()), bag, module), + } + } +} + +/// Module corresponding to the root of a project +#[derive(Debug, Clone)] +pub struct ProjectTree(pub ProjectMod); +impl ProjectTree { + /// Collect the complete list of rules to be used by the rule repository + #[must_use] + pub fn all_rules(&self) -> Vec { + let mut rules = Vec::new(); + collect_rules_rec(&mut rules, &self.0); + rules + } + + /// Extract the symbol table + #[must_use] + pub fn all_consts(&self) -> HashMap { + let mut consts = HashMap::new(); + collect_consts_rec(Substack::Bottom, &mut consts, &self.0); + consts + } +} + +/// Information about a constant +#[derive(Clone, Debug)] +pub struct ConstReport { + /// Operational comments + pub comments: Vec>, + /// Value assigned to the constant + pub value: Expr, + pub range: SourceRange, +} diff --git a/src/pipeline/project_tree/build_tree.rs b/src/pipeline/project_tree/build_tree.rs deleted file mode 100644 index 3f3e9ed..0000000 --- a/src/pipeline/project_tree/build_tree.rs +++ /dev/null @@ -1,133 +0,0 @@ -use hashbrown::HashMap; -use itertools::{Either, Itertools}; - -use super::import_tree::ImpMod; -use crate::ast::{Constant, Rule}; -use crate::error::{ConflictingRoles, ProjectError, ProjectResult}; -use crate::pipeline::source_loader::{PreItem, PreMod}; -use crate::representations::project::{ - ImpReport, ItemKind, ProjectEntry, ProjectExt, ProjectItem, -}; -use crate::sourcefile::{ - FileEntry, FileEntryKind, Member, MemberKind, ModuleBlock, -}; -use crate::tree::{ModEntry, ModMember, Module}; -use crate::utils::get_or::get_or_default; -use crate::utils::pure_seq::pushed_ref; -use crate::{Tok, VName}; - -#[must_use = "A submodule may not be integrated into the tree"] -pub struct TreeReport { - pub entries: HashMap, ProjectEntry>, - pub rules: Vec>, - /// Maps imported symbols to the absolute paths of the modules they are - /// imported from - pub imports_from: HashMap, ImpReport>, -} - -pub fn build_tree( - path: &VName, - source: Vec, - Module { entries, .. }: PreMod, - imports: ImpMod, - prelude: &[FileEntry], -) -> ProjectResult { - let source = - source.into_iter().chain(prelude.iter().cloned()).collect::>(); - let (imports_from, mut submod_imports) = (imports.entries.into_iter()) - .partition_map::, HashMap<_, _>, _, _, _>( - |(n, ent)| match ent.member { - ModMember::Item(it) => Either::Left((n, it)), - ModMember::Sub(s) => Either::Right((n, s)), - }, - ); - let mut rule_fragments = Vec::new(); - let mut submodules = HashMap::<_, Vec<_>>::new(); - let mut consts = HashMap::new(); - for FileEntry { kind, locations: _ } in source { - match kind { - FileEntryKind::Import(_) => (), - FileEntryKind::Comment(_) => (), - FileEntryKind::Export(_) => (), - FileEntryKind::Member(Member { kind, .. }) => match kind { - MemberKind::Module(ModuleBlock { body, name }) => { - get_or_default(&mut submodules, &name).extend(body.into_iter()); - }, - MemberKind::Constant(Constant { name, value }) => { - consts.insert(name, value /* .prefix(path, &|_| false) */); - }, - MemberKind::Rule(rule) => rule_fragments.push(rule), - }, - } - } - let rules = rule_fragments; - let (pre_subs, pre_items) = (entries.into_iter()) - .partition_map::, HashMap<_, _>, _, _, _>( - |(k, ModEntry { exported, member })| match member { - ModMember::Sub(s) => Either::Left((k, (exported, s))), - ModMember::Item(it) => Either::Right((k, (exported, it))), - }, - ); - let mut entries = (pre_subs.into_iter()) - .map(|(k, (exported, pre_member))| { - let impmod = (submod_imports.remove(&k)) - .expect("Imports and preparsed should line up"); - (k, exported, pre_member, impmod) - }) - .map(|(k, exported, pre, imp)| { - let source = submodules - .remove(&k) - .expect("Submodules should not disappear after reparsing"); - (k, exported, pre, imp, source) - }) - .map(|(k, exported, pre, imp, source)| { - let path = pushed_ref(path, k.clone()); - let TreeReport { entries, imports_from, rules } = - build_tree(&path, source, pre, imp, prelude)?; - let extra = ProjectExt { path, file: None, imports_from, rules }; - let member = ModMember::Sub(Module { entries, extra }); - Ok((k, ModEntry { exported, member })) - }) - .chain((pre_items.into_iter()).map( - |(k, (exported, PreItem { has_value, location }))| { - let item = match imports_from.get(&k) { - Some(_) if has_value => { - // Local value cannot be assigned to imported key - let const_loc = - consts.remove(&k).expect("has_value is true").location; - let err = ConflictingRoles { - locations: vec![location, const_loc], - name: pushed_ref(path, k), - }; - return Err(err.rc()); - }, - None => { - let k = consts.remove(&k).map_or(ItemKind::None, ItemKind::Const); - ProjectItem { kind: k } - }, - Some(report) => - ProjectItem { kind: ItemKind::Alias(report.source.clone()) }, - }; - Ok((k, ModEntry { exported, member: ModMember::Item(item) })) - }, - )) - .collect::, _>>()?; - for (k, from) in imports_from.iter() { - let (_, ent) = entries.raw_entry_mut().from_key(k).or_insert_with(|| { - (k.clone(), ModEntry { - exported: false, - member: ModMember::Item(ProjectItem { - kind: ItemKind::Alias(from.source.clone()), - }), - }) - }); - debug_assert!( - matches!( - ent.member, - ModMember::Item(ProjectItem { kind: ItemKind::Alias(_), .. }) - ), - "Should have emerged in the processing of pre_items" - ) - } - Ok(TreeReport { entries, rules, imports_from }) -} diff --git a/src/pipeline/project_tree/import_tree.rs b/src/pipeline/project_tree/import_tree.rs deleted file mode 100644 index 5223fc4..0000000 --- a/src/pipeline/project_tree/import_tree.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::cmp::Ordering; -use std::fmt::Display; -use std::rc::Rc; - -use hashbrown::HashMap; -use itertools::Itertools; - -use crate::error::{ProjectError, ProjectResult}; -use crate::pipeline::source_loader::{PreMod, Preparsed}; -use crate::representations::project::ImpReport; -use crate::sourcefile::{absolute_path, Import}; -use crate::tree::{ErrKind, ModEntry, ModMember, Module, WalkError}; -use crate::utils::boxed_iter::{box_chain, box_once}; -use crate::utils::pure_seq::pushed_ref; -use crate::utils::{unwrap_or, BoxedIter}; -use crate::{Interner, ProjectTree, Tok, VName}; - -pub type ImpMod = Module, ()>; - -/// Assert that a module identified by a path can see a given symbol -fn assert_visible<'a>( - source: &'a [Tok], // must point to a file or submodule - target: &'a [Tok], - root: &'a Module, -) -> Result<(), WalkError<'a>> { - if target.split_last().map_or(true, |(_, m)| source.starts_with(m)) { - // The global module (empty path) is always visible - return Ok(()); // Symbols in ancestor modules are always visible - } - // walk the first section where visibility is ignored - let shared_len = - source.iter().zip(target.iter()).take_while(|(a, b)| a == b).count(); - let (shared_path, deep_path) = target.split_at(shared_len + 1); - let private_root = root.walk_ref(&[], shared_path, false)?; - // walk the second part where visibility matters - private_root.walk1_ref(shared_path, deep_path, true)?; - Ok(()) -} - -pub fn assert_visible_overlay<'a>( - source: &'a [Tok], // must point to a file or submodule - target: &'a [Tok], - first: &'a Module, - fallback: &'a Module, -) -> Result<(), WalkError<'a>> { - assert_visible(source, target, first).or_else(|e1| { - if e1.kind == ErrKind::Missing { - match assert_visible(source, target, fallback) { - // if both are walk errors, report the longer of the two - Err(mut e2) if e2.kind == ErrKind::Missing => - Err(match e1.depth().cmp(&e2.depth()) { - Ordering::Less => e2, - Ordering::Greater => e1, - Ordering::Equal => { - e2.options = box_chain!(e2.options, e1.options); - e2 - }, - }), - // otherwise return the parent's result - x => x, - } - } else { - Err(e1) - } - }) -} - -pub fn process_donor_module( - module: &Module, - abs_path: Rc, -) -> impl Iterator, VName)> + '_ { - (module.entries.iter()) - .filter(|(_, ent)| ent.exported) - .map(move |(n, _)| (n.clone(), pushed_ref(abs_path.as_ref(), n.clone()))) -} - -pub fn import_tree( - modpath: VName, - pre: &PreMod, - root: &Preparsed, - prev_root: &ProjectTree, - i: &Interner, -) -> ProjectResult { - let imports = pre.extra.details().map_or(&[][..], |e| &e.imports[..]); - let entries = (imports.iter()) - // imports become leaf sets - .map(|Import { name, path, location }| -> ProjectResult> { - let mut abs_path = absolute_path(&modpath, path, i, location)?; - Ok(if let Some(name) = name { - // named imports are validated and translated 1->1 - abs_path.push(name.clone()); - assert_visible_overlay(&modpath, &abs_path, &root.0, &prev_root.0) - .map_err(|e| -> Rc { - println!("Current root: {}", &root.0); - // println!("Old root: {:#?}", &prev_root.0); - panic!("{}", e.at(location)) - })?; - box_once((name.clone(), abs_path)) - } else { - let rc_path = Rc::new(abs_path); - // wildcard imports are validated - assert_visible_overlay(&modpath, &rc_path, &root.0, &prev_root.0) - .map_err(|e| e.at(location))?; - // and instantiated for all exports of the target 1->n - let new_imports = match (root.0).walk_ref(&[], &rc_path, false) { - Err(e) if e.kind == ErrKind::Missing => Err(e), - Err(e) => return Err(e.at(location)), - Ok(module) => Ok(process_donor_module(module, rc_path.clone())) - }; - let old_m = match (prev_root.0).walk_ref(&[], &rc_path, false) { - Err(e) if e.kind != ErrKind::Missing => return Err(e.at(location)), - Err(e1) => match new_imports { - Ok(it) => return Ok(Box::new(it)), - Err(mut e2) => return Err(match e1.depth().cmp(&e2.depth()) { - Ordering::Less => e2.at(location), - Ordering::Greater => e1.at(location), - Ordering::Equal => { - e2.options = box_chain!(e2.options, e1.options); - e2.at(location) - }, - }), - }, - Ok(old_m) => old_m, - }; - let it1 = process_donor_module(old_m, rc_path.clone()); - match new_imports { - Err(_) => Box::new(it1), - Ok(it2) => box_chain!(it1, it2) - } - }) - }) - // leaf sets flattened to leaves - .flatten_ok() - // translated to entries - .map_ok(|(name, source)| { - (name, ModEntry { - exported: false, // this is irrelevant but needed - member: ModMember::Item(ImpReport { source }), - }) - }) - .chain( - (pre.entries.iter()) - // recurse on submodules - .filter_map(|(k, v)| { - Some((k, v, unwrap_or!(&v.member => ModMember::Sub; return None))) - }) - .map(|(k, v, pre)| { - let path = pushed_ref(&modpath, k.clone()); - Ok((k.clone(), ModEntry { - exported: v.exported, - member: ModMember::Sub(import_tree(path, pre, root, prev_root, i)?), - })) - }), - ) - .collect::, _>>()?; - Ok(Module { extra: (), entries }) -} diff --git a/src/pipeline/project_tree/mod.rs b/src/pipeline/project_tree/mod.rs deleted file mode 100644 index 84be367..0000000 --- a/src/pipeline/project_tree/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod build_tree; -mod import_tree; -mod rebuild_tree; - -pub use rebuild_tree::rebuild_tree; diff --git a/src/pipeline/project_tree/rebuild_tree.rs b/src/pipeline/project_tree/rebuild_tree.rs deleted file mode 100644 index 3e405a8..0000000 --- a/src/pipeline/project_tree/rebuild_tree.rs +++ /dev/null @@ -1,98 +0,0 @@ -use hashbrown::HashMap; -use itertools::Itertools; - -use super::build_tree::{build_tree, TreeReport}; -use super::import_tree::{import_tree, ImpMod}; -use crate::error::ProjectResult; -use crate::pipeline::source_loader::{ - LoadedSourceTable, PreExtra, PreMod, Preparsed, -}; -use crate::representations::project::{ProjectExt, ProjectMod}; -use crate::sourcefile::FileEntry; -use crate::tree::{ModEntry, ModMember, Module}; -use crate::utils::pure_seq::pushed_ref; -use crate::utils::unwrap_or; -use crate::{Interner, ProjectTree, Tok, VName}; - -pub fn rebuild_file( - path: Vec>, - pre: PreMod, - imports: ImpMod, - source: &LoadedSourceTable, - prelude: &[FileEntry], -) -> ProjectResult> { - let file = match &pre.extra { - PreExtra::Dir => panic!("Dir should not hand this node off"), - PreExtra::Submod(_) => panic!("should not have received this"), - PreExtra::File(f) => f, - }; - let src = source.get(&file.name).unwrap_or_else(|| { - panic!( - "{} should have been preparsed already. Preparsed files are {}", - Interner::extern_all(&file.name).join("/") + ".orc", - source - .keys() - .map(|f| Interner::extern_all(f).join("/") + ".orc") - .join(", ") - ) - }); - let entries = src.entries.clone(); - let TreeReport { entries, rules, imports_from } = - build_tree(&path, entries, pre, imports, prelude)?; - let file = Some(path.clone()); - Ok(Module { entries, extra: ProjectExt { file, path, imports_from, rules } }) -} - -pub fn rebuild_dir( - path: Vec>, - pre: PreMod, - mut imports: ImpMod, - source: &LoadedSourceTable, - prelude: &[FileEntry], -) -> ProjectResult> { - match pre.extra { - PreExtra::Dir => (), - PreExtra::File(_) => - return rebuild_file(path, pre, imports, source, prelude), - PreExtra::Submod(_) => panic!("Dirs contain dirs and files"), - } - let entries = (pre.entries.into_iter()) - .map(|(name, entry)| { - match imports.entries.remove(&name).map(|e| e.member) { - Some(ModMember::Sub(impmod)) => (name, entry, impmod), - _ => panic!("Imports must line up with modules"), - } - }) - .map(|(name, ModEntry { member, exported }, impmod)| -> ProjectResult<_> { - let path = pushed_ref(&path, name.clone()); - let pre = unwrap_or!(member => ModMember::Sub; - panic!("Dirs can only contain submodules") - ); - let module = rebuild_dir(path, pre, impmod, source, prelude)?; - Ok((name, ModEntry { exported, member: ModMember::Sub(module) })) - }) - .collect::, _>>()?; - Ok(Module { - extra: ProjectExt { - path, - imports_from: HashMap::new(), - rules: Vec::new(), - file: None, - }, - entries, - }) -} - -/// Rebuild the entire tree -pub fn rebuild_tree( - source: &LoadedSourceTable, - preparsed: Preparsed, - prev_root: &ProjectTree, - prelude: &[FileEntry], - i: &Interner, -) -> ProjectResult> { - let imports = - import_tree(Vec::new(), &preparsed.0, &preparsed, prev_root, i)?; - rebuild_dir(Vec::new(), preparsed.0, imports, source, prelude) - .map(ProjectTree) -} diff --git a/src/pipeline/source_loader/load_source.rs b/src/pipeline/source_loader/load_source.rs deleted file mode 100644 index 87a6775..0000000 --- a/src/pipeline/source_loader/load_source.rs +++ /dev/null @@ -1,166 +0,0 @@ -use std::sync::Arc; - -use hashbrown::HashMap; - -use super::loaded_source::{LoadedSource, LoadedSourceTable}; -use super::preparse::preparse; -use super::{PreExtra, Preparsed}; -use crate::error::{ - NoTargets, ProjectError, ProjectResult, UnexpectedDirectory, -}; -use crate::interner::{Interner, Tok}; -use crate::parse::{self, LexerPlugin, LineParser, ParsingContext}; -use crate::pipeline::file_loader::{IOResult, Loaded}; -use crate::pipeline::import_abs_path::import_abs_path; -use crate::representations::sourcefile::FileEntry; -use crate::tree::Module; -use crate::utils::pure_seq::pushed_ref; -use crate::utils::{split_max_prefix, unwrap_or}; -use crate::Location; - -#[derive(Clone, Copy)] -pub struct Context<'a> { - pub prelude: &'a [FileEntry], - pub i: &'a Interner, - pub lexer_plugins: &'a [&'a dyn LexerPlugin], - pub line_parsers: &'a [&'a dyn LineParser], -} - -/// Load the source at the given path or all within if it's a collection, -/// and all sources imported from these. -fn load_abs_path_rec( - referrer: &[Tok], - abs_path: &[Tok], - mut all: Preparsed, - source: &mut LoadedSourceTable, - get_source: &impl Fn(&[Tok], &[Tok]) -> IOResult, - is_injected_module: &impl Fn(&[Tok]) -> bool, - ctx @ Context { i, lexer_plugins, line_parsers, prelude }: Context, -) -> ProjectResult { - // # Termination - // - // Every recursion of this function either - // - adds one of the files in the source directory to `visited` or - // - recursively traverses a directory tree - // therefore eventually the function exits, assuming that the directory tree - // contains no cycles. - - // try splitting the path to file, swallowing any IO errors - let name_split = split_max_prefix(abs_path, &|p| { - get_source(p, referrer).map(|l| l.is_code()).unwrap_or(false) - }); - if let Some((filename, _)) = name_split { - // Termination: exit if entry already visited - if source.contains_key(filename) { - return Ok(all); - } - // if the filename is valid, load, preparse and record this file - let text = unwrap_or!(get_source(filename, referrer)? => Loaded::Code; { - return Err(UnexpectedDirectory { path: filename.to_vec() }.rc()) - }); - let entries = parse::parse_file(ParsingContext::new( - i, - Arc::new(filename.to_vec()), - text, - lexer_plugins, - line_parsers, - ))?; - let preparsed = preparse(filename.to_vec(), entries.clone(), prelude)?; - source.insert(filename.to_vec(), LoadedSource { entries }); - // recurse on all imported modules - // will be taken and returned by the closure. None iff an error is thrown - all = preparsed.0.search_all(all, &mut |modpath, - module, - mut all| - -> ProjectResult<_> { - let details = unwrap_or!(module.extra.details(); return Ok(all)); - let referrer = modpath.iter().rev_vec_clone(); - for import in &details.imports { - let origin = &Location::Unknown; - let abs_pathv = import_abs_path( - filename, - modpath.clone(), - &import.nonglob_path(), - i, - origin, - )?; - if abs_path.starts_with(&abs_pathv) { - continue; - } - // recurse on imported module - all = load_abs_path_rec( - &referrer, - &abs_pathv, - all, - source, - get_source, - is_injected_module, - ctx, - )?; - } - Ok(all) - })?; - // Combine the trees - all.0.overlay(preparsed.0).map(Preparsed) - } else { - // If the path is not within a file, load it as directory - let coll = match get_source(abs_path, referrer) { - Ok(Loaded::Collection(coll)) => coll, - Ok(Loaded::Code(_)) => { - unreachable!("split_name returned None but the path is a file") - }, - Err(e) => { - // todo: if this can actually be produced, return Err(ImportAll) instead - let parent = abs_path.split_last().expect("import path nonzero").1; - // exit without error if it was injected, or raise any IO error that was - // previously swallowed - return if is_injected_module(parent) { Ok(all) } else { Err(e) }; - }, - }; - // recurse on all files and folders within - for item in coll.iter() { - let abs_subpath = pushed_ref(abs_path, i.i(item)); - all = load_abs_path_rec( - referrer, - &abs_subpath, - all, - source, - get_source, - is_injected_module, - ctx, - )?; - } - Ok(all) - } -} - -/// Load and preparse all files reachable from the load targets via -/// imports that aren't injected. -/// -/// is_injected_module must return false for injected symbols, but may return -/// true for parents of injected modules that are not directly part of the -/// injected data (the ProjectTree doesn't make a distinction between the two) -pub fn load_source<'a>( - targets: impl Iterator]>, - ctx: Context, - get_source: &impl Fn(&[Tok], &[Tok]) -> IOResult, - is_injected_module: &impl Fn(&[Tok]) -> bool, -) -> ProjectResult<(Preparsed, LoadedSourceTable)> { - let mut table = LoadedSourceTable::new(); - let mut all = - Preparsed(Module { extra: PreExtra::Dir, entries: HashMap::new() }); - let mut any_target = false; - for target in targets { - any_target |= true; - all = load_abs_path_rec( - &[], - target, - all, - &mut table, - get_source, - is_injected_module, - ctx, - )?; - } - if any_target { Ok((all, table)) } else { Err(NoTargets.rc()) } -} diff --git a/src/pipeline/source_loader/loaded_source.rs b/src/pipeline/source_loader/loaded_source.rs deleted file mode 100644 index f58b298..0000000 --- a/src/pipeline/source_loader/loaded_source.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::collections::HashMap; - -use crate::representations::VName; -use crate::sourcefile::FileEntry; - -#[derive(Debug)] -pub struct LoadedSource { - pub entries: Vec, -} - -pub type LoadedSourceTable = HashMap; diff --git a/src/pipeline/source_loader/mod.rs b/src/pipeline/source_loader/mod.rs deleted file mode 100644 index 8aab3bc..0000000 --- a/src/pipeline/source_loader/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// PULL LOGISTICS BOUNDARY -// -// Specifying exactly what this module should be doing was an unexpectedly -// hard challenge. It is intended to encapsulate all pull logistics, but -// this definition is apparently prone to scope creep. -// -// Load files, preparse them to obtain a list of imports, follow these. -// Preparsing also returns the module tree and list of exported synbols -// for free, which is needed later so the output of preparsing is also -// attached to the module output. -// -// The module checks for IO errors, syntax errors, malformed imports and -// imports from missing files. All other errors must be checked later. -// -// Injection strategy: -// see whether names are valid in the injected tree for is_injected - -mod load_source; -mod loaded_source; -mod preparse; -mod types; - -pub use load_source::{load_source, Context}; -pub use loaded_source::{LoadedSource, LoadedSourceTable}; -pub use types::{PreExtra, PreFileExt, PreItem, PreMod, PreSubExt, Preparsed}; diff --git a/src/pipeline/source_loader/preparse.rs b/src/pipeline/source_loader/preparse.rs deleted file mode 100644 index 0c7a1d3..0000000 --- a/src/pipeline/source_loader/preparse.rs +++ /dev/null @@ -1,147 +0,0 @@ -use hashbrown::HashMap; -use itertools::Itertools; - -use super::types::{PreFileExt, PreItem, PreSubExt}; -use super::{PreExtra, Preparsed}; -use crate::ast::{Clause, Constant}; -use crate::error::{ - ConflictingRoles, ProjectError, ProjectResult, VisibilityMismatch, -}; -use crate::representations::sourcefile::{FileEntry, MemberKind}; -use crate::representations::tree::{ModEntry, ModMember, Module}; -use crate::sourcefile::{FileEntryKind, Import, Member, ModuleBlock}; -use crate::utils::get_or::{get_or_default, get_or_make}; -use crate::utils::pure_seq::pushed; -use crate::{Location, Tok, VName}; - -struct FileReport { - entries: HashMap, ModEntry>, - imports: Vec, -} - -/// Convert source lines into a module -fn to_module( - file: &[Tok], - path: VName, - src: Vec, - prelude: &[FileEntry], -) -> ProjectResult { - let mut imports = Vec::new(); - let mut items = HashMap::, (bool, PreItem)>::new(); - let mut to_export = HashMap::, Vec>::new(); - let mut submods = - HashMap::, (bool, Vec, Vec)>::new(); - let entries = prelude.iter().cloned().chain(src); - for FileEntry { kind, locations } in entries { - match kind { - FileEntryKind::Import(imp) => imports.extend(imp.into_iter()), - FileEntryKind::Export(names) => - for (t, l) in names { - get_or_default(&mut to_export, &t).push(l) - }, - FileEntryKind::Member(Member { exported, kind }) => match kind { - MemberKind::Constant(Constant { name, .. }) => { - let (prev_exported, it) = get_or_default(&mut items, &name); - if it.has_value { - let err = ConflictingRoles { name: pushed(path, name), locations }; - return Err(err.rc()); - } - if let Some(loc) = locations.get(0) { - it.location = it.location.clone().or(loc.clone()) - }; - it.has_value = true; - *prev_exported |= exported; - }, - MemberKind::Module(ModuleBlock { name, body }) => { - if let Some((prev_exported, locv, entv)) = submods.get_mut(&name) { - if *prev_exported != exported { - let mut namespace = path; - namespace.push(name.clone()); - let err = VisibilityMismatch { namespace, file: file.to_vec() }; - return Err(err.rc()); - } - locv.extend(locations.into_iter()); - entv.extend(body.into_iter()) - } else { - submods.insert(name.clone(), (exported, locations, body.clone())); - } - }, - MemberKind::Rule(r) => - if exported { - for ex in r.pattern { - ex.search_all(&mut |ex| { - if let Clause::Name(vname) = &ex.value { - if let Ok(name) = vname.iter().exactly_one() { - get_or_default(&mut to_export, name) - .push(ex.location.clone()); - } - } - None::<()> - }); - } - }, - }, - _ => (), - } - } - let mut entries = HashMap::with_capacity(items.len() + submods.len()); - entries.extend(items.into_iter().map(|(name, (exported, it))| { - (name, ModEntry { member: ModMember::Item(it), exported }) - })); - for (subname, (exported, locations, body)) in submods { - let mut name = path.clone(); - entries - .try_insert(subname.clone(), ModEntry { - member: ModMember::Sub({ - name.push(subname); - let FileReport { imports, entries: items } = - to_module(file, name.clone(), body, prelude)?; - Module { - entries: items, - extra: PreExtra::Submod(PreSubExt { imports }), - } - }), - exported, - }) - .map_err(|_| ConflictingRoles { locations, name }.rc())?; - } - for (item, locations) in to_export { - get_or_make(&mut entries, &item, || ModEntry { - member: ModMember::Item(PreItem { - has_value: false, - location: locations[0].clone(), - }), - exported: true, - }) - .exported = true - } - Ok(FileReport { entries, imports }) -} - -/// Preparse the module. At this stage, only the imports and -/// names defined by the module can be parsed -pub fn preparse( - file: VName, - entries: Vec, - prelude: &[FileEntry], -) -> ProjectResult { - let FileReport { entries, imports } = - to_module(&file, file.clone(), entries, prelude)?; - let mut module = Module { - entries, - extra: PreExtra::File(PreFileExt { - details: PreSubExt { imports }, - name: file.clone(), - }), - }; - for name in file.iter().rev() { - module = Module { - extra: PreExtra::Dir, - entries: HashMap::from([(name.clone(), ModEntry { - exported: true, - member: ModMember::Sub(module), - })]), - }; - } - Ok(Preparsed(module)) -} diff --git a/src/pipeline/source_loader/types.rs b/src/pipeline/source_loader/types.rs deleted file mode 100644 index 3e9a717..0000000 --- a/src/pipeline/source_loader/types.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::fmt::Display; -use std::ops::Add; - -use crate::error::ProjectResult; -use crate::sourcefile::Import; -use crate::tree::Module; -use crate::{Interner, Location, VName}; - -#[derive(Debug, Clone)] -pub struct PreItem { - pub has_value: bool, - pub location: Location, -} - -impl Display for PreItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { has_value, location } = self; - let description = if *has_value { "value" } else { "keyword" }; - write!(f, "{description} {location}") - } -} - -impl Default for PreItem { - fn default() -> Self { - PreItem { has_value: false, location: Location::Unknown } - } -} - -#[derive(Debug, Clone)] -pub struct PreSubExt { - pub imports: Vec, -} - -#[derive(Debug, Clone)] -pub struct PreFileExt { - pub name: VName, - pub details: PreSubExt, -} - -#[derive(Debug, Clone)] -pub enum PreExtra { - File(PreFileExt), - Submod(PreSubExt), - Dir, -} - -impl PreExtra { - /// If the module is not a directory, returns the source-only details - pub fn details(&self) -> Option<&PreSubExt> { - match self { - Self::Submod(sub) => Some(sub), - Self::File(PreFileExt { details, .. }) => Some(details), - Self::Dir => None, - } - } -} - -impl Add for PreExtra { - type Output = ProjectResult; - - fn add(self, rhs: Self) -> Self::Output { - match (self, rhs) { - (alt, Self::Dir) | (Self::Dir, alt) => Ok(alt), - (Self::File(_) | Self::Submod(_), Self::File(_) | Self::Submod(_)) => { - panic!("Each file should be parsed once.") - }, - } - } -} - -impl Display for PreExtra { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Dir => write!(f, "Directory"), - Self::File(PreFileExt { name, .. }) => { - write!(f, "File({}.orc)", Interner::extern_all(name).join("/")) - }, - Self::Submod(_) => write!(f, "Submodule"), - } - } -} - -pub type PreMod = Module; - -#[derive(Debug, Clone)] -pub struct Preparsed(pub PreMod); diff --git a/src/representations/ast.rs b/src/representations/ast.rs deleted file mode 100644 index 9ad7b6e..0000000 --- a/src/representations/ast.rs +++ /dev/null @@ -1,468 +0,0 @@ -//! Datastructures representing the units of macro execution -//! -//! These structures are produced by the pipeline, processed by the macro -//! executor, and then converted to other usable formats. - -use std::fmt::Display; -use std::hash::Hash; -use std::rc::Rc; - -use hashbrown::HashSet; -use itertools::Itertools; -use ordered_float::NotNan; - -#[allow(unused)] // for doc -use super::interpreted; -use super::location::Location; -use super::namelike::{NameLike, VName}; -use crate::foreign::{Atom, ExFn}; -use crate::interner::Tok; -use crate::parse::print_nat16; -use crate::utils::rc_tools::map_rc; - -/// A [Clause] with associated metadata -#[derive(Clone, Debug)] -pub struct Expr { - /// The actual value - pub value: Clause, - /// Information about the code that produced this value - pub location: Location, -} - -impl Expr { - /// Process all names with the given mapper. - /// Return a new object if anything was processed - #[must_use] - pub fn map_names(&self, pred: &impl Fn(&N) -> Option) -> Option { - Some(Self { - value: self.value.map_names(pred)?, - location: self.location.clone(), - }) - } - - /// Transform from one name system to another - #[must_use] - pub fn transform_names(self, pred: &impl Fn(N) -> O) -> Expr { - Expr { value: self.value.transform_names(pred), location: self.location } - } - - /// Visit all expressions in the tree. The search can be exited early by - /// returning [Some] - /// - /// See also [interpreted::ExprInst::search_all] - pub fn search_all( - &self, - f: &mut impl FnMut(&Self) -> Option, - ) -> Option { - f(self).or_else(|| self.value.search_all(f)) - } -} - -impl AsRef for Expr { - fn as_ref(&self) -> &Location { &self.location } -} - -/// Visit all expression sequences including this sequence itself. Otherwise -/// works exactly like [Expr::search_all_slcs] -pub fn search_all_slcs( - this: &[Expr], - f: &mut impl FnMut(&[Expr]) -> Option, -) -> Option { - f(this).or_else(|| this.iter().find_map(|expr| expr.value.search_all_slcs(f))) -} - -impl Expr { - /// Add the specified prefix to every Name - #[must_use] - pub fn prefix( - &self, - prefix: &[Tok], - except: &impl Fn(&Tok) -> bool, - ) -> Self { - Self { - value: self.value.prefix(prefix, except), - location: self.location.clone(), - } - } -} - -impl Display for Expr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.value.fmt(f) - } -} - -/// Various types of placeholders -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum PHClass { - /// Matches multiple tokens, lambdas or parenthesized groups - Vec { - /// If true, must match at least one clause - nonzero: bool, - /// Greediness in the allocation of tokens - prio: usize, - }, - /// Matches exactly one token, lambda or parenthesized group - Scalar, - /// Matches exactly one name - Name, -} - -/// Properties of a placeholder that matches unknown tokens in macros -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Placeholder { - /// Identifier to pair placeholders in the pattern and template - pub name: Tok, - /// The nature of the token set matched by this placeholder - pub class: PHClass, -} - -impl Display for Placeholder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let name = &self.name; - match self.class { - PHClass::Scalar => write!(f, "${name}"), - PHClass::Name => write!(f, "$_{name}"), - PHClass::Vec { nonzero, prio } => { - if nonzero { write!(f, "...") } else { write!(f, "..") }?; - write!(f, "${name}:{prio}") - }, - } - } -} - -/// Different types of brackets supported by Orchid -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum PType { - /// () - Par, - /// [] - Sqr, - /// {} - Curl, -} -impl PType { - /// Left paren character for this paren type - pub fn l(self) -> char { - match self { - PType::Curl => '{', - PType::Par => '(', - PType::Sqr => '[', - } - } - - /// Right paren character for this paren type - pub fn r(self) -> char { - match self { - PType::Curl => '}', - PType::Par => ')', - PType::Sqr => ']', - } - } -} - -/// An S-expression as read from a source file -#[derive(Debug, Clone)] -pub enum Clause { - /// An opaque function, eg. an effectful function employing CPS - ExternFn(ExFn), - /// An opaque non-callable value, eg. a file handle - Atom(Atom), - /// A c-style name or an operator, eg. `+`, `i`, `foo::bar` - Name(N), - /// A parenthesized expression - /// eg. `(print out "hello")`, `[1, 2, 3]`, `{Some(t) => t}` - S(PType, Rc>>), - /// A function expression, eg. `\x. x + 1` - Lambda(Rc>>, Rc>>), - /// A placeholder for macros, eg. `$name`, `...$body`, `...$lhs:1` - Placeh(Placeholder), -} - -impl Clause { - /// Extract the expressions from an auto, lambda or S - #[must_use] - pub fn body(&self) -> Option>>> { - match self { - Self::Lambda(_, body) | Self::S(_, body) => Some(body.clone()), - _ => None, - } - } - - /// Convert with identical meaning - #[must_use] - pub fn into_expr(self) -> Expr { - if let Self::S(PType::Par, body) = &self { - if body.len() == 1 { - body[0].clone() - } else { - Expr { value: self, location: Location::Unknown } - } - } else { - Expr { value: self, location: Location::Unknown } - } - } - - /// Convert with identical meaning - #[must_use] - pub fn from_exprs(exprs: &[Expr]) -> Option { - if exprs.is_empty() { - None - } else if exprs.len() == 1 { - Some(exprs[0].value.clone()) - } else { - Some(Self::S(PType::Par, Rc::new(exprs.to_vec()))) - } - } - - /// Convert with identical meaning - #[must_use] - pub fn from_exprv(exprv: &Rc>>) -> Option> { - if exprv.len() < 2 { - Self::from_exprs(exprv) - } else { - Some(Self::S(PType::Par, exprv.clone())) - } - } - - /// Collect all names that appear in this expression. - /// NOTICE: this isn't the total set of unbound names, it's mostly useful to - /// make weak statements for optimization. - #[must_use] - pub fn collect_names(&self) -> HashSet { - if let Self::Name(n) = self { - return HashSet::from([n.clone()]); - } - let mut glossary = HashSet::new(); - let result = self.search_all(&mut |e| { - if let Clause::Name(n) = &e.value { - glossary.insert(n.clone()); - } - None::<()> - }); - assert!(result.is_none(), "Callback never returns Some, wtf???"); - glossary - } - - /// Process all names with the given mapper. - /// Return a new object if anything was processed - #[must_use] - pub fn map_names(&self, pred: &impl Fn(&N) -> Option) -> Option { - match self { - Clause::Atom(_) | Clause::ExternFn(_) | Clause::Placeh(_) => None, - Clause::Name(name) => pred(name).map(Clause::Name), - Clause::S(c, body) => { - let mut any_some = false; - let new_body = body - .iter() - .map(|e| { - let val = e.map_names(pred); - any_some |= val.is_some(); - val.unwrap_or_else(|| e.clone()) - }) - .collect(); - if any_some { Some(Clause::S(*c, Rc::new(new_body))) } else { None } - }, - Clause::Lambda(arg, body) => { - let mut any_some = false; - let new_arg = (arg.iter()) - .map(|e| { - let val = e.map_names(pred); - any_some |= val.is_some(); - val.unwrap_or_else(|| e.clone()) - }) - .collect(); - let new_body = (body.iter()) - .map(|e| { - let val = e.map_names(pred); - any_some |= val.is_some(); - val.unwrap_or_else(|| e.clone()) - }) - .collect(); - if any_some { - Some(Clause::Lambda(Rc::new(new_arg), Rc::new(new_body))) - } else { - None - } - }, - } - } - - /// Transform from one name representation to another - #[must_use] - pub fn transform_names( - self, - pred: &impl Fn(N) -> O, - ) -> Clause { - match self { - Self::Name(n) => Clause::Name(pred(n)), - Self::Placeh(p) => Clause::Placeh(p), - Self::Atom(a) => Clause::Atom(a), - Self::ExternFn(f) => Clause::ExternFn(f), - Self::Lambda(n, b) => Clause::Lambda( - map_rc(n, |n| n.into_iter().map(|e| e.transform_names(pred)).collect()), - map_rc(b, |b| b.into_iter().map(|e| e.transform_names(pred)).collect()), - ), - Self::S(c, b) => Clause::S( - c, - map_rc(b, |b| b.into_iter().map(|e| e.transform_names(pred)).collect()), - ), - } - } - - /// Pair of [Expr::search_all] - pub fn search_all( - &self, - f: &mut impl FnMut(&Expr) -> Option, - ) -> Option { - match self { - Clause::Lambda(arg, body) => - arg.iter().chain(body.iter()).find_map(|expr| expr.search_all(f)), - Clause::Name(_) | Clause::Atom(_) => None, - Clause::ExternFn(_) | Clause::Placeh(_) => None, - Clause::S(_, body) => body.iter().find_map(|expr| expr.search_all(f)), - } - } - - /// Pair of [Expr::search_all_slcs] - pub fn search_all_slcs( - &self, - f: &mut impl FnMut(&[Expr]) -> Option, - ) -> Option { - match self { - Clause::Lambda(arg, body) => - search_all_slcs(arg, f).or_else(|| search_all_slcs(body, f)), - Clause::Name(_) | Clause::Atom(_) => None, - Clause::ExternFn(_) | Clause::Placeh(_) => None, - Clause::S(_, body) => search_all_slcs(body, f), - } - } - - /// Generate a parenthesized expression sequence - pub fn s(delimiter: char, items: impl IntoIterator) -> Self { - Self::S( - match delimiter { - '(' => PType::Par, - '[' => PType::Sqr, - '{' => PType::Curl, - _ => panic!("not an opening paren"), - }, - Rc::new(items.into_iter().map(Self::into_expr).collect()), - ) - } -} - -impl Clause { - /// Add the specified prefix to every Name - #[must_use] - pub fn prefix( - &self, - prefix: &[Tok], - except: &impl Fn(&Tok) -> bool, - ) -> Self { - self - .map_names(&|name| { - if except(&name[0]) { - return None; - } - let mut new = prefix.to_vec(); - new.extend_from_slice(name); - Some(new) - }) - .unwrap_or_else(|| self.clone()) - } -} - -impl Display for Clause { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::ExternFn(fun) => write!(f, "{fun:?}"), - Self::Atom(a) => write!(f, "{a:?}"), - Self::Name(name) => write!(f, "{}", name.to_strv().join("::")), - Self::S(t, items) => { - let body = items.iter().join(" "); - write!(f, "{}{body}{}", t.l(), t.r()) - }, - Self::Lambda(arg, body) => { - let args = arg.iter().join(" "); - let bodys = body.iter().join(" "); - write!(f, "\\{args}.{bodys}") - }, - Self::Placeh(ph) => ph.fmt(f), - } - } -} - -/// A substitution rule as read from the source -#[derive(Debug, Clone)] -pub struct Rule { - /// Tree fragment in the source code that activates this rule - pub pattern: Vec>, - /// Influences the order in which rules are checked - pub prio: NotNan, - /// Tree fragment generated by this rule - pub template: Vec>, -} - -impl Rule { - /// Namespace all tokens in the rule - #[must_use] - pub fn prefix( - &self, - prefix: &[Tok], - except: &impl Fn(&Tok) -> bool, - ) -> Self { - Self { - prio: self.prio, - pattern: self.pattern.iter().map(|e| e.prefix(prefix, except)).collect(), - template: (self.template.iter()) - .map(|e| e.prefix(prefix, except)) - .collect(), - } - } - - /// Return a list of all names that don't contain a namespace separator `::`. - /// These are exported when the rule is exported - #[must_use] - pub fn collect_single_names(&self) -> VName { - let mut names = Vec::new(); - for e in self.pattern.iter() { - e.search_all(&mut |e| { - if let Clause::Name(ns_name) = &e.value { - if ns_name.len() == 1 { - names.push(ns_name[0].clone()) - } - } - None::<()> - }); - } - names - } -} - -impl Display for Rule { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "rule {} ={}=> {}", - self.pattern.iter().join(" "), - print_nat16(self.prio), - self.template.iter().join(" ") - ) - } -} - -/// A named constant -#[derive(Debug, Clone)] -pub struct Constant { - /// Used to reference the constant - pub name: Tok, - /// The constant value inserted where the name is found - pub value: Expr, -} - -impl Display for Constant { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "const {} := {}", *self.name, self.value) - } -} diff --git a/src/representations/ast_to_interpreted.rs b/src/representations/ast_to_interpreted.rs deleted file mode 100644 index 8c4ac59..0000000 --- a/src/representations/ast_to_interpreted.rs +++ /dev/null @@ -1,15 +0,0 @@ -use super::{ast, ast_to_postmacro, interpreted, postmacro_to_interpreted}; -use crate::Sym; - -#[allow(unused)] -pub type AstError = ast_to_postmacro::Error; - -/// Attempt to convert the AST processed by macros into an executable format -#[allow(unused)] -pub fn ast_to_interpreted( - ast: &ast::Expr, - symbol: Sym, -) -> Result { - let pmtree = ast_to_postmacro::expr(ast, symbol)?; - Ok(postmacro_to_interpreted::expr(&pmtree)) -} diff --git a/src/representations/ast_to_postmacro.rs b/src/representations/ast_to_postmacro.rs deleted file mode 100644 index a839c22..0000000 --- a/src/representations/ast_to_postmacro.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::rc::Rc; - -use super::location::Location; -use super::{ast, postmacro}; -use crate::ast::PType; -use crate::error::ProjectError; -use crate::utils::substack::Substack; -use crate::utils::unwrap_or; -use crate::Sym; - -#[derive(Debug, Clone)] -pub enum ErrorKind { - /// `()` as a clause is meaningless in lambda calculus - EmptyS, - /// Only `(...)` may be converted to typed lambdas. `[...]` and `{...}` - /// left in the code are signs of incomplete macro execution - BadGroup(PType), - /// Placeholders shouldn't even occur in the code during macro - /// execution. Something is clearly terribly wrong - Placeholder, - /// Arguments can only be a single [ast::Clause::Name] - InvalidArg, -} - -#[derive(Debug, Clone)] -pub struct Error { - pub location: Location, - pub kind: ErrorKind, - pub symbol: Sym, -} -impl Error { - #[must_use] - pub fn new(kind: ErrorKind, location: &Location, symbol: Sym) -> Self { - Self { location: location.clone(), kind, symbol } - } -} -impl ProjectError for Error { - fn description(&self) -> &str { - match self.kind { - ErrorKind::BadGroup(_) => - "Only `(...)` may be converted to postmacro. `[...]` and `{...}` left \ - in the code are signs of incomplete macro execution", - ErrorKind::EmptyS => "`()` as a clause is meaningless in lambda calculus", - ErrorKind::InvalidArg => "Argument names can only be Name nodes", - ErrorKind::Placeholder => - "Placeholders shouldn't even appear in the code during macro \ - execution,this is likely a compiler bug", - } - } - - fn message(&self) -> String { - if let ErrorKind::BadGroup(t) = self.kind { - let sym = self.symbol.extern_vec().join("::"); - return format!("{}{} block found in {sym}", t.l(), t.r()); - } - format!( - "in {}, {}", - self.symbol.extern_vec().join("::"), - self.description() - ) - } - fn one_position(&self) -> Location { self.location.clone() } -} - -/// Try to convert an expression from AST format to typed lambda -pub fn expr( - expr: &ast::Expr, - symbol: Sym, -) -> Result { - expr_rec(expr, Context::new(symbol)) -} - -#[derive(Clone)] -struct Context<'a> { - names: Substack<'a, Sym>, - symbol: Sym, -} - -impl<'a> Context<'a> { - #[must_use] - fn w_name<'b>(&'b self, name: Sym) -> Context<'b> - where - 'a: 'b, - { - Context { names: self.names.push(name), symbol: self.symbol.clone() } - } -} -impl Context<'static> { - #[must_use] - fn new(symbol: Sym) -> Self { Self { names: Substack::Bottom, symbol } } -} - -/// Process an expression sequence -fn exprv_rec<'a>( - location: &'a Location, - v: &'a [ast::Expr], - ctx: Context<'a>, -) -> Result { - let (last, rest) = unwrap_or! {v.split_last(); { - return Err(Error::new(ErrorKind::EmptyS, location, ctx.symbol)); - }}; - if rest.is_empty() { - return expr_rec(&v[0], ctx); - } - let f = exprv_rec(location, rest, ctx.clone())?; - let x = expr_rec(last, ctx)?; - let value = postmacro::Clause::Apply(Rc::new(f), Rc::new(x)); - Ok(postmacro::Expr { value, location: Location::Unknown }) -} - -/// Process an expression -fn expr_rec<'a>( - ast::Expr { value, location }: &'a ast::Expr, - ctx: Context<'a>, -) -> Result { - match value { - ast::Clause::S(PType::Par, body) => - return Ok(postmacro::Expr { - value: exprv_rec(location, body.as_ref(), ctx)?.value, - location: location.clone(), - }), - ast::Clause::S(paren, _) => - return Err(Error::new(ErrorKind::BadGroup(*paren), location, ctx.symbol)), - _ => (), - } - let value = match value { - ast::Clause::Atom(a) => postmacro::Clause::Atom(a.clone()), - ast::Clause::ExternFn(fun) => postmacro::Clause::ExternFn(fun.clone()), - ast::Clause::Lambda(arg, b) => { - let name = match &arg[..] { - [ast::Expr { value: ast::Clause::Name(name), .. }] => name, - [ast::Expr { value: ast::Clause::Placeh { .. }, .. }] => - return Err(Error::new(ErrorKind::Placeholder, location, ctx.symbol)), - _ => - return Err(Error::new(ErrorKind::InvalidArg, location, ctx.symbol)), - }; - let body_ctx = ctx.w_name(name.clone()); - let body = exprv_rec(location, b.as_ref(), body_ctx)?; - postmacro::Clause::Lambda(Rc::new(body)) - }, - ast::Clause::Name(name) => { - let lvl_opt = (ctx.names.iter()) - .enumerate() - .find(|(_, n)| *n == name) - .map(|(lvl, _)| lvl); - match lvl_opt { - Some(lvl) => postmacro::Clause::LambdaArg(lvl), - None => postmacro::Clause::Constant(name.clone()), - } - }, - ast::Clause::S(PType::Par, entries) => - exprv_rec(location, entries.as_ref(), ctx)?.value, - ast::Clause::S(paren, _) => - return Err(Error::new(ErrorKind::BadGroup(*paren), location, ctx.symbol)), - ast::Clause::Placeh { .. } => - return Err(Error::new(ErrorKind::Placeholder, location, ctx.symbol)), - }; - Ok(postmacro::Expr { value, location: location.clone() }) -} diff --git a/src/representations/const_tree.rs b/src/representations/const_tree.rs deleted file mode 100644 index ce79a6f..0000000 --- a/src/representations/const_tree.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::ops::Add; - -use hashbrown::HashMap; - -use super::project::{ItemKind, ProjectItem}; -use crate::ast::{Clause, Expr}; -use crate::foreign::{Atom, Atomic, ExFn, ExternFn}; -use crate::interner::Tok; -use crate::representations::location::Location; -use crate::representations::project::{ProjectExt, ProjectMod, ProjectTree}; -use crate::representations::tree::{ModEntry, ModMember, Module}; -use crate::representations::VName; -use crate::utils::substack::Substack; - -/// A lightweight module tree that can be built declaratively by hand to -/// describe libraries of external functions in Rust. It implements [Add] for -/// added convenience -#[derive(Clone, Debug)] -pub enum ConstTree { - /// A function or constant - Const(Expr), - /// A submodule - Tree(HashMap, ConstTree>), -} -impl ConstTree { - /// Describe a [Primitive] - #[must_use] - pub fn clause(value: Clause) -> Self { - Self::Const(Expr { location: Location::Unknown, value }) - } - /// Describe an [ExternFn] - #[must_use] - pub fn xfn(xfn: impl ExternFn + 'static) -> Self { - Self::clause(Clause::ExternFn(ExFn(Box::new(xfn)))) - } - /// Describe an [Atomic] - #[must_use] - pub fn atom(atom: impl Atomic + 'static) -> Self { - Self::clause(Clause::Atom(Atom(Box::new(atom)))) - } - /// Describe a module - #[must_use] - pub fn tree(arr: impl IntoIterator, Self)>) -> Self { - Self::Tree(arr.into_iter().collect()) - } - /// Namespace the tree with the list of names - #[must_use] - pub fn namespace( - pref: impl IntoIterator>, - data: Self, - ) -> Self { - let mut iter = pref.into_iter(); - if let Some(ns) = iter.next() { - Self::tree([(ns, Self::namespace(iter, data))]) - } else { - data - } - } - /// Unwrap the table of subtrees from a tree - /// - /// # Panics - /// - /// If this is a leaf node aka. constant and not a namespace - #[must_use] - pub fn unwrap_tree(self) -> HashMap, Self> { - match self { - Self::Tree(map) => map, - _ => panic!("Attempted to unwrap leaf as tree"), - } - } -} -impl Add for ConstTree { - type Output = ConstTree; - - fn add(self, rhs: ConstTree) -> Self::Output { - if let (Self::Tree(t1), Self::Tree(mut t2)) = (self, rhs) { - let mut product = HashMap::new(); - for (key, i1) in t1 { - if let Some(i2) = t2.remove(&key) { - product.insert(key, i1 + i2); - } else { - product.insert(key, i1); - } - } - product.extend(t2); - Self::Tree(product) - } else { - panic!("cannot combine tree and value fields") - } - } -} - -#[must_use] -fn from_const_tree_rec( - path: Substack>, - consts: HashMap, ConstTree>, - file: &[Tok], -) -> ProjectMod { - let mut items = HashMap::new(); - for (name, item) in consts { - items.insert(name.clone(), ModEntry { - exported: true, - member: match item { - ConstTree::Const(c) => - ModMember::Item(ProjectItem { kind: ItemKind::Const(c) }), - ConstTree::Tree(t) => - ModMember::Sub(from_const_tree_rec(path.push(name), t, file)), - }, - }); - } - Module { - entries: items, - extra: ProjectExt { - file: Some(file.to_vec()), - imports_from: HashMap::new(), - rules: Vec::new(), - path: path.iter().rev_vec_clone(), - }, - } -} - -/// Convert a map of [ConstTree] into a [ProjectTree] that can be used with the -/// layered parsing system -#[must_use] -pub fn from_const_tree( - consts: HashMap, ConstTree>, - file: &[Tok], -) -> ProjectTree { - let module = from_const_tree_rec(Substack::Bottom, consts, file); - ProjectTree(module) -} diff --git a/src/representations/interpreted.rs b/src/representations/interpreted.rs deleted file mode 100644 index b628af2..0000000 --- a/src/representations/interpreted.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! The interpreter's changing internal representation of the code at runtime -//! -//! This code may be generated to minimize the number of states external -//! functions have to define -use std::fmt::{Debug, Display}; -use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, Mutex, TryLockError}; - -#[allow(unused)] // for doc -use super::ast; -use super::location::Location; -use super::path_set::PathSet; -#[allow(unused)] // for doc -use crate::foreign::Atomic; -use crate::foreign::{Atom, ExFn, XfnResult}; -use crate::utils::ddispatch::request; -use crate::utils::take_with_output; -use crate::Sym; - -/// An expression with metadata -#[derive(Clone)] -pub struct Expr { - /// The actual value - pub clause: Clause, - /// Information about the code that produced this value - pub location: Location, -} - -impl Debug for Expr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.location { - Location::Unknown => write!(f, "{:?}", self.clause), - loc => write!(f, "{:?}@{}", self.clause, loc), - } - } -} - -impl Display for Expr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.clause) - // match &self.location { - // Location::Unknown => write!(f, "{}", self.clause), - // loc => write!(f, "{}:({})", loc, self.clause), - // } - } -} - -/// [ExprInst::with_literal] produces this marker unit to indicate that the -/// expression is not a literal -pub struct NotALiteral; - -/// Types automatically convertible from an [ExprInst] -pub trait TryFromExprInst: Sized { - /// Match and clone the value out of an [ExprInst] - fn from_exi(exi: ExprInst) -> XfnResult; -} - -impl TryFromExprInst for ExprInst { - fn from_exi(exi: ExprInst) -> XfnResult { Ok(exi) } -} - -/// A wrapper around expressions to handle their multiple occurences in -/// the tree together -#[derive(Clone)] -pub struct ExprInst(pub Arc>); -impl ExprInst { - /// Wrap an [Expr] in a shared container so that normalizatoin steps are - /// applied to all references - #[must_use] - pub fn new(expr: Expr) -> Self { Self(Arc::new(Mutex::new(expr))) } - - /// Take the [Expr] out of this container if it's the last reference to it, or - /// clone it out. - #[must_use] - pub fn expr_val(self) -> Expr { - Arc::try_unwrap(self.0) - .map(|c| c.into_inner().unwrap()) - .unwrap_or_else(|arc| arc.lock().unwrap().clone()) - } - - /// Read-only access to the shared expression instance - /// - /// # Panics - /// - /// if the expression is already borrowed in read-write mode - #[must_use] - pub fn expr(&self) -> impl Deref + '_ { - self.0.lock().unwrap() - } - - /// Read-Write access to the shared expression instance - /// - /// # Panics - /// - /// if the expression is already borrowed - #[must_use] - pub fn expr_mut(&self) -> impl DerefMut + '_ { - self.0.lock().unwrap() - } - - /// Call a normalization function on the expression. The expr is - /// updated with the new clause which affects all copies of it - /// across the tree. - pub fn try_normalize( - &self, - mapper: impl FnOnce(Clause, &Location) -> Result<(Clause, T), E>, - ) -> Result<(Self, T), E> { - let extra = take_with_output(&mut *self.expr_mut(), |expr| { - let Expr { clause, location } = expr; - match mapper(clause, &location) { - Ok((clause, t)) => (Expr { clause, location }, Ok(t)), - Err(e) => (Expr { clause: Clause::Bottom, location }, Err(e)), - } - })?; - Ok((self.clone(), extra)) - } - - /// Run a mutation function on the expression, producing a new, - /// distinct expression. The new expression shares location info with - /// the original but is normalized independently. - pub fn try_update( - self, - mapper: impl FnOnce(Clause, Location) -> Result<(Clause, T), E>, - ) -> Result<(Self, T), E> { - let Expr { clause, location } = self.expr_val(); - let (clause, extra) = mapper(clause, location.clone())?; - Ok((Self::new(Expr { clause, location }), extra)) - } - - /// Call a predicate on the expression, returning whatever the - /// predicate returns. This is a convenience function for reaching - /// through the RefCell. - #[must_use] - pub fn inspect(&self, predicate: impl FnOnce(&Clause) -> T) -> T { - predicate(&self.expr().clause) - } - - /// Visit all expressions in the tree. The search can be exited early by - /// returning [Some] - /// - /// See also [ast::Expr::search_all] - pub fn search_all( - &self, - predicate: &mut impl FnMut(&Self) -> Option, - ) -> Option { - if let Some(t) = predicate(self) { - return Some(t); - } - self.inspect(|c| match c { - Clause::Apply { f, x } => - f.search_all(predicate).or_else(|| x.search_all(predicate)), - Clause::Lambda { body, .. } => body.search_all(predicate), - Clause::Constant(_) - | Clause::LambdaArg - | Clause::Atom(_) - | Clause::ExternFn(_) - | Clause::Bottom => None, - }) - } - - /// Convert into any type that implements [FromExprInst]. Calls to this - /// function are generated wherever a conversion is elided in an extern - /// function. - pub fn downcast(self) -> XfnResult { - T::from_exi(self) - } - - /// Get the code location data associated with this expresssion directly - #[must_use] - pub fn location(&self) -> Location { self.expr().location.clone() } - - /// If this expression is an [Atomic], request an object of the given type. - /// If it's not an atomic, fail the request automatically. - #[must_use = "your request might not have succeeded"] - pub fn request(&self) -> Option { - match &self.expr().clause { - Clause::Atom(a) => request(&*a.0), - _ => None, - } - } -} - -impl Debug for ExprInst { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.0.try_lock() { - Ok(expr) => write!(f, "{expr:?}"), - Err(TryLockError::Poisoned(_)) => write!(f, ""), - Err(TryLockError::WouldBlock) => write!(f, ""), - } - } -} - -impl Display for ExprInst { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.0.try_lock() { - Ok(expr) => write!(f, "{expr}"), - Err(TryLockError::Poisoned(_)) => write!(f, ""), - Err(TryLockError::WouldBlock) => write!(f, ""), - } - } -} - -/// Distinct types of expressions recognized by the interpreter -#[derive(Debug, Clone)] -pub enum Clause { - /// An expression that causes an error - Bottom, - /// An opaque function, eg. an effectful function employing CPS - ExternFn(ExFn), - /// An opaque non-callable value, eg. a file handle - Atom(Atom), - /// A function application - Apply { - /// Function to be applied - f: ExprInst, - /// Argument to be substituted in the function - x: ExprInst, - }, - /// A name to be looked up in the interpreter's symbol table - Constant(Sym), - /// A function - Lambda { - /// A collection of (zero or more) paths to placeholders belonging to this - /// function - args: Option, - /// The tree produced by this function, with placeholders where the - /// argument will go - body: ExprInst, - }, - /// A placeholder within a function that will be replaced upon application - LambdaArg, -} -impl Clause { - /// Wrap a constructed clause in an expression. Avoid using this to wrap - /// copied or moved clauses as it does not have debug information and - /// does not share a normalization cache list with them. - pub fn wrap(self) -> ExprInst { - ExprInst(Arc::new(Mutex::new(Expr { - location: Location::Unknown, - clause: self, - }))) - } - - /// Construct an application step - pub fn apply(f: Self, x: Self) -> Self { - Self::Apply { f: f.wrap(), x: x.wrap() } - } - - /// Construct a lambda that uses its argument. See also [Clause::constfn] - pub fn lambda(arg: PathSet, body: Self) -> Self { - Self::Lambda { args: Some(arg), body: body.wrap() } - } - - /// Construct a lambda that discards its argument. See also [Clause::lambda] - pub fn constfn(body: Self) -> Self { - Self::Lambda { args: None, body: body.wrap() } - } - - /// Construct a lambda that picks its argument and places it in a directly - /// descendant slot. Body must be a [Clause::LambdaArg] nested in an arbitrary - /// number of [Clause::Lambda]s - pub fn pick(body: Self) -> Self { - Self::Lambda { args: Some(PathSet::pick()), body: body.wrap() } - } -} - -impl Display for Clause { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Clause::ExternFn(fun) => write!(f, "{fun:?}"), - Clause::Atom(a) => write!(f, "{a:?}"), - Clause::Bottom => write!(f, "bottom"), - Clause::LambdaArg => write!(f, "arg"), - Clause::Apply { f: fun, x } => write!(f, "({fun} {x})"), - Clause::Lambda { args, body } => match args { - Some(path) => write!(f, "\\{path:?}.{body}"), - None => write!(f, "\\_.{body}"), - }, - Clause::Constant(t) => write!(f, "{}", t.extern_vec().join("::")), - } - } -} diff --git a/src/representations/location.rs b/src/representations/location.rs deleted file mode 100644 index e54797f..0000000 --- a/src/representations/location.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::fmt::{Debug, Display}; -use std::ops::Range; -use std::sync::Arc; - -use itertools::Itertools; - -use crate::VName; - -/// A location in a file, identifies a sequence of suspect characters for any -/// error. Meaningful within the context of a project. -#[derive(Clone, PartialEq, Eq, Hash)] -pub enum Location { - /// Location information lost or code generated on the fly - Unknown, - /// Only the file is known - File(Arc), - /// Character slice of the code - Range { - /// Argument to the file loading callback that produced this code - file: Arc, - /// Index of the unicode code points associated with the code - range: Range, - /// The full source code as received by the parser - source: Arc, - }, -} - -impl Location { - /// Range, if known. If the range is known, the file is always known - #[must_use] - pub fn range(&self) -> Option> { - if let Self::Range { range, .. } = self { - Some(range.clone()) - } else { - None - } - } - - /// File, if known - #[must_use] - pub fn file(&self) -> Option> { - if let Self::File(file) | Self::Range { file, .. } = self { - Some(file.clone()) - } else { - None - } - } - - /// Associated source code, if known - #[must_use] - pub fn source(&self) -> Option> { - if let Self::Range { source, .. } = self { - Some(source.clone()) - } else { - None - } - } - - /// If the two locations are ranges in the same file, connect them. - /// Otherwise choose the more accurate, preferring lhs if equal. - #[must_use] - pub fn to(self, other: Self) -> Self { - match self { - Location::Unknown => other, - Location::File(f) => match other { - Location::Range { .. } => other, - _ => Location::File(f), - }, - Location::Range { file, range: r1, source } => { - let range = match other { - Location::Range { file: f2, range: r2, .. } if file == f2 => - r1.start..r2.end, - _ => r1, - }; - Location::Range { file, source, range } - }, - } - } - - /// Choose one of the two locations, preferring better accuracy, or lhs if - /// equal - #[must_use] - pub fn or(self, alt: Self) -> Self { - match (&self, &alt) { - (Self::Unknown, _) => alt, - (Self::File { .. }, Self::Range { .. }) => alt, - _ => self, - } - } -} - -impl Display for Location { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Unknown => write!(f, "unknown"), - Self::File(file) => write!(f, "{}.orc", file.iter().join("/")), - Self::Range { file, range, source } => { - let (sl, sc) = pos2lc(source, range.start); - let (el, ec) = pos2lc(source, range.end); - write!(f, "{}.orc ", file.iter().join("/"))?; - write!(f, "{sl}:{sc}")?; - if el == sl { - if sc + 1 == ec { Ok(()) } else { write!(f, "..{ec}") } - } else { - write!(f, "..{el}:{ec}") - } - }, - } - } -} - -impl Debug for Location { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self}") - } -} - -#[must_use] -fn pos2lc(s: &str, i: usize) -> (usize, usize) { - s.chars().take(i).fold((1, 1), |(line, col), char| { - if char == '\n' { (line + 1, 1) } else { (line, col + 1) } - }) -} diff --git a/src/representations/mod.rs b/src/representations/mod.rs deleted file mode 100644 index bb357d3..0000000 --- a/src/representations/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod ast; -pub mod ast_to_interpreted; -pub mod ast_to_postmacro; -mod const_tree; -pub mod interpreted; -pub mod location; -mod namelike; -pub mod path_set; -pub mod postmacro; -pub mod postmacro_to_interpreted; -pub mod project; -pub mod sourcefile; -mod string; -pub mod tree; - -pub use const_tree::{from_const_tree, ConstTree}; -pub use location::Location; -pub use namelike::{NameLike, Sym, VName}; -pub use path_set::PathSet; -pub use string::OrcString; diff --git a/src/representations/namelike.rs b/src/representations/namelike.rs deleted file mode 100644 index b833ccd..0000000 --- a/src/representations/namelike.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::fmt::Debug; -use std::hash::Hash; - -use crate::interner::{Interner, Tok}; - -/// A mutable representation of a namespaced identifier. -/// -/// These names may be relative or otherwise partially processed. -/// -/// See also [Sym] -pub type VName = Vec>; - -/// An interned representation of a namespaced identifier. -/// -/// These names are always absolute. -/// -/// See also [VName] -pub type Sym = Tok; - -/// An abstraction over tokenized vs non-tokenized names so that they can be -/// handled together in datastructures -pub trait NameLike: 'static + Clone + Eq + Hash + Debug { - /// Fully resolve the name for printing - #[must_use] - fn to_strv(&self) -> Vec; -} - -impl NameLike for Sym { - fn to_strv(&self) -> Vec { self.extern_vec() } -} - -impl NameLike for VName { - fn to_strv(&self) -> Vec { Interner::extern_all(self) } -} diff --git a/src/representations/path_set.rs b/src/representations/path_set.rs deleted file mode 100644 index 01b3f59..0000000 --- a/src/representations/path_set.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::fmt::Debug; -use std::ops::Add; -use std::sync::Arc; - -use crate::utils::rc_tools::arc_to_owned; -use crate::utils::Side; - -/// A branching path selecting some placeholders (but at least one) in a Lambda -/// expression -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct PathSet { - /// The definite steps - pub steps: Arc>, - /// if Some, it splits. If None, it ends. - pub next: Option<(Arc, Arc)>, -} - -impl PathSet { - /// Create a path set for more than one target - pub fn branch( - steps: impl IntoIterator, - left: Self, - right: Self, - ) -> Self { - let steps = Arc::new(steps.into_iter().collect()); - Self { steps, next: Some((Arc::new(left), Arc::new(right))) } - } - - /// Create a path set for one target - pub fn end(steps: impl IntoIterator) -> Self { - Self { steps: Arc::new(steps.into_iter().collect()), next: None } - } - - /// Create a path set points to a slot that is a direct - /// child of the given lambda with no applications. In essence, this means - /// that this argument will be picked as the value of the expression after an - /// arbitrary amount of subsequent discarded parameters. - pub fn pick() -> Self { Self { steps: Arc::new(vec![]), next: None } } -} - -impl Debug for PathSet { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for s in self.steps.as_ref() { - match s { - Side::Left => write!(f, "L")?, - Side::Right => write!(f, "R")?, - } - } - match &self.next { - Some((l, r)) => write!(f, "({l:?}|{r:?})")?, - None => write!(f, "x")?, - } - Ok(()) - } -} - -impl Add for PathSet { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Self { - steps: Arc::new(vec![]), - next: Some((Arc::new(self), Arc::new(rhs))), - } - } -} - -impl Add for PathSet { - type Output = Self; - fn add(self, rhs: Side) -> Self::Output { - let PathSet { steps, next } = self; - let mut new_steps = arc_to_owned(steps); - new_steps.insert(0, rhs); - Self { steps: Arc::new(new_steps), next } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_combine() -> Result<(), &'static str> { - let ps1 = - PathSet { next: None, steps: Arc::new(vec![Side::Left, Side::Left]) }; - let ps2 = - PathSet { next: None, steps: Arc::new(vec![Side::Left, Side::Right]) }; - let sum = ps1.clone() + ps2.clone(); - assert_eq!(sum.steps.as_ref(), &[]); - let nexts = sum.next.ok_or("nexts not set")?; - assert_eq!(nexts.0.as_ref(), &ps1); - assert_eq!(nexts.1.as_ref(), &ps2); - Ok(()) - } - - fn extend_scaffold() -> PathSet { - PathSet { - next: Some(( - Arc::new(PathSet { - next: None, - steps: Arc::new(vec![Side::Left, Side::Left]), - }), - Arc::new(PathSet { - next: None, - steps: Arc::new(vec![Side::Left, Side::Right]), - }), - )), - steps: Arc::new(vec![Side::Left, Side::Right, Side::Left]), - } - } - - #[test] - fn test_extend_noclone() { - let ps = extend_scaffold(); - let new = ps + Side::Left; - assert_eq!(new.steps.as_ref().as_slice(), &[ - Side::Left, - Side::Left, - Side::Right, - Side::Left - ]) - } - - #[test] - fn test_extend_clone() { - let ps = extend_scaffold(); - let _anchor = ps.clone(); - let new = ps + Side::Left; - assert_eq!(new.steps.len(), 4); - } -} diff --git a/src/representations/postmacro_to_interpreted.rs b/src/representations/postmacro_to_interpreted.rs deleted file mode 100644 index fed105d..0000000 --- a/src/representations/postmacro_to_interpreted.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use super::path_set::PathSet; -use super::{interpreted, postmacro}; -use crate::utils::Side; - -#[must_use] -fn collect_paths_expr_rec( - expr: &postmacro::Expr, - depth: usize, -) -> Option { - collect_paths_cls_rec(&expr.value, depth) -} - -fn collect_paths_cls_rec( - cls: &postmacro::Clause, - depth: usize, -) -> Option { - match cls { - postmacro::Clause::Atom(_) | postmacro::Clause::ExternFn(_) => None, - postmacro::Clause::Constant(_) => None, - postmacro::Clause::LambdaArg(h) => match *h != depth { - true => None, - false => Some(PathSet::pick()), - }, - postmacro::Clause::Lambda(b) => collect_paths_expr_rec(b, depth + 1), - postmacro::Clause::Apply(f, x) => { - let f_opt = collect_paths_expr_rec(f, depth); - let x_opt = collect_paths_expr_rec(x, depth); - match (f_opt, x_opt) { - (Some(f_refs), Some(x_refs)) => Some(f_refs + x_refs), - (Some(f_refs), None) => Some(f_refs + Side::Left), - (None, Some(x_refs)) => Some(x_refs + Side::Right), - (None, None) => None, - } - }, - } -} - -pub fn clause(cls: &postmacro::Clause) -> interpreted::Clause { - match cls { - postmacro::Clause::Constant(name) => - interpreted::Clause::Constant(name.clone()), - postmacro::Clause::Atom(a) => interpreted::Clause::Atom(a.clone()), - postmacro::Clause::ExternFn(fun) => - interpreted::Clause::ExternFn(fun.clone()), - postmacro::Clause::Apply(f, x) => - interpreted::Clause::Apply { f: expr(f.as_ref()), x: expr(x.as_ref()) }, - postmacro::Clause::Lambda(body) => interpreted::Clause::Lambda { - args: collect_paths_expr_rec(body, 0), - body: expr(body), - }, - postmacro::Clause::LambdaArg(_) => interpreted::Clause::LambdaArg, - } -} - -pub fn expr(expr: &postmacro::Expr) -> interpreted::ExprInst { - interpreted::ExprInst(Arc::new(Mutex::new(interpreted::Expr { - location: expr.location.clone(), - clause: clause(&expr.value), - }))) -} diff --git a/src/representations/project.rs b/src/representations/project.rs deleted file mode 100644 index 49ca356..0000000 --- a/src/representations/project.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::fmt::Display; -use std::ops::Add; - -use hashbrown::HashMap; - -use crate::ast::{Expr, Rule}; -use crate::interner::{Interner, Tok}; -use crate::representations::tree::{ModMember, Module}; -use crate::representations::NameLike; -use crate::tree::ModEntry; -use crate::utils::never::{always, Always}; -use crate::utils::substack::Substack; -use crate::{Sym, VName}; - -#[derive(Debug, Clone)] -pub enum ItemKind { - /// An imported symbol or module. The value is the absolute path of - /// the symbol that should be used instead of this one. - /// - /// Notice that this is different from [ProjectExt::imports_from] the values - /// of which do not include the name they're keyed with. - Alias(VName), - None, - Const(Expr), -} - -impl Default for ItemKind { - fn default() -> Self { Self::None } -} - -#[derive(Debug, Clone, Default)] -pub struct ProjectItem { - pub kind: ItemKind, -} - -impl Display for ProjectItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.kind { - ItemKind::None => write!(f, "keyword"), - ItemKind::Const(c) => write!(f, "constant {c}"), - ItemKind::Alias(alias) => { - write!(f, "alias to {}", Interner::extern_all(alias).join("::")) - }, - } - } -} - -/// Information about an imported symbol -#[derive(Debug, Clone)] -pub struct ImpReport { - /// Absolute path of the module the symbol is imported from - pub source: N, -} - -/// Additional data about a loaded module beyond the list of constants and -/// submodules -#[derive(Clone, Debug)] -pub struct ProjectExt { - /// Full path leading to this module - pub path: VName, - /// Pairs each imported token to the absolute path of the module it is - /// imported from. The path does not include the name of referencedthe - /// symbol. - pub imports_from: HashMap, ImpReport>, - /// All rules defined in this module, exported or not - pub rules: Vec>, - /// Filename, if known, for error reporting - pub file: Option, -} - -impl Add for ProjectExt { - type Output = Always; - - fn add(mut self, rhs: Self) -> Self::Output { - let ProjectExt { path, imports_from, rules, file } = rhs; - if path != self.path { - panic!( - "Differently named trees overlain: {} vs {}", - Interner::extern_all(&path).join("::"), - Interner::extern_all(&self.path).join("::") - ) - } - self.imports_from.extend(imports_from); - self.rules.extend(rules); - if file.is_some() { - self.file = file - } - always(self) - } -} - -impl Display for ProjectExt { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Project-module extras") - .field("path", &Interner::extern_all(&self.path).join("::")) - .field("imports_from", &self.imports_from) - .field("rules", &self.rules) - .field("file", &(Interner::extern_all(&self.path).join("/") + ".orc")) - .finish() - } -} - -/// A child to a [ProjectMod] -pub type ProjectEntry = ModEntry, ProjectExt>; -/// A node in the tree describing the project -pub type ProjectMod = Module, ProjectExt>; - -/// Module corresponding to the root of a project -#[derive(Debug, Clone)] -pub struct ProjectTree(pub ProjectMod); - -fn collect_rules_rec( - bag: &mut Vec>, - module: &ProjectMod, -) { - bag.extend(module.extra.rules.iter().cloned()); - for item in module.entries.values() { - if let ModMember::Sub(module) = &item.member { - collect_rules_rec(bag, module); - } - } -} - -/// Collect the complete list of rules to be used by the rule repository from -/// the [ProjectTree] -#[must_use] -pub fn collect_rules(project: &ProjectTree) -> Vec> { - let mut rules = Vec::new(); - collect_rules_rec(&mut rules, &project.0); - rules -} - -fn collect_consts_rec( - path: Substack>, - bag: &mut HashMap>, - module: &ProjectMod, - i: &Interner, -) { - for (key, entry) in module.entries.iter() { - match &entry.member { - ModMember::Item(it) => - if let ItemKind::Const(expr) = &it.kind { - let mut name = path.iter().rev_vec_clone(); - name.push(key.clone()); - bag.insert(i.i(&name), expr.clone()); - }, - ModMember::Sub(module) => - collect_consts_rec(path.push(key.clone()), bag, module, i), - } - } -} - -/// Extract the symbol table from a [ProjectTree] -#[must_use] -pub fn collect_consts( - project: &ProjectTree, - i: &Interner, -) -> HashMap> { - let mut consts = HashMap::new(); - collect_consts_rec(Substack::Bottom, &mut consts, &project.0, i); - consts -} - -#[must_use] -fn vname_to_sym_tree_rec( - tree: ProjectMod, - i: &Interner, -) -> ProjectMod { - let process_expr = |ex: Expr| ex.transform_names(&|n| i.i(&n)); - ProjectMod { - entries: (tree.entries.into_iter()) - .map(|(k, ModEntry { exported, member })| { - (k, ModEntry { - exported, - member: match member { - ModMember::Sub(module) => - ModMember::Sub(vname_to_sym_tree_rec(module, i)), - ModMember::Item(ex) => ModMember::Item(ProjectItem { - kind: match ex.kind { - ItemKind::None => ItemKind::None, - ItemKind::Alias(n) => ItemKind::Alias(n), - ItemKind::Const(ex) => ItemKind::Const(process_expr(ex)), - }, - }), - }, - }) - }) - .collect(), - extra: ProjectExt { - path: tree.extra.path, - imports_from: (tree.extra.imports_from.into_iter()) - .map(|(k, v)| (k, ImpReport { source: i.i(&v.source) })) - .collect(), - rules: (tree.extra.rules.into_iter()) - .map(|Rule { pattern, prio, template }| Rule { - pattern: pattern.into_iter().map(process_expr).collect(), - prio, - template: template.into_iter().map(process_expr).collect(), - }) - .collect(), - file: tree.extra.file, - }, - } -} - -/// Convert a flexible vname-based tree to a more rigid but faster symbol-based -/// tree. The pipeline works with vnames, but the macro executor works with -/// symbols. -#[must_use] -pub fn vname_to_sym_tree( - tree: ProjectTree, - i: &Interner, -) -> ProjectTree { - ProjectTree(vname_to_sym_tree_rec(tree.0, i)) -} diff --git a/src/representations/sourcefile.rs b/src/representations/sourcefile.rs deleted file mode 100644 index 70eeff0..0000000 --- a/src/representations/sourcefile.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! Building blocks of a source file -use std::fmt::Display; - -use itertools::{Either, Itertools}; - -use super::namelike::VName; -use crate::ast::{Constant, Rule}; -use crate::error::{ProjectError, ProjectResult, TooManySupers}; -use crate::interner::{Interner, Tok}; -use crate::utils::pure_seq::pushed; -use crate::utils::BoxedIter; -use crate::Location; - -/// An import pointing at another module, either specifying the symbol to be -/// imported or importing all available symbols with a globstar (*) -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Import { - /// Import path, a sequence of module names. Can either start with - /// - /// - `self` to reference the current module - /// - any number of `super` to reference the parent module of the implied - /// `self` - /// - a root name - pub path: VName, - /// If name is None, this is a wildcard import - pub name: Option>, - /// Location of the final name segment, which uniquely identifies this name - pub location: Location, -} -impl Import { - /// Get the preload target space for this import - the prefix below - /// which all files should be included in the compilation - /// - /// Returns the path if this is a glob import, or the path plus the - /// name if this is a specific import - #[must_use] - pub fn nonglob_path(&self) -> VName { - let mut path_vec = self.path.clone(); - if let Some(n) = &self.name { - path_vec.push(n.clone()) - } - path_vec - } -} - -impl Display for Import { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let paths = self.path.iter().map(|t| &**t).join("::"); - let names = self.name.as_ref().map(|t| t.as_str()).unwrap_or("*"); - write!(f, "{paths}::{names}") - } -} - -/// A namespace block -#[derive(Debug, Clone)] -pub struct ModuleBlock { - /// Name prefixed to all names in the block - pub name: Tok, - /// Prefixed entries - pub body: Vec, -} - -impl Display for ModuleBlock { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let bodys = self.body.iter().map(|e| e.to_string()).join("\n"); - write!(f, "module {} {{\n{}\n}}", self.name, bodys) - } -} - -/// see [Member] -#[derive(Debug, Clone)] -pub enum MemberKind { - /// A substitution rule. Rules apply even when they're not in scope, if the - /// absolute names are present eg. because they're produced by other rules - Rule(Rule), - /// A constant (or function) associated with a name - Constant(Constant), - /// A prefixed set of other entries - Module(ModuleBlock), -} -impl MemberKind { - /// Convert to [FileEntry] - pub fn to_entry(self, exported: bool, location: Location) -> FileEntry { - FileEntryKind::Member(Member { exported, kind: self }).wrap(location) - } -} - -impl Display for MemberKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Constant(c) => c.fmt(f), - Self::Module(m) => m.fmt(f), - Self::Rule(r) => r.fmt(f), - } - } -} - -/// Things that may be prefixed with an export -/// see [MemberKind] -#[derive(Debug, Clone)] -pub struct Member { - /// Various members - pub kind: MemberKind, - /// Whether this member is exported or not - pub exported: bool, -} - -impl Display for Member { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self { exported: true, kind } => write!(f, "export {kind}"), - Self { exported: false, kind } => write!(f, "{kind}"), - } - } -} - -/// See [FileEntry] -#[derive(Debug, Clone)] -pub enum FileEntryKind { - /// Imports one or all names in a module - Import(Vec), - /// Comments are kept here in case dev tooling wants to parse documentation - Comment(String), - /// An element with visibility information - Member(Member), - /// A list of tokens exported explicitly. This can also create new exported - /// tokens that the local module doesn't actually define a role for - Export(Vec<(Tok, Location)>), -} -impl FileEntryKind { - /// Wrap with no location - pub fn wrap(self, location: Location) -> FileEntry { - FileEntry { kind: self, locations: vec![location] } - } -} - -impl Display for FileEntryKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Comment(s) => write!(f, "--[{s}]--"), - Self::Export(s) => { - write!(f, "export ::({})", s.iter().map(|t| &**t.0).join(", ")) - }, - Self::Member(member) => write!(f, "{member}"), - Self::Import(i) => { - write!(f, "import ({})", i.iter().map(|i| i.to_string()).join(", ")) - }, - } - } -} - -/// Anything the parser might encounter in a file. See [FileEntryKind] -#[derive(Debug, Clone)] -pub struct FileEntry { - /// What we encountered - pub kind: FileEntryKind, - /// Where we encountered it - pub locations: Vec, -} - -impl Display for FileEntry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.kind.fmt(f) - } -} - -/// Summarize all imports from a file in a single list of qualified names -pub fn imports<'a>( - src: impl Iterator + 'a, -) -> impl Iterator + 'a { - src - .filter_map(|ent| match &ent.kind { - FileEntryKind::Import(impv) => Some(impv.iter()), - _ => None, - }) - .flatten() -} - -/// Join the various redeclarations of namespaces. -/// Error if they're inconsistently exported -pub fn normalize_namespaces( - src: BoxedIter, -) -> Result, VName> { - let (mut namespaces, mut rest) = src - .partition_map::, Vec<_>, _, _, _>(|ent| { - match ent { - FileEntry { - kind: FileEntryKind::Member(Member { - kind: MemberKind::Module(ns), - exported, - }), - locations - } => Either::Left((exported, ns, locations)), - ent => Either::Right(ent) - } - }); - // Combine namespace blocks with the same name - namespaces.sort_unstable_by_key(|(_, ns, _)| ns.name.clone()); - let mut lumped = namespaces - .into_iter() - .group_by(|(_, ns, _)| ns.name.clone()) - .into_iter() - .map(|(name, grp)| { - let mut exported = false; - let mut internal = false; - let mut grouped_source = Vec::new(); - let mut locations = Vec::new(); - for (inst_exported, ns, locs) in grp { - if inst_exported { - exported = true - } else { - internal = true - }; - grouped_source.extend(ns.body.into_iter()); - locations.extend(locs.into_iter()); - } - if exported == internal { - debug_assert!(exported && internal, "Both false is impossible"); - return Err(vec![name]); - } - // Apply the function to the contents of these blocks too - let body = normalize_namespaces(Box::new(grouped_source.into_iter())) - .map_err(|e| pushed(e, name.clone()))?; - let kind = MemberKind::Module(ModuleBlock { name, body }); - let kind = FileEntryKind::Member(Member { kind, exported }); - Ok(FileEntry { kind, locations }) - }) - .collect::, _>>()?; - rest.append(&mut lumped); - Ok(rest) -} - -/// Turn a relative (import) path into an absolute path. -/// If the import path is empty, the return value is also empty. -/// -/// # Errors -/// -/// if the relative path contains more `super` segments than the length -/// of the absolute path. -pub fn absolute_path( - abs_location: &[Tok], - rel_path: &[Tok], - i: &Interner, - location: &Location, -) -> ProjectResult { - absolute_path_rec(abs_location, rel_path, i).ok_or_else(|| { - TooManySupers { path: rel_path.to_vec(), location: location.clone() }.rc() - }) -} - -#[must_use = "this could be None which means that there are too many supers"] -fn absolute_path_rec( - mut abs_location: &[Tok], - mut rel_path: &[Tok], - i: &Interner, -) -> Option { - let mut relative = false; - while rel_path.first() == Some(&i.i("super")) { - abs_location = abs_location.split_last()?.1; - rel_path = rel_path.split_first().expect("checked above").1; - relative = true; - } - if rel_path.first() == Some(&i.i("self")) { - relative = true; - rel_path = rel_path.split_first().expect("checked above").1; - } - match relative { - true => Some(abs_location.iter().chain(rel_path).cloned().collect()), - false => Some(rel_path.to_vec()), - } -} diff --git a/src/representations/string.rs b/src/representations/string.rs deleted file mode 100644 index 1027b4f..0000000 --- a/src/representations/string.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::fmt::Debug; -use std::hash::Hash; -use std::ops::Deref; -use std::sync::Arc; - -use crate::foreign::InertAtomic; -use crate::{Interner, Tok}; - -/// An Orchid string which may or may not be interned -#[derive(Clone, Eq)] -pub enum OrcString { - /// An interned string. Equality-conpared by reference. - Interned(Tok), - /// An uninterned bare string. Equality-compared by character - Runtime(Arc), -} - -impl Debug for OrcString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Runtime(s) => write!(f, "r\"{s}\""), - Self::Interned(t) => write!(f, "i\"{t}\""), - } - } -} - -impl OrcString { - /// Intern the contained string - pub fn intern(&mut self, i: &Interner) { - if let Self::Runtime(t) = self { - *self = Self::Interned(i.i(t.as_str())) - } - } - /// Clone out the plain Rust [String] - #[must_use] - pub fn get_string(self) -> String { - match self { - Self::Interned(s) => s.as_str().to_owned(), - Self::Runtime(rc) => - Arc::try_unwrap(rc).unwrap_or_else(|rc| (*rc).clone()), - } - } -} - -impl Deref for OrcString { - type Target = String; - - fn deref(&self) -> &Self::Target { - match self { - Self::Interned(t) => t, - Self::Runtime(r) => r, - } - } -} - -impl Hash for OrcString { - fn hash(&self, state: &mut H) { - self.as_str().hash(state) - } -} - -impl From for OrcString { - fn from(value: String) -> Self { Self::Runtime(Arc::new(value)) } -} - -impl From> for OrcString { - fn from(value: Tok) -> Self { Self::Interned(value) } -} - -impl PartialEq for OrcString { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Interned(t1), Self::Interned(t2)) => t1 == t2, - _ => **self == **other, - } - } -} - -impl InertAtomic for OrcString { - fn type_str() -> &'static str { "OrcString" } - fn strict_eq(&self, other: &Self) -> bool { self == other } -} diff --git a/src/representations/tree.rs b/src/representations/tree.rs deleted file mode 100644 index 83906a2..0000000 --- a/src/representations/tree.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! Generic module tree structure -//! -//! Used by various stages of the pipeline with different parameters -use std::fmt::{Debug, Display}; -use std::ops::Add; -use std::rc::Rc; - -use hashbrown::HashMap; - -use super::Location; -use crate::error::ProjectError; -use crate::interner::Tok; -use crate::utils::substack::Substack; -use crate::utils::BoxedIter; -use crate::{Interner, VName}; - -/// The member in a [ModEntry] which is associated with a name in a [Module] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ModMember { - /// Arbitrary data - Item(TItem), - /// A child module - Sub(Module), -} - -/// Data about a name in a [Module] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ModEntry { - /// The submodule or item - pub member: ModMember, - /// Whether the member is visible to modules other than the parent - pub exported: bool, -} -impl ModEntry { - /// Returns the item in this entry if it contains one. - #[must_use] - pub fn item(&self) -> Option<&TItem> { - match &self.member { - ModMember::Item(it) => Some(it), - ModMember::Sub(_) => None, - } - } -} - -/// A module, containing imports, -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Module { - /// Submodules and items by name - pub entries: HashMap, ModEntry>, - /// Additional information associated with the module - pub extra: TExt, -} - -/// The path taken to reach a given module -pub type ModPath<'a> = Substack<'a, Tok>; - -impl Module { - /// If the argument is false, returns all child names. - /// If the argument is true, returns all public child names. - #[must_use] - pub fn keys(&self, public: bool) -> BoxedIter> { - match public { - false => Box::new(self.entries.keys().cloned()), - true => Box::new( - (self.entries.iter()) - .filter(|(_, v)| v.exported) - .map(|(k, _)| k.clone()), - ), - } - } - - /// Return the module at the end of the given path - pub fn walk_ref<'a: 'b, 'b>( - &'a self, - prefix: &'b [Tok], - path: &'b [Tok], - public: bool, - ) -> Result<&'a Self, WalkError<'b>> { - let mut module = self; - for (pos, step) in path.iter().enumerate() { - let kind = match module.entries.get(step) { - None => ErrKind::Missing, - Some(ModEntry { exported: false, .. }) if public => ErrKind::Private, - Some(ModEntry { member: ModMember::Item(_), .. }) => ErrKind::NotModule, - Some(ModEntry { member: ModMember::Sub(next), .. }) => { - module = next; - continue; - }, - }; - let options = module.keys(public); - return Err(WalkError { kind, prefix, path, pos, options }); - } - Ok(module) - } - - /// Return the member at the end of the given path - /// - /// # Panics - /// - /// if path is empty, since the reference cannot be forwarded that way - pub fn walk1_ref<'a: 'b, 'b>( - &'a self, - prefix: &'b [Tok], - path: &'b [Tok], - public: bool, - ) -> Result<(&'a ModEntry, &'a Self), WalkError<'b>> { - let (last, parent) = path.split_last().expect("Path cannot be empty"); - let pos = path.len() - 1; - let module = self.walk_ref(prefix, parent, public)?; - if let Some(entry) = &module.entries.get(last) { - if !entry.exported && public { - let options = module.keys(public); - Err(WalkError { kind: ErrKind::Private, options, prefix, path, pos }) - } else { - Ok((entry, module)) - } - } else { - let options = module.keys(public); - Err(WalkError { kind: ErrKind::Missing, options, prefix, path, pos }) - } - } - - fn search_all_rec<'a, T, E>( - &'a self, - path: ModPath, - mut state: T, - callback: &mut impl FnMut(ModPath, &'a Self, T) -> Result, - ) -> Result { - state = callback(path.clone(), self, state)?; - for (name, entry) in &self.entries { - if let ModMember::Sub(module) = &entry.member { - state = - module.search_all_rec(path.push(name.clone()), state, callback)?; - } - } - Ok(state) - } - - /// Visit every element in the tree with the provided function - /// - /// * init - can be used for reduce, otherwise pass `()` - /// * callback - a callback applied on every module. Can return [Err] to - /// short-circuit the walk - /// * [ModPath] - a substack indicating the path to the current module from - /// wherever the walk begun - /// * [Module] - the current module - /// * T - data for reduce. If not used, destructure `()` - pub fn search_all<'a, T, E>( - &'a self, - init: T, - callback: &mut impl FnMut(ModPath, &'a Self, T) -> Result, - ) -> Result { - self.search_all_rec(Substack::Bottom, init, callback) - } - - /// Combine two module trees; wherever they conflict, the overlay is - /// preferred. - pub fn overlay(mut self, overlay: Self) -> Result - where - TExt: Add>, - { - let Module { extra, entries: items } = overlay; - let mut new_items = HashMap::new(); - for (key, right) in items { - // if both contain a submodule - match (self.entries.remove(&key), right) { - ( - Some(ModEntry { member: ModMember::Sub(lsub), .. }), - ModEntry { member: ModMember::Sub(rsub), exported }, - ) => new_items.insert(key, ModEntry { - exported, - member: ModMember::Sub(lsub.overlay(rsub)?), - }), - (_, right) => new_items.insert(key, right), - }; - } - new_items.extend(self.entries); - Ok(Module { entries: new_items, extra: (self.extra + extra)? }) - } -} - -impl Display - for Module -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Module {{\nchildren:")?; - for (name, entry) in &self.entries { - match entry.exported { - true => write!(f, "\npublic {name} = "), - false => write!(f, "\n{name} = "), - }?; - match &entry.member { - ModMember::Sub(module) => write!(f, "{module}"), - ModMember::Item(item) => write!(f, "{item}"), - }?; - } - write!(f, "\nextra: {}\n}}", &self.extra) - } -} - -/// Possible causes why the path could not be walked -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ErrKind { - /// `require_exported` was set to `true` and a module wasn't exported - Private, - /// A module was not found - Missing, - /// The path leads into a leaf node - NotModule, -} - -/// All details about a failed tree-walk -pub struct WalkError<'a> { - /// Failure mode - pub kind: ErrKind, - /// Path to the module where the walk started - pub prefix: &'a [Tok], - /// Planned walk path - pub path: &'a [Tok], - /// Index into walked path where the error occurred - pub pos: usize, - /// Alternatives to the failed steps - pub options: BoxedIter<'a, Tok>, -} -impl<'a> WalkError<'a> { - /// Total length of the path represented by this error - #[must_use] - pub fn depth(&self) -> usize { self.prefix.len() + self.pos + 1 } - - /// Attach a location to the error and convert into trait object for reporting - #[must_use] - pub fn at(self, location: &Location) -> Rc { - // panic!("hello"); - WalkErrorWithLocation { - kind: self.kind, - location: location.clone(), - path: (self.prefix.iter()) - .chain(self.path.iter().take(self.pos + 1)) - .cloned() - .collect(), - options: self.options.collect(), - } - .rc() - } -} - -/// Error produced by [WalkError::at] -struct WalkErrorWithLocation { - path: VName, - kind: ErrKind, - options: VName, - location: Location, -} -impl ProjectError for WalkErrorWithLocation { - fn description(&self) -> &str { - match self.kind { - ErrKind::Missing => "Nonexistent path", - ErrKind::NotModule => "The path leads into a leaf", - ErrKind::Private => "The path leads into a private module", - } - } - - fn message(&self) -> String { - let paths = Interner::extern_all(&self.path).join("::"); - let options = Interner::extern_all(&self.options).join(", "); - match &self.kind { - ErrKind::Missing => { - format!("{paths} does not exist, options are {options}") - }, - ErrKind::NotModule => { - format!("{paths} is not a module, options are {options}") - }, - ErrKind::Private => format!("{paths} is private, options are {options}"), - } - } - - fn one_position(&self) -> Location { self.location.clone() } -} diff --git a/src/rule/matcher.rs b/src/rule/matcher.rs index 0a456c4..9d8ae0d 100644 --- a/src/rule/matcher.rs +++ b/src/rule/matcher.rs @@ -1,10 +1,11 @@ use std::rc::Rc; use super::state::State; -use crate::ast::Expr; -use crate::Sym; +use crate::name::Sym; +use crate::parse::parsed::Expr; -pub type RuleExpr = Expr; +/// The same as [Expr], just extracted for flexibility +pub type RuleExpr = Expr; /// Cacheable optimized structures for matching patterns on slices. This is /// injected to allow experimentation in the matcher implementation. diff --git a/src/rule/matcher_vectree/any_match.rs b/src/rule/matcher_vectree/any_match.rs index ad4a138..d3c05f7 100644 --- a/src/rule/matcher_vectree/any_match.rs +++ b/src/rule/matcher_vectree/any_match.rs @@ -1,9 +1,9 @@ use super::scal_match::scalv_match; use super::shared::AnyMatcher; use super::vec_match::vec_match; +use crate::name::Sym; use crate::rule::matcher::RuleExpr; use crate::rule::state::State; -use crate::Sym; #[must_use] pub fn any_match<'a>( diff --git a/src/rule/matcher_vectree/build.rs b/src/rule/matcher_vectree/build.rs index 80fab8a..bb5431c 100644 --- a/src/rule/matcher_vectree/build.rs +++ b/src/rule/matcher_vectree/build.rs @@ -1,11 +1,11 @@ +use intern_all::Tok; use itertools::Itertools; use super::shared::{AnyMatcher, ScalMatcher, VecMatcher}; -use crate::ast::{Clause, PHClass, Placeholder}; -use crate::interner::Tok; +use crate::parse::parsed::{Clause, PHClass, Placeholder}; use crate::rule::matcher::RuleExpr; use crate::rule::vec_attrs::vec_attrs; -use crate::utils::Side; +use crate::utils::side::Side; pub type MaxVecSplit<'a> = (&'a [RuleExpr], (Tok, usize, bool), &'a [RuleExpr]); @@ -108,7 +108,6 @@ fn mk_vec(pattern: &[RuleExpr]) -> VecMatcher { fn mk_scalar(pattern: &RuleExpr) -> ScalMatcher { match &pattern.value { Clause::Atom(a) => ScalMatcher::Atom(a.clone()), - Clause::ExternFn(_) => panic!("Cannot match on ExternFn"), Clause::Name(n) => ScalMatcher::Name(n.clone()), Clause::Placeh(Placeholder { name, class }) => match class { PHClass::Vec { .. } => { @@ -128,43 +127,49 @@ fn mk_scalar(pattern: &RuleExpr) -> ScalMatcher { #[cfg(test)] mod test { use std::rc::Rc; + use std::sync::Arc; + + use intern_all::i; use super::mk_any; - use crate::ast::{Clause, PHClass, PType, Placeholder}; - use crate::interner::Interner; + use crate::location::{SourceCode, SourceRange}; + use crate::name::{Sym, VPath}; + use crate::parse::parsed::{Clause, PHClass, PType, Placeholder}; #[test] fn test_scan() { - let i = Interner::new(); + let range = SourceRange { + range: 0..1, + code: SourceCode { + path: Arc::new(VPath(vec![])), + source: Arc::new(String::new()), + }, + }; + let ex = |c: Clause| c.into_expr(range.clone()); let pattern = vec![ - Clause::Placeh(Placeholder { + ex(Clause::Placeh(Placeholder { class: PHClass::Vec { nonzero: false, prio: 0 }, - name: i.i("::prefix"), - }) - .into_expr(), - Clause::Name(i.i(&[i.i("prelude"), i.i("do")][..])).into_expr(), - Clause::S( + name: i("::prefix"), + })), + ex(Clause::Name(Sym::literal("prelude::do"))), + ex(Clause::S( PType::Par, Rc::new(vec![ - Clause::Placeh(Placeholder { + ex(Clause::Placeh(Placeholder { class: PHClass::Vec { nonzero: false, prio: 0 }, - name: i.i("expr"), - }) - .into_expr(), - Clause::Name(i.i(&[i.i("prelude"), i.i(";")][..])).into_expr(), - Clause::Placeh(Placeholder { + name: i("expr"), + })), + ex(Clause::Name(Sym::literal("prelude::;"))), + ex(Clause::Placeh(Placeholder { class: PHClass::Vec { nonzero: false, prio: 1 }, - name: i.i("rest"), - }) - .into_expr(), + name: i("rest"), + })), ]), - ) - .into_expr(), - Clause::Placeh(Placeholder { + )), + ex(Clause::Placeh(Placeholder { class: PHClass::Vec { nonzero: false, prio: 0 }, - name: i.i("::suffix"), - }) - .into_expr(), + name: i("::suffix"), + })), ]; let matcher = mk_any(&pattern); println!("{matcher}"); diff --git a/src/rule/matcher_vectree/mod.rs b/src/rule/matcher_vectree/mod.rs index 4fce5b3..d6f9442 100644 --- a/src/rule/matcher_vectree/mod.rs +++ b/src/rule/matcher_vectree/mod.rs @@ -13,8 +13,5 @@ mod any_match; mod build; mod scal_match; -mod shared; +pub mod shared; mod vec_match; - -// pub use build::mk_matcher; -pub use shared::VectreeMatcher; diff --git a/src/rule/matcher_vectree/scal_match.rs b/src/rule/matcher_vectree/scal_match.rs index 5171613..f98bf96 100644 --- a/src/rule/matcher_vectree/scal_match.rs +++ b/src/rule/matcher_vectree/scal_match.rs @@ -1,9 +1,9 @@ use super::any_match::any_match; use super::shared::ScalMatcher; -use crate::ast::Clause; +use crate::name::Sym; +use crate::parse::parsed::Clause; use crate::rule::matcher::RuleExpr; use crate::rule::state::{State, StateEntry}; -use crate::Sym; #[must_use] pub fn scal_match<'a>( @@ -12,15 +12,16 @@ pub fn scal_match<'a>( save_loc: &impl Fn(Sym) -> bool, ) -> Option> { match (matcher, &expr.value) { - (ScalMatcher::Atom(a1), Clause::Atom(a2)) if a1.0.strict_eq(&a2.0) => + (ScalMatcher::Atom(a1), Clause::Atom(a2)) + if a1.run().0.parser_eq(&a2.run().0) => Some(State::default()), (ScalMatcher::Name(n1), Clause::Name(n2)) if n1 == n2 => Some(match save_loc(n1.clone()) { - true => State::from_name(n1.clone(), expr.location.clone()), + true => State::from_name(n1.clone(), expr.range.clone()), false => State::default(), }), (ScalMatcher::Placeh { key, name_only: true }, Clause::Name(n)) => - Some(State::from_ph(key.clone(), StateEntry::Name(n, &expr.location))), + Some(State::from_ph(key.clone(), StateEntry::Name(n, &expr.range))), (ScalMatcher::Placeh { key, name_only: false }, _) => Some(State::from_ph(key.clone(), StateEntry::Scalar(expr))), (ScalMatcher::S(c1, b_mat), Clause::S(c2, body)) if c1 == c2 => diff --git a/src/rule/matcher_vectree/shared.rs b/src/rule/matcher_vectree/shared.rs index 5d67882..6ac6b97 100644 --- a/src/rule/matcher_vectree/shared.rs +++ b/src/rule/matcher_vectree/shared.rs @@ -1,27 +1,27 @@ use std::fmt::{Display, Write}; use std::rc::Rc; +use intern_all::Tok; use itertools::Itertools; use super::any_match::any_match; use super::build::mk_any; -use crate::ast::PType; -use crate::foreign::Atom; -use crate::interner::Tok; +use crate::foreign::atom::AtomGenerator; +use crate::name::Sym; +use crate::parse::parsed::PType; use crate::rule::matcher::{Matcher, RuleExpr}; use crate::rule::state::State; -use crate::utils::Side; -use crate::{Sym, VName}; +use crate::utils::side::Side; -pub enum ScalMatcher { - Atom(Atom), +pub(super) enum ScalMatcher { + Atom(AtomGenerator), Name(Sym), S(PType, Box), Lambda(Box, Box), Placeh { key: Tok, name_only: bool }, } -pub enum VecMatcher { +pub(super) enum VecMatcher { Placeh { key: Tok, nonzero: bool, @@ -48,11 +48,11 @@ pub enum VecMatcher { /// the length of matches on either side. /// /// Vectorial keys that appear on either side, in priority order - key_order: VName, + key_order: Vec>, }, } -pub enum AnyMatcher { +pub(super) enum AnyMatcher { Scalar(Vec), Vec { left: Vec, mid: VecMatcher, right: Vec }, } @@ -78,7 +78,7 @@ impl Display for ScalMatcher { false => write!(f, "${key}"), true => write!(f, "$_{key}"), }, - Self::Name(n) => write!(f, "{}", n.extern_vec().join("::")), + Self::Name(n) => write!(f, "{n}"), Self::S(t, body) => write!(f, "{}{body}{}", t.l(), t.r()), Self::Lambda(arg, body) => write!(f, "\\{arg}.{body}"), } diff --git a/src/rule/matcher_vectree/vec_match.rs b/src/rule/matcher_vectree/vec_match.rs index 30523ab..1bbf0de 100644 --- a/src/rule/matcher_vectree/vec_match.rs +++ b/src/rule/matcher_vectree/vec_match.rs @@ -4,9 +4,9 @@ use itertools::Itertools; use super::scal_match::scalv_match; use super::shared::VecMatcher; +use crate::name::Sym; use crate::rule::matcher::RuleExpr; use crate::rule::state::{State, StateEntry}; -use crate::Sym; #[must_use] pub fn vec_match<'a>( diff --git a/src/rule/mod.rs b/src/rule/mod.rs index cf5c6fb..43db211 100644 --- a/src/rule/mod.rs +++ b/src/rule/mod.rs @@ -1,14 +1,9 @@ //! Substitution rule processing -mod matcher; -mod matcher_vectree; +pub mod matcher; +pub mod matcher_vectree; mod prepare_rule; -mod repository; -mod rule_error; +pub mod repository; +pub mod rule_error; mod state; mod update_first_seq; mod vec_attrs; - -pub use matcher::Matcher; -pub use matcher_vectree::VectreeMatcher; -pub use repository::{Repo, Repository}; -pub use rule_error::RuleError; diff --git a/src/rule/prepare_rule.rs b/src/rule/prepare_rule.rs index 88021ca..6053473 100644 --- a/src/rule/prepare_rule.rs +++ b/src/rule/prepare_rule.rs @@ -1,43 +1,35 @@ use hashbrown::HashMap; +use intern_all::{i, Tok}; use itertools::Itertools; use super::matcher::RuleExpr; +use super::rule_error::RuleError; use super::vec_attrs::vec_attrs; -use super::RuleError; -use crate::ast::{Clause, Expr, PHClass, Placeholder, Rule}; -use crate::interner::{Interner, Tok}; -use crate::representations::location::Location; -use crate::Sym; +use crate::parse::parsed::{Clause, PHClass, Placeholder}; +use crate::pipeline::project::ProjRule; /// Ensure that the rule's source begins and ends with a vectorial without /// changing its meaning #[must_use] -fn pad(mut rule: Rule, i: &Interner) -> Rule { +fn pad(rule: ProjRule) -> ProjRule { + let prefix_name = i("__gen__orchid__rule__prefix"); + let suffix_name = i("__gen__orchid__rule__suffix"); let class: PHClass = PHClass::Vec { nonzero: false, prio: 0 }; - let empty: &[Expr] = &[]; - let prefix: &[Expr] = &[Expr { - location: Location::Unknown, - value: Clause::Placeh(Placeholder { name: i.i("::prefix"), class }), - }]; - let suffix: &[Expr] = &[Expr { - location: Location::Unknown, - value: Clause::Placeh(Placeholder { name: i.i("::suffix"), class }), - }]; - let rule_head = rule.pattern.first().expect("Src can never be empty!"); - let prefix_explicit = vec_attrs(rule_head).is_some(); - let rule_tail = rule.pattern.last().expect("Unreachable branch!"); - let suffix_explicit = vec_attrs(rule_tail).is_some(); - let prefix_v = if prefix_explicit { empty } else { prefix }; - let suffix_v = if suffix_explicit { empty } else { suffix }; - rule.pattern = (prefix_v.iter().cloned()) - .chain(rule.pattern) - .chain(suffix_v.iter().cloned()) - .collect(); - rule.template = (prefix_v.iter().cloned()) - .chain(rule.template) - .chain(suffix_v.iter().cloned()) - .collect(); - rule + let ProjRule { comments, pattern, prio, template } = rule; + let rule_head = pattern.first().expect("Pattern can never be empty!"); + let rule_tail = pattern.last().unwrap(); + let prefix = vec_attrs(rule_head).is_none().then(|| { + Clause::Placeh(Placeholder { name: prefix_name, class }) + .into_expr(rule_head.range.map_range(|r| r.start..r.start)) + }); + let suffix = vec_attrs(rule_tail).is_none().then(|| { + Clause::Placeh(Placeholder { name: suffix_name, class }) + .into_expr(rule_tail.range.map_range(|r| r.start..r.start)) + }); + let pattern = + prefix.iter().cloned().chain(pattern).chain(suffix.clone()).collect(); + let template = prefix.into_iter().chain(template).chain(suffix).collect(); + ProjRule { comments, prio, pattern, template } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -63,7 +55,6 @@ fn check_rec_expr( ) -> Result<(), RuleError> { match &expr.value { Clause::Name(_) | Clause::Atom(_) => Ok(()), - Clause::ExternFn(_) => Err(RuleError::ExternFn), Clause::Placeh(Placeholder { name, class }) => { let typ = (*class).into(); // in a template, the type must be known and identical @@ -111,14 +102,11 @@ fn check_rec_exprv( } } -pub fn prepare_rule( - rule: Rule, - i: &Interner, -) -> Result, RuleError> { +pub fn prepare_rule(rule: ProjRule) -> Result { // Dimension check let mut types = HashMap::new(); check_rec_exprv(&rule.pattern, &mut types, false)?; check_rec_exprv(&rule.template, &mut types, true)?; // Padding - Ok(pad(rule, i)) + Ok(pad(rule)) } diff --git a/src/rule/repository.rs b/src/rule/repository.rs index f194dfb..034f302 100644 --- a/src/rule/repository.rs +++ b/src/rule/repository.rs @@ -6,16 +6,17 @@ use itertools::Itertools; use ordered_float::NotNan; use super::matcher::{Matcher, RuleExpr}; +use super::matcher_vectree::shared::VectreeMatcher; use super::prepare_rule::prepare_rule; +use super::rule_error::RuleError; use super::state::apply_exprv; -use super::{update_first_seq, RuleError, VectreeMatcher}; -use crate::ast::Rule; -use crate::interner::Interner; -use crate::parse::print_nat16; -use crate::Sym; +use super::update_first_seq; +use crate::name::Sym; +use crate::parse::numeric::print_nat16; +use crate::pipeline::project::ProjRule; #[derive(Debug)] -pub struct CachedRule { +pub(super) struct CachedRule { matcher: M, pattern: Vec, pat_glossary: HashSet, @@ -47,16 +48,13 @@ pub struct Repository { } impl Repository { /// Build a new repository to hold the given set of rules - pub fn new( - mut rules: Vec>, - i: &Interner, - ) -> Result, RuleError)> { + pub fn new(mut rules: Vec) -> Result { rules.sort_by_key(|r| -r.prio); let cache = rules .into_iter() .map(|r| { - let Rule { pattern, prio, template } = - prepare_rule(r.clone(), i).map_err(|e| (r, e))?; + let ProjRule { pattern, prio, template, comments: _ } = + prepare_rule(r.clone()).map_err(|e| (r, e))?; let mut pat_glossary = HashSet::new(); pat_glossary.extend( pattern.iter().flat_map(|e| e.value.collect_names().into_iter()), @@ -160,8 +158,7 @@ impl Display for Repository { writeln!(f, "Repository[")?; for (rule, p) in self.cache.iter() { let prio = print_nat16(*p); - let deps = - rule.pat_glossary.iter().map(|t| t.extern_vec().join("::")).join(", "); + let deps = rule.pat_glossary.iter().join(", "); writeln!(f, " priority: {prio}\tdependencies: [{deps}]")?; writeln!(f, " {rule}")?; } diff --git a/src/rule/rule_error.rs b/src/rule/rule_error.rs index b99745b..4322f27 100644 --- a/src/rule/rule_error.rs +++ b/src/rule/rule_error.rs @@ -1,15 +1,13 @@ use std::fmt::{self, Display}; -use std::rc::Rc; use hashbrown::HashSet; +use intern_all::Tok; -use crate::ast::{self, search_all_slcs, PHClass, Placeholder, Rule}; -use crate::error::{ErrorPosition, ProjectError}; -#[allow(unused)] // for doc -use crate::foreign::ExternFn; -use crate::interner::Tok; -use crate::utils::BoxedIter; -use crate::{Location, Sym}; +use crate::error::{ErrorPosition, ProjectError, ProjectErrorObj}; +use crate::location::{CodeLocation, SourceRange}; +use crate::parse::parsed::{search_all_slcs, Clause, PHClass, Placeholder}; +use crate::pipeline::project::ProjRule; +use crate::utils::boxed_iter::BoxedIter; /// Various reasons why a substitution rule may be invalid #[derive(Debug, Clone, PartialEq, Eq)] @@ -22,20 +20,16 @@ pub enum RuleError { Multiple(Tok), /// Two vectorial placeholders are next to each other VecNeighbors(Tok, Tok), - /// Found an [ExternFn] in the pattern. This is a really unlikely mistake - /// caused only by rogue systems. - ExternFn, } impl RuleError { /// Convert into a unified error trait object shared by all Orchid errors #[must_use] - pub fn to_project_error(self, rule: &Rule) -> Rc { + pub fn to_project(self, rule: &ProjRule) -> ProjectErrorObj { match self { - Self::Missing(name) => Missing::new(rule, name).rc(), - Self::Multiple(name) => Multiple::new(rule, name).rc(), - Self::ArityMismatch(name) => ArityMismatch::new(rule, name).rc(), - Self::VecNeighbors(n1, n2) => VecNeighbors::new(rule, n1, n2).rc(), - Self::ExternFn => ExternFnInPattern(rule.clone()).rc(), + Self::Missing(name) => Missing::new(rule, name).pack(), + Self::Multiple(name) => Multiple::new(rule, name).pack(), + Self::ArityMismatch(name) => ArityMismatch::new(rule, name).pack(), + Self::VecNeighbors(n1, n2) => VecNeighbors::new(rule, n1, n2).pack(), } } } @@ -43,7 +37,6 @@ impl RuleError { impl Display for RuleError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::ExternFn => write!(f, "Found an ExternFn in the pattern"), Self::Missing(key) => write!(f, "Key {key} not in match pattern"), Self::ArityMismatch(key) => { write!(f, "Key {key} used inconsistently with and without ellipsis") @@ -60,19 +53,19 @@ impl Display for RuleError { /// A key is present in the template but not the pattern of a rule #[derive(Debug)] -pub struct Missing { - locations: HashSet, +struct Missing { + locations: HashSet, name: Tok, } impl Missing { #[must_use] - pub fn new(rule: &ast::Rule, name: Tok) -> Self { + pub fn new(rule: &ProjRule, name: Tok) -> Self { let mut locations = HashSet::new(); for expr in rule.template.iter() { expr.search_all(&mut |e| { - if let ast::Clause::Placeh(ph) = &e.value { + if let Clause::Placeh(ph) = &e.value { if ph.name == name { - locations.insert(e.location.clone()); + locations.insert(e.range.clone()); } } None::<()> @@ -82,39 +75,37 @@ impl Missing { } } impl ProjectError for Missing { - fn description(&self) -> &str { - "A key appears in the template but not the pattern of a rule" - } + const DESCRIPTION: &'static str = + "A key appears in the template but not the pattern of a rule"; fn message(&self) -> String { format!( "The key {} appears in the template but not the pattern of this rule", self.name ) } - fn positions(&self) -> BoxedIter { - Box::new( - (self.locations.iter()) - .cloned() - .map(|location| ErrorPosition { location, message: None }), - ) + fn positions(&self) -> impl IntoIterator { + (self.locations.iter()).cloned().map(|range| ErrorPosition { + location: CodeLocation::Source(range), + message: None, + }) } } /// A key is present multiple times in the pattern of a rule #[derive(Debug)] -pub struct Multiple { - locations: HashSet, +struct Multiple { + locations: HashSet, name: Tok, } impl Multiple { #[must_use] - pub fn new(rule: &ast::Rule, name: Tok) -> Self { + pub fn new(rule: &ProjRule, name: Tok) -> Self { let mut locations = HashSet::new(); for expr in rule.template.iter() { expr.search_all(&mut |e| { - if let ast::Clause::Placeh(ph) = &e.value { + if let Clause::Placeh(ph) = &e.value { if ph.name == name { - locations.insert(e.location.clone()); + locations.insert(e.range.clone()); } } None::<()> @@ -124,36 +115,34 @@ impl Multiple { } } impl ProjectError for Multiple { - fn description(&self) -> &str { - "A key appears multiple times in the pattern of a rule" - } + const DESCRIPTION: &'static str = + "A key appears multiple times in the pattern of a rule"; fn message(&self) -> String { format!("The key {} appears multiple times in this pattern", self.name) } - fn positions(&self) -> BoxedIter { - Box::new( - (self.locations.iter()) - .cloned() - .map(|location| ErrorPosition { location, message: None }), - ) + fn positions(&self) -> impl IntoIterator { + (self.locations.iter()).cloned().map(|range| ErrorPosition { + location: CodeLocation::Source(range), + message: None, + }) } } /// A key is present multiple times in the pattern of a rule #[derive(Debug)] -pub struct ArityMismatch { - locations: HashSet<(Location, ast::PHClass)>, +struct ArityMismatch { + locations: HashSet<(SourceRange, PHClass)>, name: Tok, } impl ArityMismatch { #[must_use] - pub fn new(rule: &ast::Rule, name: Tok) -> Self { + pub fn new(rule: &ProjRule, name: Tok) -> Self { let mut locations = HashSet::new(); for expr in rule.template.iter() { expr.search_all(&mut |e| { - if let ast::Clause::Placeh(ph) = &e.value { + if let Clause::Placeh(ph) = &e.value { if ph.name == name { - locations.insert((e.location.clone(), ph.class)); + locations.insert((e.range.clone(), ph.class)); } } None::<()> @@ -163,57 +152,53 @@ impl ArityMismatch { } } impl ProjectError for ArityMismatch { - fn description(&self) -> &str { - "A key appears with different arities in a rule" - } + const DESCRIPTION: &'static str = + "A key appears with different arities in a rule"; fn message(&self) -> String { format!( "The key {} appears multiple times with different arities in this rule", self.name ) } - fn positions(&self) -> BoxedIter { - Box::new((self.locations.iter()).cloned().map(|(location, class)| { - ErrorPosition { - location, - message: Some( - "This instance represents ".to_string() - + match class { - ast::PHClass::Scalar => "one clause", - ast::PHClass::Name => "one name", - ast::PHClass::Vec { nonzero: true, .. } => "one or more clauses", - ast::PHClass::Vec { nonzero: false, .. } => - "any number of clauses", - }, - ), - } - })) + fn positions(&self) -> impl IntoIterator { + (self.locations.iter()).cloned().map(|(location, class)| ErrorPosition { + location: CodeLocation::Source(location), + message: Some( + "This instance represents ".to_string() + + match class { + PHClass::Scalar => "one clause", + PHClass::Name => "one name", + PHClass::Vec { nonzero: true, .. } => "one or more clauses", + PHClass::Vec { nonzero: false, .. } => "any number of clauses", + }, + ), + }) } } /// Two vectorial placeholders appear next to each other #[derive(Debug)] -pub struct VecNeighbors { - locations: HashSet, +struct VecNeighbors { + locations: HashSet, n1: Tok, n2: Tok, } impl VecNeighbors { #[must_use] - pub fn new(rule: &ast::Rule, n1: Tok, n2: Tok) -> Self { + pub fn new(rule: &ProjRule, n1: Tok, n2: Tok) -> Self { let mut locations = HashSet::new(); search_all_slcs(&rule.template[..], &mut |ev| { for pair in ev.windows(2) { let (a, b) = (&pair[0], &pair[1]); - let a_vec = matches!(&a.value, ast::Clause::Placeh( + let a_vec = matches!(&a.value, Clause::Placeh( Placeholder{ class: PHClass::Vec { .. }, name } ) if name == &n1); - let b_vec = matches!(&b.value, ast::Clause::Placeh( + let b_vec = matches!(&b.value, Clause::Placeh( Placeholder{ class: PHClass::Vec { .. }, name } ) if name == &n2); if a_vec && b_vec { - locations.insert(a.location.clone()); - locations.insert(b.location.clone()); + locations.insert(a.range.clone()); + locations.insert(b.range.clone()); } } None::<()> @@ -222,32 +207,18 @@ impl VecNeighbors { } } impl ProjectError for VecNeighbors { - fn description(&self) -> &str { - "Two vectorial placeholders appear next to each other" - } + const DESCRIPTION: &'static str = + "Two vectorial placeholders appear next to each other"; fn message(&self) -> String { format!( "The keys {} and {} appear next to each other with a vectorial arity", self.n1, self.n2 ) } - fn positions(&self) -> BoxedIter { - Box::new( - (self.locations.iter()) - .cloned() - .map(|location| ErrorPosition { location, message: None }), - ) - } -} - -/// Not referencing by location because it's most likely unknown -#[derive(Debug)] -pub struct ExternFnInPattern(ast::Rule); -impl ProjectError for ExternFnInPattern { - fn description(&self) -> &str { - "Found an ExternFn in a pattern. Unlikely error caused by a system" - } - fn message(&self) -> String { - format!("Found ExternFn in pattern {}", self.0) + fn positions(&self) -> impl IntoIterator { + (self.locations.iter()).cloned().map(|location| ErrorPosition { + location: CodeLocation::Source(location), + message: None, + }) } } diff --git a/src/rule/state.rs b/src/rule/state.rs index c4aa6f5..1b99376 100644 --- a/src/rule/state.rs +++ b/src/rule/state.rs @@ -1,24 +1,25 @@ use std::rc::Rc; use hashbrown::HashMap; +use intern_all::Tok; use itertools::{EitherOrBoth, Itertools}; use super::matcher::RuleExpr; -use crate::ast::{Clause, Expr, PHClass, Placeholder}; -use crate::interner::Tok; -use crate::utils::unwrap_or; -use crate::{Location, Sym}; +use crate::location::SourceRange; +use crate::name::Sym; +use crate::parse::parsed::{Clause, Expr, PHClass, Placeholder}; +use crate::utils::unwrap_or::unwrap_or; #[derive(Clone, Copy, Debug)] pub enum StateEntry<'a> { Vec(&'a [RuleExpr]), Scalar(&'a RuleExpr), - Name(&'a Sym, &'a Location), + Name(&'a Sym, &'a SourceRange), } #[derive(Clone)] pub struct State<'a> { placeholders: HashMap, StateEntry<'a>>, - name_locations: HashMap>, + name_locations: HashMap>, } impl<'a> State<'a> { pub fn from_ph(key: Tok, entry: StateEntry<'a>) -> Self { @@ -55,7 +56,7 @@ impl<'a> State<'a> { _ => None, } } - pub fn from_name(name: Sym, location: Location) -> Self { + pub fn from_name(name: Sym, location: SourceRange) -> Self { Self { name_locations: HashMap::from([(name, vec![location])]), placeholders: HashMap::new(), @@ -79,13 +80,11 @@ pub fn apply_exprv(template: &[RuleExpr], state: &State) -> Vec { #[must_use] pub fn apply_expr(template: &RuleExpr, state: &State) -> Vec { - let Expr { location, value } = template; + let Expr { range, value } = template; match value { - Clause::Atom(_) | Clause::Name(_) | Clause::ExternFn(_) => { - vec![template.clone()] - }, + Clause::Atom(_) | Clause::Name(_) => vec![template.clone()], Clause::S(c, body) => vec![Expr { - location: location.clone(), + range: range.clone(), value: Clause::S(*c, Rc::new(apply_exprv(body.as_slice(), state))), }], Clause::Placeh(Placeholder { name, class }) => { @@ -95,14 +94,14 @@ pub fn apply_expr(template: &RuleExpr, state: &State) -> Vec { match (class, value) { (PHClass::Scalar, StateEntry::Scalar(item)) => vec![item.clone()], (PHClass::Vec { .. }, StateEntry::Vec(chunk)) => chunk.to_vec(), - (PHClass::Name, StateEntry::Name(n, l)) => { - vec![RuleExpr { value: Clause::Name(n.clone()), location: l.clone() }] + (PHClass::Name, StateEntry::Name(n, r)) => { + vec![RuleExpr { value: Clause::Name(n.clone()), range: r.clone() }] }, _ => panic!("Type mismatch between template and state"), } }, Clause::Lambda(arg, body) => vec![Expr { - location: location.clone(), + range: range.clone(), value: Clause::Lambda( Rc::new(apply_exprv(arg, state)), Rc::new(apply_exprv(&body[..], state)), diff --git a/src/rule/update_first_seq.rs b/src/rule/update_first_seq.rs index 7480ad6..aab677a 100644 --- a/src/rule/update_first_seq.rs +++ b/src/rule/update_first_seq.rs @@ -1,9 +1,8 @@ +use std::iter; use std::rc::Rc; use super::matcher::RuleExpr; -use crate::ast::{Clause, Expr}; -use crate::utils::replace_first; -use crate::Sym; +use crate::parse::parsed::{Clause, Expr}; /// Traverse the tree, calling pred on every sibling list until it returns /// some vec then replace the sibling list with that vec and return true @@ -26,17 +25,16 @@ pub fn expr>) -> Option>>>( pred: &mut F, ) -> Option { clause(&input.value, pred) - .map(|value| Expr { value, location: input.location.clone() }) + .map(|value| Expr { value, range: input.range.clone() }) } #[must_use] pub fn clause>) -> Option>>>( - c: &Clause, + c: &Clause, pred: &mut F, -) -> Option> { +) -> Option { match c { Clause::Atom(_) | Clause::Placeh { .. } | Clause::Name { .. } => None, - Clause::ExternFn(_) => None, Clause::Lambda(arg, body) => if let Some(arg) = exprv(arg.clone(), pred) { Some(Clause::Lambda(arg, body.clone())) @@ -46,3 +44,22 @@ pub fn clause>) -> Option>>>( Clause::S(c, body) => Some(Clause::S(*c, exprv(body.clone(), pred)?)), } } + +/// Iterate over a sequence with the first element updated for which the +/// function returns Some(), but only if there is such an element. +pub fn replace_first Option>( + slice: &[T], + mut f: F, +) -> Option + '_> { + for i in 0..slice.len() { + if let Some(new) = f(&slice[i]) { + let subbed_iter = slice[0..i] + .iter() + .cloned() + .chain(iter::once(new)) + .chain(slice[i + 1..].iter().cloned()); + return Some(subbed_iter); + } + } + None +} diff --git a/src/rule/vec_attrs.rs b/src/rule/vec_attrs.rs index ccf4e77..2c9a3f9 100644 --- a/src/rule/vec_attrs.rs +++ b/src/rule/vec_attrs.rs @@ -1,6 +1,7 @@ +use intern_all::Tok; + use super::matcher::RuleExpr; -use crate::ast::{Clause, PHClass, Placeholder}; -use crate::interner::Tok; +use crate::parse::parsed::{Clause, PHClass, Placeholder}; /// Returns the name, priority and nonzero of the expression if it is /// a vectorial placeholder diff --git a/src/systems/codegen.rs b/src/systems/codegen.rs deleted file mode 100644 index b0d31a3..0000000 --- a/src/systems/codegen.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Utilities for generating Orchid code in Rust - -use crate::interpreted::{Clause, ExprInst}; -use crate::utils::unwrap_or; -use crate::{PathSet, Side}; - -/// Convert a rust Option into an Orchid Option -pub fn opt(x: Option) -> Clause { - match x { - Some(x) => Clause::constfn(Clause::lambda( - PathSet::end([Side::Left]), - Clause::Apply { f: Clause::LambdaArg.wrap(), x }, - )), - None => Clause::pick(Clause::constfn(Clause::LambdaArg)), - } -} - -/// Convert a rust Result into an Orchid Result -pub fn res(x: Result) -> Clause { - let mk_body = |x| Clause::Apply { f: Clause::LambdaArg.wrap(), x }; - let pick_fn = |b| Clause::lambda(PathSet::end([Side::Left]), b); - match x { - Ok(x) => Clause::constfn(pick_fn(mk_body(x))), - Err(x) => pick_fn(Clause::constfn(mk_body(x))), - } -} - -/// Define a clause that can be called with a callback and passes the provided -/// values to the callback in order. -pub fn tuple(data: impl IntoIterator) -> Clause { - let mut steps = Vec::new(); - let mut body = Clause::LambdaArg; - for x in data.into_iter() { - steps.push(Side::Left); - body = Clause::Apply { f: body.wrap(), x } - } - Clause::lambda(PathSet::end(steps), body) -} - -#[cfg(test)] -mod test { - use crate::foreign::Atomic; - use crate::systems::codegen::tuple; - - #[test] - fn tuple_printer() { - println!("Binary tuple: {}", tuple([0usize.atom_exi(), 1usize.atom_exi()])) - } -} - -/// Generate a function call with the specified arugment array. -pub fn call(f: ExprInst, args: impl IntoIterator) -> Clause { - let mut it = args.into_iter(); - let x = unwrap_or!(it.by_ref().next(); return f.inspect(Clause::clone)); - it.fold(Clause::Apply { f, x }, |acc, x| Clause::Apply { f: acc.wrap(), x }) -} - -/// Build an Orchid list from a Rust iterator -pub fn list(items: impl IntoIterator) -> Clause { - let mut iter = items.into_iter(); - opt(iter.next().map(|it| tuple([it, list(iter).wrap()]).wrap())) -} diff --git a/src/systems/directfs/commands.rs b/src/systems/directfs/commands.rs deleted file mode 100644 index 24d859e..0000000 --- a/src/systems/directfs/commands.rs +++ /dev/null @@ -1,204 +0,0 @@ -use std::ffi::OsString; -use std::fs::File; -use std::io::{BufReader, Read, Write}; -use std::path::{Path, PathBuf}; - -use hashbrown::HashMap; -use itertools::Itertools; - -use super::osstring::os_string_lib; -use crate::ddispatch::Responder; -use crate::error::RuntimeError; -use crate::facade::{IntoSystem, System}; -use crate::foreign::cps_box::{init_cps, CPSBox}; -use crate::foreign::{ - xfn_1ary, xfn_2ary, Atomic, AtomicReturn, InertAtomic, StrictEq, XfnResult, -}; -use crate::interpreted::{Clause, ExprInst}; -use crate::interpreter::HandlerTable; -use crate::systems::codegen::{call, list, opt, tuple}; -use crate::systems::io::{wrap_io_error, Source}; -use crate::systems::scheduler::{SeqScheduler, SharedHandle}; -use crate::utils::unwrap_or; -use crate::ConstTree; - -#[derive(Debug, Clone)] -pub struct CurrentDir; -impl Responder for CurrentDir {} -impl StrictEq for CurrentDir { - // never appears in macros - fn strict_eq(&self, _: &dyn std::any::Any) -> bool { false } -} -impl Atomic for CurrentDir { - fn as_any(self: Box) -> Box { self } - fn as_any_ref(&self) -> &dyn std::any::Any { self } - fn run( - self: Box, - ctx: crate::interpreter::Context, - ) -> crate::foreign::AtomicResult { - let cwd = std::env::current_dir() - .map_err(|e| RuntimeError::ext(e.to_string(), "reading CWD"))?; - Ok(AtomicReturn { - clause: cwd.into_os_string().atom_cls(), - gas: ctx.gas.map(|g| g - 1), - inert: false, - }) - } -} - -#[derive(Debug, Clone)] -pub struct ReadFileCmd(OsString); -impl InertAtomic for ReadFileCmd { - fn type_str() -> &'static str { "readfile command" } -} - -#[derive(Debug, Clone)] -pub struct ReadDirCmd(OsString); -impl InertAtomic for ReadDirCmd { - fn type_str() -> &'static str { "readdir command" } -} - -#[derive(Debug, Clone)] -pub struct WriteFile { - name: OsString, - append: bool, -} -impl InertAtomic for WriteFile { - fn type_str() -> &'static str { "writefile command" } -} - -#[must_use] -fn read_file(sched: &SeqScheduler, cmd: CPSBox) -> ExprInst { - let (ReadFileCmd(name), succ, fail, cont) = cmd.unpack3(); - let cancel = sched.run_orphan( - move |_| File::open(name), - |file, _| match file { - Err(e) => vec![call(fail, [wrap_io_error(e)]).wrap()], - Ok(f) => { - let source: Source = - BufReader::new(Box::new(f) as Box); - vec![call(succ, [SharedHandle::wrap(source).atom_exi()]).wrap()] - }, - }, - ); - call(cont, [init_cps(1, cancel).wrap()]).wrap() -} - -#[must_use] -fn read_dir(sched: &SeqScheduler, cmd: CPSBox) -> ExprInst { - let (ReadDirCmd(name), succ, fail, cont) = cmd.unpack3(); - let cancel = sched.run_orphan( - move |_| { - Path::new(&name) - .read_dir()? - .map(|r| r.and_then(|e| Ok((e.file_name(), e.file_type()?.is_dir())))) - .collect() - }, - |items: std::io::Result>, _| match items { - Err(e) => vec![call(fail, [wrap_io_error(e)]).wrap()], - Ok(os_namev) => { - let converted = (os_namev.into_iter()) - .map(|(n, d)| Ok(tuple([n.atom_exi(), d.atom_exi()]).wrap())) - .collect::, Clause>>(); - match converted { - Err(e) => vec![call(fail, [e.wrap()]).wrap()], - Ok(names) => vec![call(succ, [list(names).wrap()]).wrap()], - } - }, - }, - ); - call(cont, [init_cps(1, cancel).wrap()]).wrap() -} - -#[must_use] -pub fn write_file(sched: &SeqScheduler, cmd: CPSBox) -> ExprInst { - let (WriteFile { name, append }, succ, fail, cont) = cmd.unpack3(); - let cancel = sched.run_orphan( - move |_| File::options().write(true).append(append).open(name), - |file, _| match file { - Err(e) => vec![call(fail, [wrap_io_error(e)]).wrap()], - Ok(f) => { - let handle = SharedHandle::wrap(Box::new(f) as Box); - vec![call(succ, [handle.atom_exi()]).wrap()] - }, - }, - ); - call(cont, [init_cps(1, cancel).wrap()]).wrap() -} - -pub fn open_file_read_cmd(name: OsString) -> XfnResult { - Ok(init_cps(3, ReadFileCmd(name))) -} - -pub fn read_dir_cmd(name: OsString) -> XfnResult { - Ok(init_cps(3, ReadDirCmd(name))) -} - -pub fn open_file_write_cmd(name: OsString) -> XfnResult { - Ok(init_cps(3, WriteFile { name, append: false })) -} - -pub fn open_file_append_cmd(name: OsString) -> XfnResult { - Ok(init_cps(3, WriteFile { name, append: true })) -} - -pub fn join_paths(root: OsString, sub: OsString) -> XfnResult { - let mut path = PathBuf::from(root); - path.push(sub); - Ok(path.into_os_string()) -} - -pub fn pop_path(path: OsString) -> XfnResult { - let mut path = PathBuf::from(path); - let sub = unwrap_or! {path.file_name(); { - return Ok(opt(None)) - }} - .to_owned(); - debug_assert!(path.pop(), "file_name above returned Some"); - Ok(opt(Some( - tuple([path.into_os_string().atom_exi(), sub.atom_exi()]).wrap(), - ))) -} - -/// A rudimentary system to read and write files. -#[derive(Clone)] -pub struct DirectFS { - scheduler: SeqScheduler, -} -impl DirectFS { - /// Create a new instance of the system. - pub fn new(scheduler: SeqScheduler) -> Self { Self { scheduler } } -} - -impl IntoSystem<'static> for DirectFS { - fn into_system(self, i: &crate::Interner) -> System<'static> { - let mut handlers = HandlerTable::new(); - let sched = self.scheduler.clone(); - handlers.register(move |cmd| Ok(read_file(&sched, *cmd))); - let sched = self.scheduler.clone(); - handlers.register(move |cmd| Ok(read_dir(&sched, *cmd))); - let sched = self.scheduler; - handlers.register(move |cmd| Ok(write_file(&sched, *cmd))); - System { - name: ["system", "directfs"].into_iter().map_into().collect(), - code: HashMap::new(), - prelude: Vec::new(), - lexer_plugins: vec![], - line_parsers: vec![], - constants: ConstTree::namespace( - [i.i("system"), i.i("fs")], - ConstTree::tree([ - (i.i("read_file"), ConstTree::xfn(xfn_1ary(open_file_read_cmd))), - (i.i("read_dir"), ConstTree::xfn(xfn_1ary(read_dir_cmd))), - (i.i("write_file"), ConstTree::xfn(xfn_1ary(open_file_write_cmd))), - (i.i("append_file"), ConstTree::xfn(xfn_1ary(open_file_append_cmd))), - (i.i("join_paths"), ConstTree::xfn(xfn_2ary(join_paths))), - (i.i("pop_path"), ConstTree::xfn(xfn_1ary(pop_path))), - (i.i("cwd"), ConstTree::atom(CurrentDir)), - ]) + os_string_lib(i), - ) - .unwrap_tree(), - handlers, - } - } -} diff --git a/src/systems/directfs/osstring.rs b/src/systems/directfs/osstring.rs deleted file mode 100644 index 4267c33..0000000 --- a/src/systems/directfs/osstring.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::ffi::OsString; - -use crate::foreign::{xfn_1ary, InertAtomic, XfnResult}; -use crate::{ConstTree, Interner, OrcString}; - -impl InertAtomic for OsString { - fn type_str() -> &'static str { "OsString" } -} - -pub fn os_to_string(os: OsString) -> XfnResult> { - Ok(os.into_string()) -} - -pub fn string_to_os(str: OrcString) -> XfnResult { - Ok(str.get_string().into()) -} - -pub fn os_print(os: OsString) -> XfnResult { - Ok(os.into_string().unwrap_or_else(|e| e.to_string_lossy().to_string())) -} - -pub fn os_string_lib(i: &Interner) -> ConstTree { - ConstTree::tree([ - (i.i("os_to_string"), ConstTree::xfn(xfn_1ary(os_to_string))), - (i.i("string_to_os"), ConstTree::xfn(xfn_1ary(string_to_os))), - (i.i("os_print"), ConstTree::xfn(xfn_1ary(os_print))), - ]) -} diff --git a/src/systems/io/bindings.rs b/src/systems/io/bindings.rs deleted file mode 100644 index 1b4230a..0000000 --- a/src/systems/io/bindings.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::flow::IOCmdHandlePack; -use super::instances::{BRead, ReadCmd, SRead, Sink, Source, WriteCmd}; -use crate::error::RuntimeError; -use crate::foreign::cps_box::init_cps; -use crate::foreign::{xfn_1ary, xfn_2ary, Atom, Atomic, XfnResult}; -use crate::interpreted::Clause; -use crate::representations::OrcString; -use crate::systems::scheduler::SharedHandle; -use crate::systems::stl::Binary; -use crate::{ast, ConstTree, Interner}; - -type WriteHandle = SharedHandle; -type ReadHandle = SharedHandle; - -pub fn read_string(handle: ReadHandle) -> XfnResult { - Ok(init_cps(3, IOCmdHandlePack { handle, cmd: ReadCmd::RStr(SRead::All) })) -} -pub fn read_line(handle: ReadHandle) -> XfnResult { - Ok(init_cps(3, IOCmdHandlePack { handle, cmd: ReadCmd::RStr(SRead::Line) })) -} -pub fn read_bin(handle: ReadHandle) -> XfnResult { - Ok(init_cps(3, IOCmdHandlePack { handle, cmd: ReadCmd::RBytes(BRead::All) })) -} -pub fn read_bytes(handle: ReadHandle, n: usize) -> XfnResult { - let cmd = ReadCmd::RBytes(BRead::N(n)); - Ok(init_cps(3, IOCmdHandlePack { cmd, handle })) -} -pub fn read_until(handle: ReadHandle, pattern: usize) -> XfnResult { - let delim = pattern.try_into().map_err(|_| { - let msg = "greater than 255".to_string(); - RuntimeError::ext(msg, "converting number to byte") - })?; - let cmd = ReadCmd::RBytes(BRead::Until(delim)); - Ok(init_cps(3, IOCmdHandlePack { handle, cmd })) -} -pub fn write_str(handle: WriteHandle, string: OrcString) -> XfnResult { - let cmd = WriteCmd::WStr(string.get_string()); - Ok(init_cps(3, IOCmdHandlePack { handle, cmd })) -} -pub fn write_bin(handle: WriteHandle, bytes: Binary) -> XfnResult { - Ok(init_cps(3, IOCmdHandlePack { handle, cmd: WriteCmd::WBytes(bytes) })) -} -pub fn flush(handle: WriteHandle) -> XfnResult { - Ok(init_cps(3, IOCmdHandlePack { handle, cmd: WriteCmd::Flush })) -} - -pub fn io_bindings<'a>( - i: &Interner, - std_streams: impl IntoIterator)>, -) -> ConstTree { - ConstTree::namespace( - [i.i("system"), i.i("io")], - ConstTree::tree([ - (i.i("read_string"), ConstTree::xfn(xfn_1ary(read_string))), - (i.i("read_line"), ConstTree::xfn(xfn_1ary(read_line))), - (i.i("read_bin"), ConstTree::xfn(xfn_1ary(read_bin))), - (i.i("read_n_bytes"), ConstTree::xfn(xfn_2ary(read_bytes))), - (i.i("read_until"), ConstTree::xfn(xfn_2ary(read_until))), - (i.i("write_str"), ConstTree::xfn(xfn_2ary(write_str))), - (i.i("write_bin"), ConstTree::xfn(xfn_2ary(write_bin))), - (i.i("flush"), ConstTree::xfn(xfn_1ary(flush))), - ]) + ConstTree::Tree( - std_streams - .into_iter() - .map(|(n, at)| (i.i(n), ConstTree::clause(ast::Clause::Atom(Atom(at))))) - .collect(), - ), - ) -} diff --git a/src/systems/io/facade.rs b/src/systems/io/facade.rs deleted file mode 100644 index 4d40ae9..0000000 --- a/src/systems/io/facade.rs +++ /dev/null @@ -1,151 +0,0 @@ -#![allow(non_upper_case_globals)] // RustEmbed is sloppy -use std::cell::RefCell; -use std::rc::Rc; - -use rust_embed::RustEmbed; -use trait_set::trait_set; - -use super::bindings::io_bindings; -use super::flow::{IOCmdHandlePack, IOManager, NoActiveStream}; -use super::instances::{ - ReadCmd, ReadManager, Sink, SinkHandle, Source, SourceHandle, WriteCmd, - WriteManager, -}; -use crate::facade::{IntoSystem, System}; -use crate::foreign::cps_box::CPSBox; -use crate::foreign::{Atomic, ExternError}; -use crate::interpreter::HandlerTable; -use crate::pipeline::file_loader::embed_to_map; -use crate::sourcefile::{FileEntry, FileEntryKind, Import}; -use crate::systems::asynch::AsynchSystem; -use crate::{Interner, Location}; - -trait_set! { - pub trait StreamTable = IntoIterator -} - -#[derive(RustEmbed)] -#[folder = "src/systems/io"] -#[prefix = "system/"] -#[include = "*.orc"] -struct IOEmbed; - -/// A registry that stores IO streams and executes blocking operations on them -/// in a distinct thread pool -pub struct IOSystem { - read_system: Rc>, - write_system: Rc>, - global_streams: ST, -} -impl IOSystem { - fn new( - asynch: &AsynchSystem, - on_sink_close: Option>, - on_source_close: Option>, - global_streams: ST, - ) -> Self { - Self { - read_system: Rc::new(RefCell::new(IOManager::new( - asynch.get_port(), - on_source_close, - ))), - write_system: Rc::new(RefCell::new(IOManager::new( - asynch.get_port(), - on_sink_close, - ))), - global_streams, - } - } - /// Register a new source so that it can be used with IO commands - pub fn add_source(&self, source: Source) -> SourceHandle { - self.read_system.borrow_mut().add_stream(source) - } - /// Register a new sink so that it can be used with IO operations - pub fn add_sink(&self, sink: Sink) -> SinkHandle { - self.write_system.borrow_mut().add_stream(sink) - } - /// Schedule a source to be closed when all currently enqueued IO operations - /// finish. - pub fn close_source( - &self, - handle: SourceHandle, - ) -> Result<(), NoActiveStream> { - self.read_system.borrow_mut().close_stream(handle) - } - /// Schedule a sink to be closed when all current IO operations finish. - pub fn close_sink(&self, handle: SinkHandle) -> Result<(), NoActiveStream> { - self.write_system.borrow_mut().close_stream(handle) - } -} - -/// A shared type for sinks and sources -pub enum IOStream { - /// A Source, aka. a BufReader - Source(Source), - /// A Sink, aka. a Writer - Sink(Sink), -} - -/// Construct an [IOSystem]. An event loop ([AsynchConfig]) is required to -/// sequence IO events on the interpreter thread. -/// -/// This is a distinct function because [IOSystem] -/// takes a generic parameter which is initialized from an existential in the -/// [AsynchConfig]. -pub fn io_system( - asynch: &'_ mut AsynchSystem, - on_sink_close: Option>, - on_source_close: Option>, - std_streams: impl IntoIterator, -) -> IOSystem { - let this = IOSystem::new(asynch, on_sink_close, on_source_close, std_streams); - let (r, w) = (this.read_system.clone(), this.write_system.clone()); - asynch.register(move |event| vec![r.borrow_mut().dispatch(*event)]); - asynch.register(move |event| vec![w.borrow_mut().dispatch(*event)]); - this -} - -impl<'a, ST: StreamTable + 'a> IntoSystem<'a> for IOSystem { - fn into_system(self, i: &Interner) -> System<'a> { - let (r, w) = (self.read_system.clone(), self.write_system.clone()); - let mut handlers = HandlerTable::new(); - handlers.register(move |cps: &CPSBox>| { - let (IOCmdHandlePack { cmd, handle }, succ, fail, tail) = cps.unpack3(); - (r.borrow_mut()) - .command(*handle, *cmd, (succ.clone(), fail.clone())) - .map_err(|e| e.into_extern())?; - Ok(tail.clone()) - }); - handlers.register(move |cps: &CPSBox>| { - let (IOCmdHandlePack { cmd, handle }, succ, fail, tail) = cps.unpack3(); - (w.borrow_mut()) - .command(*handle, cmd.clone(), (succ.clone(), fail.clone())) - .map_err(|e| e.into_extern())?; - Ok(tail.clone()) - }); - let streams = self.global_streams.into_iter().map(|(n, stream)| { - let handle = match stream { - IOStream::Sink(sink) => - Box::new(self.write_system.borrow_mut().add_stream(sink)) - as Box, - IOStream::Source(source) => - Box::new(self.read_system.borrow_mut().add_stream(source)), - }; - (n, handle) - }); - System { - name: vec!["system".to_string(), "io".to_string()], - constants: io_bindings(i, streams).unwrap_tree(), - code: embed_to_map::(".orc", i), - prelude: vec![FileEntry { - locations: vec![Location::Unknown], - kind: FileEntryKind::Import(vec![Import { - location: Location::Unknown, - path: vec![i.i("system"), i.i("io"), i.i("prelude")], - name: None, - }]), - }], - handlers, - } - } -} diff --git a/src/systems/io/mod.rs b/src/systems/io/mod.rs deleted file mode 100644 index 9bda352..0000000 --- a/src/systems/io/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! System that allows Orchid to interact with trait objects of Rust's `Writer` -//! and with `BufReader`s of `Reader` trait objects - -mod bindings; -// mod facade; -mod flow; -mod instances; -mod service; - -// pub use facade::{io_system, IOStream, IOSystem}; -pub use instances::{wrap_io_error, Sink, Source}; -pub use service::{Service, Stream, StreamTable}; diff --git a/src/systems/io/service.rs b/src/systems/io/service.rs deleted file mode 100644 index 8361ccf..0000000 --- a/src/systems/io/service.rs +++ /dev/null @@ -1,120 +0,0 @@ -#[allow(unused)] // for doc -use std::io::{BufReader, Read, Write}; - -use itertools::Itertools; -use rust_embed::RustEmbed; -use trait_set::trait_set; - -use super::bindings::io_bindings; -use super::flow::{IOCmd, IOCmdHandlePack}; -use super::instances::{ReadCmd, Sink, Source, WriteCmd}; -use crate::facade::{IntoSystem, System}; -use crate::foreign::cps_box::{init_cps, CPSBox}; -use crate::foreign::Atomic; -use crate::interpreter::HandlerTable; -use crate::pipeline::file_loader::embed_to_map; -use crate::sourcefile::{FileEntry, FileEntryKind, Import}; -use crate::systems::codegen::call; -use crate::systems::scheduler::{SeqScheduler, SharedHandle}; -use crate::Location; - -/// A shared type for sinks and sources -pub enum Stream { - /// A Source, aka. a BufReader - Source(Source), - /// A Sink, aka. a Writer - Sink(Sink), -} - -trait_set! { - /// The table of default streams to be overlain on the I/O module, typicially - /// stdin, stdout, stderr. - pub trait StreamTable<'a> = IntoIterator -} - -#[derive(RustEmbed)] -#[folder = "src/systems/io"] -#[prefix = "system/"] -#[include = "*.orc"] -struct IOEmbed; - -/// A streaming I/O service for interacting with Rust's [Write] and [Read] -/// traits. -pub struct Service<'a, ST: IntoIterator> { - scheduler: SeqScheduler, - global_streams: ST, -} -impl<'a, ST: IntoIterator> Service<'a, ST> { - /// Construct a new instance of the service - pub fn new(scheduler: SeqScheduler, global_streams: ST) -> Self { - Self { scheduler, global_streams } - } -} - -impl<'a, ST: IntoIterator> IntoSystem<'static> - for Service<'a, ST> -{ - fn into_system(self, i: &crate::Interner) -> crate::facade::System<'static> { - let scheduler = self.scheduler.clone(); - let mut handlers = HandlerTable::new(); - handlers.register(move |cps: Box>>| { - let (IOCmdHandlePack { cmd, handle }, succ, fail, tail) = cps.unpack3(); - let fail1 = fail.clone(); - let result = scheduler.schedule( - handle, - move |mut stream, cancel| { - let ret = cmd.execute(&mut stream, cancel); - (stream, ret) - }, - move |stream, res, _cancel| (stream, res.dispatch(succ, fail1)), - |stream| (stream, Vec::new()), - ); - match result { - Ok(cancel) => Ok(call(tail, [init_cps(1, cancel).wrap()]).wrap()), - Err(e) => Ok(call(fail, [e.atom_exi()]).wrap()), - } - }); - let scheduler = self.scheduler.clone(); - handlers.register(move |cps: Box>>| { - let (IOCmdHandlePack { cmd, handle }, succ, fail, tail) = cps.unpack3(); - let (succ1, fail1) = (succ, fail.clone()); - let result = scheduler.schedule( - handle, - move |mut stream, cancel| { - let ret = cmd.execute(&mut stream, cancel); - (stream, ret) - }, - move |stream, res, _cancel| (stream, res.dispatch(succ1, fail1)), - |stream| (stream, Vec::new()), - ); - match result { - Ok(cancel) => Ok(call(tail, [init_cps(1, cancel).wrap()]).wrap()), - Err(e) => Ok(call(fail, [e.atom_exi()]).wrap()), - } - }); - let streams = self.global_streams.into_iter().map(|(n, stream)| { - let handle = match stream { - Stream::Sink(sink) => - Box::new(SharedHandle::wrap(sink)) as Box, - Stream::Source(source) => Box::new(SharedHandle::wrap(source)), - }; - (n, handle) - }); - System { - handlers, - name: ["system", "io"].into_iter().map_into().collect(), - constants: io_bindings(i, streams).unwrap_tree(), - code: embed_to_map::(".orc", i), - prelude: vec![FileEntry { - locations: vec![Location::Unknown], - kind: FileEntryKind::Import(vec![Import { - location: Location::Unknown, - path: vec![i.i("system"), i.i("io"), i.i("prelude")], - name: None, - }]), - }], - lexer_plugins: vec![], - line_parsers: vec![], - } - } -} diff --git a/src/systems/parse_custom_line.rs b/src/systems/parse_custom_line.rs deleted file mode 100644 index ae22f41..0000000 --- a/src/systems/parse_custom_line.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! A helper for defining custom lines. See [custom_line] -use crate::error::{ProjectError, ProjectResult}; -use crate::parse::errors::{Expected, ExpectedName}; -use crate::parse::{Entry, Lexeme, Stream}; -use crate::{Location, Tok}; - -/// An exported line with a name for which the line parser denies exports -pub struct Unexportable(Entry); -impl ProjectError for Unexportable { - fn description(&self) -> &str { "this line type cannot be exported" } - fn message(&self) -> String { format!("{} cannot be exported", &self.0) } - fn one_position(&self) -> Location { self.0.location() } -} - -/// Parse a line identified by the specified leading keyword. Although not -/// required, plugins are encouraged to prefix their lines with a globally -/// unique keyword which makes or breaks their parsing, to avoid accidental -/// failure to recognize -pub fn custom_line( - tail: Stream<'_>, - keyword: Tok, - exportable: bool, -) -> Option, Location)>> { - let line_loc = tail.location(); - let (fst, tail) = tail.pop().ok()?; - let fst_name = ExpectedName::expect(fst).ok()?; - let (exported, n_ent, tail) = if fst_name == keyword { - (false, fst, tail.trim()) - } else if fst_name.as_str() == "export" { - let (snd, tail) = tail.pop().ok()?; - Expected::expect(Lexeme::Name(keyword), snd).ok()?; - (true, snd, tail.trim()) - } else { - return None; - }; - Some(match exported && !exportable { - true => Err(Unexportable(n_ent.clone()).rc()), - false => Ok((exported, tail, line_loc)), - }) -} diff --git a/src/systems/scheduler/mod.rs b/src/systems/scheduler/mod.rs deleted file mode 100644 index f9e4ead..0000000 --- a/src/systems/scheduler/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! A generic utility to sequence long blocking mutations that require a mutable -//! reference to a shared resource. - -mod busy; -mod canceller; -mod system; - -pub use busy::HandlerRes; -pub use canceller::Canceller; -pub use system::{SealedOrTaken, SeqScheduler, SharedHandle, SharedState}; diff --git a/src/systems/stl/binary.rs b/src/systems/stl/binary.rs deleted file mode 100644 index f424417..0000000 --- a/src/systems/stl/binary.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::fmt::Debug; -use std::sync::Arc; - -use itertools::Itertools; - -use crate::error::RuntimeError; -use crate::foreign::{ - xfn_1ary, xfn_2ary, xfn_3ary, xfn_4ary, Atomic, InertAtomic, XfnResult, -}; -use crate::interpreted::Clause; -use crate::systems::codegen::{opt, tuple}; -use crate::utils::{iter_find, unwrap_or}; -use crate::{ConstTree, Interner}; - -const INT_BYTES: usize = usize::BITS as usize / 8; - -/// A block of binary data -#[derive(Clone, Hash, PartialEq, Eq)] -pub struct Binary(pub Arc>); -impl InertAtomic for Binary { - fn type_str() -> &'static str { "a binary blob" } -} - -impl Debug for Binary { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut iter = self.0.iter().copied(); - f.write_str("Binary")?; - for mut chunk in iter.by_ref().take(32).chunks(4).into_iter() { - let a = chunk.next().expect("Chunks cannot be empty"); - let b = unwrap_or!(chunk.next(); return write!(f, "{a:02x}")); - let c = unwrap_or!(chunk.next(); return write!(f, "{a:02x}{b:02x}")); - let d = - unwrap_or!(chunk.next(); return write!(f, "{a:02x}{b:02x}{c:02x}")); - write!(f, "{a:02x}{b:02x}{c:02x}{d:02x}")? - } - if iter.next().is_some() { write!(f, "...") } else { Ok(()) } - } -} - -/// Append two binary data blocks -pub fn concatenate(a: Binary, b: Binary) -> XfnResult { - let data = a.0.iter().chain(b.0.iter()).copied().collect(); - Ok(Binary(Arc::new(data))) -} - -/// Extract a subsection of the binary data -pub fn slice(s: Binary, i: usize, len: usize) -> XfnResult { - if i + len < s.0.len() { - RuntimeError::fail( - "Byte index out of bounds".to_string(), - "indexing binary", - )? - } - Ok(Binary(Arc::new(s.0[i..i + len].to_vec()))) -} - -/// Return the index where the first argument first contains the second, if any -pub fn find(haystack: Binary, needle: Binary) -> XfnResult { - let found = iter_find(haystack.0.iter(), needle.0.iter()); - Ok(opt(found.map(usize::atom_exi))) -} - -/// Split binary data block into two smaller blocks -pub fn split(bin: Binary, i: usize) -> XfnResult { - if bin.0.len() < i { - RuntimeError::fail( - "Byte index out of bounds".to_string(), - "splitting binary", - )? - } - let (asl, bsl) = bin.0.split_at(i); - Ok(tuple([asl, bsl].map(|s| Binary(Arc::new(s.to_vec())).atom_exi()))) -} - -/// Read a number from a binary blob -pub fn get_num( - buf: Binary, - loc: usize, - size: usize, - is_le: bool, -) -> XfnResult { - if buf.0.len() < (loc + size) { - RuntimeError::fail( - "section out of range".to_string(), - "reading number from binary data", - )? - } - if INT_BYTES < size { - RuntimeError::fail( - "more than std::bin::int_bytes bytes provided".to_string(), - "reading number from binary data", - )? - } - let mut data = [0u8; INT_BYTES]; - let section = &buf.0[loc..(loc + size)]; - let num = if is_le { - data[0..size].copy_from_slice(section); - usize::from_le_bytes(data) - } else { - data[INT_BYTES - size..].copy_from_slice(section); - usize::from_be_bytes(data) - }; - Ok(num) -} - -/// Convert a number into a blob -pub fn from_num(size: usize, is_le: bool, data: usize) -> XfnResult { - if INT_BYTES < size { - RuntimeError::fail( - "more than std::bin::int_bytes bytes requested".to_string(), - "converting number to binary", - )? - } - let bytes = match is_le { - true => data.to_le_bytes()[0..size].to_vec(), - false => data.to_be_bytes()[8 - size..].to_vec(), - }; - Ok(Binary(Arc::new(bytes))) -} - -/// Detect the number of bytes in the blob -pub fn size(b: Binary) -> XfnResult { Ok(b.0.len()) } - -pub fn bin(i: &Interner) -> ConstTree { - ConstTree::tree([( - i.i("binary"), - ConstTree::tree([ - (i.i("concat"), ConstTree::xfn(xfn_2ary(concatenate))), - (i.i("slice"), ConstTree::xfn(xfn_3ary(slice))), - (i.i("find"), ConstTree::xfn(xfn_2ary(find))), - (i.i("split"), ConstTree::xfn(xfn_2ary(split))), - (i.i("get_num"), ConstTree::xfn(xfn_4ary(get_num))), - (i.i("from_num"), ConstTree::xfn(xfn_3ary(from_num))), - (i.i("size"), ConstTree::xfn(xfn_1ary(size))), - (i.i("int_bytes"), ConstTree::atom(INT_BYTES)), - ]), - )]) -} diff --git a/src/systems/stl/bool.orc b/src/systems/stl/bool.orc deleted file mode 100644 index 5f2ecad..0000000 --- a/src/systems/stl/bool.orc +++ /dev/null @@ -1,46 +0,0 @@ -import std::match - -export ::(!=, ==) - -export const not := \bool. if bool then false else true -macro ...$a != ...$b =0x3p36=> (not (...$a == ...$b)) -macro ...$a == ...$b =0x3p36=> (equals (...$a) (...$b)) -export macro ...$a and ...$b =0x4p36=> (ifthenelse (...$a) (...$b) false) -export macro ...$a or ...$b =0x4p36=> (ifthenelse (...$a) true (...$b)) -export macro if ...$cond then ...$true else ...$false:1 =0x1p84=> ( - ifthenelse (...$cond) (...$true) (...$false) -) - -( - macro match::request (== ...$other) - =0x1p230=> match::response ( - if match::value == (...$other) - then match::pass - else match::fail - ) - ( match::no_binds ) -) - -( - macro match::request (!= ...$other) - =0x1p230=> match::response ( - if match::value != (...$other) - then match::pass - else match::fail - ) - ( match::no_binds ) -) - -( - macro match::request (true) - =0x1p230=> match::response - (if match::value then match::pass else match::fail) - ( match::no_binds ) -) - -( - macro match::request (false) - =0x1p230=> match::response - (if match::value then match::fail else match::pass) - ( match::no_binds ) -) diff --git a/src/systems/stl/bool.rs b/src/systems/stl/bool.rs deleted file mode 100644 index 79e0909..0000000 --- a/src/systems/stl/bool.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::Numeric; -use crate::error::AssertionError; -use crate::foreign::{xfn_1ary, xfn_2ary, Atom, XfnResult}; -use crate::interner::Interner; -use crate::representations::interpreted::Clause; -use crate::{ConstTree, Location, OrcString}; - -/// Takes a boolean and two branches, runs the first if the bool is true, the -/// second if it's false. -// Even though it's a ternary function, IfThenElse is implemented as an unary -// foreign function, as the rest of the logic can be defined in Orchid. -pub fn if_then_else(b: bool) -> XfnResult { - Ok(match b { - true => Clause::pick(Clause::constfn(Clause::LambdaArg)), - false => Clause::constfn(Clause::pick(Clause::LambdaArg)), - }) -} - -/// Compares the inner values if -/// -/// - both are string, -/// - both are bool, -/// - both are either uint or num -pub fn equals(a: Atom, b: Atom) -> XfnResult { - let (a, b) = - match (a.try_downcast::(), b.try_downcast::()) { - (Ok(a), Ok(b)) => return Ok(a == b), - (Err(a), Err(b)) => (a, b), - _ => return Ok(false), - }; - match (a.request::(), b.request::()) { - (Some(a), Some(b)) => return Ok(a.as_float() == b.as_float()), - (None, None) => (), - _ => return Ok(false), - }; - match (a.try_downcast::(), b.try_downcast::()) { - (Ok(a), Ok(b)) => return Ok(a == b), - (Err(_), Err(_)) => (), - _ => return Ok(false), - }; - AssertionError::fail(Location::Unknown, "the expected type") -} - -pub fn bool(i: &Interner) -> ConstTree { - ConstTree::tree([( - i.i("bool"), - ConstTree::tree([ - (i.i("ifthenelse"), ConstTree::xfn(xfn_1ary(if_then_else))), - (i.i("equals"), ConstTree::xfn(xfn_2ary(equals))), - (i.i("true"), ConstTree::atom(true)), - (i.i("false"), ConstTree::atom(false)), - ]), - )]) -} diff --git a/src/systems/stl/conv.rs b/src/systems/stl/conv.rs deleted file mode 100644 index ad15e1b..0000000 --- a/src/systems/stl/conv.rs +++ /dev/null @@ -1,51 +0,0 @@ -use ordered_float::NotNan; - -use super::Numeric; -use crate::error::AssertionError; -use crate::foreign::{xfn_1ary, Atom, XfnResult}; -use crate::interner::Interner; -use crate::parse::parse_num; -use crate::{ConstTree, Location, OrcString}; - -fn to_numeric(a: Atom) -> XfnResult { - if let Some(n) = a.request::() { - return Ok(n); - } - if let Some(s) = a.request::() { - return parse_num(s.as_str()) - .map_err(|_| AssertionError::ext(Location::Unknown, "number syntax")); - } - AssertionError::fail(Location::Unknown, "string or number") -} - -/// parse a number. Accepts the same syntax Orchid does. -pub fn to_float(a: Atom) -> XfnResult> { - to_numeric(a).map(|n| n.as_float()) -} - -/// Parse an unsigned integer. Accepts the same formats Orchid does. If the -/// input is a number, floors it. -pub fn to_uint(a: Atom) -> XfnResult { - to_numeric(a).map(|n| match n { - Numeric::Float(f) => f.floor() as usize, - Numeric::Uint(i) => i, - }) -} - -/// Convert a literal to a string using Rust's conversions for floats, chars and -/// uints respectively -pub fn to_string(a: Atom) -> XfnResult { - a.try_downcast::() - .or_else(|e| e.try_downcast::().map(|i| i.to_string().into())) - .or_else(|e| e.try_downcast::>().map(|i| i.to_string().into())) - .or_else(|e| e.try_downcast::().map(|i| i.to_string().into())) - .map_err(|_| AssertionError::ext(Location::Unknown, "string or number")) -} - -pub fn conv(i: &Interner) -> ConstTree { - ConstTree::tree([ - (i.i("to_float"), ConstTree::xfn(xfn_1ary(to_float))), - (i.i("to_uint"), ConstTree::xfn(xfn_1ary(to_uint))), - (i.i("to_string"), ConstTree::xfn(xfn_1ary(to_string))), - ]) -} diff --git a/src/systems/stl/cross_pipeline.rs b/src/systems/stl/cross_pipeline.rs deleted file mode 100644 index 12e750c..0000000 --- a/src/systems/stl/cross_pipeline.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::collections::VecDeque; -use std::fmt::Debug; -use std::iter; -use std::ops::Deref; -use std::rc::Rc; -use std::sync::{Arc, Mutex}; - -use crate::ast::{self, PType}; -use crate::ddispatch::Responder; -use crate::foreign::{ - xfn_1ary, Atomic, AtomicReturn, ExFn, StrictEq, ToClause, XfnResult, -}; -use crate::interpreted::{self, TryFromExprInst}; -use crate::utils::pure_seq::pushed; -use crate::{interpreter, VName}; - -pub trait DeferredRuntimeCallback: - Fn(Vec<(T, U)>) -> XfnResult + Clone + Send + 'static -{ -} -impl< - T, - U, - R: ToClause, - F: Fn(Vec<(T, U)>) -> XfnResult + Clone + Send + 'static, -> DeferredRuntimeCallback for F -{ -} - -fn table_receiver_rec< - T: Clone + Send + 'static, - U: TryFromExprInst + Clone + Send + 'static, - R: ToClause + 'static, ->( - results: Vec<(T, U)>, - mut remaining_keys: VecDeque, - callback: impl DeferredRuntimeCallback, -) -> XfnResult { - match remaining_keys.pop_front() { - None => callback(results).map(|v| v.to_clause()), - Some(t) => Ok(interpreted::Clause::ExternFn(ExFn(Box::new(xfn_1ary( - move |u: U| { - table_receiver_rec(pushed(results, (t, u)), remaining_keys, callback) - }, - ))))), - } -} - -#[derive(Clone)] -pub struct EphemeralAtom( - Arc XfnResult + Sync + Send>, -); -impl Debug for EphemeralAtom { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("EphemeralAtom") - } -} -impl Responder for EphemeralAtom { - fn respond(&self, _request: crate::ddispatch::Request) {} -} -impl StrictEq for EphemeralAtom { - fn strict_eq(&self, _: &dyn std::any::Any) -> bool { false } -} -impl Atomic for EphemeralAtom { - fn as_any(self: Box) -> Box { self } - fn as_any_ref(&self) -> &dyn std::any::Any { self } - fn run( - self: Box, - ctx: interpreter::Context, - ) -> crate::foreign::AtomicResult { - Ok(AtomicReturn { clause: (self.0)()?, gas: ctx.gas, inert: false }) - } -} - -fn table_receiver< - T: Clone + Send + 'static, - U: TryFromExprInst + Clone + Send + 'static, - R: ToClause + 'static, ->( - keys: VecDeque, - callback: impl DeferredRuntimeCallback, -) -> ast::Clause { - if keys.is_empty() { - let result = - Arc::new(Mutex::new(callback(Vec::new()).map(|v| v.to_clause()))); - EphemeralAtom(Arc::new(move || result.lock().unwrap().deref().clone())) - .ast_cls() - } else { - match table_receiver_rec(Vec::new(), keys, callback) { - Ok(interpreted::Clause::ExternFn(xfn)) => ast::Clause::ExternFn(xfn), - _ => unreachable!("details"), - } - } -} - -pub fn defer_to_runtime< - T: Clone + Send + 'static, - U: TryFromExprInst + Clone + Send + 'static, - R: ToClause + 'static, ->( - pairs: impl IntoIterator>)>, - callback: impl DeferredRuntimeCallback, -) -> ast::Clause { - let (keys, ast_values) = - pairs.into_iter().unzip::<_, _, VecDeque<_>, Vec<_>>(); - ast::Clause::s( - '(', - iter::once(table_receiver(keys, callback)).chain( - ast_values.into_iter().map(|v| ast::Clause::S(PType::Par, Rc::new(v))), - ), - ) -} diff --git a/src/systems/stl/exit_status.rs b/src/systems/stl/exit_status.rs deleted file mode 100644 index 6e88c20..0000000 --- a/src/systems/stl/exit_status.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::foreign::{xfn_1ary, InertAtomic}; -use crate::{ConstTree, Interner}; - -/// An Orchid equivalent to Rust's binary exit status model -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ExitStatus { - /// unix exit code 0 - Success, - /// unix exit code 1 - Failure, -} - -impl InertAtomic for ExitStatus { - fn type_str() -> &'static str { "ExitStatus" } -} - -pub fn exit_status(i: &Interner) -> ConstTree { - let is_success = |es: ExitStatus| Ok(es == ExitStatus::Success); - ConstTree::namespace( - [i.i("exit_status")], - ConstTree::tree([ - (i.i("success"), ConstTree::atom(ExitStatus::Success)), - (i.i("failure"), ConstTree::atom(ExitStatus::Failure)), - (i.i("is_success"), ConstTree::xfn(xfn_1ary(is_success))), - ]), - ) -} diff --git a/src/systems/stl/inspect.rs b/src/systems/stl/inspect.rs deleted file mode 100644 index 38a3581..0000000 --- a/src/systems/stl/inspect.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::fmt::Debug; - -use crate::foreign::{ExternFn, XfnResult}; -use crate::interpreted::Clause; -use crate::interpreter::Context; -use crate::representations::interpreted::ExprInst; -use crate::{ConstTree, Interner}; - -#[derive(Debug, Clone)] -struct Inspect; -impl ExternFn for Inspect { - fn name(&self) -> &str { "inspect" } - fn apply(self: Box, arg: ExprInst, _: Context) -> XfnResult { - println!("{arg}"); - Ok(arg.expr().clause.clone()) - } -} - -pub fn inspect(i: &Interner) -> ConstTree { - ConstTree::tree([(i.i("inspect"), ConstTree::xfn(Inspect))]) -} diff --git a/src/systems/stl/list.orc b/src/systems/stl/list.orc deleted file mode 100644 index f813d5d..0000000 --- a/src/systems/stl/list.orc +++ /dev/null @@ -1,165 +0,0 @@ -import super::(option, match, macro) -import super::(functional::*, procedural::*) -import super::(loop::*, bool::*, known::*, number::*, tuple::*) - -export type ty ( - import super::super::(option, tuple, panic) - import super::super::(known::*, bool::*) - - export const cons := \hd. \tl. wrap (option::some tuple::t[hd, unwrap tl]) - export const end := wrap option::none - export const pop := \list. \default. \f. ( - option::handle (unwrap list) - default - \pair. tuple::apply pair - \len. if len == 2 - then ( \hd. \tl. f hd (wrap tl) ) - else panic "list element must be 2-ple" - ) -) - -export const cons := ty::cons -export const end := ty::end -export const pop := ty::pop - --- Operators - ---[ - Fold each element into an accumulator using an `acc -> el -> acc`. - This evaluates the entire list, and is always tail recursive. -]-- -export const fold := \list. \acc. \f. ( - loop_over (list, acc) { - cps head, list = pop list acc; - let acc = f acc head; - } -) - ---[ - Fold each element into an accumulator in reverse order. - This evaulates the entire list, and is never tail recursive. -]-- -export const rfold := \list. \acc. \f. ( - recursive r (list) - pop list acc \head. \tail. - f (r tail) head -) - ---[ - Fold each element into a shared element with an `el -> el -> el`. - This evaluates the entire list, and is never tail recursive. -]-- -export const reduce := \list. \f. do{ - cps head, list = pop list option::none; - option::some $ fold list head f -} - ---[ - Return a new list that contains only the elements from the input list - for which the function returns true. This operation is lazy. -]-- -export const filter := \list. \f. ( - pop list end \head. \tail. - if (f head) - then cons head (filter tail f) - else filter tail f -) - ---[ - Transform each element of the list with an `el -> any`. -]-- -export const map := \list. \f. ( - recursive r (list) - pop list end \head. \tail. - cons (f head) (r tail) -) - ---[ - Skip `n` elements from the list and return the tail - If `n` is not an integer, this returns `end`. -]-- -export const skip := \foo. \n. ( - loop_over (foo, n) { - cps _head, foo = if n == 0 - then return foo - else pop foo end; - let n = n - 1; - } -) - ---[ - Return `n` elements from the list and discard the rest. - This operation is lazy. -]-- -export const take := \list. \n. ( - recursive r (list, n) - if n == 0 - then end - else pop list end \head. \tail. - cons head $ r tail $ n - 1 -) - ---[ - Return the `n`th element from the list. - This operation is tail recursive. -]-- -export const get := \list. \n. ( - loop_over (list, n) { - cps head, list = pop list option::none; - cps if n == 0 - then return (option::some head) - else identity; - let n = n - 1; - } -) - ---[ - Map every element to a pair of the index and the original element -]-- -export const enumerate := \list. ( - recursive r (list, n = 0) - pop list end \head. \tail. - cons t[n, head] $ r tail $ n + 1 -) - ---[ - Turn a list of CPS commands into a sequence. This is achieved by calling every - element on the return value of the next element with the tail passed to it. - The continuation is passed to the very last argument. -]-- -export const chain := \list. \cont. loop_over (list) { - cps head, list = pop list cont; - cps head; -} - -macro new[..$items] =0x2p84=> mk_list macro::comma_list (..$items) - -macro mk_list ( macro::list_item $item $tail ) =0x1p254=> (cons $item mk_list $tail) -macro mk_list macro::list_end =0x1p254=> end - -export ::(new) - -( macro match::request (cons $head $tail) - =0x1p230=> await_subpatterns - (match::request ($head)) - (match::request ($tail)) -) -( macro await_subpatterns - (match::response $h_expr ( $h_binds )) - (match::response $t_expr ( $t_binds )) - =0x1p230=> match::response ( - pop - match::value - match::fail - \head. \tail. ( - (\match::pass. (\match::value. $h_expr) head) - (match::take_binds $h_binds ( - (\match::pass. (\match::value. $t_expr) tail) - (match::take_binds $t_binds ( - match::give_binds match::chain_binds $h_binds $t_binds match::pass - )) - )) - ) - ) - (match::chain_binds $h_binds $t_binds) -) diff --git a/src/systems/stl/map.orc b/src/systems/stl/map.orc deleted file mode 100644 index a2d1428..0000000 --- a/src/systems/stl/map.orc +++ /dev/null @@ -1,96 +0,0 @@ -import super::(bool::*, functional::*, known::*, loop::*, procedural::*) -import super::(panic, match, macro, option, list) - -export type ty ( - import super::super::(panic, macro, list, tuple, option) - import super::super::(bool::*, functional::*, known::*, loop::*, procedural::*) - - --[ Constructors ]-- - - const empty := wrap list::end - const add := \m. \k. \v. wrap ( - list::cons - tuple::t[k, v] - (unwrap m) - ) - - --[ List constructor ]-- - - export ::new - macro new[..$items] =0x2p84=> mk_map macro::comma_list (..$items) - - macro mk_map macro::list_end =0x1p254=> empty - ( macro mk_map ( macro::list_item ( ...$key = ...$value:1 ) $tail ) - =0x1p254=> ( set mk_map $tail (...$key) (...$value) ) - ) - - --[ Queries ]-- - - -- return the last occurrence of a key if exists - export const get := \m. \key. ( - loop_over (m=unwrap m) { - cps record, m = list::pop m option::none; - cps if tuple::pick record 0 == key - then return $ option::some $ tuple::pick record 1 - else identity; - } - ) - - --[ Commands ]-- - - -- remove one occurrence of a key - export const del := \m. \k. wrap ( - recursive r (m=unwrap m) - list::pop m list::end \head. \tail. - if tuple::pick head 0 == k then tail - else list::cons head $ r tail - ) - - -- replace at most one occurrence of a key - export const set := \m. \k. \v. m |> del k |> add k v -) - -macro new =0x1p200=> ty::new - -export const empty := ty::empty -export const add := ty::add -export const get := ty::get -export const set := ty::set -export const del := ty::del - -export ::having -( macro match::request (having [..$items]) - =0x1p230=> having_pattern ( - pattern_walker - macro::comma_list ( ..$items ) - ) -) -( macro having_pattern ( tail_result $expr ( $binds ) ) - =0x1p254=> match::response $expr ( $binds ) -) -( macro pattern_walker macro::list_end - =0x1p254=> tail_result match::pass ( match::no_binds ) -) -( macro pattern_walker ( macro::list_item ( ...$key = ...$value:1 ) $tail ) - =0x1p254=> await_pattern ( ...$key ) - ( match::request (...$value) ) - ( pattern_walker $tail ) -) -( macro await_pattern $key - ( match::response $expr ( $binds ) ) - ( tail_result $t_expr ( $t_binds ) ) - =0x1p254=> tail_result ( - option::handle (get match::value $key) - match::fail - \value. (\match::pass. (\match::value. $expr) value) ( - match::take_binds $binds ( - (\match::pass. $t_expr) ( - match::take_binds $t_binds ( - match::give_binds match::chain_binds $binds $t_binds match::pass - ) - ) - ) - ) - ) - ( match::chain_binds $binds $t_binds ) -) diff --git a/src/systems/stl/mod.rs b/src/systems/stl/mod.rs deleted file mode 100644 index 5d45d4e..0000000 --- a/src/systems/stl/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Basic types and their functions, frequently used tools with no environmental -//! dependencies. -mod arithmetic_error; -mod binary; -mod bool; -mod conv; -mod cross_pipeline; -mod exit_status; -mod inspect; -mod number; -mod panic; -mod protocol; -mod reflect; -mod state; -mod stl_system; -mod string; -pub use arithmetic_error::ArithmeticError; -pub use binary::Binary; -pub use exit_status::ExitStatus; -pub use number::Numeric; -pub use stl_system::StlConfig; diff --git a/src/systems/stl/option.orc b/src/systems/stl/option.orc deleted file mode 100644 index b89e6db..0000000 --- a/src/systems/stl/option.orc +++ /dev/null @@ -1,40 +0,0 @@ -import std::(panic, match) - -export type ty ( - export const some := \v. wrap \d. \f. f v - export const none := wrap \d. \f. d - - export const handle := \t. \d. \f. (unwrap t) d f -) - -export const some := ty::some -export const none := ty::none -export const handle := ty::handle - -export const map := \option. \f. handle option none f -export const flatten := \option. handle option none \opt. opt -export const flatmap := \option. \f. handle option none \opt. map opt f -export const unwrap := \option. handle option (panic "value expected") \x.x - -( - macro match::request ( none ) - =0x1p230=> match::response ( - handle match::value - match::pass - \_. match::fail - ) ( match::no_binds ) -) - -( - macro match::request ( some ...$value ) - =0x1p230=> await_some_subpattern ( match::request (...$value) ) -) - -( - macro await_some_subpattern ( match::response $expr ( $binds ) ) - =0x1p254=> match::response ( - handle match::value - match::fail - \match::value. $expr - ) ( $binds ) -) diff --git a/src/systems/stl/protocol.rs b/src/systems/stl/protocol.rs deleted file mode 100644 index b68c664..0000000 --- a/src/systems/stl/protocol.rs +++ /dev/null @@ -1,283 +0,0 @@ -use std::fmt::Debug; -use std::sync::Arc; - -use hashbrown::HashMap; -use itertools::Itertools; - -use super::cross_pipeline::defer_to_runtime; -use super::reflect::RefEqual; -use crate::ast::{self, Constant, Expr, PType}; -use crate::error::{ProjectResult, RuntimeError}; -use crate::foreign::{xfn_2ary, Atomic, InertAtomic, XfnResult}; -use crate::interpreted::ExprInst; -use crate::parse::errors::{Expected, ExpectedBlock, ExpectedName}; -use crate::parse::{ - parse_entries, parse_exprv, parse_line, parse_nsname, split_lines, - vec_to_single, Context, Lexeme, LineParser, LineParserOut, Stream, -}; -use crate::sourcefile::{ - FileEntry, FileEntryKind, Member, MemberKind, ModuleBlock, -}; -use crate::systems::parse_custom_line::custom_line; -use crate::utils::pure_seq::pushed; -use crate::{ConstTree, Interner, Location, Tok, VName}; - -pub struct TypeData { - pub id: RefEqual, - pub display_name: Tok, - pub impls: HashMap, -} - -#[derive(Clone)] -pub struct Protocol(pub Arc); -impl Debug for Protocol { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple(&self.0.display_name).field(&self.0.id.id()).finish() - } -} -impl InertAtomic for Protocol { - fn type_str() -> &'static str { "Protocol" } -} - -#[derive(Clone)] -pub struct Tag(pub Arc); -impl Debug for Tag { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple(&self.0.display_name).field(&self.0.id.id()).finish() - } -} -impl InertAtomic for Tag { - fn type_str() -> &'static str { "Tag" } - fn strict_eq(&self, other: &Self) -> bool { self.0.id == other.0.id } -} - -#[derive(Clone)] -pub struct Tagged { - pub tag: Tag, - pub value: ExprInst, -} -impl Debug for Tagged { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Tagged").field(&self.tag).field(&self.value).finish() - } -} -impl InertAtomic for Tagged { - fn type_str() -> &'static str { "Tagged" } -} - -fn parse_impl( - tail: Stream, - ctx: &(impl Context + ?Sized), -) -> Option)>> { - custom_line(tail, ctx.interner().i("impl"), false).map(|res| { - let (_, tail, _) = res?; - let (name, tail) = parse_nsname(tail, ctx)?; - let (walrus, tail) = tail.trim().pop()?; - Expected::expect(Lexeme::Walrus, walrus)?; - let (body, empty) = parse_exprv(tail, None, ctx)?; - empty.expect_empty()?; - let value = vec_to_single(tail.fallback, body)?; - Ok((name, value)) - }) -} - -struct Impl { - target: VName, - value: Expr, -} - -fn extract_impls( - tail: Stream, - ctx: &(impl Context + ?Sized), - location: Location, - typeid_name: Tok, -) -> ProjectResult<(Vec, Vec)> { - let mut lines = Vec::new(); - let mut impls = Vec::new(); // name1, value1, name2, value2, etc... - for line in split_lines(tail) { - match parse_impl(line, ctx) { - Some(result) => { - let (name, value) = result?; - impls.push(Impl { target: pushed(name, typeid_name.clone()), value }); - }, - None => lines.extend( - parse_line(line, ctx)?.into_iter().map(|k| k.wrap(location.clone())), - ), - } - } - Ok((lines, impls)) -} - -pub fn protocol_parser<'a>( - tail: Stream<'_>, - ctx: &'a (impl Context + ?Sized + 'a), -) -> LineParserOut { - let i = ctx.interner(); - custom_line(tail, i.i("protocol"), true).map(|res| { - let (exported, tail, line_loc) = res?; - let (name, tail) = tail.pop()?; - let name = ExpectedName::expect(name)?; - let tail = ExpectedBlock::expect(tail, PType::Par)?; - let protoid = RefEqual::new(); - let (lines, impls) = - extract_impls(tail, ctx, line_loc.clone(), i.i("__type_id__"))?; - let prelude = " - import std::protocol - const resolve := protocol::resolve __protocol__ - const get_impl := protocol::get_impl __protocol__ - "; - let body = parse_entries(ctx, prelude, line_loc.clone())? - .into_iter() - .chain( - [ - ("__protocol_id__", protoid.clone().ast_cls()), - ( - "__protocol__", - defer_to_runtime( - impls.into_iter().flat_map(|Impl { target, value }| { - [ast::Clause::Name(target).into_expr(), value] - .map(|e| ((), vec![e])) - }), - { - let name = name.clone(); - move |pairs: Vec<((), ExprInst)>| { - let mut impls = HashMap::new(); - debug_assert!( - pairs.len() % 2 == 0, - "names and values pair up" - ); - let mut nvnvnv = pairs.into_iter().map(|t| t.1); - while let Some((name, value)) = nvnvnv.next_tuple() { - let key = name.downcast::()?; - impls.insert(key, value); - } - let id = protoid.clone(); - let display_name = name.clone(); - Ok(Protocol(Arc::new(TypeData { id, display_name, impls }))) - } - }, - ), - ), - ] - .map(|(n, value)| { - let value = Expr { value, location: line_loc.clone() }; - MemberKind::Constant(Constant { name: i.i(n), value }) - .to_entry(true, line_loc.clone()) - }), - ) - .chain(lines) - .collect(); - let kind = MemberKind::Module(ModuleBlock { name, body }); - Ok(vec![FileEntryKind::Member(Member { exported, kind })]) - }) -} - -pub fn type_parser( - tail: Stream, - ctx: &(impl Context + ?Sized), -) -> LineParserOut { - let i = ctx.interner(); - custom_line(tail, ctx.interner().i("type"), true).map(|res| { - let (exported, tail, line_loc) = res?; - let (name, tail) = tail.pop()?; - let name = ExpectedName::expect(name)?; - let tail = ExpectedBlock::expect(tail, PType::Par)?; - let typeid = RefEqual::new(); - let (lines, impls) = - extract_impls(tail, ctx, line_loc.clone(), i.i("__protocol_id__"))?; - let prelude = " - import std::protocol - const unwrap := protocol::unwrap __type_tag__ - const wrap := protocol::wrap __type_tag__ - "; - let body = parse_entries(ctx, prelude, line_loc.clone())? - .into_iter() - .chain( - [ - ("__type_id__", typeid.clone().ast_cls()), - ( - "__type_tag__", - defer_to_runtime( - impls.into_iter().flat_map(|Impl { target, value }| { - [ast::Clause::Name(target).into_expr(), value] - .map(|e| ((), vec![e])) - }), - { - let name = name.clone(); - move |pairs: Vec<((), ExprInst)>| { - let mut impls = HashMap::new(); - debug_assert!( - pairs.len() % 2 == 0, - "names and values pair up" - ); - let mut nvnvnv = pairs.into_iter().map(|t| t.1); - while let Some((name, value)) = nvnvnv.next_tuple() { - let key = name.downcast::()?; - impls.insert(key, value); - } - let id = typeid.clone(); - let display_name = name.clone(); - Ok(Tag(Arc::new(TypeData { id, display_name, impls }))) - } - }, - ), - ), - ] - .map(|(n, value)| { - let value = Expr { value, location: line_loc.clone() }; - MemberKind::Constant(Constant { name: i.i(n), value }) - .to_entry(true, line_loc.clone()) - }), - ) - .chain(lines) - .collect(); - let kind = MemberKind::Module(ModuleBlock { name, body }); - Ok(vec![FileEntryKind::Member(Member { exported, kind })]) - }) -} - -pub fn parsers() -> Vec> { - vec![ - Box::new(|tail, ctx| protocol_parser(tail, ctx)), - Box::new(|tail, ctx| type_parser(tail, ctx)), - ] -} - -pub fn unwrap(tag: Tag, tagged: Tagged) -> XfnResult { - if tagged.tag.strict_eq(&tag) { - return Ok(tagged.value); - } - let msg = format!("{:?} is not {:?}", tagged.tag, tag); - RuntimeError::fail(msg, "unwrapping type-tagged value") -} - -pub fn wrap(tag: Tag, value: ExprInst) -> XfnResult { - Ok(Tagged { tag, value }) -} - -pub fn resolve(protocol: Protocol, tagged: Tagged) -> XfnResult { - get_impl(protocol, tagged.tag) -} - -pub fn get_impl(proto: Protocol, tag: Tag) -> XfnResult { - if let Some(implem) = proto.0.impls.get(&tag.0.id) { - return Ok(implem.clone()); - } - if let Some(implem) = tag.0.impls.get(&proto.0.id) { - return Ok(implem.clone()); - } - let message = format!("{:?} doesn't implement {:?}", tag, proto); - RuntimeError::fail(message, "dispatching protocol") -} - -pub fn protocol_lib(i: &Interner) -> ConstTree { - ConstTree::namespace( - [i.i("protocol")], - ConstTree::tree([ - (i.i("unwrap"), ConstTree::xfn(xfn_2ary(unwrap))), - (i.i("wrap"), ConstTree::xfn(xfn_2ary(wrap))), - (i.i("get_impl"), ConstTree::xfn(xfn_2ary(get_impl))), - (i.i("resolve"), ConstTree::xfn(xfn_2ary(resolve))), - ]), - ) -} diff --git a/src/systems/stl/reflect.rs b/src/systems/stl/reflect.rs deleted file mode 100644 index 3578dfb..0000000 --- a/src/systems/stl/reflect.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::cmp::Ordering; -use std::fmt::Debug; -use std::hash::Hash; -use std::sync::Arc; - -use crate::foreign::{xfn_2ary, InertAtomic}; -use crate::{ConstTree, Interner, Sym}; - -#[derive(Debug, Clone)] -pub struct SymbolName(pub Sym); -impl InertAtomic for SymbolName { - fn type_str() -> &'static str { "SymbolName" } -} - -// #[derive(Debug, Clone)] -// pub struct GetSymName; -// impl ExternFn for GetSymName { -// fn name(&self) -> &str { "GetSymName" } -// fn apply( -// self: Box, -// arg: ExprInst, -// _: Context, -// ) -> XfnResult { arg.inspect(|c| match c { Clause::Constant(name) -// => Ok(SymbolName(name.clone()).atom_cls()), _ => -// AssertionError::fail(arg.location(), "is not a constant name"), }) -// } -// } - -#[derive(Clone)] -pub struct RefEqual(Arc); -impl RefEqual { - pub fn new() -> Self { Self(Arc::new(0u8)) } - pub fn id(&self) -> usize { &*self.0 as *const u8 as usize } -} -impl Debug for RefEqual { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("RefEqual").field(&self.id()).finish() - } -} -impl InertAtomic for RefEqual { - fn type_str() -> &'static str { "RefEqual" } - fn strict_eq(&self, other: &Self) -> bool { self == other } -} -impl Eq for RefEqual {} -impl PartialEq for RefEqual { - fn eq(&self, other: &Self) -> bool { self.id() == other.id() } -} -impl Ord for RefEqual { - fn cmp(&self, other: &Self) -> Ordering { self.id().cmp(&other.id()) } -} -impl PartialOrd for RefEqual { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Hash for RefEqual { - fn hash(&self, state: &mut H) { self.id().hash(state) } -} - -pub fn reflect(i: &Interner) -> ConstTree { - // ConstTree::tree([]) - ConstTree::namespace( - [i.i("reflect")], - ConstTree::tree([( - i.i("ref_equal"), - ConstTree::xfn(xfn_2ary(|l: RefEqual, r: RefEqual| Ok(l.id() == r.id()))), - )]), - ) -} diff --git a/src/systems/stl/result.orc b/src/systems/stl/result.orc deleted file mode 100644 index 72eea1b..0000000 --- a/src/systems/stl/result.orc +++ /dev/null @@ -1,10 +0,0 @@ -import std::panic - -export const ok := \v. \fe. \fv. fv v -export const err := \e. \fe. \fv. fe e - -export const map := \result. \fv. result err fv -export const map_err := \result. \fe. result fe ok -export const flatten := \result. result err \res. res -export const and_then := \result. \f. result err \v. f v -export const unwrap := \result. result (\e. panic "value expected") \v.v diff --git a/src/systems/stl/state.rs b/src/systems/stl/state.rs deleted file mode 100644 index 7539b03..0000000 --- a/src/systems/stl/state.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::ops::Deref; -use std::sync::{Arc, RwLock}; - -use crate::foreign::cps_box::{const_cps, init_cps, CPSBox}; -use crate::foreign::{xfn_1ary, Atomic, InertAtomic, XfnResult}; -use crate::interpreted::{Clause, ExprInst}; -use crate::interpreter::HandlerTable; -use crate::systems::codegen::call; -use crate::{ConstTree, Interner}; - -#[derive(Debug, Clone)] -pub struct State(Arc>); -impl InertAtomic for State { - fn type_str() -> &'static str { "State" } -} - -#[derive(Debug, Clone)] -struct NewStateCmd; - -#[derive(Debug, Clone)] -struct SetStateCmd(State); - -#[derive(Debug, Clone)] -struct GetStateCmd(State); - -fn get_state(s: State) -> XfnResult { Ok(init_cps(2, GetStateCmd(s))) } - -fn set_state(s: State) -> XfnResult { Ok(init_cps(2, SetStateCmd(s))) } - -fn new_state_handler(cmd: CPSBox) -> Result { - let (_, default, handler) = cmd.unpack2(); - let state = State(Arc::new(RwLock::new(default))); - Ok(call(handler, [state.atom_exi()]).wrap()) -} - -fn set_state_handler(cmd: CPSBox) -> Result { - let (SetStateCmd(state), value, handler) = cmd.unpack2(); - *state.0.as_ref().write().unwrap() = value; - Ok(handler) -} - -fn get_state_handler(cmd: CPSBox) -> Result { - let (GetStateCmd(state), handler) = cmd.unpack1(); - let val = match Arc::try_unwrap(state.0) { - Ok(lock) => lock.into_inner().unwrap(), - Err(arc) => arc.as_ref().read().unwrap().deref().clone(), - }; - Ok(call(handler, [val]).wrap()) -} - -pub fn state_handlers() -> HandlerTable<'static> { - let mut handlers = HandlerTable::new(); - handlers.register(|b| new_state_handler(*b)); - handlers.register(|b| get_state_handler(*b)); - handlers.register(|b| set_state_handler(*b)); - handlers -} - -pub fn state_lib(i: &Interner) -> ConstTree { - ConstTree::namespace( - [i.i("state")], - ConstTree::tree([ - (i.i("new_state"), const_cps(2, NewStateCmd)), - (i.i("get_state"), ConstTree::xfn(xfn_1ary(get_state))), - (i.i("set_state"), ConstTree::xfn(xfn_1ary(set_state))), - ]), - ) -} diff --git a/src/systems/stl/stl_system.rs b/src/systems/stl/stl_system.rs deleted file mode 100644 index 58307e9..0000000 --- a/src/systems/stl/stl_system.rs +++ /dev/null @@ -1,68 +0,0 @@ -#![allow(non_upper_case_globals)] - -use hashbrown::HashMap; -use rust_embed::RustEmbed; - -use super::binary::bin; -use super::bool::bool; -use super::conv::conv; -use super::exit_status::exit_status; -use super::inspect::inspect; -use super::number::num; -use super::panic::panic; -use super::protocol::{parsers, protocol_lib}; -use super::reflect::reflect; -use super::state::{state_handlers, state_lib}; -use super::string::str; -use crate::facade::{IntoSystem, System}; -use crate::interner::Interner; -use crate::pipeline::file_loader::embed_to_map; -use crate::sourcefile::{FileEntry, FileEntryKind, Import}; -use crate::Location; - -/// Feature flags for the STL. -#[derive(Default)] -pub struct StlConfig { - /// Whether impure functions (such as io::debug) are allowed. An embedder - /// would typically disable this flag - pub impure: bool, -} - -#[derive(RustEmbed)] -#[folder = "src/systems/stl"] -#[prefix = "std/"] -#[include = "*.orc"] -struct StlEmbed; - -impl IntoSystem<'static> for StlConfig { - fn into_system(self, i: &Interner) -> System<'static> { - let pure_tree = bin(i) - + bool(i) - + conv(i) - + exit_status(i) - + num(i) - + panic(i) - + reflect(i) - + state_lib(i) - + str(i) - + protocol_lib(i); - let mk_impure_fns = || inspect(i); - let fns = if self.impure { pure_tree + mk_impure_fns() } else { pure_tree }; - System { - name: vec!["std".to_string()], - constants: HashMap::from([(i.i("std"), fns)]), - code: embed_to_map::(".orc", i), - prelude: vec![FileEntry { - locations: vec![Location::Unknown], - kind: FileEntryKind::Import(vec![Import { - location: Location::Unknown, - path: vec![i.i("std"), i.i("prelude")], - name: None, - }]), - }], - handlers: state_handlers(), - lexer_plugins: vec![], - line_parsers: parsers(), - } - } -} diff --git a/src/systems/stl/string.rs b/src/systems/stl/string.rs deleted file mode 100644 index da225a4..0000000 --- a/src/systems/stl/string.rs +++ /dev/null @@ -1,77 +0,0 @@ -use unicode_segmentation::UnicodeSegmentation; - -use crate::error::RuntimeError; -use crate::foreign::{ - xfn_1ary, xfn_2ary, xfn_3ary, Atomic, ToClause, XfnResult, -}; -use crate::interner::Interner; -use crate::interpreted::Clause; -use crate::representations::OrcString; -use crate::systems::codegen::{opt, tuple}; -use crate::utils::iter_find; -use crate::ConstTree; - -pub fn len(s: OrcString) -> XfnResult { Ok(s.graphemes(true).count()) } - -pub fn size(s: OrcString) -> XfnResult { Ok(s.as_bytes().len()) } - -/// Append a string to another -pub fn concatenate(a: OrcString, b: OrcString) -> XfnResult { - Ok(a.get_string() + b.as_str()) -} - -pub fn slice(s: OrcString, i: usize, len: usize) -> XfnResult { - let graphs = s.as_str().graphemes(true); - if i == 0 { - return Ok(graphs.take(len).collect::()); - } - let mut prefix = graphs.skip(i - 1); - if prefix.next().is_none() { - return Err(RuntimeError::ext( - "Character index out of bounds".to_string(), - "indexing string", - )); - } - let mut count = 0; - let ret = (prefix.take(len)) - .map(|x| { - count += 1; - x - }) - .collect::(); - if count == len { - Ok(ret) - } else { - RuntimeError::fail( - "Character index out of bounds".to_string(), - "indexing string", - ) - } -} - -pub fn find(haystack: OrcString, needle: OrcString) -> XfnResult { - let haystack_graphs = haystack.as_str().graphemes(true); - let found = iter_find(haystack_graphs, needle.as_str().graphemes(true)); - Ok(opt(found.map(|x| x.atom_exi()))) -} - -pub fn split(s: OrcString, i: usize) -> XfnResult { - let mut graphs = s.as_str().graphemes(true); - let a = graphs.by_ref().take(i).collect::(); - let b = graphs.collect::(); - Ok(tuple([a.to_exi(), b.to_exi()])) -} - -pub fn str(i: &Interner) -> ConstTree { - ConstTree::tree([( - i.i("string"), - ConstTree::tree([ - (i.i("concat"), ConstTree::xfn(xfn_2ary(concatenate))), - (i.i("slice"), ConstTree::xfn(xfn_3ary(slice))), - (i.i("find"), ConstTree::xfn(xfn_2ary(find))), - (i.i("split"), ConstTree::xfn(xfn_2ary(split))), - (i.i("len"), ConstTree::xfn(xfn_1ary(len))), - (i.i("size"), ConstTree::xfn(xfn_1ary(size))), - ]), - )]) -} diff --git a/src/systems/stl/tuple.orc b/src/systems/stl/tuple.orc deleted file mode 100644 index 6371038..0000000 --- a/src/systems/stl/tuple.orc +++ /dev/null @@ -1,84 +0,0 @@ -import super::(known::*, bool::*, number::*, match, macro) - -export type ty ( - import super::super::(number::*, bool::*, macro, panic) - - const discard_args := \n. \value. ( - if n == 0 then value - else \_. discard_args (n - 1) value - ) - - macro gen_call macro::list_end =0x1p254=> \f.f - macro gen_call ( macro::list_item $item $tail ) =0x1p254=> \f. (gen_call $tail) (f $item) - export macro new ( $list ) =0x1p84=> wrap \f. (gen_call $list) (f (macro::length $list)) - - export const pick := \tuple. \i. (unwrap tuple) ( \size. - if size <= i then panic "Tuple index out of bounds" - else discard_args i \val. discard_args (size - 1 - i) val - ) - - export const length := \tuple. (unwrap tuple) \size. discard_args size size - - export const apply := \tuple. \f. (unwrap tuple) f -) - -const pick := ty::pick -const length := ty::length -const apply := ty::apply - -macro t[..$items] =0x2p84=> ( ty::new ( macro::comma_list (..$items) ) ) - -export ::(t, size) - -macro size ( t[..$items] ) =0x1p230=> macro::length macro::comma_list (..$items) - ---[ - request l -> pattern_walker l - pattern_walker end -> pattern_result - pattern_walker h ++ t -> await_pattern - await_pattern -> pattern_result -]-- - -( macro match::request ( t[ ..$items ] ) - =0x1p230=> tuple_pattern - ( macro::length macro::comma_list ( ..$items ) ) - ( - pattern_walker - (0) -- index of next item - macro::comma_list ( ..$items ) -- leftover items - ) -) -( macro tuple_pattern $length ( pattern_result $expr ( $binds ) ) - =0x1p254=> match::response ( - if length match::value == $length - then $expr - else match::fail - ) ( $binds ) -) -( macro pattern_walker $length macro::list_end - =0x1p254=> pattern_result match::pass ( match::no_binds ) -) -( macro pattern_walker (...$length) ( macro::list_item $next $tail ) - =0x1p254=> pattern_await - (...$length) - ( match::request $next ) - ( pattern_walker (...$length + 1) $tail ) -) -( macro pattern_await $length - ( match::response $expr ( $binds ) ) - ( pattern_result $tail_expr ( $tail_binds ) ) - =0x1p254=> - pattern_result - ( - (\match::pass. (\match::value. $expr) (pick match::value $length)) ( - match::take_binds $binds ( - (\match::pass. $tail_expr) ( match::take_binds $tail_binds ( - match::give_binds - match::chain_binds $binds $tail_binds - match::pass - )) - ) - ) - ) - ( match::chain_binds $binds $tail_binds ) -) diff --git a/src/tree.rs b/src/tree.rs new file mode 100644 index 0000000..20685c3 --- /dev/null +++ b/src/tree.rs @@ -0,0 +1,658 @@ +//! Generic module tree structure +//! +//! Used by various stages of the pipeline with different parameters +use std::fmt::{Debug, Display}; + +use hashbrown::HashMap; +use intern_all::{ev, i, Tok}; +use never::Never; +use substack::Substack; +use trait_set::trait_set; + +use crate::error::{ProjectError, ProjectErrorObj}; +use crate::location::CodeLocation; +use crate::name::{VName, VPath}; +use crate::utils::boxed_iter::BoxedIter; +use crate::utils::combine::Combine; +use crate::utils::join::try_join_maps; +use crate::utils::sequence::Sequence; + +/// An umbrella trait for operations you can carry out on any part of the tree +/// structure +pub trait TreeTransforms { + /// Data held at the leaves of the tree + type Item; + /// Data associated with modules + type XMod; + /// Data associated with entries inside modules + type XEnt; + /// Recursive type to enable [TreeTransforms::map_data] to transform the whole + /// tree + type SelfType: TreeTransforms; + + /// Transform all the data in the tree without changing its structure + fn map_data( + self, + item: &impl Fn(Substack>, Self::Item) -> T, + module: &impl Fn(Substack>, Self::XMod) -> U, + entry: &impl Fn(Substack>, Self::XEnt) -> V, + path: Substack>, + ) -> Self::SelfType; + + /// Visit all elements in the tree. This is like [TreeTransforms::search] but + /// without the early exit + /// + /// * init - can be used for reduce, otherwise pass `()` + /// * callback - a callback applied on every module. + /// * [Substack>] - the walked path + /// * [Module] - the current module + /// * T - data for reduce. + fn search_all<'a, T>( + &'a self, + init: T, + mut callback: impl FnMut( + Substack>, + ModMemberRef<'a, Self::Item, Self::XMod, Self::XEnt>, + T, + ) -> T, + ) -> T { + let res = self.search(init, |stack, member, state| { + Ok::(callback(stack, member, state)) + }); + res.unwrap_or_else(|e| match e {}) + } + + /// Visit elements in the tree depth first with the provided function + /// + /// * init - can be used for reduce, otherwise pass `()` + /// * callback - a callback applied on every module. Can return [Err] to + /// short-circuit the walk + /// * [Substack>] - the walked path + /// * [Module] - the current module + /// * T - data for reduce. + fn search<'a, T, E>( + &'a self, + init: T, + mut callback: impl FnMut( + Substack>, + ModMemberRef<'a, Self::Item, Self::XMod, Self::XEnt>, + T, + ) -> Result, + ) -> Result { + self.search_rec(init, Substack::Bottom, &mut callback) + } + + /// Internal version of [TreeTransforms::search_all] + fn search_rec<'a, T, E>( + &'a self, + state: T, + stack: Substack>, + callback: &mut impl FnMut( + Substack>, + ModMemberRef<'a, Self::Item, Self::XMod, Self::XEnt>, + T, + ) -> Result, + ) -> Result; +} + +/// The member in a [ModEntry] which is associated with a name in a [Module] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ModMember { + /// Arbitrary data + Item(Item), + /// A child module + Sub(Module), +} + +impl TreeTransforms for ModMember { + type Item = Item; + type XEnt = XEnt; + type XMod = XMod; + type SelfType = ModMember; + + fn map_data( + self, + item: &impl Fn(Substack>, Item) -> T, + module: &impl Fn(Substack>, XMod) -> U, + entry: &impl Fn(Substack>, XEnt) -> V, + path: Substack>, + ) -> Self::SelfType { + match self { + Self::Item(it) => ModMember::Item(item(path, it)), + Self::Sub(sub) => ModMember::Sub(sub.map_data(item, module, entry, path)), + } + } + + fn search_rec<'a, T, E>( + &'a self, + state: T, + stack: Substack>, + callback: &mut impl FnMut( + Substack>, + ModMemberRef<'a, Item, XMod, XEnt>, + T, + ) -> Result, + ) -> Result { + match self { + Self::Item(it) => callback(stack, ModMemberRef::Item(it), state), + Self::Sub(m) => m.search_rec(state, stack, callback), + } + } +} + +/// Reasons why merging trees might fail +pub enum ConflictKind { + /// Error during the merging of items + Item(Item::Error), + /// Error during the merging of module metadata + Module(XMod::Error), + /// Error during the merging of entry metadata + XEnt(XEnt::Error), + /// An item appeared in one tree where the other contained a submodule + ItemModule, +} + +macro_rules! impl_for_conflict { + ($target:ty, $deps:tt, $for:ty, $body:tt) => { + impl $target + for $for + where + Item::Error: $deps, + XMod::Error: $deps, + XEnt::Error: $deps, + $body + }; +} + +impl_for_conflict!(Clone, Clone, ConflictKind, { + fn clone(&self) -> Self { + match self { + ConflictKind::Item(it_e) => ConflictKind::Item(it_e.clone()), + ConflictKind::Module(mod_e) => ConflictKind::Module(mod_e.clone()), + ConflictKind::XEnt(ent_e) => ConflictKind::XEnt(ent_e.clone()), + ConflictKind::ItemModule => ConflictKind::ItemModule, + } + } +}); + +impl_for_conflict!(Debug, Debug, ConflictKind, { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConflictKind::Item(it_e) => + f.debug_tuple("TreeCombineErr::Item").field(it_e).finish(), + ConflictKind::Module(mod_e) => + f.debug_tuple("TreeCombineErr::Module").field(mod_e).finish(), + ConflictKind::XEnt(ent_e) => + f.debug_tuple("TreeCombineErr::XEnt").field(ent_e).finish(), + ConflictKind::ItemModule => write!(f, "TreeCombineErr::Item2Module"), + } + } +}); + +/// Error produced when two trees cannot be merged +pub struct TreeConflict { + /// Which subtree caused the failure + pub path: VPath, + /// What type of failure occurred + pub kind: ConflictKind, +} +impl + TreeConflict +{ + fn new(kind: ConflictKind) -> Self { + Self { path: VPath::new([]), kind } + } + + fn push(self, seg: Tok) -> Self { + Self { path: self.path.prefix([seg]), kind: self.kind } + } +} + +impl_for_conflict!(Clone, Clone, TreeConflict, { + fn clone(&self) -> Self { + Self { path: self.path.clone(), kind: self.kind.clone() } + } +}); + +impl_for_conflict!(Debug, Debug, TreeConflict, { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TreeConflict") + .field("path", &self.path) + .field("kind", &self.kind) + .finish() + } +}); + +impl Combine + for ModMember +{ + type Error = TreeConflict; + + fn combine(self, other: Self) -> Result { + match (self, other) { + (Self::Item(i1), Self::Item(i2)) => match i1.combine(i2) { + Ok(i) => Ok(Self::Item(i)), + Err(e) => Err(TreeConflict::new(ConflictKind::Item(e))), + }, + (Self::Sub(m1), Self::Sub(m2)) => m1.combine(m2).map(Self::Sub), + (..) => Err(TreeConflict::new(ConflictKind::ItemModule)), + } + } +} + +/// Data about a name in a [Module] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModEntry { + /// The submodule or item + pub member: ModMember, + /// Additional fields + pub x: XEnt, +} +impl Combine + for ModEntry +{ + type Error = TreeConflict; + fn combine(self, other: Self) -> Result { + match self.x.combine(other.x) { + Err(e) => Err(TreeConflict::new(ConflictKind::XEnt(e))), + Ok(x) => Ok(Self { x, member: self.member.combine(other.member)? }), + } + } +} +impl ModEntry { + /// Returns the item in this entry if it contains one. + #[must_use] + pub fn item(&self) -> Option<&Item> { + match &self.member { + ModMember::Item(it) => Some(it), + ModMember::Sub(_) => None, + } + } +} + +impl TreeTransforms for ModEntry { + type Item = Item; + type XEnt = XEnt; + type XMod = XMod; + type SelfType = ModEntry; + + fn map_data( + self, + item: &impl Fn(Substack>, Item) -> T, + module: &impl Fn(Substack>, XMod) -> U, + entry: &impl Fn(Substack>, XEnt) -> V, + path: Substack>, + ) -> Self::SelfType { + ModEntry { + member: self.member.map_data(item, module, entry, path.clone()), + x: entry(path, self.x), + } + } + + fn search_rec<'a, T, E>( + &'a self, + state: T, + stack: Substack>, + callback: &mut impl FnMut( + Substack>, + ModMemberRef<'a, Item, XMod, XEnt>, + T, + ) -> Result, + ) -> Result { + self.member.search_rec(state, stack, callback) + } +} +impl ModEntry { + /// Wrap a member directly with trivial metadata + pub fn wrap(member: ModMember) -> Self { + Self { member, x: XEnt::default() } + } + /// Wrap an item directly with trivial metadata + pub fn leaf(item: Item) -> Self { Self::wrap(ModMember::Item(item)) } +} +impl ModEntry { + /// Create an empty submodule + pub fn empty() -> Self { Self::wrap(ModMember::Sub(Module::wrap([]))) } + + /// Create a module + #[must_use] + pub fn tree>(arr: impl IntoIterator) -> Self { + Self::wrap(ModMember::Sub(Module::wrap( + arr.into_iter().map(|(k, v)| (i(k.as_ref()), v)), + ))) + } + + /// Create a record in the list passed to [ModEntry#tree] which describes a + /// submodule. This mostly exists to deal with strange rustfmt block + /// breaking behaviour + pub fn tree_ent>( + key: K, + arr: impl IntoIterator, + ) -> (K, Self) { + (key, Self::tree(arr)) + } + + /// Namespace the tree with the list of names + /// + /// The unarray is used to trick rustfmt into breaking the sub-item + /// into a block without breaking anything else. + #[must_use] + pub fn ns(name: impl AsRef, [mut end]: [Self; 1]) -> Self { + let elements = name.as_ref().split("::").collect::>(); + for name in elements.into_iter().rev() { + end = Self::tree([(name, end)]); + } + end + } + + fn not_mod_panic() -> T { panic!("Expected module but found leaf") } + + /// Return the wrapped module. Panic if the entry wraps an item + pub fn unwrap_mod(self) -> Module { + if let ModMember::Sub(m) = self.member { m } else { Self::not_mod_panic() } + } + + /// Return the wrapped module. Panic if the entry wraps an item + pub fn unwrap_mod_ref(&self) -> &Module { + if let ModMember::Sub(m) = &self.member { m } else { Self::not_mod_panic() } + } +} + +/// A module, containing imports, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Module { + /// Submodules and items by name + pub entries: HashMap, ModEntry>, + /// Additional fields + pub x: XMod, +} + +trait_set! { + /// A filter applied to a module tree + pub trait Filter<'a, Item, XMod, XEnt> = + for<'b> Fn(&'b ModEntry) -> bool + Clone + 'a +} + +/// A line in a [Module] +pub type Record = (Tok, ModEntry); + +impl Module { + /// Returns child names for which the value matches a filter + #[must_use] + pub fn keys<'a>( + &'a self, + filter: impl for<'b> Fn(&'b ModEntry) -> bool + 'a, + ) -> BoxedIter> { + Box::new( + (self.entries.iter()) + .filter(move |(_, v)| filter(v)) + .map(|(k, _)| k.clone()), + ) + } + + /// Return the module at the end of the given path + pub fn walk_ref<'a: 'b, 'b>( + &'a self, + prefix: &'b [Tok], + path: &'b [Tok], + filter: impl Filter<'b, Item, XMod, XEnt>, + ) -> Result<&'a Self, WalkError<'b>> { + let mut module = self; + for (pos, step) in path.iter().enumerate() { + let kind = match module.entries.get(step) { + None => ErrKind::Missing, + Some(ent) if !filter(ent) => ErrKind::Filtered, + Some(ModEntry { member: ModMember::Item(_), .. }) => ErrKind::NotModule, + Some(ModEntry { member: ModMember::Sub(next), .. }) => { + module = next; + continue; + }, + }; + let options = Sequence::new(move || module.keys(filter.clone())); + return Err(WalkError { kind, prefix, path, pos, options }); + } + Ok(module) + } + + /// Return the member at the end of the given path + /// + /// # Panics + /// + /// if path is empty, since the reference cannot be forwarded that way + pub fn walk1_ref<'a: 'b, 'b>( + &'a self, + prefix: &'b [Tok], + path: &'b [Tok], + filter: impl Filter<'b, Item, XMod, XEnt>, + ) -> Result<(&'a ModEntry, &'a Self), WalkError<'b>> { + let (last, parent) = path.split_last().expect("Path cannot be empty"); + let pos = path.len() - 1; + let module = self.walk_ref(prefix, parent, filter.clone())?; + let err_kind = match &module.entries.get(last) { + Some(entry) if filter(entry) => return Ok((entry, module)), + Some(_) => ErrKind::Filtered, + None => ErrKind::Missing, + }; + let options = Sequence::new(move || module.keys(filter.clone())); + Err(WalkError { kind: err_kind, options, prefix, path, pos }) + } + + /// Walk from one node to another in a tree, asserting that the origin can see + /// the target. + /// + /// # Panics + /// + /// If the target is the root node + pub fn inner_walk<'a: 'b, 'b>( + &'a self, + origin: &[Tok], + target: &'b [Tok], + is_exported: impl for<'c> Fn(&'c ModEntry) -> bool + + Clone + + 'b, + ) -> Result<(&'a ModEntry, &'a Self), WalkError<'b>> { + let ignore_vis_len = + 1 + origin.iter().zip(target).take_while(|(a, b)| a == b).count(); + if target.len() <= ignore_vis_len { + return self.walk1_ref(&[], target, |_| true); + } + let (ignore_vis_path, hidden_path) = target.split_at(ignore_vis_len); + let first_divergence = self.walk_ref(&[], ignore_vis_path, |_| true)?; + first_divergence.walk1_ref(ignore_vis_path, hidden_path, is_exported) + } + + /// Wrap entry table in a module with trivial metadata + pub fn wrap( + entries: impl IntoIterator>, + ) -> Self + where XMod: Default { + Self { entries: entries.into_iter().collect(), x: XMod::default() } + } +} + +impl TreeTransforms for Module { + type Item = Item; + type XEnt = XEnt; + type XMod = XMod; + type SelfType = Module; + + fn map_data( + self, + item: &impl Fn(Substack>, Item) -> T, + module: &impl Fn(Substack>, XMod) -> U, + entry: &impl Fn(Substack>, XEnt) -> V, + path: Substack>, + ) -> Self::SelfType { + Module { + x: module(path.clone(), self.x), + entries: (self.entries.into_iter()) + .map(|(k, e)| { + (k.clone(), e.map_data(item, module, entry, path.push(k))) + }) + .collect(), + } + } + + fn search_rec<'a, T, E>( + &'a self, + mut state: T, + stack: Substack>, + callback: &mut impl FnMut( + Substack>, + ModMemberRef<'a, Item, XMod, XEnt>, + T, + ) -> Result, + ) -> Result { + state = callback(stack.clone(), ModMemberRef::Mod(self), state)?; + for (key, value) in &self.entries { + state = value.search_rec(state, stack.push(key.clone()), callback)?; + } + Ok(state) + } +} + +impl Combine + for Module +{ + type Error = TreeConflict; + fn combine(self, Self { entries, x }: Self) -> Result { + let entries = try_join_maps(self.entries, entries, |k, l, r| { + l.combine(r).map_err(|e| e.push(k.clone())) + })?; + let x = (self.x.combine(x)) + .map_err(|e| TreeConflict::new(ConflictKind::Module(e)))?; + Ok(Self { x, entries }) + } +} + +impl Display + for Module +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "module {{")?; + for (name, ModEntry { member, x: extra }) in &self.entries { + match member { + ModMember::Sub(module) => write!(f, "\n{name} {extra} = {module}"), + ModMember::Item(item) => write!(f, "\n{name} {extra} = {item}"), + }?; + } + write!(f, "\n\n{}\n}}", &self.x) + } +} + +/// A non-owning version of [ModMember]. Either an item-ref or a module-ref. +pub enum ModMemberRef<'a, Item, XMod, XEnt> { + /// Leaf + Item(&'a Item), + /// Node + Mod(&'a Module), +} + +/// Possible causes why the path could not be walked +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ErrKind { + /// `require_exported` was set to `true` and a module wasn't exported + Filtered, + /// A module was not found + Missing, + /// The path leads into a leaf node + NotModule, +} + +#[derive(Clone)] +/// All details about a failed tree-walk +pub struct WalkError<'a> { + /// Failure mode + kind: ErrKind, + /// Path to the module where the walk started + prefix: &'a [Tok], + /// Planned walk path + path: &'a [Tok], + /// Index into walked path where the error occurred + pos: usize, + /// Alternatives to the failed steps + options: Sequence<'a, Tok>, +} +impl<'a> WalkError<'a> { + /// Total length of the path represented by this error + #[must_use] + pub fn depth(&self) -> usize { self.prefix.len() + self.pos + 1 } + + /// Attach a location to the error and convert into trait object for reporting + #[must_use] + pub fn at(self, location: &CodeLocation) -> ProjectErrorObj { + let details = WalkErrorDetails { + location: location.clone(), + path: VName::new( + (self.prefix.iter()) + .chain(self.path.iter().take(self.pos + 1)) + .cloned(), + ) + .expect("empty paths don't cause an error"), + options: self.options.iter().collect(), + }; + match self.kind { + ErrKind::Filtered => FilteredError(details).pack(), + ErrKind::Missing => MissingError(details).pack(), + ErrKind::NotModule => NotModuleError(details).pack(), + } + } + /// Construct an error for the very last item in a slice. This is often done + /// outside [super::tree] so it gets a function rather than exposing the + /// fields of [WalkError] + pub fn last( + path: &'a [Tok], + kind: ErrKind, + options: Sequence<'a, Tok>, + ) -> Self { + WalkError { kind, path, options, pos: path.len() - 1, prefix: &[] } + } +} +impl<'a> Debug for WalkError<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WalkError") + .field("kind", &self.kind) + .field("prefix", &self.prefix) + .field("path", &self.path) + .field("pos", &self.pos) + .finish_non_exhaustive() + } +} + +struct WalkErrorDetails { + path: VName, + options: Vec>, + location: CodeLocation, +} +impl WalkErrorDetails { + fn print_options(&self) -> String { + format!("options are {}", ev(&self.options).join(", ")) + } +} + +struct FilteredError(WalkErrorDetails); +impl ProjectError for FilteredError { + const DESCRIPTION: &'static str = "The path leads into a private module"; + fn one_position(&self) -> CodeLocation { self.0.location.clone() } + fn message(&self) -> String { + format!("{} is private, {}", self.0.path, self.0.print_options()) + } +} + +struct MissingError(WalkErrorDetails); +impl ProjectError for MissingError { + const DESCRIPTION: &'static str = "Nonexistent path"; + fn one_position(&self) -> CodeLocation { self.0.location.clone() } + fn message(&self) -> String { + format!("{} does not exist, {}", self.0.path, self.0.print_options()) + } +} + +struct NotModuleError(WalkErrorDetails); +impl ProjectError for NotModuleError { + const DESCRIPTION: &'static str = "The path leads into a leaf"; + fn one_position(&self) -> CodeLocation { self.0.location.clone() } + fn message(&self) -> String { + format!("{} is not a module, {}", self.0.path, self.0.print_options()) + } +} diff --git a/src/utils/cache.rs b/src/utils/cache.rs deleted file mode 100644 index 2695eaa..0000000 --- a/src/utils/cache.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::borrow::Borrow; -use std::cell::RefCell; -use std::hash::Hash; - -use hashbrown::HashMap; -use trait_set::trait_set; - -// TODO: make this a crate -trait_set! { - pub trait Callback<'a, I, O: 'static> = Fn(I, &Cache<'a, I, O>) -> O; -} -pub type CbBox<'a, I, O> = Box + 'a>; - -/// Cache the return values of an effectless closure in a hashmap -/// Inspired by the closure_cacher crate. -pub struct Cache<'a, I, O: 'static> { - store: RefCell>, - closure: CbBox<'a, I, O>, -} - -impl<'a, I: Eq + Hash + Clone, O: Clone> Cache<'a, I, O> { - pub fn new>(closure: F) -> Self { - Self { store: RefCell::new(HashMap::new()), closure: Box::new(closure) } - } - - /// Produce and cache a result by cloning I if necessary - pub fn find(&self, q: &Q) -> O - where - Q: Eq + Hash + ToOwned, - I: Borrow, - { - let closure = &self.closure; - if let Some(v) = self.store.borrow().get(q) { - return v.clone(); - } - // In the moment of invocation the refcell is on immutable - // this is important for recursive calculations - let result = closure(q.to_owned(), self); - let mut store = self.store.borrow_mut(); - store - .raw_entry_mut() - .from_key(q) - .or_insert_with(|| (q.to_owned(), result)) - .1 - .clone() - } -} - -impl<'a, I, O> IntoIterator for Cache<'a, I, O> { - type IntoIter = hashbrown::hash_map::IntoIter; - type Item = (I, O); - fn into_iter(self) -> Self::IntoIter { - let Cache { store, .. } = self; - let map = store.into_inner(); - map.into_iter() - } -} diff --git a/src/utils/clonable_iter.rs b/src/utils/clonable_iter.rs new file mode 100644 index 0000000..49d2c7f --- /dev/null +++ b/src/utils/clonable_iter.rs @@ -0,0 +1,83 @@ +use std::sync::{Arc, Mutex}; + +use crate::utils::take_with_output::take_with_output; + +enum State { + End, + Head(I), + Cont(Clonable, I::Item), +} + +/// Wraps a regular iterator and buffers previously emitted elements, to ensure +/// that clones of this iterator emit the same sequence of elements +/// independently of each other. Note that this ruins pretty much all of Rust's +/// iterator-related optimizations and allocates each buffered element on the +/// heap. +pub struct Clonable(Arc>>); +impl Clonable +where + I: Iterator, + I::Item: Clone, +{ + pub fn new(iter: impl IntoIterator) -> Self { + Self::wrap(State::Head(iter.into_iter())) + } + + fn wrap(s: State) -> Self { Self(Arc::new(Mutex::new(s))) } +} + +impl Iterator for Clonable +where + I: Iterator, + I::Item: Clone, +{ + type Item = I::Item; + fn next(&mut self) -> Option { + take_with_output(self, |Self(arc)| match Arc::try_unwrap(arc) { + Ok(mutex) => match mutex.into_inner().unwrap() { + State::End => (Self::wrap(State::End), None), + State::Cont(next, data) => (next, Some(data)), + State::Head(mut iter) => match iter.next() { + None => (Self::wrap(State::End), None), + Some(data) => (Self::wrap(State::Head(iter)), Some(data)), + }, + }, + Err(arc) => take_with_output(&mut *arc.lock().unwrap(), |s| match s { + State::End => (State::End, (Self::wrap(State::End), None)), + State::Cont(next, data) => + (State::Cont(next.clone(), data.clone()), (next, Some(data))), + State::Head(mut iter) => match iter.next() { + None => (State::End, (Self::wrap(State::End), None)), + Some(data) => { + let head = Self::wrap(State::Head(iter)); + (State::Cont(head.clone(), data.clone()), (head, Some(data))) + }, + }, + }), + }) + } + fn size_hint(&self) -> (usize, Option) { + let mut steps = 0; + let mut cur = self.0.clone(); + loop { + let guard = cur.lock().unwrap(); + match &*guard { + State::End => break (steps, Some(steps)), + State::Head(i) => { + let (min, max) = i.size_hint(); + break (min + steps, max.map(|s| s + steps)); + }, + State::Cont(next, _) => { + let tmp = next.0.clone(); + drop(guard); + cur = tmp; + steps += 1; + }, + } + } + } +} + +impl Clone for Clonable { + fn clone(&self) -> Self { Self(self.0.clone()) } +} diff --git a/src/utils/combine.rs b/src/utils/combine.rs new file mode 100644 index 0000000..d11aca4 --- /dev/null +++ b/src/utils/combine.rs @@ -0,0 +1,18 @@ +use never::Never; + +/// Fallible variant of [Add] +pub trait Combine: Sized { + type Error; + + fn combine(self, other: Self) -> Result; +} + +impl Combine for Never { + type Error = Never; + fn combine(self, _: Self) -> Result { match self {} } +} + +impl Combine for () { + type Error = Never; + fn combine(self, (): Self) -> Result { Ok(()) } +} diff --git a/src/utils/get_or.rs b/src/utils/get_or.rs index 1bfc3e2..b899a4d 100644 --- a/src/utils/get_or.rs +++ b/src/utils/get_or.rs @@ -2,15 +2,8 @@ use std::hash::Hash; use hashbrown::HashMap; -/// Return the given value from the map or default-initialize if it doesn't -/// exist, then retunrn a mutable reference. -pub fn get_or_default<'a, K: Eq + Hash + Clone, V: Default>( - map: &'a mut HashMap, - k: &K, -) -> &'a mut V { - get_or_make(map, k, || V::default()) -} - +/// Get the given value from the map or initialize it with the callback if it +/// doesn't exist, then return a mutable reference. pub fn get_or_make<'a, K: Eq + Hash + Clone, V>( map: &'a mut HashMap, k: &K, diff --git a/src/utils/iter_find.rs b/src/utils/iter_find.rs index 552965e..a321665 100644 --- a/src/utils/iter_find.rs +++ b/src/utils/iter_find.rs @@ -34,13 +34,10 @@ fn iter_starts_with( ) -> ISWResult { // if a starts with b then for every element in b for item in b { - // a has to contain the same element - if let Some(comp) = a.next() { - if item != comp { - return ISWResult::Difference; - } - } else { - return ISWResult::Shorter; + match a.next() { + Some(comp) if item == comp => (), + Some(_) => return ISWResult::Difference, + None => return ISWResult::Shorter, } } ISWResult::StartsWith diff --git a/src/utils/join.rs b/src/utils/join.rs new file mode 100644 index 0000000..c86d1ec --- /dev/null +++ b/src/utils/join.rs @@ -0,0 +1,33 @@ +use std::hash::Hash; + +use hashbrown::HashMap; +use never::Never; + +/// Combine two hashmaps via an infallible value merger. See also +/// [try_join_maps] +pub fn join_maps( + left: HashMap, + right: HashMap, + mut merge: impl FnMut(&K, V, V) -> V, +) -> HashMap { + try_join_maps(left, right, |k, l, r| Ok(merge(k, l, r))) + .unwrap_or_else(|e: Never| match e {}) +} + +/// Combine two hashmaps via a fallible value merger. See also [join_maps] +pub fn try_join_maps( + left: HashMap, + mut right: HashMap, + mut merge: impl FnMut(&K, V, V) -> Result, +) -> Result, E> { + let mut mixed = HashMap::with_capacity(left.len() + right.len()); + for (key, lval) in left { + let val = match right.remove(&key) { + None => lval, + Some(rval) => merge(&key, lval, rval)?, + }; + mixed.insert(key, val); + } + mixed.extend(right.into_iter()); + Ok(mixed) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4b52625..6bd684d 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,31 +1,19 @@ -mod cache; -pub mod ddispatch; -mod delete_cell; -pub mod get_or; -mod id_map; -mod iter_find; -pub mod never; -pub mod poller; -pub mod pure_seq; -pub mod rc_tools; -mod replace_first; -mod side; -mod split_max_prefix; -mod string_from_charset; -pub mod substack; -mod take_with_output; -pub mod thread_pool; -mod unwrap_or; +//! Utilities that don't necessarily have a well-defined role in the +//! problem-domain of Orchid but are rather designed to fulfill abstract +//! solution-domain tasks. +//! +//! An unreferenced util should be either moved out to a package or deleted -pub use cache::Cache; -pub use replace_first::replace_first; -pub use side::Side; -pub use split_max_prefix::split_max_prefix; -pub(crate) use unwrap_or::unwrap_or; -pub mod boxed_iter; -pub use boxed_iter::BoxedIter; -pub use delete_cell::DeleteCell; -pub use id_map::IdMap; -pub use iter_find::iter_find; -pub use string_from_charset::string_from_charset; -pub use take_with_output::take_with_output; +pub(crate) mod boxed_iter; +pub(crate) mod clonable_iter; +pub(crate) mod combine; +pub mod ddispatch; +pub(crate) mod get_or; +pub(crate) mod iter_find; +pub mod pure_seq; +pub mod sequence; +pub mod side; +pub mod string_from_charset; +pub mod take_with_output; +pub(crate) mod unwrap_or; +pub mod join; diff --git a/src/utils/never.rs b/src/utils/never.rs deleted file mode 100644 index 1fa6e9c..0000000 --- a/src/utils/never.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! A stable implementation of the never and infallible results - -/// An enum with no values -pub enum Never {} - -/// An infallible result -pub type Always = Result; - -/// Wrap value in a result with an impossible failure mode -pub fn always(t: T) -> Result { Ok(t) } - -/// Take success value out of a result with an impossible failure mode -pub fn unwrap_always(result: Result) -> T { - result.unwrap_or_else(|_| unreachable!("Never has no values")) -} diff --git a/src/utils/pure_seq.rs b/src/utils/pure_seq.rs index bfdf34f..4f0ba44 100644 --- a/src/utils/pure_seq.rs +++ b/src/utils/pure_seq.rs @@ -1,10 +1,15 @@ +//! Methods to operate on Rust vectors in a declarative manner + use std::iter; /// Pure version of [Vec::push] /// /// Create a new vector consisting of the provided vector with the /// element appended. See [pushed_ref] to use it with a slice -pub fn pushed(vec: impl IntoIterator, t: T) -> Vec { +pub fn pushed>( + vec: I, + t: I::Item, +) -> C { vec.into_iter().chain(iter::once(t)).collect() } @@ -12,17 +17,22 @@ pub fn pushed(vec: impl IntoIterator, t: T) -> Vec { /// /// Create a new vector consisting of the provided slice with the /// element appended. See [pushed] for the owned version -pub fn pushed_ref<'a, T: Clone + 'a>( +pub fn pushed_ref<'a, T: Clone + 'a, C: FromIterator>( vec: impl IntoIterator, t: T, -) -> Vec { +) -> C { vec.into_iter().cloned().chain(iter::once(t)).collect() } -/// Pure version of [Iterator::next] -/// -/// Remove an item from the iterator. If successful, returns the item and the -/// iterator. If the iterator is empty it is consumed. -pub fn next(mut i: I) -> Option<(I::Item, I)> { - i.next().map(|item| (item, i)) +/// Push an element on the adhoc stack, pass it to the callback, then pop the +/// element out again. +pub fn with_pushed( + vec: &mut Vec, + item: T, + cb: impl for<'a> FnOnce(&'a mut Vec) -> U, +) -> (T, U) { + vec.push(item); + let out = cb(vec); + let item = vec.pop().expect("top element stolen by callback"); + (item, out) } diff --git a/src/utils/rc_tools.rs b/src/utils/rc_tools.rs deleted file mode 100644 index 57c06a5..0000000 --- a/src/utils/rc_tools.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; - -pub fn rc_to_owned(rc: Rc) -> T { - Rc::try_unwrap(rc).unwrap_or_else(|rc| rc.as_ref().clone()) -} - -pub fn arc_to_owned(rc: Arc) -> T { - Arc::try_unwrap(rc).unwrap_or_else(|rc| rc.as_ref().clone()) -} - -pub fn map_rc(rc: Rc, pred: impl FnOnce(T) -> U) -> Rc { - Rc::new(pred(rc_to_owned(rc))) -} diff --git a/src/utils/replace_first.rs b/src/utils/replace_first.rs deleted file mode 100644 index abbb0a0..0000000 --- a/src/utils/replace_first.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::iter; - -/// Iterate over a sequence with the first element updated for which the -/// function returns Some(), but only if there is such an element. -pub fn replace_first Option>( - slice: &[T], - mut f: F, -) -> Option + '_> { - for i in 0..slice.len() { - if let Some(new) = f(&slice[i]) { - let subbed_iter = slice[0..i] - .iter() - .cloned() - .chain(iter::once(new)) - .chain(slice[i + 1..].iter().cloned()); - return Some(subbed_iter); - } - } - None -} diff --git a/src/utils/sequence.rs b/src/utils/sequence.rs new file mode 100644 index 0000000..f62bc99 --- /dev/null +++ b/src/utils/sequence.rs @@ -0,0 +1,28 @@ +//! An alternative to `Iterable` in many languages, a [Fn] that returns an +//! iterator. + +use dyn_clone::{clone_box, DynClone}; +use trait_set::trait_set; + +use super::boxed_iter::BoxedIter; + +trait_set! { + trait Payload<'a, T> = DynClone + Fn() -> BoxedIter<'a, T> + 'a; +} + +/// Dynamic iterator building callback. Given how many trait objects this +/// involves, it may actually be slower than C#. +pub struct Sequence<'a, T: 'a>(Box>); +impl<'a, T: 'a> Sequence<'a, T> { + /// Construct from a concrete function returning a concrete iterator + pub fn new + 'a>( + f: impl Fn() -> I + 'a + Clone, + ) -> Self { + Self(Box::new(move || Box::new(f().into_iter()))) + } + /// Get an iterator from the function + pub fn iter(&self) -> impl Iterator + '_ { (self.0)() } +} +impl<'a, T: 'a> Clone for Sequence<'a, T> { + fn clone(&self) -> Self { Self(clone_box(&*self.0)) } +} diff --git a/src/utils/side.rs b/src/utils/side.rs index a9cb345..3b655bc 100644 --- a/src/utils/side.rs +++ b/src/utils/side.rs @@ -1,7 +1,10 @@ +//! Named left/right. I tried bools, I couldn't consistently remember which one +//! is left, so I made an enum. Rust should optimize this into a bool anyway. + use std::fmt::Display; use std::ops::Not; -use super::BoxedIter; +use super::boxed_iter::BoxedIter; /// A primitive for encoding the two sides Left and Right. While booleans /// are technically usable for this purpose, they're very easy to confuse diff --git a/src/utils/split_max_prefix.rs b/src/utils/split_max_prefix.rs deleted file mode 100644 index 26a0572..0000000 --- a/src/utils/split_max_prefix.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Split off the longest prefix accepted by the validator -pub fn split_max_prefix<'a, T>( - path: &'a [T], - is_valid: &impl Fn(&[T]) -> bool, -) -> Option<(&'a [T], &'a [T])> { - (0..=path.len()) - .rev() - .map(|i| path.split_at(i)) - .find(|(file, _)| is_valid(file)) -} diff --git a/src/utils/string_from_charset.rs b/src/utils/string_from_charset.rs index 423cbd1..c1e195e 100644 --- a/src/utils/string_from_charset.rs +++ b/src/utils/string_from_charset.rs @@ -1,3 +1,6 @@ +//! Generate valid names from numbers and a character set. For small numbers, +//! the results should be substantially more memorable than the plain numbers. + fn string_from_charset_rec(val: u64, digits: &str) -> String { let radix = digits.len() as u64; let mut prefix = if val > radix { @@ -13,7 +16,7 @@ fn string_from_charset_rec(val: u64, digits: &str) -> String { } /// Generate alphabetized names from numbers using a set of permitted -/// characters +/// characters. Especially practical in combination with De Bruijn indices pub fn string_from_charset(val: u64, digits: &str) -> String { string_from_charset_rec(val + 1, digits) } diff --git a/src/utils/substack.rs b/src/utils/substack.rs deleted file mode 100644 index f4b75d4..0000000 --- a/src/utils/substack.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::collections::VecDeque; -use std::fmt::Debug; - -// TODO: extract to crate - -/// A frame of [Substack] which contains an element and a reference to the -/// rest of the stack. -#[derive(Clone, Copy)] -pub struct Stackframe<'a, T> { - pub item: T, - pub prev: &'a Substack<'a, T>, - pub len: usize, -} - -/// A FILO stack that lives on the regular call stack as a linked list. -/// Mainly useful to detect loops in recursive algorithms where -/// the recursion isn't deep enough to warrant a heap-allocated set. -#[derive(Clone, Copy)] -pub enum Substack<'a, T> { - /// A level in the linked list - Frame(Stackframe<'a, T>), - /// The end of the list - Bottom, -} - -impl<'a, T> Substack<'a, T> { - /// Convert the substack into an option of stackframe - pub fn opt(&'a self) -> Option<&'a Stackframe<'a, T>> { - match self { - Self::Frame(f) => Some(f), - Self::Bottom => None, - } - } - /// Construct an iterator over the listlike, very fast O(1) - pub fn iter(&self) -> SubstackIterator { SubstackIterator { curr: self } } - /// Add the item to this substack - pub fn push(&'a self, item: T) -> Self { Self::Frame(self.new_frame(item)) } - /// Create a new frame on top of this substack - pub fn new_frame(&'a self, item: T) -> Stackframe<'a, T> { - Stackframe { item, prev: self, len: self.opt().map_or(1, |s| s.len + 1) } - } - /// obtain the previous stackframe if one exists - pub fn pop(&'a self, count: usize) -> &'a Substack<'a, T> { - if count == 0 { - self - } else if let Self::Frame(p) = self { - p.prev.pop(count - 1) - } else { - &Substack::Bottom - } - } - /// number of stackframes - pub fn len(&self) -> usize { - match self { - Self::Frame(f) => f.len, - Self::Bottom => 0, - } - } - /// is this the bottom of the stack - pub fn is_empty(&self) -> bool { self.len() == 0 } -} - -impl<'a, T: Debug> Debug for Substack<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Substack")?; - f.debug_list().entries(self.iter()).finish() - } -} - -/// Iterates over a substack from the top down -pub struct SubstackIterator<'a, T> { - curr: &'a Substack<'a, T>, -} - -impl<'a, T> SubstackIterator<'a, T> { - pub fn first_some Option>(&mut self, f: F) -> Option { - for x in self.by_ref() { - if let Some(result) = f(x) { - return Some(result); - } - } - None - } - - /// Returns an iterator that starts from the bottom of the stack - /// and ends at the current position. - pub fn rev_vec_clone(self) -> Vec - where - T: Clone, - { - let mut deque = VecDeque::with_capacity(self.curr.len()); - for item in self { - deque.push_front(item.clone()) - } - deque.into() - } -} - -impl<'a, T> Copy for SubstackIterator<'a, T> {} -impl<'a, T> Clone for SubstackIterator<'a, T> { - fn clone(&self) -> Self { *self } -} - -impl<'a, T> Iterator for SubstackIterator<'a, T> { - type Item = &'a T; - fn next(&mut self) -> Option<&'a T> { - let curr = self.curr.opt()?; - let item = &curr.item; - let prev = curr.prev; - self.curr = prev; - Some(item) - } - - fn size_hint(&self) -> (usize, Option) { - (self.curr.len(), Some(self.curr.len())) - } -} diff --git a/src/utils/take_with_output.rs b/src/utils/take_with_output.rs index 6ce49e7..eebcf3d 100644 --- a/src/utils/take_with_output.rs +++ b/src/utils/take_with_output.rs @@ -1,3 +1,5 @@ +//! Map over a `&mut` and return an additional value + /// A variation on [take_mut::take] that allows the callback to return a value pub fn take_with_output(src: &mut T, cb: impl FnOnce(T) -> (T, U)) -> U { take_mut::scoped::scope(|scope| { diff --git a/src/utils/unwrap_or.rs b/src/utils/unwrap_or.rs index 6692981..bacf999 100644 --- a/src/utils/unwrap_or.rs +++ b/src/utils/unwrap_or.rs @@ -17,12 +17,11 @@ /// without an `if`, which can be misleading. It should only be used for small, /// straightforward jumps. macro_rules! unwrap_or { - ($m:expr; $fail:expr) => {{ - if let Some(res) = ($m) { res } else { $fail } - }}; - ($m:expr => $pattern:path; $fail:expr) => {{ - if let $pattern(res) = ($m) { res } else { $fail } - }}; + ($m:expr; $fail:expr) => {{ if let Some(res) = ($m) { res } else { $fail } }}; + ($m:expr => $pattern:path; $fail:expr) => { + // rustfmt keeps inlining this and then complaining about its length + { if let $pattern(res) = ($m) { res } else { $fail } } + }; } pub(crate) use unwrap_or; diff --git a/src/virt_fs/common.rs b/src/virt_fs/common.rs new file mode 100644 index 0000000..68a207a --- /dev/null +++ b/src/virt_fs/common.rs @@ -0,0 +1,66 @@ +use std::rc::Rc; +use std::sync::Arc; + +use intern_all::Tok; + +use crate::error::{ErrorSansLocation, ErrorSansLocationObj}; +use crate::name::{PathSlice, VPath}; + +/// Represents the result of loading code from a string-tree form such +/// as the file system. Cheap to clone. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Loaded { + /// Conceptually equivalent to a sourcefile + Code(Arc), + /// Conceptually equivalent to the list of *.orc files in a folder, without + /// the extension + Collection(Arc>>), +} +impl Loaded { + /// Is the loaded item source code (not a collection)? + pub fn is_code(&self) -> bool { matches!(self, Loaded::Code(_)) } + /// Collect the elements in a collection rreport + pub fn collection(items: impl IntoIterator>) -> Self { + Self::Collection(Arc::new(items.into_iter().collect())) + } +} + +/// Returned by any source loading callback +pub type FSResult = Result; + +/// Distinguished error for missing code +#[derive(Clone, PartialEq, Eq)] +pub struct CodeNotFound(pub VPath); +impl ErrorSansLocation for CodeNotFound { + const DESCRIPTION: &'static str = "No source code for path"; + fn message(&self) -> String { format!("{} not found", self.0) } +} + +/// A simplified view of a file system for the purposes of source code loading. +/// This includes the real FS and source code, but also various in-memory +/// formats and other sources for libraries and dependencies. +pub trait VirtFS { + /// Implementation of [VirtFS::read] + fn get(&self, path: &[Tok], full_path: PathSlice) -> FSResult; + /// Convert a path into a human-readable string that is meaningful in the + /// target context. + fn display(&self, path: &[Tok]) -> Option; + /// Convert the FS handler into a type-erased version of itself for packing in + /// a tree. + fn rc(self) -> Rc + where Self: Sized + 'static { + Rc::new(self) + } + /// Read a path, returning either a text file, a directory listing or an + /// error. Wrapper for [VirtFS::get] + fn read(&self, path: PathSlice) -> FSResult { self.get(path.0, path) } +} + +impl VirtFS for &dyn VirtFS { + fn get(&self, path: &[Tok], full_path: PathSlice) -> FSResult { + (*self).get(path, full_path) + } + fn display(&self, path: &[Tok]) -> Option { + (*self).display(path) + } +} diff --git a/src/virt_fs/decl.rs b/src/virt_fs/decl.rs new file mode 100644 index 0000000..8d16bb8 --- /dev/null +++ b/src/virt_fs/decl.rs @@ -0,0 +1,46 @@ +use std::rc::Rc; + +use intern_all::Tok; + +use super::common::CodeNotFound; +use super::{FSResult, Loaded, VirtFS}; +use crate::error::ErrorSansLocation; +use crate::name::PathSlice; +use crate::tree::{ModEntry, ModMember}; +use crate::utils::combine::Combine; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConflictingTrees; + +impl Combine for Rc { + type Error = ConflictingTrees; + fn combine(self, _: Self) -> Result { + Err(ConflictingTrees) + } +} + +/// A declarative in-memory tree with [VirtFS] objects for leaves. Paths are +/// followed to a leaf and the leftover handled by it. +pub type DeclTree = ModEntry, (), ()>; + +impl VirtFS for DeclTree { + fn get(&self, path: &[Tok], full_path: PathSlice) -> FSResult { + match &self.member { + ModMember::Item(it) => it.get(path, full_path), + ModMember::Sub(module) => match path.split_first() { + None => Ok(Loaded::collection(module.keys(|_| true))), + Some((head, tail)) => (module.entries.get(head)) + .ok_or_else(|| CodeNotFound(full_path.to_vpath()).pack()) + .and_then(|ent| ent.get(tail, full_path)), + }, + } + } + + fn display(&self, path: &[Tok]) -> Option { + let (head, tail) = path.split_first()?; + match &self.member { + ModMember::Item(it) => it.display(path), + ModMember::Sub(module) => module.entries.get(head)?.display(tail), + } + } +} diff --git a/src/virt_fs/dir.rs b/src/virt_fs/dir.rs new file mode 100644 index 0000000..cc27531 --- /dev/null +++ b/src/virt_fs/dir.rs @@ -0,0 +1,131 @@ +use std::cell::RefCell; +use std::fs::File; +use std::io; +use std::io::{ErrorKind, Read}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use hashbrown::HashMap; +use intern_all::{i, Tok}; + +use super::common::CodeNotFound; +use super::{FSResult, Loaded, VirtFS}; +use crate::error::{ErrorSansLocation, ErrorSansLocationObj}; +use crate::name::PathSlice; + +#[derive(Clone)] +struct OpenError { + file: Arc>, + dir: Arc>, +} +impl OpenError { + pub fn wrap(file: io::Error, dir: io::Error) -> ErrorSansLocationObj { + Self { dir: Arc::new(Mutex::new(dir)), file: Arc::new(Mutex::new(file)) } + .pack() + } +} +impl ErrorSansLocation for OpenError { + const DESCRIPTION: &'static str = "A file system error occurred"; + fn message(&self) -> String { + let Self { dir, file } = self; + format!( + "File system errors other than not found occurred\n\ + as a file: {}\nas a directory: {}", + file.lock().unwrap(), + dir.lock().unwrap() + ) + } +} + +#[derive(Clone)] +struct IOError(Arc>); +impl IOError { + pub fn wrap(inner: io::Error) -> ErrorSansLocationObj { + Self(Arc::new(Mutex::new(inner))).pack() + } +} +impl ErrorSansLocation for IOError { + const DESCRIPTION: &'static str = "an I/O error occured"; + fn message(&self) -> String { + format!("File read error: {}", self.0.lock().unwrap()) + } +} + +#[derive(Clone)] +struct NotUtf8(PathBuf); +impl NotUtf8 { + pub fn wrap(path: &Path) -> ErrorSansLocationObj { + Self(path.to_owned()).pack() + } +} +impl ErrorSansLocation for NotUtf8 { + const DESCRIPTION: &'static str = "Source files must be UTF-8"; + fn message(&self) -> String { + format!("{} is a source file but contains invalid UTF-8", self.0.display()) + } +} + +/// A real file system directory linked into the virtual FS +pub struct DirNode { + cached: RefCell>, + root: PathBuf, + suffix: &'static str, +} +impl DirNode { + /// Reference a real file system directory in the virtual FS + pub fn new(root: PathBuf, suffix: &'static str) -> Self { + assert!(suffix.starts_with('.'), "Extension must begin with ."); + Self { cached: RefCell::default(), root, suffix } + } + + fn ext(&self) -> &str { + self.suffix.strip_prefix('.').expect("Checked in constructor") + } + + fn load_file(&self, fpath: &Path, orig_path: PathSlice) -> FSResult { + match fpath.read_dir() { + Err(dir_e) => { + let fpath = fpath.with_extension(self.ext()); + let mut file = File::open(&fpath).map_err(|file_e| { + match (dir_e.kind(), file_e.kind()) { + (ErrorKind::NotFound, ErrorKind::NotFound) => + CodeNotFound(orig_path.to_vpath()).pack(), + _ => OpenError::wrap(file_e, dir_e), + } + })?; + let mut buf = Vec::new(); + file.read_to_end(&mut buf).map_err(IOError::wrap)?; + let text = String::from_utf8(buf).map_err(|_| NotUtf8::wrap(&fpath))?; + Ok(Loaded::Code(Arc::new(text))) + }, + Ok(dir) => Ok(Loaded::collection(dir.filter_map(|ent_r| { + let ent = ent_r.ok()?; + let name = ent.file_name().into_string().ok()?; + match ent.metadata().ok()?.is_dir() { + false => Some(i(name.strip_suffix(self.suffix)?)), + true => Some(i(&name)), + } + }))), + } + } + + fn mk_pathbuf(&self, path: &[Tok]) -> PathBuf { + let mut fpath = self.root.clone(); + path.iter().for_each(|seg| fpath.push(seg.as_str())); + fpath + } +} +impl VirtFS for DirNode { + fn get(&self, path: &[Tok], full_path: PathSlice) -> FSResult { + let fpath = self.mk_pathbuf(path); + let mut binding = self.cached.borrow_mut(); + let (_, res) = (binding.raw_entry_mut().from_key(&fpath)) + .or_insert_with(|| (fpath.clone(), self.load_file(&fpath, full_path))); + res.clone() + } + + fn display(&self, path: &[Tok]) -> Option { + let pathbuf = self.mk_pathbuf(path).with_extension(self.ext()); + Some(pathbuf.to_string_lossy().to_string()) + } +} diff --git a/src/virt_fs/embed.rs b/src/virt_fs/embed.rs new file mode 100644 index 0000000..c8c2a9b --- /dev/null +++ b/src/virt_fs/embed.rs @@ -0,0 +1,74 @@ +use std::sync::Arc; + +use intern_all::{ev, i, Tok}; +use rust_embed::RustEmbed; + +use super::common::CodeNotFound; +use super::{FSResult, Loaded, VirtFS}; +use crate::error::ErrorSansLocation; +use crate::location::CodeGenInfo; +use crate::name::PathSlice; +use crate::tree::{ModEntry, ModMember, Module}; + +/// An in-memory FS tree for libraries managed internally by the interpreter +pub struct EmbeddedFS { + tree: Module, (), ()>, + suffix: &'static str, + gen: CodeGenInfo, +} +impl EmbeddedFS { + /// Expose a directory embedded in a binary wiht [RustEmbed] to the + /// interpreter + pub fn new(suffix: &'static str, gen: CodeGenInfo) -> Self { + let mut tree = Module::wrap([]); + for path in T::iter() { + let data_buf = T::get(&path).expect("path from iterator").data.to_vec(); + let data = String::from_utf8(data_buf).expect("embed must be utf8"); + let mut cur_node = &mut tree; + let path_no_suffix = + path.strip_suffix(suffix).expect("embed filtered for suffix"); + let mut segments = path_no_suffix.split('/').map(i); + let mut cur_seg = segments.next().expect("Embed is a directory"); + for next_seg in segments { + if !cur_node.entries.contains_key(&cur_seg) { + let ent = ModEntry::wrap(ModMember::Sub(Module::wrap([]))); + cur_node.entries.insert(cur_seg.clone(), ent); + } + let ent = cur_node.entries.get_mut(&cur_seg).expect("just constructed"); + match &mut ent.member { + ModMember::Sub(submod) => cur_node = submod, + _ => panic!("Aliased file and folder"), + }; + cur_seg = next_seg; + } + let data_ent = ModEntry::wrap(ModMember::Item(Arc::new(data))); + let prev = cur_node.entries.insert(cur_seg, data_ent); + debug_assert!(prev.is_none(), "file name unique"); + } + // if gen.generator == "std" { + // panic!( + // "{:?}", + // tree.map_data(&|_, s| (), &|_, x| x, &|_, x| x, Substack::Bottom) + // ); + // }; + Self { gen, suffix, tree } + } +} + +impl VirtFS for EmbeddedFS { + fn get(&self, path: &[Tok], full_path: PathSlice) -> FSResult { + if path.is_empty() { + return Ok(Loaded::collection(self.tree.keys(|_| true))); + } + let entry = (self.tree.walk1_ref(&[], path, |_| true)) + .map_err(|_| CodeNotFound(full_path.to_vpath()).pack())?; + Ok(match &entry.0.member { + ModMember::Item(text) => Loaded::Code(text.clone()), + ModMember::Sub(sub) => Loaded::collection(sub.keys(|_| true)), + }) + } + fn display(&self, path: &[Tok]) -> Option { + let Self { gen, suffix, .. } = self; + Some(format!("{}{suffix} in {gen}", ev(path).join("/"))) + } +} diff --git a/src/virt_fs/mod.rs b/src/virt_fs/mod.rs new file mode 100644 index 0000000..b652eac --- /dev/null +++ b/src/virt_fs/mod.rs @@ -0,0 +1,18 @@ +//! Abstractions and primitives to help define the namespace tree used by +//! Orchid. +//! +//! Although this may make it seem like the namespace tree is very flexible, +//! libraries are generally permitted and expected to hardcode their own +//! location in the tree, so it's up to the embedder to ensure that the flexible +//! structure retains the assumed location of all code. +mod common; +mod decl; +mod dir; +mod embed; +mod prefix; + +pub use common::{CodeNotFound, FSResult, Loaded, VirtFS}; +pub use decl::DeclTree; +pub use dir::DirNode; +pub use embed::EmbeddedFS; +pub use prefix::PrefixFS; diff --git a/src/virt_fs/prefix.rs b/src/virt_fs/prefix.rs new file mode 100644 index 0000000..79db969 --- /dev/null +++ b/src/virt_fs/prefix.rs @@ -0,0 +1,42 @@ +use intern_all::Tok; +use itertools::Itertools; + +use super::common::CodeNotFound; +use super::VirtFS; +use crate::error::ErrorSansLocation; +use crate::name::{PathSlice, VPath}; + +/// Modify the prefix of a nested file tree +pub struct PrefixFS { + remove: VPath, + add: VPath, + wrapped: Box, +} +impl PrefixFS { + /// Modify the prefix of a file tree + pub fn new( + wrapped: impl VirtFS + 'static, + remove: impl AsRef, + add: impl AsRef, + ) -> Self { + Self { + wrapped: Box::new(wrapped), + remove: VPath::parse(remove.as_ref()), + add: VPath::parse(add.as_ref()), + } + } + fn proc_path(&self, path: &[Tok]) -> Option>> { + let path = path.strip_prefix(&self.remove[..])?; + Some(self.add.0.iter().chain(path).cloned().collect_vec()) + } +} +impl VirtFS for PrefixFS { + fn get(&self, path: &[Tok], full_path: PathSlice) -> super::FSResult { + let path = (self.proc_path(path)) + .ok_or_else(|| CodeNotFound(full_path.to_vpath()).pack())?; + self.wrapped.get(&path, full_path) + } + fn display(&self, path: &[Tok]) -> Option { + self.wrapped.display(&self.proc_path(path)?) + } +}