diff --git a/Cargo.lock b/Cargo.lock index 299d79b..3899947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "anstream" version = "0.3.2" @@ -183,6 +189,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "cpufeatures" version = "0.2.7" @@ -192,6 +213,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -202,6 +232,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -306,11 +349,12 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash 0.8.3", + "allocator-api2", ] [[package]] @@ -350,9 +394,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -405,12 +449,14 @@ version = "0.2.2" dependencies = [ "chumsky", "clap", + "derive_more", "duplicate", "dyn-clone", - "hashbrown 0.13.2", + "hashbrown 0.14.0", "itertools", "ordered-float", "paste", + "polling", "rust-embed", "take_mut", "thiserror", @@ -433,6 +479,28 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -536,6 +604,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.19" @@ -559,6 +636,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + [[package]] name = "serde" version = "1.0.160" diff --git a/Cargo.toml b/Cargo.toml index e29c0c7..60f3492 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" [[bin]] name = "orcx" -path = "src/bin/main.rs" +path = "src/bin/orcx.rs" doc = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -24,9 +24,9 @@ doc = false [dependencies] thiserror = "1.0" chumsky = "0.9" -hashbrown = "0.13" +hashbrown = "0.14" ordered-float = "3.7" -itertools = "0.10" +itertools = "0.11" dyn-clone = "1.0" clap = { version = "4.3", features = ["derive"] } trait-set = "0.3" @@ -35,3 +35,5 @@ rust-embed = { version = "6.6", features = ["include-exclude"] } duplicate = "1.0.0" take_mut = "0.2.2" unicode-segmentation = "1.10.1" +polling = "2.8.0" +derive_more = "0.99.17" diff --git a/ROADMAP.md b/ROADMAP.md index ea9893d..5e1f585 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,49 @@ -# IO +This document is a wishlist, its items aren't ordered in any way other than inline notes about dependency relations -All IO is event-based via callbacks. -Driven streams such as stdin expose single-fire events for the results of functions such as "read until terminator" or "read N bytes". -Network IO exposes repeated events such as "connect", "message", etc. \ No newline at end of file +# Language + +## Operator declarations +A dedicated (exportable) line type for declaring operators. Still just names, only you can write them next to other things without whitespace + +- ops may not contain c-ident-safe characters +- clusters of operator characters are broken up with a greedy algorithm + +## Typeclasses +Elixir-style protocols probably, only with n-ary dispatch which I saw in SICP-js + +# Rules + +## Placeholder constraints +Simultaneously match a pattern to a subexpression and give it a name to copy it over + +- Copy unique 1->1 names over by default to preserve their location info + +# STL + +## Command short-circuiting +Functions for each command type which destructure it and pass it to an Orchid callback + +## Runtime error handling +result? multipath cps utils? Not sure yet. + +## Pattern matching +This was the main trick in Orchid, still want to do it, still need to polish the language first + +## Macro error handling +Error tokens with rules to lift them out. Kinda depends on preservation of location info in rules to be really useful + +# Systems + +## Async +Join allows to run code when a tuple of pending events all resolve on the event poller + +## New: FS +Exposes tree operations to Orchid +Uses existing IO to open and read files +Uses the event bus to read directories in batches without blocking other Orchid code + +## New: Network +Event-driven I/O with single-fire events and resubscription to relay backpressure to the OS. Initially TCP + +## New: Marshall +Serialization of Orchid data, including code, given customizable sets of serializable foreign items. Alternatively, code reflection so that all this can go in the STL diff --git a/examples/calculator/main.orc b/examples/calculator/main.orc index 99da9f2..541c7f1 100644 --- a/examples/calculator/main.orc +++ b/examples/calculator/main.orc @@ -1,14 +1,15 @@ -import std::(proc::*, to_float, to_string, io::(readline, print)) +import std::(proc::*, to_float, to_string, panic, str::char_at) -export main := do{ +export const main := do{ cps print "left operand: "; - cps data = readline; + cps data = readln; let a = to_float data; cps print "operator: "; - cps op = readline; - cps print ("you selected \"" ++ op ++ "\"\n"); + cps op = readln; + let op = char_at op 0; + cps println ("you selected \"" ++ op ++ "\""); cps print "right operand: "; - cps data = readline; + cps data = readln; let b = to_float data; let result = ( if op == "+" then a + b @@ -17,6 +18,6 @@ export main := do{ else if op == "/" then a / b else (panic "Unsupported operation") ); - cps print ("Result: " ++ to_string result ++ "\n"); + cps println ("Result: " ++ to_string result); 0 -} \ No newline at end of file +} diff --git a/examples/hello-world/main.orc b/examples/hello-world/main.orc index 4141ef2..3abdd18 100644 --- a/examples/hello-world/main.orc +++ b/examples/hello-world/main.orc @@ -1,3 +1,5 @@ -import std::io::print - -main := print "Hello, world!\n" "goodbye" \ No newline at end of file +const main := ( + println "Hello, world!" + "success" +) +-- main := "Hello, World!\n" diff --git a/examples/list-processing/main.orc b/examples/list-processing/main.orc index f016138..63c1bdd 100644 --- a/examples/list-processing/main.orc +++ b/examples/list-processing/main.orc @@ -1,6 +1,6 @@ -import std::(proc::*, io::print, to_string) +import std::(proc::*, to_string) -export main := do{ +export const main := do{ let foo = list::new[1, 2, 3, 4, 5, 6]; let bar = list::map foo n => n * 2; let sum = bar @@ -8,6 +8,6 @@ export main := do{ |> list::take 3 |> list::reduce (\a.\b. a + b) |> option::unwrap; - cps print $ to_string sum ++ "\n"; + cps println $ to_string sum; 0 -} \ No newline at end of file +} diff --git a/examples/maps/main.orc b/examples/maps/main.orc index 8e3b5ae..e63be17 100644 --- a/examples/maps/main.orc +++ b/examples/maps/main.orc @@ -1,6 +1,6 @@ -import std::(proc::*, io::print, to_string) +import std::(proc::*, to_string) -export main := do{ +export const main := do{ let foo = map::new[ "foo" = 1, "bar" = 2, @@ -9,6 +9,6 @@ export main := do{ ]; let num = map::get foo "bar" |> option::unwrap; - cps print (to_string num ++ "\n"); + cps println $ to_string num; 0 -} \ No newline at end of file +} diff --git a/orchid.code-workspace b/orchid.code-workspace index dd9e8ad..6d3c470 100644 --- a/orchid.code-workspace +++ b/orchid.code-workspace @@ -36,7 +36,6 @@ }, "extensions": { "recommendations": [ - "bungcip.better-toml", "maptz.regionfolder", "serayuzgur.crates", "tamasfe.even-better-toml", diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index 0c353be..0000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,183 +0,0 @@ -mod cli; - -use std::fs::File; -use std::path::{Path, PathBuf}; -use std::{iter, process}; - -use clap::Parser; -use hashbrown::HashMap; -use itertools::Itertools; -use orchidlang::interner::InternedDisplay; -use orchidlang::{ - ast, ast_to_interpreted, collect_consts, collect_rules, interpreter, - pipeline, rule, stl, vname_to_sym_tree, Interner, ProjectTree, Stok, Sym, - VName, -}; - -use crate::cli::cmd_prompt; - -/// Orchid interpreter -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -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, - /// 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 dump_repo: bool, - /// Step through the macro execution process in the specified symbol - #[arg(long, default_value = "")] - pub macro_debug: String, -} -impl Args { - /// Validate the project directory and the - pub fn chk_dir_main(&self) -> Result<(), String> { - let dir_path = PathBuf::from(&self.dir); - if !dir_path.is_dir() { - return Err(format!("{} is not a directory", dir_path.display())); - } - let segs = self.main.split("::").collect::>(); - if segs.len() < 2 { - return Err("Entry point too short".to_string()); - } - let (pathsegs, _) = segs.split_at(segs.len() - 1); - 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() - )); - } - Ok(()) - } - - pub fn chk_proj(&self) -> Result<(), String> { - self.chk_dir_main() - } -} - -/// Load and parse all source related to the symbol `target` or all symbols -/// in the namespace `target` in the context of the STL. All sourcefiles must -/// reside within `dir`. -fn load_dir(dir: &Path, target: &[Stok], i: &Interner) -> ProjectTree { - let file_cache = pipeline::file_loader::mk_dir_cache(dir.to_path_buf(), i); - let library = stl::mk_stl(i, stl::StlOptions::default()); - pipeline::parse_layer( - iter::once(target), - &|path| file_cache.find(path), - &library, - &stl::mk_prelude(i), - i, - ) - .expect("Failed to load source code") -} - -pub fn to_vname(data: &str, i: &Interner) -> VName { - data.split("::").map(|s| i.i(s)).collect::>() -} - -/// A little utility to step through the resolution of a macro set -pub fn macro_debug(repo: rule::Repo, mut code: ast::Expr, i: &Interner) { - let mut idx = 0; - println!("Macro debugger working on {}", code.bundle(i)); - loop { - let (cmd, _) = cmd_prompt("cmd> ").unwrap(); - match cmd.trim() { - "" | "n" | "next" => - if let Some(c) = repo.step(&code) { - idx += 1; - code = c; - println!("Step {idx}: {}", code.bundle(i)); - }, - "p" | "print" => println!("Step {idx}: {}", code.bundle(i)), - "d" | "dump" => println!("Rules: {}", repo.bundle(i)), - "q" | "quit" => return, - "h" | "help" => println!( - "Available commands: - \t, n, next\t\ttake a step - \tp, print\t\tprint the current state - \tq, quit\t\texit - \th, help\t\tprint this text" - ), - _ => { - println!("unrecognized command \"{}\", try \"help\"", cmd); - continue; - }, - } - } -} - -pub fn main() { - 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 project = vname_to_sym_tree(load_dir(&dir, &main, &i), &i); - let rules = collect_rules(&project); - let consts = collect_consts(&project, &i); - let repo = rule::Repo::new(rules, &i).unwrap_or_else(|(rule, error)| { - panic!( - "Rule error: {} - Offending rule: {}", - error.bundle(&i), - rule.bundle(&i) - ) - }); - if args.dump_repo { - println!("Parsed rules: {}", repo.bundle(&i)); - return; - } else if !args.macro_debug.is_empty() { - let name = i.i(&to_vname(&args.macro_debug, &i)); - let code = consts - .get(&name) - .unwrap_or_else(|| panic!("Constant {} not found", args.macro_debug)); - return macro_debug(repo, code.clone(), &i); - } - let mut exec_table = HashMap::new(); - for (name, source) in consts.iter() { - let displayname = i.extern_vec(*name).join("::"); - let (unmatched, steps_left) = repo.long_step(source, args.macro_limit + 1); - assert!(steps_left > 0, "Macro execution in {displayname} did not halt"); - let runtree = ast_to_interpreted(&unmatched).unwrap_or_else(|e| { - panic!("Postmacro conversion error in {displayname}: {e}") - }); - exec_table.insert(*name, runtree); - } - let ctx = - interpreter::Context { symbols: &exec_table, interner: &i, gas: None }; - let entrypoint = exec_table.get(&i.i(&main)).unwrap_or_else(|| { - let main = args.main; - let symbols = - exec_table.keys().map(|t| i.extern_vec(*t).join("::")).join(", "); - panic!( - "Entrypoint not found! - Entrypoint was {main} - known keys are {symbols}" - ) - }); - let io_handler = orchidlang::stl::handleIO; - let ret = interpreter::run_handler(entrypoint.clone(), io_handler, ctx); - let interpreter::Return { gas, state, inert } = - ret.unwrap_or_else(|e| panic!("Runtime error: {}", e)); - if inert { - println!("Settled at {}", state.expr().clause.bundle(&i)); - if let Some(g) = gas { - println!("Remaining gas: {g}") - } - } else if gas == Some(0) { - eprintln!("Ran out of gas!"); - process::exit(-1); - } -} diff --git a/src/bin/orcx.rs b/src/bin/orcx.rs new file mode 100644 index 0000000..2fa1289 --- /dev/null +++ b/src/bin/orcx.rs @@ -0,0 +1,172 @@ +mod cli; + +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; +use std::process; + +use clap::Parser; +use itertools::Itertools; +use orchidlang::facade::{Environment, PreMacro}; +use orchidlang::interner::InternedDisplay; +use orchidlang::systems::stl::StlConfig; +use orchidlang::systems::{io_system, AsynchConfig, IOStream}; +use orchidlang::{ast, interpreted, interpreter, Interner, Sym, VName}; + +use crate::cli::cmd_prompt; + +/// Orchid interpreter +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +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, + /// 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 dump_repo: bool, + /// Step through the macro execution process in the specified symbol + #[arg(long, default_value = "")] + pub macro_debug: String, +} +impl Args { + /// Validate the project directory and the + pub fn chk_dir_main(&self) -> Result<(), String> { + let dir_path = PathBuf::from(&self.dir); + if !dir_path.is_dir() { + return Err(format!("{} is not a directory", dir_path.display())); + } + let segs = self.main.split("::").collect::>(); + if segs.len() < 2 { + return Err("Entry point too short".to_string()); + } + let (pathsegs, _) = segs.split_at(segs.len() - 1); + 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() + )); + } + Ok(()) + } + + pub fn chk_proj(&self) -> Result<(), String> { + self.chk_dir_main() + } +} + +pub fn to_vname(data: &str, i: &Interner) -> VName { + data.split("::").map(|s| i.i(s)).collect::>() +} + +fn print_for_debug(e: &ast::Expr, i: &Interner) { + print!( + "code: {}\nglossary: {}", + e.bundle(i), + (e.value.collect_names().into_iter()) + .map(|t| i.extern_vec(t).join("::")) + .join(", ") + ) +} + +/// A little utility to step through the resolution of a macro set +pub fn macro_debug(premacro: PreMacro, sym: Sym, i: &Interner) { + let (mut code, location) = (premacro.consts.get(&sym)) + .unwrap_or_else(|| { + panic!( + "Symbol {} not found\nvalid symbols: \n\t{}\n", + i.extern_vec(sym).join("::"), + (premacro.consts.keys()) + .map(|t| i.extern_vec(*t).join("::")) + .join("\n\t") + ) + }) + .clone(); + println!( + "Debugging macros in {} defined at {}. + Initial state: ", + i.extern_vec(sym).join("::"), + location + ); + print_for_debug(&code, i); + 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, i); + } else { + print!("Halted") + }, + "p" | "print" => print_for_debug(&code, i), + "d" | "dump" => print!("Rules: {}", premacro.repo.bundle(i)), + "q" | "quit" => return, + "h" | "help" => print!( + "Available commands: + \t, n, next\t\ttake a step + \tp, print\t\tprint the current state + \tq, quit\t\texit + \th, help\t\tprint this text" + ), + _ => { + print!("unrecognized command \"{}\", try \"help\"", cmd); + continue; + }, + } + } +} + +pub fn main() { + 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 = AsynchConfig::new(); + let io = io_system(&mut asynch, None, None, [ + ("stdin", IOStream::Source(BufReader::new(Box::new(std::io::stdin())))), + ("stdout", IOStream::Sink(Box::new(std::io::stdout()))), + ("stderr", IOStream::Sink(Box::new(std::io::stderr()))), + ]); + let env = Environment::new(&i) + .add_system(StlConfig { impure: true }) + .add_system(asynch) + .add_system(io); + let premacro = i.unwrap(env.load_dir(&dir, &main)); + if args.dump_repo { + println!("Parsed rules: {}", premacro.repo.bundle(&i)); + return; + } + if !args.macro_debug.is_empty() { + let sym = i.i(&to_vname(&args.macro_debug, &i)); + return macro_debug(premacro, sym, &i); + } + let mut proc = i.unwrap(premacro.build_process(Some(args.macro_limit))); + let main = interpreted::Clause::Constant(i.i(&main)).wrap(); + let ret = i.unwrap(proc.run(main, None)); + let interpreter::Return { gas, state, inert } = ret; + drop(proc); + if inert { + println!("Settled at {}", state.expr().clause.bundle(&i)); + if let Some(g) = gas { + println!("Remaining gas: {g}") + } + } else if gas == Some(0) { + eprintln!("Ran out of gas!"); + process::exit(-1); + } +} diff --git a/src/pipeline/error/import_all.rs b/src/error/import_all.rs similarity index 54% rename from src/pipeline/error/import_all.rs rename to src/error/import_all.rs index 6961a0b..3c26fbb 100644 --- a/src/pipeline/error/import_all.rs +++ b/src/error/import_all.rs @@ -4,27 +4,31 @@ use super::{ErrorPosition, ProjectError}; use crate::representations::location::Location; use crate::utils::iter::box_once; use crate::utils::BoxedIter; +use crate::{Interner, 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: Vec, + pub offender_file: Rc>, /// The module containing the offending import - pub offender_mod: Vec, + pub offender_mod: Rc, } impl ProjectError for ImportAll { fn description(&self) -> &str { "a top-level glob import was used" } - fn message(&self) -> String { - format!("{} imports *", self.offender_mod.join("::")) + fn message(&self, i: &Interner) -> String { + format!("{} imports *", i.extern_all(&self.offender_mod).join("::")) } - fn positions(&self) -> BoxedIter { + fn positions(&self, i: &Interner) -> BoxedIter { box_once(ErrorPosition { - location: Location::File(Rc::new(self.offender_file.clone())), - message: Some(format!("{} imports *", self.offender_mod.join("::"))), + location: Location::File(self.offender_file.clone()), + message: Some(format!( + "{} imports *", + i.extern_all(&self.offender_mod).join("::") + )), }) } } diff --git a/src/pipeline/error/mod.rs b/src/error/mod.rs similarity index 67% rename from src/pipeline/error/mod.rs rename to src/error/mod.rs index 42e40f1..01e502a 100644 --- a/src/pipeline/error/mod.rs +++ b/src/error/mod.rs @@ -1,18 +1,20 @@ //! Various errors the pipeline can produce mod import_all; +mod no_targets; mod not_exported; mod not_found; -mod parse_error_with_path; +mod parse_error_with_tokens; mod project_error; mod too_many_supers; mod unexpected_directory; mod visibility_mismatch; pub use import_all::ImportAll; +pub use no_targets::NoTargets; pub use not_exported::NotExported; pub use not_found::NotFound; -pub use parse_error_with_path::ParseErrorWithPath; -pub use project_error::{ErrorPosition, ProjectError}; +pub use parse_error_with_tokens::ParseErrorWithTokens; +pub use project_error::{ErrorPosition, ProjectError, ProjectResult}; 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 new file mode 100644 index 0000000..836deff --- /dev/null +++ b/src/error/no_targets.rs @@ -0,0 +1,23 @@ +use super::{ErrorPosition, ProjectError}; +#[allow(unused)] // for doc +use crate::parse_layer; +use crate::utils::iter::box_empty; +use crate::utils::BoxedIter; +use crate::Interner; + +/// 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, _i: &Interner) -> BoxedIter { + box_empty() + } +} diff --git a/src/pipeline/error/not_exported.rs b/src/error/not_exported.rs similarity index 59% rename from src/pipeline/error/not_exported.rs rename to src/error/not_exported.rs index 69eaeab..6d2587a 100644 --- a/src/pipeline/error/not_exported.rs +++ b/src/error/not_exported.rs @@ -3,35 +3,39 @@ use std::rc::Rc; 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: Vec, + pub file: VName, /// The path leading to the unexported module - pub subpath: Vec, + pub subpath: VName, /// The offending file - pub referrer_file: Vec, + pub referrer_file: VName, /// The module containing the offending import - pub referrer_subpath: Vec, + 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 { + fn positions(&self, i: &Interner) -> BoxedIter { Box::new( [ ErrorPosition { - location: Location::File(Rc::new(self.file.clone())), - message: Some(format!("{} isn't exported", self.subpath.join("::"))), + location: Location::File(Rc::new(i.extern_all(&self.file))), + message: Some(format!( + "{} isn't exported", + i.extern_all(&self.subpath).join("::") + )), }, ErrorPosition { - location: Location::File(Rc::new(self.referrer_file.clone())), + location: Location::File(Rc::new(i.extern_all(&self.referrer_file))), message: Some(format!( "{} cannot see this symbol", - self.referrer_subpath.join("::") + i.extern_all(&self.referrer_subpath).join("::") )), }, ] diff --git a/src/pipeline/error/not_found.rs b/src/error/not_found.rs similarity index 65% rename from src/pipeline/error/not_found.rs rename to src/error/not_found.rs index e69af07..5611bba 100644 --- a/src/pipeline/error/not_found.rs +++ b/src/error/not_found.rs @@ -5,15 +5,17 @@ use crate::tree::Module; use crate::tree::WalkError; use crate::utils::iter::box_once; use crate::utils::BoxedIter; -use crate::{Interner, NameLike, Tok}; +use crate::{Interner, NameLike, Tok, VName}; /// Error produced when an import refers to a nonexistent module #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct NotFound { - /// The file containing the invalid import - pub file: Vec, + /// The module that imported the invalid path + pub source: Option, + /// The file not containing the expected path + pub file: VName, /// The invalid import path - pub subpath: Vec, + pub subpath: VName, } impl NotFound { /// Produce this error from the parameters of [Module]`::walk_ref` and a @@ -27,23 +29,27 @@ impl NotFound { /// Basically, if `e` was not produced by the `walk*` methods called on /// `path`. pub fn from_walk_error( + source: &[Tok], prefix: &[Tok], path: &[Tok], orig: &ProjectModule, e: WalkError, - i: &Interner, ) -> Self { let last_mod = orig.walk_ref(&path[..e.pos], false).expect("error occured on next step"); - let mut whole_path = - prefix.iter().chain(path.iter()).map(|t| i.r(*t)).cloned(); + let mut whole_path = prefix.iter().chain(path.iter()).copied(); if let Some(file) = &last_mod.extra.file { Self { + source: Some(source.to_vec()), file: whole_path.by_ref().take(file.len()).collect(), subpath: whole_path.collect(), } } else { - Self { file: whole_path.collect(), subpath: Vec::new() } + Self { + source: Some(source.to_vec()), + file: whole_path.collect(), + subpath: Vec::new(), + } } } } @@ -51,14 +57,14 @@ impl ProjectError for NotFound { fn description(&self) -> &str { "an import refers to a nonexistent module" } - fn message(&self) -> String { + fn message(&self, i: &Interner) -> String { format!( "module {} in {} was not found", - self.subpath.join("::"), - self.file.join("/"), + i.extern_all(&self.subpath).join("::"), + i.extern_all(&self.file).join("/"), ) } - fn positions(&self) -> BoxedIter { - box_once(ErrorPosition::just_file(self.file.clone())) + fn positions(&self, i: &Interner) -> BoxedIter { + box_once(ErrorPosition::just_file(i.extern_all(&self.file))) } } diff --git a/src/error/parse_error_with_tokens.rs b/src/error/parse_error_with_tokens.rs new file mode 100644 index 0000000..044bca6 --- /dev/null +++ b/src/error/parse_error_with_tokens.rs @@ -0,0 +1,34 @@ +use std::rc::Rc; + +use itertools::Itertools; + +use super::{ErrorPosition, ProjectError}; +use crate::interner::InternedDisplay; +use crate::parse::Entry; +use crate::utils::BoxedIter; +use crate::Interner; + +/// Produced by stages that parse text when it fails. +pub struct ParseErrorWithTokens { + /// The complete source of the faulty file + pub full_source: String, + /// Tokens, if the error did not occur during tokenization + pub tokens: Vec, + /// The parse error produced by Chumsky + pub error: Rc, +} +impl ProjectError for ParseErrorWithTokens { + fn description(&self) -> &str { + self.error.description() + } + fn message(&self, i: &Interner) -> String { + format!( + "Failed to parse code: {}\nTokenized source for context:\n{}", + self.error.message(i), + self.tokens.iter().map(|t| t.to_string_i(i)).join(" "), + ) + } + fn positions(&self, i: &Interner) -> BoxedIter { + self.error.positions(i) + } +} diff --git a/src/pipeline/error/project_error.rs b/src/error/project_error.rs similarity index 64% rename from src/pipeline/error/project_error.rs rename to src/error/project_error.rs index a80b60b..b2805f2 100644 --- a/src/pipeline/error/project_error.rs +++ b/src/error/project_error.rs @@ -1,8 +1,9 @@ -use std::fmt::{Debug, Display}; use std::rc::Rc; +use crate::interner::InternedDisplay; use crate::representations::location::Location; use crate::utils::BoxedIter; +use crate::Interner; /// A point of interest in resolving the error, such as the point where /// processing got stuck, a command that is likely to be incorrect @@ -22,15 +23,15 @@ impl ErrorPosition { /// Errors addressed to the developer which are to be resolved with /// code changes -pub trait ProjectError: Debug { +pub trait ProjectError { /// A general description of this type of error fn description(&self) -> &str; /// A formatted message that includes specific parameters - fn message(&self) -> String { - String::new() + fn message(&self, _i: &Interner) -> String { + self.description().to_string() } /// Code positions relevant to this error - fn positions(&self) -> BoxedIter; + fn positions(&self, i: &Interner) -> BoxedIter; /// Convert the error into an `Rc` to be able to /// handle various errors together fn rc(self) -> Rc @@ -41,14 +42,18 @@ pub trait ProjectError: Debug { } } -impl Display for dyn ProjectError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl InternedDisplay for dyn ProjectError { + fn fmt_i( + &self, + f: &mut std::fmt::Formatter<'_>, + i: &Interner, + ) -> std::fmt::Result { let description = self.description(); - let message = self.message(); - let positions = self.positions(); - write!(f, "Problem with the project: {description}; {message}")?; + let message = self.message(i); + let positions = self.positions(i); + writeln!(f, "Project error: {description}\n{message}")?; for ErrorPosition { location, message } in positions { - write!( + writeln!( f, "@{location}: {}", message.unwrap_or("location of interest".to_string()) @@ -57,3 +62,7 @@ impl Display for dyn ProjectError { Ok(()) } } + +/// 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/pipeline/error/too_many_supers.rs b/src/error/too_many_supers.rs similarity index 64% rename from src/pipeline/error/too_many_supers.rs rename to src/error/too_many_supers.rs index fa69006..edb74a3 100644 --- a/src/pipeline/error/too_many_supers.rs +++ b/src/error/too_many_supers.rs @@ -4,38 +4,39 @@ use super::{ErrorPosition, ProjectError}; use crate::representations::location::Location; use crate::utils::iter::box_once; use crate::utils::BoxedIter; +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: Vec, + pub path: VName, /// The file containing the offending import - pub offender_file: Vec, + pub offender_file: VName, /// The module containing the offending import - pub offender_mod: Vec, + pub offender_mod: VName, } 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 { + fn message(&self, i: &Interner) -> String { format!( "path {} in {} contains too many `super` steps.", - self.path.join("::"), - self.offender_mod.join("::") + i.extern_all(&self.path).join("::"), + i.extern_all(&self.offender_mod).join("::") ) } - fn positions(&self) -> BoxedIter { + fn positions(&self, i: &Interner) -> BoxedIter { box_once(ErrorPosition { - location: Location::File(Rc::new(self.offender_file.clone())), + location: Location::File(Rc::new(i.extern_all(&self.offender_file))), message: Some(format!( "path {} in {} contains too many `super` steps.", - self.path.join("::"), - self.offender_mod.join("::") + i.extern_all(&self.path).join("::"), + i.extern_all(&self.offender_mod).join("::") )), }) } diff --git a/src/pipeline/error/unexpected_directory.rs b/src/error/unexpected_directory.rs similarity index 68% rename from src/pipeline/error/unexpected_directory.rs rename to src/error/unexpected_directory.rs index 0430b47..fe7d569 100644 --- a/src/pipeline/error/unexpected_directory.rs +++ b/src/error/unexpected_directory.rs @@ -1,26 +1,27 @@ use super::{ErrorPosition, ProjectError}; use crate::utils::iter::box_once; use crate::utils::BoxedIter; +use crate::{Interner, 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: Vec, + 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 positions(&self) -> BoxedIter { - box_once(ErrorPosition::just_file(self.path.clone())) + fn positions(&self, i: &Interner) -> BoxedIter { + box_once(ErrorPosition::just_file(i.extern_all(&self.path))) } - fn message(&self) -> String { + fn message(&self, i: &Interner) -> String { format!( "{} was expected to be a file but a directory was found", - self.path.join("/") + i.extern_all(&self.path).join("/") ) } } diff --git a/src/pipeline/error/visibility_mismatch.rs b/src/error/visibility_mismatch.rs similarity index 82% rename from src/pipeline/error/visibility_mismatch.rs rename to src/error/visibility_mismatch.rs index de6a349..32e0775 100644 --- a/src/pipeline/error/visibility_mismatch.rs +++ b/src/error/visibility_mismatch.rs @@ -4,12 +4,13 @@ use super::project_error::{ErrorPosition, ProjectError}; use crate::representations::location::Location; use crate::utils::iter::box_once; use crate::utils::BoxedIter; +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: Vec, + pub namespace: VName, /// The file containing the namespace pub file: Rc>, } @@ -17,12 +18,12 @@ impl ProjectError for VisibilityMismatch { fn description(&self) -> &str { "Some occurences of a namespace are exported but others are not" } - fn positions(&self) -> BoxedIter { + fn positions(&self, i: &Interner) -> BoxedIter { box_once(ErrorPosition { location: Location::File(self.file.clone()), message: Some(format!( "{} is opened multiple times with different visibilities", - self.namespace.join("::") + i.extern_all(&self.namespace).join("::") )), }) } diff --git a/src/facade/environment.rs b/src/facade/environment.rs new file mode 100644 index 0000000..72402b5 --- /dev/null +++ b/src/facade/environment.rs @@ -0,0 +1,91 @@ +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::{ + 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 + pub fn new(i: &'a Interner) -> Self { + Self { i, systems: Vec::new() } + } + + /// Register a new system in the environment + 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(tree.0.overlay(system_tree.0)); + } + let mut prelude = vec![]; + for sys in systems.iter() { + if !sys.code.is_empty() { + tree = parse_layer( + sys.code.keys().map(|sym| &sym[..]), + &|k| sys.load_file(k), + &tree, + &prelude, + 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(), i); + let vname_tree = parse_layer( + iter::once(target), + &|path| file_cache.find(path), + &tree, + &prelude, + 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/mod.rs b/src/facade/mod.rs new file mode 100644 index 0000000..5044b89 --- /dev/null +++ b/src/facade/mod.rs @@ -0,0 +1,12 @@ +//! 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}; diff --git a/src/facade/pre_macro.rs b/src/facade/pre_macro.rs new file mode 100644 index 0000000..f7901e4 --- /dev/null +++ b/src/facade/pre_macro.rs @@ -0,0 +1,134 @@ +use std::iter; +use std::rc::Rc; + +use hashbrown::HashMap; + +use super::{Process, System}; +use crate::error::{ErrorPosition, ProjectError, ProjectResult}; +use crate::interpreter::HandlerTable; +use crate::rule::Repo; +use crate::utils::iter::box_once; +use crate::utils::BoxedIter; +use crate::{ + ast, ast_to_interpreted, collect_consts, collect_rules, rule, Interner, + Location, ProjectTree, Sym, +}; + +/// 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)>, + /// 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, + 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)| { + let location = (i.r(name).split_last()) + .and_then(|(_, path)| { + let origin = (tree.0.walk_ref(path, false)) + .expect("path sourced from symbol names"); + origin.extra.file.as_ref().map(|path| i.extern_all(&path[..])) + }) + .map(|p| Location::File(Rc::new(p))) + .unwrap_or(Location::Unknown); + (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( + 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, + limit, + } + .rc(), + ); + } else { + unmatched + } + } else { + repo.pass(source).unwrap_or_else(|| source.clone()) + }; + let runtree = ast_to_interpreted(&unmatched).map_err(|e| e.rc())?; + symbols.insert(*name, runtree); + } + Ok(Process { + symbols, + i, + handlers: (systems.into_iter()) + .fold(HandlerTable::new(), |tbl, sys| tbl.combine(sys.handlers)), + }) + } + + /// 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(); + 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: Location, + symbol: Sym, + limit: usize, +} +impl ProjectError for MacroTimeout { + fn description(&self) -> &str { + "Macro execution has not halted" + } + + fn message(&self, i: &Interner) -> String { + format!( + "Macro execution during the processing of {} took more than {} steps", + i.extern_vec(self.symbol).join("::"), + self.limit + ) + } + + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.location.clone(), message: None }) + } +} diff --git a/src/facade/process.rs b/src/facade/process.rs new file mode 100644 index 0000000..857f5e4 --- /dev/null +++ b/src/facade/process.rs @@ -0,0 +1,90 @@ +use hashbrown::HashMap; + +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::utils::iter::box_once; +use crate::utils::BoxedIter; +use crate::{Interner, Location, Sym}; + +/// 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) handlers: HandlerTable<'a>, + pub(crate) i: &'a Interner, +} +impl<'a> Process<'a> { + /// 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. + /// + /// This is useful to catch infinite loops or ensure that a tenant program + /// yields + pub fn run( + &mut self, + prompt: ExprInst, + gas: Option, + ) -> Result { + let ctx = Context { gas, interner: self.i, 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. + pub fn unbound_refs(&self, key: Sym) -> Vec<(Sym, Location)> { + 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 { + if !self.symbols.contains_key(&sym) { + errors.push((sym, expr.location.clone())) + } + } + None::<()> + }); + errors + } + + /// Assert that, unless [interpreted::Clause::Constant]s are created + /// procedurally, a [interpreter::RuntimeError::MissingSymbol] cannot be + /// produced + pub fn validate_refs(&self) -> ProjectResult<()> { + for key in self.symbols.keys() { + if let Some((symbol, location)) = self.unbound_refs(*key).pop() { + return Err(MissingSymbol { location, referrer: *key, symbol }.rc()); + } + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct MissingSymbol { + referrer: Sym, + location: Location, + symbol: Sym, +} +impl ProjectError for MissingSymbol { + fn description(&self) -> &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." + } + + fn message(&self, i: &Interner) -> String { + format!( + "The symbol {} referenced in {} does not exist", + i.extern_vec(self.symbol).join("::"), + i.extern_vec(self.referrer).join("::") + ) + } + + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.location.clone(), message: None }) + } +} diff --git a/src/facade/system.rs b/src/facade/system.rs new file mode 100644 index 0000000..c2263c8 --- /dev/null +++ b/src/facade/system.rs @@ -0,0 +1,72 @@ +use hashbrown::HashMap; + +use crate::error::{ErrorPosition, ProjectError}; +use crate::interpreter::HandlerTable; +use crate::pipeline::file_loader::{IOResult, Loaded}; +use crate::sourcefile::FileEntry; +use crate::utils::iter::box_empty; +use crate::utils::BoxedIter; +use crate::{ConstTree, Interner, Tok, VName}; + +/// 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, + /// External functions and other constant values defined in AST form + pub constants: HashMap, ConstTree>, + /// Orchid libraries defined by this system + pub code: HashMap>, Loaded>, + /// 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, + /// Handlers for actions defined in this system + pub handlers: HandlerTable<'a>, +} +impl<'a> System<'a> { + /// Intern the name of the system so that it can be used as an Orchid + /// namespace + pub fn vname(&self, i: &Interner) -> Vec> { + self.name.iter().map(|s| i.i(s)).collect::>() + } + + /// Load a file from the system + pub fn load_file(&self, path: &[Tok]) -> IOResult { + (self.code.get(path)).cloned().ok_or_else(|| { + let err = + MissingSystemCode { path: path.to_vec(), system: self.name.clone() }; + err.rc() + }) + } +} + +/// An error raised when a system fails to load a path. This usually means that +/// another system the current one depends on did not get loaded +#[derive(Debug)] +pub struct MissingSystemCode { + path: VName, + system: Vec, +} +impl ProjectError for MissingSystemCode { + fn description(&self) -> &str { + "A system tried to import a path that doesn't exist" + } + fn message(&self, i: &Interner) -> String { + format!( + "Path {} is not defined by {} or any system before it", + i.extern_all(&self.path).join("::"), + self.system.join("::") + ) + } + fn positions(&self, _i: &Interner) -> BoxedIter { + box_empty() + } +} + +/// Trait for objects that can be converted into a [System] in the presence +/// of an [Interner]. +pub trait IntoSystem<'a>: 'a { + /// Convert this object into a system using an interner + fn into_system(self, i: &Interner) -> System<'a>; +} diff --git a/src/foreign.rs b/src/foreign/atom.rs similarity index 59% rename from src/foreign.rs rename to src/foreign/atom.rs index 80529d0..559f54d 100644 --- a/src/foreign.rs +++ b/src/foreign/atom.rs @@ -1,19 +1,12 @@ -//! Interaction with foreign code -//! -//! Structures and traits used in the exposure of external functions and values -//! to Orchid code use std::any::Any; -use std::error::Error; -use std::fmt::{Debug, Display}; -use std::hash::Hash; -use std::rc::Rc; +use std::fmt::Debug; use dyn_clone::DynClone; +use crate::interpreted::ExprInst; use crate::interpreter::{Context, RuntimeError}; -pub use crate::representations::interpreted::Clause; -use crate::representations::interpreted::ExprInst; -use crate::representations::Primitive; +use crate::representations::interpreted::Clause; +use crate::Primitive; /// Information returned by [Atomic::run]. This mirrors /// [crate::interpreter::Return] but with a clause instead of an Expr. @@ -29,73 +22,12 @@ pub struct AtomicReturn { impl AtomicReturn { /// Wrap an inert atomic for delivery to the supervisor pub fn from_data(d: D, c: Context) -> Self { - AtomicReturn { clause: d.to_atom_cls(), gas: c.gas, inert: false } + AtomicReturn { clause: d.atom_cls(), gas: c.gas, inert: false } } } -/// A type-erased error in external code -pub type RcError = Rc; /// Returned by [Atomic::run] pub type AtomicResult = Result; -/// Returned by [ExternFn::apply] -pub type XfnResult = Result; - -/// Errors produced by external code -pub trait ExternError: Display { - /// Convert into trait object - fn into_extern(self) -> Rc - where - Self: 'static + Sized, - { - Rc::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 { - /// Display name of the function - fn name(&self) -> &str; - /// Combine the function with an argument to produce a new clause - fn apply(&self, 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]. - fn to_xfn_cls(self) -> Clause - where - Self: Sized + 'static, - { - Clause::P(Primitive::ExternFn(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()) - } -} /// Functionality the interpreter needs to handle a value pub trait Atomic: Any + Debug + DynClone @@ -106,17 +38,27 @@ where /// during introspection by other external code. There is no other way to /// interact with values of unknown types at the moment. fn as_any(&self) -> &dyn Any; + /// 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, ctx: Context) -> AtomicResult; + /// Wrap the atom in a clause to be placed in an [AtomicResult]. - fn to_atom_cls(self) -> Clause + fn atom_cls(self) -> Clause where Self: Sized, { Clause::P(Primitive::Atom(Atom(Box::new(self)))) } + + /// Wrap the atom in a new expression instance to be placed in a tree + fn atom_exi(self) -> ExprInst + where + Self: Sized, + { + self.atom_cls().wrap() + } } /// Represents a black box unit of code with its own normalization steps. diff --git a/src/foreign/cps_box.rs b/src/foreign/cps_box.rs new file mode 100644 index 0000000..de2a358 --- /dev/null +++ b/src/foreign/cps_box.rs @@ -0,0 +1,122 @@ +//! Automated wrappers to make working with CPS commands easier. + +use std::fmt::Debug; +use std::iter; + +use trait_set::trait_set; + +use super::{Atomic, AtomicResult, AtomicReturn, ExternFn, XfnResult}; +use crate::interpreted::{Clause, ExprInst}; +use crate::interpreter::{Context, HandlerRes}; +use crate::{atomic_defaults, ConstTree}; + +trait_set! { + /// A "well behaved" type that can be used as payload in a CPS box + pub trait CPSPayload = Clone + Debug + 'static; + /// A function to handle a CPS box with a specific payload + pub trait CPSHandler = FnMut(&T, &ExprInst) -> HandlerRes; +} + +/// The pre-argument version of CPSBox +#[derive(Debug, Clone)] +struct CPSFn { + pub argc: usize, + pub continuations: Vec, + pub payload: T, +} +impl CPSFn { + 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, arg: ExprInst, _ctx: Context) -> XfnResult { + let payload = self.payload.clone(); + let continuations = (self.continuations.iter()) + .cloned() + .chain(iter::once(arg)) + .collect::>(); + 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 +#[derive(Debug, Clone)] +pub struct CPSBox { + /// Details about the command + pub payload: T, + /// Possible continuations, in the order they were provided + pub continuations: Vec, +} +impl CPSBox { + /// Assert that the command was instantiated with the correct number of + /// possible continuations. This is decided by the native bindings, not user + /// code, therefore this error may be uncovered by usercode but can never be + /// produced at will. + pub fn assert_count(&self, expect: usize) { + let real = self.continuations.len(); + debug_assert!( + real == expect, + "Tried to read {expect} argument(s) but {real} were provided for {:?}", + self.payload + ) + } + /// Unpack the wrapped command and the continuation + pub fn unpack1(&self) -> (&T, &ExprInst) { + self.assert_count(1); + (&self.payload, &self.continuations[0]) + } + /// Unpack the wrapped command and 2 continuations (usually an async and a + /// sync) + pub fn unpack2(&self) -> (&T, &ExprInst, &ExprInst) { + self.assert_count(2); + (&self.payload, &self.continuations[0], &self.continuations[1]) + } + /// Unpack the wrapped command and 3 continuations (usually an async success, + /// an async fail and a sync) + pub fn unpack3(&self) -> (&T, &ExprInst, &ExprInst, &ExprInst) { + self.assert_count(3); + ( + &self.payload, + &self.continuations[0], + &self.continuations[1], + &self.continuations[2], + ) + } +} + +impl Atomic for CPSBox { + atomic_defaults!(); + fn run(&self, ctx: Context) -> AtomicResult { + Ok(AtomicReturn { + clause: self.clone().atom_cls(), + gas: ctx.gas, + inert: true, + }) + } +} + +/// Like [init_cps] but wrapped in a [ConstTree] for init-time usage +pub fn mk_const(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] +pub fn init_cps(argc: usize, payload: T) -> Clause { + CPSFn::new(argc, payload).xfn_cls() +} diff --git a/src/foreign/extern_fn.rs b/src/foreign/extern_fn.rs new file mode 100644 index 0000000..9e7465e --- /dev/null +++ b/src/foreign/extern_fn.rs @@ -0,0 +1,71 @@ +use std::error::Error; +use std::fmt::{Debug, Display}; +use std::hash::Hash; +use std::rc::Rc; + +use dyn_clone::DynClone; + +use crate::interpreted::ExprInst; +use crate::interpreter::Context; +use crate::representations::interpreted::Clause; +use crate::Primitive; + +/// Returned by [ExternFn::apply] +pub type XfnResult = Result>; + +/// Errors produced by external code +pub trait ExternError: Display { + /// Convert into trait object + fn into_extern(self) -> Rc + where + Self: 'static + Sized, + { + Rc::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 { + /// Display name of the function + fn name(&self) -> &str; + /// Combine the function with an argument to produce a new clause + fn apply(&self, 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]. + fn xfn_cls(self) -> Clause + where + Self: Sized + 'static, + { + Clause::P(Primitive::ExternFn(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()) + } +} diff --git a/src/foreign/mod.rs b/src/foreign/mod.rs new file mode 100644 index 0000000..771a45e --- /dev/null +++ b/src/foreign/mod.rs @@ -0,0 +1,17 @@ +//! Interaction with foreign code +//! +//! Structures and traits used in the exposure of external functions and values +//! to Orchid code +mod atom; +pub mod cps_box; +mod extern_fn; + +use std::rc::Rc; + +pub use atom::{Atom, Atomic, AtomicResult, AtomicReturn}; +pub use extern_fn::{ExternError, ExternFn, XfnResult}; + +pub use crate::representations::interpreted::Clause; + +/// A type-erased error in external code +pub type RcError = Rc; diff --git a/src/foreign_macros/atomic_impl.rs b/src/foreign_macros/atomic_impl.rs index b417054..e9d6ae5 100644 --- a/src/foreign_macros/atomic_impl.rs +++ b/src/foreign_macros/atomic_impl.rs @@ -74,7 +74,7 @@ macro_rules! atomic_impl { ($typ:ident) => { $crate::atomic_impl! {$typ, |this: &Self, _: $crate::interpreter::Context| { use $crate::foreign::ExternFn; - Ok(this.clone().to_xfn_cls()) + Ok(this.clone().xfn_cls()) }} }; ($typ:ident, $next_phase:expr) => { @@ -108,7 +108,7 @@ macro_rules! atomic_impl { Err(e) => return Err($crate::interpreter::RuntimeError::Extern(e)), } } else { - next_self.to_atom_cls() + next_self.atom_cls() }; // package and return Ok($crate::foreign::AtomicReturn { clause, gas, inert: false }) diff --git a/src/foreign_macros/atomic_inert.rs b/src/foreign_macros/atomic_inert.rs index 913e55d..c836a28 100644 --- a/src/foreign_macros/atomic_inert.rs +++ b/src/foreign_macros/atomic_inert.rs @@ -14,7 +14,7 @@ use crate::foreign::Atomic; /// on [Any], [Debug] and [DynClone]. #[macro_export] macro_rules! atomic_inert { - ($typ:ident) => { + ($typ:ident, $typename:expr) => { impl $crate::foreign::Atomic for $typ { $crate::atomic_defaults! {} @@ -23,11 +23,25 @@ macro_rules! atomic_inert { ctx: $crate::interpreter::Context, ) -> $crate::foreign::AtomicResult { Ok($crate::foreign::AtomicReturn { - clause: self.clone().to_atom_cls(), + clause: self.clone().atom_cls(), gas: ctx.gas, inert: true, }) } } + + impl TryFrom<&ExprInst> for $typ { + type Error = std::rc::Rc; + + fn try_from( + value: &$crate::interpreted::ExprInst, + ) -> Result { + $crate::systems::cast_exprinst::with_atom( + value, + $typename, + |a: &$typ| Ok(a.clone()), + ) + } + } }; } diff --git a/src/foreign_macros/define_fn.rs b/src/foreign_macros/define_fn.rs index 3ebdf4b..6fa1276 100644 --- a/src/foreign_macros/define_fn.rs +++ b/src/foreign_macros/define_fn.rs @@ -74,25 +74,27 @@ use crate::write_fn_step; #[macro_export] macro_rules! define_fn { // Unary function entry - ($( #[ $attr:meta ] )* $qual:vis $name:ident = $body:expr) => {paste::paste!{ - $crate::write_fn_step!( - $( #[ $attr ] )* $qual $name - > - [< Internal $name >] - ); - $crate::write_fn_step!( - [< Internal $name >] - {} - out = expr => Ok(expr); - { - let lambda = $body; - lambda(out) - } - ); - }}; + ($( #[ $attr:meta ] )* $qual:vis $name:ident = |$x:ident| $body:expr) => { + paste::paste!{ + $crate::write_fn_step!( + $( #[ $attr ] )* $qual $name + > + [< Internal $name >] + ); + $crate::write_fn_step!( + [< Internal $name >] + {} + out = expr => Ok(expr); + { + let lambda = |$x: &$crate::interpreted::ExprInst| $body; + lambda(out) + } + ); + } + }; // xname is optional only if every conversion is implicit ($( #[ $attr:meta ] )* $qual:vis $name:ident { - $( $arg:ident: $typ:ty ),+ + $( $arg:ident: $typ:ty ),+ $(,)? } => $body:expr) => { $crate::define_fn!{expr=expr in $( #[ $attr ] )* $qual $name { @@ -105,7 +107,7 @@ macro_rules! define_fn { $( #[ $attr:meta ] )* $qual:vis $name:ident { $arg0:ident: $typ0:ty $( as $parse0:expr )? - $(, $arg:ident: $typ:ty $( as $parse:expr )? )* + $(, $arg:ident: $typ:ty $( as $parse:expr )? )* $(,)? } => $body:expr ) => {paste::paste!{ // Generate initial state diff --git a/src/foreign_macros/write_fn_step.rs b/src/foreign_macros/write_fn_step.rs index f3a91b4..a2f4fea 100644 --- a/src/foreign_macros/write_fn_step.rs +++ b/src/foreign_macros/write_fn_step.rs @@ -21,7 +21,7 @@ use crate::interpreted::ExprInst; /// ```no_run /// // FIXME this is a very old example that wouldn't compile now /// use unicode_segmentation::UnicodeSegmentation; -/// +/// /// use orchidlang::{write_fn_step, Literal, Primitive}; /// use orchidlang::interpreted::Clause; /// use orchidlang::stl::litconv::{with_str, with_uint}; diff --git a/src/interner/multitype.rs b/src/interner/multitype.rs index a65fcbd..1885b9f 100644 --- a/src/interner/multitype.rs +++ b/src/interner/multitype.rs @@ -8,6 +8,7 @@ use hashbrown::HashMap; use super::monotype::TypedInterner; use super::token::Tok; +use super::InternedDisplay; /// 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 @@ -59,6 +60,29 @@ impl Interner { ) -> Vec { s.iter().map(|t| self.r(*t)).cloned().collect() } + + /// A variant of `unwrap` using [InternedDisplay] to circumvent `unwrap`'s + /// dependencyon [Debug]. For clarity, [expect] should be preferred. + pub fn unwrap(&self, result: Result) -> T { + result.unwrap_or_else(|e| { + println!("Unwrapped Error: {}", e.bundle(self)); + panic!("Unwrapped an error"); + }) + } + + /// A variant of `expect` using [InternedDisplay] to circumvent `expect`'s + /// depeendency on [Debug]. + pub fn expect( + &self, + result: Result, + msg: &str, + ) -> T { + result.unwrap_or_else(|e| { + println!("Expectation failed: {msg}"); + println!("Error: {}", e.bundle(self)); + panic!("Expected an error"); + }) + } } impl Default for Interner { diff --git a/src/interner/traits.rs b/src/interner/traits.rs index e365802..9c4d01a 100644 --- a/src/interner/traits.rs +++ b/src/interner/traits.rs @@ -1,5 +1,6 @@ -use core::fmt::Formatter; -use std::fmt::Display; +use core::fmt::{self, Display, Formatter}; +use core::ops::Deref; +use std::rc::Rc; use crate::interner::Interner; @@ -29,16 +30,13 @@ pub trait InternedDisplay { } } -impl InternedDisplay for T +// Special loophole for Rc +impl InternedDisplay for Rc where - T: Display, + T: InternedDisplay, { - fn fmt_i( - &self, - f: &mut std::fmt::Formatter<'_>, - _i: &Interner, - ) -> std::fmt::Result { - ::fmt(self, f) + fn fmt_i(&self, f: &mut Formatter<'_>, i: &Interner) -> fmt::Result { + self.deref().fmt_i(f, i) } } diff --git a/src/interpreter/apply.rs b/src/interpreter/apply.rs index 9739148..f5df076 100644 --- a/src/interpreter/apply.rs +++ b/src/interpreter/apply.rs @@ -15,7 +15,7 @@ fn map_at( mapper: &mut impl FnMut(&Clause) -> Result, ) -> Result { source - .try_update(|value| { + .try_update(|value, _loc| { // Pass right through lambdas if let Clause::Lambda { args, body } = value { return Ok(( @@ -87,7 +87,7 @@ pub fn apply( x: ExprInst, ctx: Context, ) -> Result { - let (state, (gas, inert)) = f.try_update(|clause| match clause { + let (state, (gas, inert)) = f.try_update(|clause, loc| match clause { // apply an ExternFn or an internal function Clause::P(Primitive::ExternFn(f)) => { let clause = @@ -104,17 +104,12 @@ pub fn apply( } else { (body.expr().clause.clone(), (ctx.gas, false)) }), - Clause::Constant(name) => { - let symval = if let Some(sym) = ctx.symbols.get(name) { - sym.clone() + Clause::Constant(name) => + if let Some(sym) = ctx.symbols.get(name) { + Ok((Clause::Apply { f: sym.clone(), x }, (ctx.gas, false))) } else { - panic!( - "missing symbol for function {}", - ctx.interner.extern_vec(*name).join("::") - ) - }; - Ok((Clause::Apply { f: symval, x }, (ctx.gas, false))) - }, + Err(RuntimeError::MissingSymbol(*name, loc.clone())) + }, Clause::P(Primitive::Atom(atom)) => { // take a step in expanding atom let AtomicReturn { clause, gas, inert } = atom.run(ctx.clone())?; diff --git a/src/interpreter/error.rs b/src/interpreter/error.rs index b8c4d34..3066297 100644 --- a/src/interpreter/error.rs +++ b/src/interpreter/error.rs @@ -1,8 +1,9 @@ -use std::fmt::Display; use std::rc::Rc; use crate::foreign::ExternError; +use crate::interner::InternedDisplay; use crate::representations::interpreted::ExprInst; +use crate::{Location, Sym}; /// Problems in the process of execution #[derive(Clone, Debug)] @@ -11,6 +12,8 @@ pub enum RuntimeError { Extern(Rc), /// Primitive applied as function NonFunctionApplication(ExprInst), + /// Symbol not in context + MissingSymbol(Sym, Location), } impl From> for RuntimeError { @@ -19,12 +22,23 @@ impl From> for RuntimeError { } } -impl Display for RuntimeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl InternedDisplay for RuntimeError { + fn fmt_i( + &self, + f: &mut std::fmt::Formatter<'_>, + i: &crate::Interner, + ) -> std::fmt::Result { match self { Self::Extern(e) => write!(f, "Error in external function: {e}"), - Self::NonFunctionApplication(loc) => { - write!(f, "Primitive applied as function at {loc:?}") + Self::NonFunctionApplication(expr) => { + write!(f, "Primitive applied as function at {}", expr.expr().location) + }, + Self::MissingSymbol(sym, loc) => { + write!( + f, + "{}, called at {loc} is not loaded", + i.extern_vec(*sym).join("::") + ) }, } } diff --git a/src/interpreter/handler.rs b/src/interpreter/handler.rs new file mode 100644 index 0000000..82db766 --- /dev/null +++ b/src/interpreter/handler.rs @@ -0,0 +1,84 @@ +use std::any::{Any, TypeId}; +use std::mem; +use std::rc::Rc; + +use hashbrown::HashMap; +use trait_set::trait_set; + +use super::{run, Context, Return, RuntimeError}; +use crate::foreign::ExternError; +use crate::interpreted::{Clause, ExprInst}; +use crate::utils::unwrap_or; +use crate::Primitive; + +trait_set! { + trait Handler = for<'b> FnMut(&'b dyn Any) -> HandlerRes; +} + +/// A table of command handlers +#[derive(Default)] +pub struct HandlerTable<'a> { + handlers: HashMap>, +} +impl<'a> HandlerTable<'a> { + /// Create a new [HandlerTable] + 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( + &mut self, + mut f: impl for<'b> FnMut(&'b T) -> HandlerRes + 'a, + ) { + let cb = move |a: &dyn Any| f(a.downcast_ref().expect("found by TypeId")); + let prev = self.handlers.insert(TypeId::of::(), Box::new(cb)); + assert!(prev.is_none(), "A handler for this type is already registered"); + } + + /// Find and execute the corresponding handler for this type + pub fn dispatch(&mut self, arg: &dyn Any) -> Option { + self.handlers.get_mut(&arg.type_id()).map(|f| f(arg)) + } + + /// Combine two non-overlapping handler sets + pub fn combine(mut self, other: Self) -> Self { + for (key, value) in other.handlers { + let prev = self.handlers.insert(key, value); + assert!(prev.is_none(), "Duplicate handlers") + } + self + } +} + +/// 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, + handlers: &mut HandlerTable, + mut ctx: Context, +) -> Result { + loop { + let ret = run(expr.clone(), ctx.clone())?; + if ret.gas == Some(0) { + return Ok(ret); + } + let state_ex = ret.state.expr(); + let a = if let Clause::P(Primitive::Atom(a)) = &state_ex.clause { + a + } else { + mem::drop(state_ex); + return Ok(ret); + }; + expr = unwrap_or!(handlers.dispatch(a.0.as_any()); { + mem::drop(state_ex); + return Ok(ret) + })?; + ctx.gas = ret.gas; + } +} diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 92dad7c..988f945 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -2,8 +2,10 @@ mod apply; mod context; mod error; +mod handler; mod run; pub use context::{Context, Return}; pub use error::RuntimeError; -pub use run::{run, run_handler, Handler, HandlerErr, HandlerParm, HandlerRes}; +pub use handler::{run_handler, HandlerRes, HandlerTable}; +pub use run::run; diff --git a/src/interpreter/run.rs b/src/interpreter/run.rs index ad27513..e48f1c7 100644 --- a/src/interpreter/run.rs +++ b/src/interpreter/run.rs @@ -1,17 +1,14 @@ -use std::mem; -use std::rc::Rc; - use super::apply::apply; use super::context::{Context, Return}; use super::error::RuntimeError; -use crate::foreign::{Atom, Atomic, AtomicReturn, ExternError}; +use crate::foreign::AtomicReturn; use crate::representations::interpreted::{Clause, ExprInst}; use crate::representations::Primitive; /// Normalize an expression using beta reduction with memoization pub fn run(expr: ExprInst, mut ctx: Context) -> Result { let (state, (gas, inert)) = - expr.try_normalize(|cls| -> Result<(Clause, _), RuntimeError> { + expr.try_normalize(|cls, loc| -> Result<(Clause, _), RuntimeError> { let mut i = cls.clone(); while ctx.gas.map(|g| g > 0).unwrap_or(true) { match &i { @@ -33,7 +30,8 @@ pub fn run(expr: ExprInst, mut ctx: Context) -> Result { i = clause.clone(); }, Clause::Constant(c) => { - let symval = ctx.symbols.get(c).expect("missing symbol for value"); + let symval = (ctx.symbols.get(c)) + .ok_or_else(|| RuntimeError::MissingSymbol(*c, loc.clone()))?; ctx.gas = ctx.gas.map(|g| g - 1); // cost of lookup i = symval.expr().clause.clone(); }, @@ -46,111 +44,3 @@ pub fn run(expr: ExprInst, mut ctx: Context) -> Result { })?; Ok(Return { state, gas, inert }) } - -/// Opaque inert data that may encode a command to a [Handler] -pub type HandlerParm = Box; - -/// Reasons why a [Handler] could not interpret a command. Convertible from -/// either variant -pub enum HandlerErr { - /// The command was addressed to us but its execution resulted in an error - Extern(Rc), - /// This handler is not applicable, either because the [HandlerParm] is not a - /// command or because it's meant for some other handler - NA(HandlerParm), -} -impl From> for HandlerErr { - fn from(value: Rc) -> Self { - Self::Extern(value) - } -} -impl From for HandlerErr -where - T: ExternError + 'static, -{ - fn from(value: T) -> Self { - Self::Extern(value.into_extern()) - } -} -impl From for HandlerErr { - fn from(value: HandlerParm) -> Self { - Self::NA(value) - } -} - -/// 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; - -/// A trait for things that may be able to handle commands returned by Orchid -/// code. This trait is implemented for `FnMut(HandlerParm) -> HandlerRes` and -/// `(Handler, Handler)`, users are not supposed to implement it themselves. -/// -/// A handler receives an arbitrary inert [Atomic] and uses [Atomic::as_any] -/// then downcast_ref of [std::any::Any] to obtain a known type. If this fails, -/// it returns the box in [HandlerErr::NA] which will be passed to the next -/// handler. -pub trait Handler { - /// Attempt to resolve a command with this handler. - fn resolve(&mut self, data: HandlerParm) -> HandlerRes; - - /// If this handler isn't applicable, try the other one. - fn or(self, t: T) -> (Self, T) - where - Self: Sized, - { - (self, t) - } -} - -impl Handler for F -where - F: FnMut(HandlerParm) -> HandlerRes, -{ - fn resolve(&mut self, data: HandlerParm) -> HandlerRes { - self(data) - } -} - -impl Handler for (T, U) { - fn resolve(&mut self, data: HandlerParm) -> HandlerRes { - match self.0.resolve(data) { - Err(HandlerErr::NA(data)) => self.1.resolve(data), - x => x, - } - } -} - -/// [run] orchid code, executing any commands it returns using the specified -/// [Handler]s. -pub fn run_handler( - mut expr: ExprInst, - mut handler: impl Handler, - mut ctx: Context, -) -> Result { - loop { - let ret = run(expr.clone(), ctx.clone())?; - if ret.gas == Some(0) { - return Ok(ret); - } - let state_ex = ret.state.expr(); - let a = if let Clause::P(Primitive::Atom(a)) = &state_ex.clause { - a - } else { - mem::drop(state_ex); - return Ok(ret); - }; - let boxed = a.clone().0; - expr = match handler.resolve(boxed) { - Ok(expr) => expr, - Err(HandlerErr::Extern(ext)) => Err(ext)?, - Err(HandlerErr::NA(atomic)) => - return Ok(Return { - gas: ret.gas, - inert: ret.inert, - state: Clause::P(Primitive::Atom(Atom(atomic))).wrap(), - }), - }; - ctx.gas = ret.gas; - } -} diff --git a/src/lib.rs b/src/lib.rs index ce341e9..66a9e0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(missing_docs)] +#![warn(missing_docs)] #![doc( html_logo_url = "https://raw.githubusercontent.com/lbfalvy/orchid/master/icon.svg" )] @@ -7,6 +7,8 @@ )] //! Orchid is a lazy, pure scripting language to be embedded in Rust //! applications. Check out the repo for examples and other links. +pub mod error; +pub mod facade; pub mod foreign; mod foreign_macros; pub mod interner; @@ -15,7 +17,7 @@ mod parse; pub mod pipeline; mod representations; pub mod rule; -pub mod stl; +pub mod systems; mod utils; pub use interner::{Interner, Tok}; @@ -32,4 +34,4 @@ pub use representations::{ ast, from_const_tree, interpreted, sourcefile, tree, ConstTree, Literal, Location, PathSet, Primitive, }; -pub use utils::{Side, Substack}; +pub use utils::{Side, Substack, ThreadPool}; diff --git a/src/parse/decls.rs b/src/parse/decls.rs index d89ea33..ce351da 100644 --- a/src/parse/decls.rs +++ b/src/parse/decls.rs @@ -1,7 +1,6 @@ use std::hash::Hash; use chumsky::prelude::Simple; -use chumsky::recursive::Recursive; use chumsky::{BoxedParser, Parser}; use trait_set::trait_set; @@ -12,6 +11,3 @@ trait_set! { } /// Boxed version of [SimpleParser] pub type BoxedSimpleParser<'a, I, O> = BoxedParser<'a, I, O, Simple>; -/// [Recursive] specialization of [SimpleParser] to parameterize calls to -/// [chumsky::recursive::recursive] -pub type SimpleRecursive<'a, I, O> = Recursive<'a, I, O, Simple>; diff --git a/src/parse/enum_filter.rs b/src/parse/enum_filter.rs deleted file mode 100644 index af015f6..0000000 --- a/src/parse/enum_filter.rs +++ /dev/null @@ -1,51 +0,0 @@ -/// Produces filter_mapping functions for enum types: -/// ```rs -/// enum_parser!(Foo::Bar | "Some error!") -/// // Foo::Bar(T) into T -/// enum_parser!(Foo::Bar) -/// // same as above but with the default error "Expected Foo::Bar" -/// enum_parser!(Foo >> Quz; Bar, Baz) -/// // Foo::Bar(T) into Quz::Bar(T) -/// // Foo::Baz(U) into Quz::Baz(U) -/// ``` -macro_rules! enum_filter { - ($p:path | $m:tt) => { - { - |l| { - if let $p(x) = l { Ok(x) } - else { Err($m) } - } - } - }; - ($p:path >> $q:path; $i:ident | $m:tt) => { - { - use $p as srcpath; - use $q as tgtpath; - let base = enum_filter!(srcpath::$i | $m); - move |l| base(l).map(tgtpath::$i) - } - }; - ($p:path >> $q:path; $i:ident) => { - enum_filter!($p >> $q; $i | {concat!("Expected ", stringify!($i))}) - }; - ($p:path >> $q:path; $($i:ident),+ | $m:tt) => { - { - use $p as srcpath; - use $q as tgtpath; - |l| match l { - $( srcpath::$i(x) => Ok(tgtpath::$i(x)), )+ - _ => Err($m) - } - } - }; - ($p:path >> $q:path; $($i:ident),+) => { - enum_filter!($p >> $q; $($i),+ | { - concat!("Expected one of ", $(stringify!($i), " "),+) - }) - }; - ($p:path) => { - enum_filter!($p | {concat!("Expected ", stringify!($p))}) - }; -} - -pub(crate) use enum_filter; diff --git a/src/parse/errors.rs b/src/parse/errors.rs new file mode 100644 index 0000000..ac7220f --- /dev/null +++ b/src/parse/errors.rs @@ -0,0 +1,241 @@ +use std::rc::Rc; + +use chumsky::prelude::Simple; +use itertools::Itertools; + +use super::{Entry, Lexeme}; +use crate::error::{ErrorPosition, ProjectError}; +use crate::interner::InternedDisplay; +use crate::utils::iter::box_once; +use crate::utils::BoxedIter; +use crate::{Interner, Location, Tok}; + +#[derive(Debug)] +pub struct LineNeedsPrefix { + pub entry: Entry, +} +impl ProjectError for LineNeedsPrefix { + fn description(&self) -> &str { + "This linetype requires a prefix" + } + fn message(&self, i: &Interner) -> String { + format!("{} cannot appear at the beginning of a line", self.entry.bundle(i)) + } + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { message: None, location: self.entry.location() }) + } +} + +#[derive(Debug)] +pub struct UnexpectedEOL { + /// Last entry before EOL + pub entry: Entry, +} +impl ProjectError for UnexpectedEOL { + fn description(&self) -> &str { + "The line ended abruptly" + } + + fn message(&self, _i: &Interner) -> String { + "The line ends unexpectedly here. In Orchid, all line breaks outside \ + parentheses start a new declaration" + .to_string() + } + + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { message: None, location: self.entry.location() }) + } +} + +pub struct ExpectedEOL { + pub location: Location, +} +impl ProjectError for ExpectedEOL { + fn description(&self) -> &str { + "Expected the end of the line" + } + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.location.clone(), message: None }) + } +} + +#[derive(Debug)] +pub struct ExpectedName { + pub entry: Entry, +} +impl ExpectedName { + pub fn expect(entry: &Entry) -> Result, Rc> { + match entry.lexeme { + Lexeme::Name(n) => Ok(n), + _ => Err(Self { entry: entry.clone() }.rc()), + } + } +} +impl ProjectError for ExpectedName { + fn description(&self) -> &str { + "A name was expected here, but something else was found" + } + + fn message(&self, i: &Interner) -> String { + if self.entry.is_keyword() { + format!( + "{} is a restricted keyword and cannot be used as a name", + self.entry.bundle(i) + ) + } else { + format!("Expected a name, found {}", self.entry.bundle(i)) + } + } + + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.entry.location(), message: None }) + } +} + +#[derive()] +pub struct Expected { + pub expected: Vec, + pub or_name: bool, + pub found: Entry, +} +impl Expected { + pub fn expect(l: Lexeme, e: &Entry) -> Result<(), Rc> { + if e.lexeme != l { + return Err( + Self { expected: vec![l], or_name: false, found: e.clone() }.rc(), + ); + } + Ok(()) + } +} +impl ProjectError for Expected { + fn description(&self) -> &str { + "A concrete token was expected but something else was found" + } + fn message(&self, i: &Interner) -> String { + let list = match &self.expected[..] { + &[] => return "Unsatisfiable expectation".to_string(), + [only] => only.to_string_i(i), + [a, b] => format!("either {} or {}", a.bundle(i), b.bundle(i)), + [variants @ .., last] => format!( + "any of {} or {}", + variants.iter().map(|l| l.to_string_i(i)).join(", "), + last.bundle(i) + ), + }; + let or_name = if self.or_name { " or a name" } else { "" }; + format!("Expected {}{} but found {}", list, or_name, self.found.bundle(i)) + } + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.found.location(), message: None }) + } +} + +pub struct ReservedToken { + pub entry: Entry, +} +impl ProjectError for ReservedToken { + fn description(&self) -> &str { + "A token reserved for future use was found in the code" + } + + fn message(&self, i: &Interner) -> String { + format!("{} is a reserved token", self.entry.bundle(i)) + } + + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.entry.location(), message: None }) + } +} + +pub struct BadTokenInRegion { + pub entry: Entry, + pub region: &'static str, +} +impl ProjectError for BadTokenInRegion { + fn description(&self) -> &str { + "A token was found in a region where it should not appear" + } + + fn message(&self, i: &Interner) -> String { + format!("{} cannot appear in {}", self.entry.bundle(i), self.region) + } + + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.entry.location(), message: None }) + } +} + +pub struct NotFound { + pub expected: &'static str, + pub location: Location, +} +impl ProjectError for NotFound { + fn description(&self) -> &str { + "A specific lexeme was expected but not found in the given range" + } + + fn message(&self, _i: &Interner) -> String { + format!("{} was expected", self.expected) + } + + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.location.clone(), message: None }) + } +} + +pub struct LeadingNS { + pub location: Location, +} +impl ProjectError for LeadingNS { + fn description(&self) -> &str { + ":: can only follow a name token" + } + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.location.clone(), message: None }) + } +} + +pub struct MisalignedParen { + pub entry: Entry, +} +impl ProjectError for MisalignedParen { + fn description(&self) -> &str { + "Parentheses (), [] and {} must always pair up" + } + fn message(&self, i: &Interner) -> String { + format!("This {} has no pair", self.entry.bundle(i)) + } + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.entry.location(), message: None }) + } +} + +pub struct NamespacedExport { + pub location: Location, +} +impl ProjectError for NamespacedExport { + fn description(&self) -> &str { + "Exports can only refer to unnamespaced names in the local namespace" + } + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.location.clone(), message: None }) + } +} + +pub struct LexError { + pub errors: Vec>, + pub file: Rc>, +} +impl ProjectError for LexError { + fn description(&self) -> &str { + "An error occured during tokenization" + } + fn positions(&self, _i: &Interner) -> BoxedIter { + let file = self.file.clone(); + Box::new(self.errors.iter().map(move |s| ErrorPosition { + location: Location::Range { file: file.clone(), range: s.span() }, + message: Some(format!("{}", s)), + })) + } +} diff --git a/src/parse/expression.rs b/src/parse/expression.rs deleted file mode 100644 index 0a9507d..0000000 --- a/src/parse/expression.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::ops::Range; -use std::rc::Rc; - -use chumsky::prelude::*; -use chumsky::{self, Parser}; - -use super::context::Context; -use super::decls::SimpleParser; -use super::enum_filter::enum_filter; -use super::lexer::{filter_map_lex, Entry, Lexeme}; -use crate::representations::ast::{Clause, Expr}; -use crate::representations::location::Location; -use crate::representations::{Primitive, VName}; - -/// Parses any number of expr wrapped in (), [] or {} -fn sexpr_parser( - expr: impl SimpleParser> + Clone, -) -> impl SimpleParser, Range)> + Clone { - let body = expr.repeated(); - choice(( - Lexeme::LP('(').parser().then(body.clone()).then(Lexeme::RP('(').parser()), - Lexeme::LP('[').parser().then(body.clone()).then(Lexeme::RP('[').parser()), - Lexeme::LP('{').parser().then(body).then(Lexeme::RP('{').parser()), - )) - .map(|((lp, body), rp)| { - let Entry { lexeme, range: Range { start, .. } } = lp; - let end = rp.range.end; - let char = if let Lexeme::LP(c) = lexeme { - c - } else { - unreachable!("The parser only matches Lexeme::LP") - }; - (Clause::S(char, Rc::new(body)), start..end) - }) - .labelled("S-expression") -} - -/// Parses `\name.body` or `\name:type.body` where name is any valid name -/// and type and body are both expressions. Comments are allowed -/// and ignored everywhere in between the tokens -fn lambda_parser<'a>( - expr: impl SimpleParser> + Clone + 'a, - ctx: impl Context + 'a, -) -> impl SimpleParser, Range)> + Clone + 'a { - Lexeme::BS - .parser() - .ignore_then(expr.clone()) - .then_ignore(Lexeme::Name(ctx.interner().i(".")).parser()) - .then(expr.repeated().at_least(1)) - .map_with_span(move |(arg, body), span| { - (Clause::Lambda(Rc::new(arg), Rc::new(body)), span) - }) - .labelled("Lambda") -} - -/// Parses a sequence of names separated by ::
-/// Comments and line breaks are allowed and ignored in between -pub fn ns_name_parser<'a>() --> impl SimpleParser)> + Clone + 'a { - filter_map_lex(enum_filter!(Lexeme::Name)) - .separated_by(Lexeme::NS.parser()) - .at_least(1) - .map(move |elements| { - let start = elements.first().expect("can never be empty").1.start; - let end = elements.last().expect("can never be empty").1.end; - let tokens = (elements.iter().map(|(t, _)| *t)).collect::>(); - (tokens, start..end) - }) - .labelled("Namespaced name") -} - -pub fn namelike_parser<'a>() --> impl SimpleParser, Range)> + Clone + 'a { - choice(( - filter_map_lex(enum_filter!(Lexeme::PH)) - .map(|(ph, range)| (Clause::Placeh(ph), range)), - ns_name_parser().map(|(token, range)| (Clause::Name(token), range)), - )) -} - -pub fn clause_parser<'a>( - expr: impl SimpleParser> + Clone + 'a, - ctx: impl Context + 'a, -) -> impl SimpleParser, Range)> + Clone + 'a { - choice(( - filter_map_lex(enum_filter!(Lexeme >> Primitive; Literal)) - .map(|(p, s)| (Clause::P(p), s)) - .labelled("Literal"), - sexpr_parser(expr.clone()), - lambda_parser(expr, ctx), - namelike_parser(), - )) - .labelled("Clause") -} - -/// Parse an expression -pub fn xpr_parser<'a>( - ctx: impl Context + 'a, -) -> impl SimpleParser> + 'a { - recursive(move |expr| { - clause_parser(expr, ctx.clone()).map(move |(value, range)| Expr { - value, - location: Location::Range { file: ctx.file(), range }, - }) - }) - .labelled("Expression") -} diff --git a/src/parse/facade.rs b/src/parse/facade.rs index b7b11ca..444ff7f 100644 --- a/src/parse/facade.rs +++ b/src/parse/facade.rs @@ -1,59 +1,22 @@ -use std::fmt::Debug; - -use chumsky::prelude::*; use chumsky::Parser; -use thiserror::Error; use super::context::Context; -use super::{lexer, line_parser, Entry}; -use crate::parse::sourcefile::split_lines; +use super::errors::LexError; +use super::lexer; +use super::sourcefile::parse_module_body; +use super::stream::Stream; +use crate::error::{ParseErrorWithTokens, ProjectError, ProjectResult}; use crate::representations::sourcefile::FileEntry; -#[derive(Error, Debug, Clone)] -pub enum ParseError { - #[error("Could not tokenize {0:?}")] - Lex(Vec>), - #[error( - "Could not parse {:?} on line {}", - .0.first().unwrap().1.span(), - .0.first().unwrap().0 - )] - Ast(Vec<(usize, Simple)>), -} - -/// Parse a string of code into a collection of module elements; -/// imports, exports, comments, declarations, etc. -/// -/// Notice that because the lexer splits operators based on the provided -/// list, the output will only be correct if operator list already -/// contains all operators defined or imported by this module. -pub fn parse( - data: &str, - ctx: impl Context, -) -> Result, ParseError> { - // TODO: wrap `i`, `ops` and `prefix` in a parsing context +pub fn parse2(data: &str, ctx: impl Context) -> ProjectResult> { let lexie = lexer(ctx.clone()); - let token_batchv = lexie.parse(data).map_err(ParseError::Lex)?; - let parsr = line_parser(ctx).then_ignore(end()); - let (parsed_lines, errors_per_line) = split_lines(&token_batchv) - .enumerate() - .map(|(i, entv)| { - (i, entv.iter().filter(|e| !e.is_filler()).cloned().collect::>()) - }) - .filter(|(_, l)| !l.is_empty()) - .map(|(i, l)| (i, parsr.parse(l))) - .map(|(i, res)| match res { - Ok(r) => (Some(r), (i, vec![])), - Err(e) => (None, (i, e)), - }) - .unzip::<_, _, Vec<_>, Vec<_>>(); - let total_err = errors_per_line - .into_iter() - .flat_map(|(i, v)| v.into_iter().map(move |e| (i, e))) - .collect::>(); - if !total_err.is_empty() { - Err(ParseError::Ast(total_err)) + let tokens = (lexie.parse(data)) + .map_err(|errors| LexError { errors, file: ctx.file() }.rc())?; + if tokens.is_empty() { + Ok(Vec::new()) } else { - Ok(parsed_lines.into_iter().map(Option::unwrap).collect()) + parse_module_body(Stream::from_slice(&tokens), ctx).map_err(|error| { + ParseErrorWithTokens { error, full_source: data.to_string(), tokens }.rc() + }) } } diff --git a/src/parse/import.rs b/src/parse/import.rs deleted file mode 100644 index a7200ea..0000000 --- a/src/parse/import.rs +++ /dev/null @@ -1,93 +0,0 @@ -use chumsky::prelude::*; -use chumsky::Parser; -use itertools::Itertools; - -use super::context::Context; -use super::decls::{SimpleParser, SimpleRecursive}; -use super::enum_filter::enum_filter; -use super::lexer::{filter_map_lex, Lexeme}; -use super::Entry; -use crate::interner::Tok; -use crate::representations::sourcefile::Import; -use crate::utils::iter::{ - box_chain, box_flatten, box_once, into_boxed_iter, BoxedIterIter, -}; - -/// initialize an iterator of iterator with a single element. -fn init_table(name: Tok) -> BoxedIterIter<'static, Tok> { - box_once(box_once(name)) -} - -/// Parse an import command -/// Syntax is same as Rust's `use` except the verb is import, no trailing -/// semi and the delimiters are plain parentheses. Namespaces should -/// preferably contain crossplatform filename-legal characters but the -/// symbols are explicitly allowed to go wild. -/// There's a blacklist in [crate::parse::name::NOT_NAME_CHAR] -pub fn import_parser<'a>( - ctx: impl Context + 'a, -) -> impl SimpleParser> + 'a { - // TODO: this algorithm isn't cache friendly and copies a lot - recursive({ - let ctx = ctx.clone(); - move |expr: SimpleRecursive>>| { - filter_map_lex(enum_filter!(Lexeme::Name)) - .map(|(t, _)| t) - .separated_by(Lexeme::NS.parser()) - .then( - Lexeme::NS - .parser() - .ignore_then(choice(( - expr - .clone() - .separated_by(Lexeme::Name(ctx.interner().i(",")).parser()) - .delimited_by( - Lexeme::LP('(').parser(), - Lexeme::RP('(').parser(), - ) - .map(|v| box_flatten(v.into_iter())) - .labelled("import group"), - // Each expr returns a list of imports, flatten into common list - Lexeme::Name(ctx.interner().i("*")) - .parser() - .map(move |_| init_table(ctx.interner().i("*"))) - .labelled("wildcard import"), // Just a *, wrapped - filter_map_lex(enum_filter!(Lexeme::Name)) - .map(|(t, _)| init_table(t)) - .labelled("import terminal"), // Just a name, wrapped - ))) - .or_not(), - ) - .map( - |(name, opt_post): ( - Vec>, - Option>>, - )| - -> BoxedIterIter> { - if let Some(post) = opt_post { - Box::new( - post.map(move |el| box_chain!(name.clone().into_iter(), el)), - ) - } else { - box_once(into_boxed_iter(name)) - } - }, - ) - } - }) - .map(move |paths| { - paths - .filter_map(|namespaces| { - let mut path = namespaces.collect_vec(); - let name = path.pop()?; - Some(Import { - path: ctx.interner().i(&path), - name: { - if name == ctx.interner().i("*") { None } else { Some(name) } - }, - }) - }) - .collect() - }) - .labelled("import") -} diff --git a/src/parse/lexer.rs b/src/parse/lexer.rs index 706ff23..44fbfdf 100644 --- a/src/parse/lexer.rs +++ b/src/parse/lexer.rs @@ -1,27 +1,53 @@ use std::fmt; use std::ops::Range; +use std::rc::Rc; use chumsky::prelude::*; use chumsky::text::keyword; -use chumsky::{Parser, Span}; +use chumsky::Parser; use ordered_float::NotNan; use super::context::Context; use super::decls::SimpleParser; +use super::number::print_nat16; use super::{comment, name, number, placeholder, string}; use crate::ast::{PHClass, Placeholder}; use crate::interner::{InternedDisplay, Interner, Tok}; use crate::representations::Literal; +use crate::Location; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Entry { pub lexeme: Lexeme, - pub range: Range, + pub location: Location, } impl Entry { + /// Checks if the lexeme is a comment or line break pub fn is_filler(&self) -> bool { - matches!(self.lexeme, Lexeme::Comment(_)) - || matches!(self.lexeme, Lexeme::BR) + matches!(self.lexeme, Lexeme::Comment(_) | Lexeme::BR) + } + + pub fn is_keyword(&self) -> bool { + matches!( + self.lexeme, + Lexeme::Const + | Lexeme::Export + | Lexeme::Import + | Lexeme::Macro + | Lexeme::Module + ) + } + + pub fn location(&self) -> Location { + self.location.clone() + } + + pub fn range(&self) -> Range { + self.location.range().expect("An Entry can only have a known location") + } + + pub fn file(&self) -> Rc> { + self.location.file().expect("An Entry can only have a range location") } } @@ -35,27 +61,33 @@ impl InternedDisplay for Entry { } } -impl From for (Lexeme, Range) { - fn from(ent: Entry) -> Self { - (ent.lexeme, ent.range) - } -} +// impl From for (Lexeme, Range) { +// fn from(ent: Entry) -> Self { +// (ent.lexeme.clone(), ent.range()) +// } +// } -impl Span for Entry { - type Context = Lexeme; - type Offset = usize; +// impl Span for Entry { +// type Context = (Lexeme, Rc>); +// type Offset = usize; - fn context(&self) -> Self::Context { - self.lexeme.clone() - } - fn start(&self) -> Self::Offset { - self.range.start() - } - fn end(&self) -> Self::Offset { - self.range.end() - } - fn new(context: Self::Context, range: Range) -> Self { - Self { lexeme: context, range } +// fn context(&self) -> Self::Context { +// (self.lexeme.clone(), self.file()) +// } +// fn start(&self) -> Self::Offset { +// self.range().start() +// } +// fn end(&self) -> Self::Offset { +// self.range().end() +// } +// fn new((lexeme, file): Self::Context, range: Range) -> Self { +// Self { lexeme, location: Location::Range { file, range } } +// } +// } + +impl AsRef for Entry { + fn as_ref(&self) -> &Location { + &self.location } } @@ -63,9 +95,9 @@ impl Span for Entry { pub enum Lexeme { Literal(Literal), Name(Tok), - Rule(NotNan), + Arrow(NotNan), /// Walrus operator (formerly shorthand macro) - Const, + Walrus, /// Line break BR, /// Namespace separator @@ -77,12 +109,15 @@ pub enum Lexeme { /// Backslash BS, At, + Dot, Type, // type operator Comment(String), Export, Import, - Namespace, - PH(Placeholder), + Module, + Macro, + Const, + Placeh(Placeholder), } impl InternedDisplay for Lexeme { @@ -94,8 +129,8 @@ impl InternedDisplay for Lexeme { match self { Self::Literal(l) => write!(f, "{:?}", l), Self::Name(token) => write!(f, "{}", i.r(*token)), - Self::Const => write!(f, ":="), - Self::Rule(prio) => write!(f, "={}=>", prio), + Self::Walrus => write!(f, ":="), + Self::Arrow(prio) => write!(f, "={}=>", print_nat16(*prio)), Self::NS => write!(f, "::"), Self::LP(l) => write!(f, "{}", l), Self::RP(l) => match l { @@ -107,12 +142,15 @@ impl InternedDisplay for Lexeme { Self::BR => writeln!(f), Self::BS => write!(f, "\\"), Self::At => write!(f, "@"), + Self::Dot => write!(f, "."), Self::Type => write!(f, ":"), Self::Comment(text) => write!(f, "--[{}]--", text), Self::Export => write!(f, "export"), Self::Import => write!(f, "import"), - Self::Namespace => write!(f, "namespace"), - Self::PH(Placeholder { name, class }) => match *class { + Self::Module => write!(f, "module"), + Self::Const => write!(f, "const"), + Self::Macro => write!(f, "macro"), + Self::Placeh(Placeholder { name, class }) => match *class { PHClass::Scalar => write!(f, "${}", i.r(*name)), PHClass::Vec { nonzero, prio } => { if nonzero { write!(f, "...") } else { write!(f, "..") }?; @@ -129,7 +167,9 @@ impl InternedDisplay for Lexeme { impl Lexeme { pub fn rule(prio: impl Into) -> Self { - Lexeme::Rule(NotNan::new(prio.into()).expect("Rule priority cannot be NaN")) + Lexeme::Arrow( + NotNan::new(prio.into()).expect("Rule priority cannot be NaN"), + ) } pub fn parser>( @@ -179,38 +219,37 @@ pub fn lexer<'a>( .collect::>(); choice(( keyword("export").to(Lexeme::Export), - keyword("module").to(Lexeme::Namespace), + keyword("module").to(Lexeme::Module), keyword("import").to(Lexeme::Import), + keyword("macro").to(Lexeme::Macro), + keyword("const").to(Lexeme::Const), paren_parser('(', ')'), paren_parser('[', ']'), paren_parser('{', '}'), - just(":=").to(Lexeme::Const), + just(":=").to(Lexeme::Walrus), just("=") .ignore_then(number::float_parser()) .then_ignore(just("=>")) .map(Lexeme::rule), comment::comment_parser().map(Lexeme::Comment), + placeholder::placeholder_parser(ctx.clone()).map(Lexeme::Placeh), just("::").to(Lexeme::NS), just('\\').to(Lexeme::BS), just('@').to(Lexeme::At), just(':').to(Lexeme::Type), just('\n').to(Lexeme::BR), - placeholder::placeholder_parser(ctx.clone()).map(Lexeme::PH), + just('.').to(Lexeme::Dot), literal_parser().map(Lexeme::Literal), - name::name_parser(&all_ops) - .map(move |n| Lexeme::Name(ctx.interner().i(&n))), + name::name_parser(&all_ops).map({ + let ctx = ctx.clone(); + move |n| Lexeme::Name(ctx.interner().i(&n)) + }), )) - .map_with_span(|lexeme, range| Entry { lexeme, range }) + .map_with_span(move |lexeme, range| Entry { + lexeme, + location: Location::Range { range, file: ctx.file() }, + }) .padded_by(one_of(" \t").repeated()) .repeated() .then_ignore(end()) } - -pub fn filter_map_lex<'a, O, M: ToString>( - f: impl Fn(Lexeme) -> Result + Clone + 'a, -) -> impl SimpleParser)> + Clone + 'a { - filter_map(move |s: Range, e: Entry| { - let out = f(e.lexeme).map_err(|msg| Simple::custom(s.clone(), msg))?; - Ok((out, s)) - }) -} diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 4c782e0..f82795b 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,20 +1,19 @@ mod comment; mod context; mod decls; -mod enum_filter; -mod expression; +mod errors; mod facade; -mod import; mod lexer; +mod multiname; mod name; mod number; mod placeholder; mod sourcefile; +mod stream; mod string; pub use context::ParsingContext; -pub use facade::{parse, ParseError}; +pub use facade::parse2; pub use lexer::{lexer, Entry, Lexeme}; pub use name::is_op; pub use number::{float_parser, int_parser}; -pub use sourcefile::line_parser; diff --git a/src/parse/multiname.rs b/src/parse/multiname.rs new file mode 100644 index 0000000..824c6f2 --- /dev/null +++ b/src/parse/multiname.rs @@ -0,0 +1,88 @@ +use std::collections::VecDeque; +use std::iter; + +use super::context::Context; +use super::errors::{Expected, ExpectedName}; +use super::stream::Stream; +use super::Lexeme; +use crate::error::{ProjectError, ProjectResult}; +use crate::utils::iter::{box_chain, box_once, BoxedIterIter}; +use crate::Tok; + +fn parse_multiname_branch( + cursor: Stream<'_>, + ctx: impl Context, +) -> ProjectResult<(BoxedIterIter>, Stream<'_>)> { + let comma = ctx.interner().i(","); + let (subnames, cursor) = parse_multiname_rec(cursor, ctx.clone())?; + let (delim, cursor) = cursor.trim().pop()?; + match delim.lexeme { + Lexeme::Name(n) if n == comma => { + let (tail, cont) = parse_multiname_branch(cursor, ctx)?; + Ok((box_chain!(subnames, tail), cont)) + }, + Lexeme::RP('(') => Ok((subnames, cursor)), + _ => Err( + Expected { + expected: vec![Lexeme::Name(comma), Lexeme::RP('(')], + or_name: false, + found: delim.clone(), + } + .rc(), + ), + } +} + +pub fn parse_multiname_rec( + curosr: Stream<'_>, + ctx: impl Context, +) -> ProjectResult<(BoxedIterIter>, Stream<'_>)> { + let comma = ctx.interner().i(","); + let (head, cursor) = curosr.trim().pop()?; + match &head.lexeme { + Lexeme::LP('(') => parse_multiname_branch(cursor, ctx), + Lexeme::LP('[') => { + let (op_ent, cursor) = cursor.trim().pop()?; + let op = ExpectedName::expect(op_ent)?; + let (rp_ent, cursor) = cursor.trim().pop()?; + Expected::expect(Lexeme::RP('['), rp_ent)?; + Ok((box_once(box_once(op)), cursor)) + }, + Lexeme::Name(n) if *n != comma => { + let cursor = cursor.trim(); + if cursor.get(0).ok().map(|e| &e.lexeme) == Some(&Lexeme::NS) { + let cursor = cursor.step()?; + let (out, cursor) = parse_multiname_rec(cursor, ctx)?; + let out = Box::new(out.map(|i| box_chain!(i, iter::once(*n)))); + Ok((out, cursor)) + } else { + Ok((box_once(box_once(*n)), cursor)) + } + }, + _ => Err( + Expected { + expected: vec![Lexeme::LP('(')], + or_name: true, + found: head.clone(), + } + .rc(), + ), + } +} + +pub fn parse_multiname( + cursor: Stream<'_>, + ctx: impl Context, +) -> ProjectResult<(Vec>>, Stream<'_>)> { + let (output, cont) = parse_multiname_rec(cursor, ctx)?; + let output = output + .map(|it| { + let mut deque = VecDeque::with_capacity(it.size_hint().0); + for item in it { + deque.push_front(item) + } + deque.into() + }) + .collect(); + Ok((output, cont)) +} diff --git a/src/parse/number.rs b/src/parse/number.rs index 05fe4ca..89917fe 100644 --- a/src/parse/number.rs +++ b/src/parse/number.rs @@ -123,3 +123,9 @@ pub fn float_parser() -> impl SimpleParser> { )) .labelled("float") } + +pub fn print_nat16(num: NotNan) -> String { + let exp = num.log(16.0).floor(); + let man = num / 16_f64.powf(exp); + format!("{man}p{exp:.0}") +} diff --git a/src/parse/sourcefile.rs b/src/parse/sourcefile.rs index 1271e03..b66be47 100644 --- a/src/parse/sourcefile.rs +++ b/src/parse/sourcefile.rs @@ -1,138 +1,28 @@ use std::iter; use std::rc::Rc; -use chumsky::prelude::*; -use chumsky::Parser; use itertools::Itertools; use super::context::Context; -use super::decls::{SimpleParser, SimpleRecursive}; -use super::enum_filter::enum_filter; -use super::expression::xpr_parser; -use super::import::import_parser; -use super::lexer::{filter_map_lex, Lexeme}; +use super::errors::{ + BadTokenInRegion, Expected, ExpectedName, LeadingNS, MisalignedParen, + NamespacedExport, ReservedToken, UnexpectedEOL, +}; +use super::lexer::Lexeme; +use super::multiname::parse_multiname; +use super::stream::Stream; use super::Entry; use crate::ast::{Clause, Constant, Expr, Rule}; +use crate::error::{ProjectError, ProjectResult}; use crate::representations::location::Location; -use crate::representations::sourcefile::{FileEntry, Member, Namespace}; +use crate::representations::sourcefile::{FileEntry, Member, ModuleBlock}; use crate::representations::VName; +use crate::sourcefile::Import; +use crate::Primitive; -fn rule_parser<'a>( - ctx: impl Context + 'a, -) -> impl SimpleParser> + 'a { - xpr_parser(ctx.clone()) - .repeated() - .at_least(1) - .then(filter_map_lex(enum_filter!(Lexeme::Rule))) - .then(xpr_parser(ctx).repeated().at_least(1)) - .map(|((p, (prio, _)), t)| Rule { pattern: p, prio, template: t }) - .labelled("Rule") -} - -fn const_parser<'a>( - ctx: impl Context + 'a, -) -> impl SimpleParser + 'a { - filter_map_lex(enum_filter!(Lexeme::Name)) - .then_ignore(Lexeme::Const.parser()) - .then(xpr_parser(ctx.clone()).repeated().at_least(1)) - .map(move |((name, _), value)| Constant { - name, - value: if let Ok(ex) = value.iter().exactly_one() { - ex.clone() - } else { - let start = value - .first() - .expect("value cannot be empty") - .location - .range() - .expect("all locations in parsed source are known") - .start; - let end = value - .last() - .expect("asserted right above") - .location - .range() - .expect("all locations in parsed source are known") - .end; - Expr { - location: Location::Range { file: ctx.file(), range: start..end }, - value: Clause::S('(', Rc::new(value)), - } - }, - }) -} - -pub fn collect_errors>(e: Vec) -> E { - e.into_iter() - .reduce(chumsky::Error::merge) - .expect("Error list must be non_enmpty") -} - -fn namespace_parser<'a>( - line: impl SimpleParser + 'a, -) -> impl SimpleParser + 'a { - Lexeme::Namespace - .parser() - .ignore_then(filter_map_lex(enum_filter!(Lexeme::Name))) - .then( - any() - .repeated() - .delimited_by(Lexeme::LP('(').parser(), Lexeme::RP('(').parser()) - .try_map(move |body, _| { - split_lines(&body) - .map(|l| line.parse(l)) - .collect::, _>>() - .map_err(collect_errors) - }), - ) - .map(move |((name, _), body)| Namespace { name, body }) -} - -fn member_parser<'a>( - line: impl SimpleParser + 'a, - ctx: impl Context + 'a, -) -> impl SimpleParser + 'a { - choice(( - namespace_parser(line).map(Member::Namespace), - rule_parser(ctx.clone()).map(Member::Rule), - const_parser(ctx).map(Member::Constant), - )) -} - -pub fn line_parser<'a>( - ctx: impl Context + 'a, -) -> impl SimpleParser + 'a { - recursive(|line: SimpleRecursive| { - choice(( - // In case the usercode wants to parse doc - filter_map_lex(enum_filter!(Lexeme >> FileEntry; Comment)) - .map(|(ent, _)| ent), - // plain old imports - Lexeme::Import - .parser() - .ignore_then(import_parser(ctx.clone()).map(FileEntry::Import)), - Lexeme::Export.parser().ignore_then(choice(( - // token collection - Lexeme::NS - .parser() - .ignore_then( - filter_map_lex(enum_filter!(Lexeme::Name)) - .map(|(e, _)| e) - .separated_by(Lexeme::Name(ctx.interner().i(",")).parser()) - .delimited_by(Lexeme::LP('(').parser(), Lexeme::RP('(').parser()), - ) - .map(FileEntry::Export), - // public declaration - member_parser(line.clone(), ctx.clone()).map(FileEntry::Exported), - ))), - // This could match almost anything so it has to go last - member_parser(line, ctx).map(FileEntry::Internal), - )) - }) -} - -pub fn split_lines(data: &[Entry]) -> impl Iterator { - let mut source = data.iter().enumerate(); +pub fn split_lines(module: Stream<'_>) -> impl Iterator> { + let mut source = module.data.iter().enumerate(); + let mut fallback = module.fallback; let mut last_slice = 0; let mut finished = false; iter::from_fn(move || { @@ -144,7 +34,9 @@ pub fn split_lines(data: &[Entry]) -> impl Iterator { Lexeme::BR if paren_count == 0 => { let begin = last_slice; last_slice = i + 1; - return Some(&data[begin..i]); + let cur_prev = fallback; + fallback = &module.data[i]; + return Some(Stream::new(cur_prev, &module.data[begin..i])); }, _ => (), } @@ -152,9 +44,242 @@ pub fn split_lines(data: &[Entry]) -> impl Iterator { // Include last line even without trailing newline if !finished { finished = true; - return Some(&data[last_slice..]); + return Some(Stream::new(fallback, &module.data[last_slice..])); } None }) - .filter(|s| !s.is_empty()) +} + +pub fn parse_module_body( + cursor: Stream<'_>, + ctx: impl Context, +) -> ProjectResult> { + split_lines(cursor) + .map(Stream::trim) + .filter(|l| !l.data.is_empty()) + .map(|l| parse_line(l, ctx.clone())) + .collect() +} + +pub fn parse_line( + cursor: Stream<'_>, + ctx: impl Context, +) -> ProjectResult { + match cursor.get(0)?.lexeme { + Lexeme::BR | Lexeme::Comment(_) => parse_line(cursor.step()?, ctx), + Lexeme::Export => parse_export_line(cursor.step()?, ctx), + Lexeme::Const | Lexeme::Macro | Lexeme::Module => + Ok(FileEntry::Internal(parse_member(cursor, ctx)?)), + Lexeme::Import => { + let globstar = ctx.interner().i("*"); + let (names, cont) = parse_multiname(cursor.step()?, ctx.clone())?; + cont.expect_empty()?; + let imports = (names.into_iter()) + .map(|mut nsname| { + let name = nsname.pop().expect("multinames cannot be zero-length"); + Import { + path: ctx.interner().i(&nsname), + name: if name == globstar { None } else { Some(name) }, + } + }) + .collect(); + Ok(FileEntry::Import(imports)) + }, + _ => { + let err = BadTokenInRegion { + entry: cursor.get(0)?.clone(), + region: "start of line", + }; + Err(err.rc()) + }, + } +} + +pub fn parse_export_line( + cursor: Stream<'_>, + ctx: impl Context, +) -> ProjectResult { + let cursor = cursor.trim(); + match cursor.get(0)?.lexeme { + Lexeme::NS => { + let (names, cont) = parse_multiname(cursor.step()?, ctx)?; + cont.expect_empty()?; + let names = (names.into_iter()) + .map(|i| if i.len() == 1 { Some(i[0]) } else { None }) + .collect::>>() + .ok_or_else(|| NamespacedExport { location: cursor.location() }.rc())?; + Ok(FileEntry::Export(names)) + }, + Lexeme::Const | Lexeme::Macro | Lexeme::Module => + Ok(FileEntry::Exported(parse_member(cursor, ctx)?)), + _ => { + let err = BadTokenInRegion { + entry: cursor.get(0)?.clone(), + region: "exported line", + }; + Err(err.rc()) + }, + } +} + +fn parse_member( + cursor: Stream<'_>, + ctx: impl Context, +) -> ProjectResult { + let (typemark, cursor) = cursor.trim().pop()?; + match typemark.lexeme { + Lexeme::Const => { + let constant = parse_const(cursor, ctx)?; + Ok(Member::Constant(constant)) + }, + Lexeme::Macro => { + let rule = parse_rule(cursor, ctx)?; + Ok(Member::Rule(rule)) + }, + Lexeme::Module => { + let module = parse_module(cursor, ctx)?; + Ok(Member::Module(module)) + }, + _ => { + let err = + BadTokenInRegion { entry: typemark.clone(), region: "member type" }; + Err(err.rc()) + }, + } +} + +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, + })?; + let (pattern, _) = parse_exprv(pattern, None, ctx.clone())?; + let (template, _) = parse_exprv(template, None, ctx)?; + Ok(Rule { pattern, prio, template }) +} + +fn parse_const( + cursor: Stream<'_>, + ctx: impl Context, +) -> 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 (body, _) = parse_exprv(cursor, None, ctx)?; + Ok(Constant { name, value: vec_to_single(walrus_ent, body)? }) +} + +fn parse_module( + cursor: Stream<'_>, + ctx: impl Context, +) -> ProjectResult { + let (name_ent, cursor) = cursor.trim().pop()?; + let name = ExpectedName::expect(name_ent)?; + let (lp_ent, cursor) = cursor.trim().pop()?; + Expected::expect(Lexeme::LP('('), lp_ent)?; + let (last, cursor) = cursor.pop_back()?; + Expected::expect(Lexeme::RP('('), last)?; + let body = parse_module_body(cursor, ctx)?; + Ok(ModuleBlock { name, body }) +} + +fn parse_exprv( + mut cursor: Stream<'_>, + paren: Option, + ctx: impl Context, +) -> ProjectResult<(Vec>, Stream<'_>)> { + let mut output = Vec::new(); + cursor = cursor.trim(); + while let Ok(current) = cursor.get(0) { + match ¤t.lexeme { + Lexeme::BR | Lexeme::Comment(_) => unreachable!("Fillers skipped"), + Lexeme::At | Lexeme::Type => + return Err(ReservedToken { entry: current.clone() }.rc()), + Lexeme::Literal(l) => { + output.push(Expr { + value: Clause::P(Primitive::Literal(l.clone())), + location: current.location(), + }); + cursor = cursor.step()?; + }, + Lexeme::Placeh(ph) => { + output.push(Expr { + value: Clause::Placeh(*ph), + location: current.location(), + }); + cursor = cursor.step()?; + }, + Lexeme::Name(n) => { + let location = cursor.location(); + let mut fullname = vec![*n]; + while cursor.get(1).ok().map(|e| &e.lexeme) == Some(&Lexeme::NS) { + fullname.push(ExpectedName::expect(cursor.get(2)?)?); + cursor = cursor.step()?.step()?; + } + output.push(Expr { value: Clause::Name(fullname), location }); + cursor = cursor.step()?; + }, + Lexeme::NS => + return Err(LeadingNS { location: current.location() }.rc()), + Lexeme::RP(c) => + return if Some(*c) == paren { + Ok((output, cursor.step()?)) + } else { + Err(MisalignedParen { entry: cursor.get(0)?.clone() }.rc()) + }, + Lexeme::LP(c) => { + let (result, leftover) = + parse_exprv(cursor.step()?, Some(*c), ctx.clone())?; + output.push(Expr { + value: Clause::S(*c, Rc::new(result)), + location: cursor.get(0)?.location().to(leftover.fallback.location()), + }); + cursor = leftover; + }, + Lexeme::BS => { + let (arg, body) = + cursor.step()?.find("A '.'", |l| l == &Lexeme::Dot)?; + let (arg, _) = parse_exprv(arg, None, ctx.clone())?; + let (body, leftover) = parse_exprv(body, paren, ctx)?; + output.push(Expr { + location: cursor.location(), + 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()); + }, + } + cursor = cursor.trim(); + } + Ok((output, Stream::new(cursor.fallback, &[]))) +} + +fn vec_to_single( + fallback: &Entry, + v: Vec>, +) -> ProjectResult> { + match v.len() { + 0 => return Err(UnexpectedEOL { entry: fallback.clone() }.rc()), + 1 => Ok(v.into_iter().exactly_one().unwrap()), + _ => Ok(Expr { + location: expr_slice_location(&v), + value: Clause::S('(', Rc::new(v)), + }), + } +} + +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 new file mode 100644 index 0000000..52781fd --- /dev/null +++ b/src/parse/stream.rs @@ -0,0 +1,119 @@ +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. +#[derive(Clone, Copy)] +pub struct Stream<'a> { + pub fallback: &'a Entry, + pub data: &'a [Entry], +} +impl<'a> Stream<'a> { + pub fn new(fallback: &'a Entry, data: &'a [Entry]) -> Self { + Self { fallback, data } + } + + 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 } + } + + 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 }) + } + + 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() + }) + } + + pub fn location(self) -> Location { + self.data.first().map_or_else( + || self.fallback.location(), + |f| f.location().to(self.data.last().unwrap().location()), + ) + } + + 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))) + } + + 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)) + } + + 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 } + } + + pub fn expect_empty(self) -> ProjectResult<()> { + if let Some(x) = self.data.first() { + Err(ExpectedEOL { location: x.location() }.rc()) + } else { + Ok(()) + } + } +} + +// impl<'a> From<(&'a Entry, &'a [Entry])> for Stream<'a> { +// fn from((fallback, data): (&'a Entry, &'a [Entry])) -> Self { +// Self::new(fallback, data) +// } +// } + +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/pipeline/error/parse_error_with_path.rs b/src/pipeline/error/parse_error_with_path.rs deleted file mode 100644 index 1dc5d0e..0000000 --- a/src/pipeline/error/parse_error_with_path.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::rc::Rc; - -use super::{ErrorPosition, ProjectError}; -use crate::parse::ParseError; -use crate::representations::location::Location; -use crate::utils::BoxedIter; - -/// Produced by stages that parse text when it fails. -#[derive(Debug)] -pub struct ParseErrorWithPath { - /// The complete source of the faulty file - pub full_source: String, - /// The path to the faulty file - pub path: Vec, - /// The parse error produced by Chumsky - pub error: ParseError, -} -impl ProjectError for ParseErrorWithPath { - fn description(&self) -> &str { - "Failed to parse code" - } - fn positions(&self) -> BoxedIter { - match &self.error { - ParseError::Lex(lex) => Box::new(lex.iter().map(|s| ErrorPosition { - location: Location::Range { - file: Rc::new(self.path.clone()), - range: s.span(), - }, - message: Some(s.to_string()), - })), - ParseError::Ast(ast) => Box::new(ast.iter().map(|(_i, s)| { - ErrorPosition { - location: s - .found() - .map(|e| Location::Range { - file: Rc::new(self.path.clone()), - range: e.range.clone(), - }) - .unwrap_or_else(|| Location::File(Rc::new(self.path.clone()))), - message: Some(s.label().unwrap_or("Parse error").to_string()), - } - })), - } - } -} diff --git a/src/pipeline/file_loader.rs b/src/pipeline/file_loader.rs index ea7e3f4..f21542c 100644 --- a/src/pipeline/file_loader.rs +++ b/src/pipeline/file_loader.rs @@ -3,11 +3,13 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::{fs, io}; -use chumsky::text::Character; +use hashbrown::{HashMap, HashSet}; use rust_embed::RustEmbed; +use crate::error::{ErrorPosition, ProjectError, ProjectResult}; +#[allow(unused)] // for doc +use crate::facade::System; use crate::interner::Interner; -use crate::pipeline::error::{ErrorPosition, ProjectError}; use crate::utils::iter::box_once; use crate::utils::{BoxedIter, Cache}; use crate::{Stok, VName}; @@ -23,10 +25,10 @@ impl ProjectError for FileLoadingError { fn description(&self) -> &str { "Neither a file nor a directory could be read from the requested path" } - fn positions(&self) -> BoxedIter { + fn positions(&self, _i: &Interner) -> BoxedIter { box_once(ErrorPosition::just_file(self.path.clone())) } - fn message(&self) -> String { + fn message(&self, _i: &Interner) -> String { format!("File: {}\nDirectory: {}", self.file, self.dir) } } @@ -49,7 +51,7 @@ impl Loaded { } /// Returned by any source loading callback -pub type IOResult = Result>; +pub type IOResult = ProjectResult; /// Load a file from a path expressed in Rust strings, but relative to /// a root expressed as an OS Path. @@ -103,7 +105,8 @@ pub fn mk_dir_cache(root: PathBuf, i: &Interner) -> Cache { 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 = file.data.iter().map(|c| c.to_char()).collect::(); + let s = + String::from_utf8(file.data.to_vec()).expect("Embed must be valid UTF-8"); Ok(Loaded::Code(Rc::new(s))) } else { let entries = T::iter() @@ -137,3 +140,38 @@ pub fn mk_embed_cache<'a, T: 'static + RustEmbed>( load_embed::(&path, ext) }) } + +/// Load all files from an embed and convert them into a map usable in a +/// [System] +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(Rc::new(s)))) + .chain((dirs.into_iter()).map(|(k, entv)| { + (k, Loaded::Collection(Rc::new(entv.into_iter().collect()))) + })) + .collect() +} diff --git a/src/pipeline/import_abs_path.rs b/src/pipeline/import_abs_path.rs index a732228..e2fad06 100644 --- a/src/pipeline/import_abs_path.rs +++ b/src/pipeline/import_abs_path.rs @@ -1,6 +1,4 @@ -use std::rc::Rc; - -use super::error::{ProjectError, TooManySupers}; +use crate::error::{ProjectError, ProjectResult, TooManySupers}; use crate::interner::{Interner, Tok}; use crate::representations::sourcefile::absolute_path; use crate::utils::Substack; @@ -10,7 +8,7 @@ pub fn import_abs_path( mod_stack: Substack>, import_path: &[Tok], i: &Interner, -) -> Result>, Rc> { +) -> ProjectResult>> { // path of module within file let mod_pathv = mod_stack.iter().rev_vec_clone(); // path of module within compilation @@ -23,9 +21,9 @@ pub fn import_abs_path( // preload-target path within compilation absolute_path(&abs_pathv, import_path, i).map_err(|_| { TooManySupers { - path: import_path.iter().map(|t| i.r(*t)).cloned().collect(), - offender_file: src_path.iter().map(|t| i.r(*t)).cloned().collect(), - offender_mod: mod_pathv.iter().map(|t| i.r(*t)).cloned().collect(), + path: import_path.to_vec(), + offender_file: src_path.to_vec(), + offender_mod: mod_pathv, } .rc() }) diff --git a/src/pipeline/import_resolution/collect_aliases.rs b/src/pipeline/import_resolution/collect_aliases.rs index 22abe04..fd8acd9 100644 --- a/src/pipeline/import_resolution/collect_aliases.rs +++ b/src/pipeline/import_resolution/collect_aliases.rs @@ -1,9 +1,7 @@ -use std::rc::Rc; - use super::alias_map::AliasMap; use super::decls::UpdatedFn; -use crate::interner::{Interner, Tok}; -use crate::pipeline::error::{NotExported, NotFound, ProjectError}; +use crate::error::{NotExported, NotFound, ProjectError, ProjectResult}; +use crate::interner::Tok; use crate::pipeline::project_tree::split_path; use crate::representations::project::{ProjectModule, ProjectTree}; use crate::representations::tree::{ModMember, WalkErrorKind}; @@ -15,8 +13,7 @@ fn assert_visible( source: &[Tok], // must point to a file or submodule target: &[Tok], // may point to a symbol or module of any kind project: &ProjectTree, - i: &Interner, -) -> Result<(), Rc> { +) -> ProjectResult<()> { let (tgt_item, tgt_path) = unwrap_or!(target.split_last(); return Ok(())); let shared_len = source.iter().zip(tgt_path.iter()).take_while(|(a, b)| a == b).count(); @@ -27,11 +24,11 @@ fn assert_visible( WalkErrorKind::Private => unreachable!("visibility is not being checked here"), WalkErrorKind::Missing => NotFound::from_walk_error( + source, &[], &tgt_path[..vis_ignored_len], &project.0, e, - i, ) .rc(), })?; @@ -39,37 +36,51 @@ fn assert_visible( .walk_ref(&tgt_path[vis_ignored_len..], true) .map_err(|e| match e.kind { WalkErrorKind::Missing => NotFound::from_walk_error( + source, &tgt_path[..vis_ignored_len], &tgt_path[vis_ignored_len..], &project.0, e, - i, ) .rc(), WalkErrorKind::Private => { let full_path = &tgt_path[..shared_len + e.pos]; - let (file, sub) = split_path(full_path, project); - let (ref_file, ref_sub) = split_path(source, project); - NotExported { - file: i.extern_all(file), - subpath: i.extern_all(sub), - referrer_file: i.extern_all(ref_file), - referrer_subpath: i.extern_all(ref_sub), + // These errors are encountered during error reporting but they're more + // fundamental / higher prio than the error to be raised and would + // emerge nonetheless so they take over and the original error is + // swallowed + match split_path(full_path, project) { + Err(e) => + NotFound::from_walk_error(source, &[], full_path, &project.0, e) + .rc(), + Ok((file, sub)) => { + let (ref_file, ref_sub) = split_path(source, project) + .expect("Source path assumed to be valid"); + NotExported { + file: file.to_vec(), + subpath: sub.to_vec(), + referrer_file: ref_file.to_vec(), + referrer_subpath: ref_sub.to_vec(), + } + .rc() + }, } - .rc() }, })?; let tgt_item_exported = direct_parent.extra.exports.contains_key(tgt_item); let target_prefixes_source = shared_len == tgt_path.len(); if !tgt_item_exported && !target_prefixes_source { - let (file, sub) = split_path(target, project); - let (ref_file, ref_sub) = split_path(source, project); + let (file, sub) = split_path(target, project).map_err(|e| { + NotFound::from_walk_error(source, &[], target, &project.0, e).rc() + })?; + let (ref_file, ref_sub) = split_path(source, project) + .expect("The source path is assumed to be valid"); Err( NotExported { - file: i.extern_all(file), - subpath: i.extern_all(sub), - referrer_file: i.extern_all(ref_file), - referrer_subpath: i.extern_all(ref_sub), + file: file.to_vec(), + subpath: sub.to_vec(), + referrer_file: ref_file.to_vec(), + referrer_subpath: ref_sub.to_vec(), } .rc(), ) @@ -84,9 +95,8 @@ fn collect_aliases_rec( module: &ProjectModule, project: &ProjectTree, alias_map: &mut AliasMap, - i: &Interner, updated: &impl UpdatedFn, -) -> Result<(), Rc> { +) -> ProjectResult<()> { // Assume injected module has been alias-resolved let mod_path_v = path.iter().rev_vec_clone(); if !updated(&mod_path_v) { @@ -94,20 +104,18 @@ fn collect_aliases_rec( }; for (&name, target_mod_name) in module.extra.imports_from.iter() { let target_sym_v = pushed(target_mod_name, name); - assert_visible(&mod_path_v, &target_sym_v, project, i)?; + assert_visible(&mod_path_v, &target_sym_v, project)?; let sym_path_v = pushed(&mod_path_v, name); let target_mod = (project.0.walk_ref(target_mod_name, false)) .expect("checked above in assert_visible"); - let target_sym = target_mod - .extra - .exports - .get(&name) + let target_sym = (target_mod.extra.exports.get(&name)) .ok_or_else(|| { let file_len = target_mod.extra.file.as_ref().unwrap_or(target_mod_name).len(); NotFound { - file: i.extern_all(&target_mod_name[..file_len]), - subpath: i.extern_all(&target_sym_v[file_len..]), + source: Some(mod_path_v.clone()), + file: target_mod_name[..file_len].to_vec(), + subpath: target_sym_v[file_len..].to_vec(), } .rc() })? @@ -121,7 +129,6 @@ fn collect_aliases_rec( submodule, project, alias_map, - i, updated, )? } @@ -133,8 +140,7 @@ pub fn collect_aliases( module: &ProjectModule, project: &ProjectTree, alias_map: &mut AliasMap, - i: &Interner, updated: &impl UpdatedFn, -) -> Result<(), Rc> { - collect_aliases_rec(Substack::Bottom, module, project, alias_map, i, updated) +) -> ProjectResult<()> { + collect_aliases_rec(Substack::Bottom, module, project, alias_map, updated) } diff --git a/src/pipeline/import_resolution/resolve_imports.rs b/src/pipeline/import_resolution/resolve_imports.rs index 22912ab..a1d6f0c 100644 --- a/src/pipeline/import_resolution/resolve_imports.rs +++ b/src/pipeline/import_resolution/resolve_imports.rs @@ -1,11 +1,8 @@ -use std::rc::Rc; - use super::alias_map::AliasMap; use super::apply_aliases::apply_aliases; use super::collect_aliases::collect_aliases; use super::decls::{InjectedAsFn, UpdatedFn}; -use crate::interner::Interner; -use crate::pipeline::error::ProjectError; +use crate::error::ProjectResult; use crate::representations::project::ProjectTree; use crate::representations::VName; @@ -13,12 +10,11 @@ use crate::representations::VName; /// replace these aliases with the original names throughout the tree pub fn resolve_imports( project: ProjectTree, - i: &Interner, injected_as: &impl InjectedAsFn, updated: &impl UpdatedFn, -) -> Result, Rc> { +) -> ProjectResult> { let mut map = AliasMap::new(); - collect_aliases(&project.0, &project, &mut map, i, updated)?; + collect_aliases(&project.0, &project, &mut map, updated)?; let new_mod = apply_aliases(&project.0, &map, injected_as, updated); Ok(ProjectTree(new_mod)) } diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index 242ff21..2e142ab 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -1,5 +1,4 @@ //! Loading Orchid modules from source -pub mod error; pub mod file_loader; mod import_abs_path; mod import_resolution; diff --git a/src/pipeline/parse_layer.rs b/src/pipeline/parse_layer.rs index f7e41d6..8ad5798 100644 --- a/src/pipeline/parse_layer.rs +++ b/src/pipeline/parse_layer.rs @@ -1,8 +1,8 @@ use std::rc::Rc; -use super::error::ProjectError; use super::file_loader::IOResult; use super::{import_resolution, project_tree, source_loader}; +use crate::error::ProjectResult; use crate::interner::{Interner, Tok}; use crate::representations::sourcefile::FileEntry; use crate::representations::VName; @@ -22,7 +22,7 @@ pub fn parse_layer<'a>( environment: &'a ProjectTree, prelude: &[FileEntry], i: &Interner, -) -> Result, Rc> { +) -> ProjectResult> { // A path is injected if it is walkable in the injected tree let injected_as = |path: &[Tok]| { let (item, modpath) = path.split_last()?; @@ -40,7 +40,7 @@ pub fn parse_layer<'a>( let tree = project_tree::build_tree(source, i, prelude, &injected_names)?; let sum = ProjectTree(environment.0.clone().overlay(tree.0.clone())); let resolvd = - import_resolution::resolve_imports(sum, i, &injected_as, &|path| { + import_resolution::resolve_imports(sum, &injected_as, &|path| { tree.0.walk_ref(path, false).is_ok() })?; // Addition among modules favours the left hand side. diff --git a/src/pipeline/project_tree/add_prelude.rs b/src/pipeline/project_tree/add_prelude.rs index 7a10051..0334429 100644 --- a/src/pipeline/project_tree/add_prelude.rs +++ b/src/pipeline/project_tree/add_prelude.rs @@ -1,5 +1,5 @@ use crate::interner::Tok; -use crate::representations::sourcefile::{FileEntry, Member, Namespace}; +use crate::representations::sourcefile::{FileEntry, Member, ModuleBlock}; fn member_rec( // object @@ -9,9 +9,9 @@ fn member_rec( prelude: &[FileEntry], ) -> Member { match member { - Member::Namespace(Namespace { name, body }) => { + Member::Module(ModuleBlock { name, body }) => { let new_body = entv_rec(body, path, prelude); - Member::Namespace(Namespace { name, body: new_body }) + Member::Module(ModuleBlock { name, body: new_body }) }, any => any, } diff --git a/src/pipeline/project_tree/build_tree.rs b/src/pipeline/project_tree/build_tree.rs index e12ab70..9640ca9 100644 --- a/src/pipeline/project_tree/build_tree.rs +++ b/src/pipeline/project_tree/build_tree.rs @@ -1,19 +1,18 @@ -use std::rc::Rc; - use hashbrown::HashMap; use itertools::Itertools; use super::collect_ops; use super::collect_ops::InjectedOperatorsFn; use super::parse_file::parse_file; -use crate::ast::{Constant, Expr}; +use crate::ast::{Clause, Constant, Expr}; +use crate::error::{ProjectError, ProjectResult, TooManySupers}; use crate::interner::{Interner, Tok}; -use crate::pipeline::error::{ProjectError, TooManySupers}; use crate::pipeline::source_loader::{LoadedSource, LoadedSourceTable}; use crate::representations::project::{ProjectExt, ProjectTree}; use crate::representations::sourcefile::{absolute_path, FileEntry, Member}; use crate::representations::tree::{ModEntry, ModMember, Module}; use crate::representations::{NameLike, VName}; +use crate::tree::{WalkError, WalkErrorKind}; use crate::utils::iter::{box_empty, box_once}; use crate::utils::{pushed, unwrap_or, Substack}; @@ -26,25 +25,27 @@ struct ParsedSource<'a> { /// Split a path into file- and subpath in knowledge /// -/// # Panics +/// # Errors /// /// if the path is invalid +#[allow(clippy::type_complexity)] // bit too sensitive here IMO pub fn split_path<'a>( path: &'a [Tok], proj: &'a ProjectTree, -) -> (&'a [Tok], &'a [Tok]) { +) -> Result<(&'a [Tok], &'a [Tok]), WalkError> { let (end, body) = unwrap_or!(path.split_last(); { - return (&[], &[]) + return Ok((&[], &[])) }); - let mut module = (proj.0.walk_ref(body, false)) - .expect("invalid path can't have been split above"); - if let ModMember::Sub(m) = &module.items[end].member { + let mut module = (proj.0.walk_ref(body, false))?; + let entry = (module.items.get(end)) + .ok_or(WalkError { pos: path.len() - 1, kind: WalkErrorKind::Missing })?; + if let ModMember::Sub(m) = &entry.member { module = m; } let file = module.extra.file.as_ref().map(|s| &path[..s.len()]).unwrap_or(path); let subpath = &path[file.len()..]; - (file, subpath) + Ok((file, subpath)) } /// Convert normalized, prefixed source into a module @@ -63,7 +64,7 @@ fn source_to_module( // context i: &Interner, filepath_len: usize, -) -> Result, ProjectExt>, Rc> { +) -> ProjectResult, ProjectExt>> { let path_v = path.iter().rev_vec_clone(); let imports = (data.iter()) .filter_map(|ent| { @@ -73,16 +74,16 @@ fn source_to_module( .cloned() .collect::>(); let imports_from = (imports.iter()) - .map(|imp| -> Result<_, Rc> { + .map(|imp| -> ProjectResult<_> { let mut imp_path_v = i.r(imp.path).clone(); imp_path_v.push(imp.name.expect("glob imports had just been resolved")); let mut abs_path = absolute_path(&path_v, &imp_path_v, i) .expect("should have failed in preparsing"); let name = abs_path.pop().ok_or_else(|| { TooManySupers { - offender_file: i.extern_all(&path_v[..filepath_len]), - offender_mod: i.extern_all(&path_v[filepath_len..]), - path: i.extern_all(&imp_path_v), + offender_file: path_v[..filepath_len].to_vec(), + offender_mod: path_v[filepath_len..].to_vec(), + path: imp_path_v, } .rc() })?; @@ -96,15 +97,18 @@ fn source_to_module( FileEntry::Export(names) => Box::new(names.iter().copied().map(mk_ent)), FileEntry::Exported(mem) => match mem { Member::Constant(constant) => box_once(mk_ent(constant.name)), - Member::Namespace(ns) => box_once(mk_ent(ns.name)), + Member::Module(ns) => box_once(mk_ent(ns.name)), Member::Rule(rule) => { let mut names = Vec::new(); for e in rule.pattern.iter() { - e.visit_names(Substack::Bottom, &mut |n| { - if let Some([name]) = n.strip_prefix(&path_v[..]) { - names.push((*name, n.clone())) + e.search_all(&mut |e| { + if let Clause::Name(n) = &e.value { + if let Some([name]) = n.strip_prefix(&path_v[..]) { + names.push((*name, n.clone())) + } } - }) + None::<()> + }); } Box::new(names.into_iter()) }, @@ -124,7 +128,7 @@ fn source_to_module( let items = (data.into_iter()) .filter_map(|ent| { let member_to_item = |exported, member| match member { - Member::Namespace(ns) => { + Member::Module(ns) => { let new_prep = unwrap_or!( &preparsed.items[&ns.name].member => ModMember::Sub; panic!("Preparsed should include entries for all submodules") @@ -171,7 +175,7 @@ fn files_to_module( path: Substack>, files: Vec, i: &Interner, -) -> Result, ProjectExt>, Rc> { +) -> ProjectResult, ProjectExt>> { let lvl = path.len(); debug_assert!( files.iter().map(|f| f.path.len()).max().unwrap() >= lvl, @@ -190,7 +194,7 @@ fn files_to_module( let items = (files.into_iter()) .group_by(|f| f.path[lvl]) .into_iter() - .map(|(namespace, files)| -> Result<_, Rc> { + .map(|(namespace, files)| -> ProjectResult<_> { let subpath = path.push(namespace); let files_v = files.collect::>(); let module = files_to_module(subpath, files_v, i)?; @@ -217,7 +221,7 @@ pub fn build_tree( i: &Interner, prelude: &[FileEntry], injected: &impl InjectedOperatorsFn, -) -> Result, Rc> { +) -> ProjectResult> { assert!(!files.is_empty(), "A tree requires at least one module"); let ops_cache = collect_ops::mk_cache(&files, i, injected); let mut entries = files @@ -225,7 +229,7 @@ pub fn build_tree( .map(|(path, loaded)| { Ok((path, loaded, parse_file(path, &files, &ops_cache, i, prelude)?)) }) - .collect::, Rc>>()?; + .collect::>>()?; // sort by similarity, then longest-first entries.sort_unstable_by(|a, b| a.0.cmp(b.0).reverse()); let files = entries diff --git a/src/pipeline/project_tree/collect_ops/exported_ops.rs b/src/pipeline/project_tree/collect_ops/exported_ops.rs index 3c389b2..e2ece7c 100644 --- a/src/pipeline/project_tree/collect_ops/exported_ops.rs +++ b/src/pipeline/project_tree/collect_ops/exported_ops.rs @@ -3,14 +3,14 @@ use std::rc::Rc; use hashbrown::HashSet; use trait_set::trait_set; +use crate::error::{NotFound, ProjectError, ProjectResult}; use crate::interner::{Interner, Tok}; -use crate::pipeline::error::{NotFound, ProjectError}; use crate::pipeline::source_loader::LoadedSourceTable; use crate::representations::tree::WalkErrorKind; use crate::utils::{split_max_prefix, unwrap_or, Cache}; use crate::Sym; -pub type OpsResult = Result>>, Rc>; +pub type OpsResult = ProjectResult>>>; pub type ExportedOpsCache<'a> = Cache<'a, Sym, OpsResult>; trait_set! { @@ -54,12 +54,9 @@ pub fn collect_exported_ops( unreachable!("visibility is not being checked here") }, WalkErrorKind::Missing => NotFound { - file: i.extern_all(fpath), - subpath: (subpath.iter()) - .take(walk_err.pos) - .map(|t| i.r(*t)) - .cloned() - .collect(), + source: None, + file: fpath.to_vec(), + subpath: subpath[..walk_err.pos].to_vec(), } .rc(), }, diff --git a/src/pipeline/project_tree/collect_ops/ops_for.rs b/src/pipeline/project_tree/collect_ops/ops_for.rs index f866a91..c830258 100644 --- a/src/pipeline/project_tree/collect_ops/ops_for.rs +++ b/src/pipeline/project_tree/collect_ops/ops_for.rs @@ -3,9 +3,9 @@ use std::rc::Rc; use hashbrown::HashSet; use super::exported_ops::{ExportedOpsCache, OpsResult}; +use crate::error::ProjectResult; use crate::interner::{Interner, Tok}; use crate::parse::is_op; -use crate::pipeline::error::ProjectError; use crate::pipeline::import_abs_path::import_abs_path; use crate::pipeline::source_loader::LoadedSourceTable; use crate::representations::tree::{ModMember, Module}; @@ -39,16 +39,17 @@ pub fn collect_ops_for( let tree = &loaded[file].preparsed.0; let mut ret = HashSet::new(); tree_all_ops(tree, &mut ret); - tree.visit_all_imports(&mut |modpath, _module, import| { + tree.visit_all_imports(&mut |modpath, _m, import| -> ProjectResult<()> { if let Some(n) = import.name { ret.insert(n); } else { - let path = import_abs_path(file, modpath, &i.r(import.path)[..], i) - .expect("This error should have been caught during loading"); + let path = i.expect( + import_abs_path(file, modpath, &i.r(import.path)[..], i), + "This error should have been caught during loading", + ); ret.extend(ops_cache.find(&i.i(&path))?.iter().copied()); } - Ok::<_, Rc>(()) + Ok(()) })?; - ret.drain_filter(|t| !is_op(i.r(*t))); - Ok(Rc::new(ret)) + Ok(Rc::new(ret.into_iter().filter(|t| is_op(i.r(*t))).collect())) } diff --git a/src/pipeline/project_tree/normalize_imports.rs b/src/pipeline/project_tree/normalize_imports.rs index ff72906..2d32da9 100644 --- a/src/pipeline/project_tree/normalize_imports.rs +++ b/src/pipeline/project_tree/normalize_imports.rs @@ -2,7 +2,7 @@ use super::collect_ops::ExportedOpsCache; use crate::interner::{Interner, Tok}; use crate::pipeline::import_abs_path::import_abs_path; use crate::representations::sourcefile::{ - FileEntry, Import, Member, Namespace, + FileEntry, Import, Member, ModuleBlock, }; use crate::representations::tree::{ModMember, Module}; use crate::utils::iter::box_once; @@ -20,14 +20,14 @@ fn member_rec( i: &Interner, ) -> Member { match member { - Member::Namespace(Namespace { name, body }) => { + Member::Module(ModuleBlock { name, body }) => { let subprep = unwrap_or!( &preparsed.items[&name].member => ModMember::Sub; unreachable!("This name must point to a namespace") ); let new_body = entv_rec(mod_stack.push(name), subprep, body, path, ops_cache, i); - Member::Namespace(Namespace { name, body: new_body }) + Member::Module(ModuleBlock { name, body: new_body }) }, any => any, } @@ -58,10 +58,14 @@ fn entv_rec( .into_iter() .flat_map(|import| { if let Import { name: None, path } = import { - let p = import_abs_path(mod_path, mod_stack, &i.r(path)[..], i) - .expect("Should have emerged in preparsing"); - let names = (ops_cache.find(&i.i(&p))) - .expect("Should have emerged in second parsing"); + let p = i.expect( + import_abs_path(mod_path, mod_stack, &i.r(path)[..], i), + "Should have emerged in preparsing", + ); + let names = i.expect( + ops_cache.find(&i.i(&p)), + "Should have emerged in second parsing", + ); let imports = (names.iter()) .map(move |&n| Import { name: Some(n), path }) .collect::>(); diff --git a/src/pipeline/project_tree/parse_file.rs b/src/pipeline/project_tree/parse_file.rs index eed6ee3..d030ef9 100644 --- a/src/pipeline/project_tree/parse_file.rs +++ b/src/pipeline/project_tree/parse_file.rs @@ -4,9 +4,9 @@ use super::add_prelude::add_prelude; use super::collect_ops::{collect_ops_for, ExportedOpsCache}; use super::normalize_imports::normalize_imports; use super::prefix::prefix; +use crate::error::ProjectResult; use crate::interner::{Interner, Tok}; use crate::parse; -use crate::pipeline::error::ProjectError; use crate::pipeline::source_loader::LoadedSourceTable; use crate::representations::sourcefile::{normalize_namespaces, FileEntry}; @@ -24,7 +24,7 @@ pub fn parse_file( ops_cache: &ExportedOpsCache, i: &Interner, prelude: &[FileEntry], -) -> Result, Rc> { +) -> ProjectResult> { let ld = &loaded[path]; // let ops_cache = collect_ops::mk_cache(loaded, i); let ops = collect_ops_for(path, loaded, ops_cache, i)?; @@ -34,8 +34,10 @@ pub fn parse_file( ops: &ops_vec, file: Rc::new(i.extern_all(path)), }; - let entries = parse::parse(ld.text.as_str(), ctx) - .expect("This error should have been caught during loading"); + let entries = i.expect( + parse::parse2(ld.text.as_str(), ctx), + "This error should have been caught during loading", + ); let with_prelude = add_prelude(entries, path, prelude); let impnormalized = normalize_imports(&ld.preparsed.0, with_prelude, path, ops_cache, i); diff --git a/src/pipeline/project_tree/prefix.rs b/src/pipeline/project_tree/prefix.rs index 2c04d18..60719e3 100644 --- a/src/pipeline/project_tree/prefix.rs +++ b/src/pipeline/project_tree/prefix.rs @@ -1,7 +1,7 @@ use super::collect_ops::ExportedOpsCache; use crate::ast::{Constant, Rule}; use crate::interner::{Interner, Tok}; -use crate::representations::sourcefile::{FileEntry, Member, Namespace}; +use crate::representations::sourcefile::{FileEntry, Member, ModuleBlock}; use crate::utils::Substack; fn member_rec( @@ -19,9 +19,9 @@ fn member_rec( .chain(mod_stack.iter().rev_vec_clone().into_iter()) .collect::>(); match data { - Member::Namespace(Namespace { name, body }) => { + Member::Module(ModuleBlock { name, body }) => { let new_body = entv_rec(mod_stack.push(name), body, path, ops_cache, i); - Member::Namespace(Namespace { name, body: new_body }) + Member::Module(ModuleBlock { name, body: new_body }) }, Member::Constant(constant) => Member::Constant(Constant { name: constant.name, diff --git a/src/pipeline/source_loader/load_source.rs b/src/pipeline/source_loader/load_source.rs index c338afa..2c57558 100644 --- a/src/pipeline/source_loader/load_source.rs +++ b/src/pipeline/source_loader/load_source.rs @@ -1,10 +1,11 @@ use std::iter; -use std::rc::Rc; use super::loaded_source::{LoadedSource, LoadedSourceTable}; use super::preparse::preparse; +use crate::error::{ + NoTargets, ProjectError, ProjectResult, UnexpectedDirectory, +}; use crate::interner::{Interner, Tok}; -use crate::pipeline::error::{ProjectError, UnexpectedDirectory}; use crate::pipeline::file_loader::{IOResult, Loaded}; use crate::pipeline::import_abs_path::import_abs_path; use crate::representations::sourcefile::FileEntry; @@ -19,7 +20,7 @@ fn load_abs_path_rec( i: &Interner, get_source: &impl Fn(&[Tok]) -> IOResult, is_injected_module: &impl Fn(&[Tok]) -> bool, -) -> Result<(), Rc> { +) -> ProjectResult<()> { // # Termination // // Every recursion of this function either @@ -40,7 +41,7 @@ fn load_abs_path_rec( if let Some((filename, _)) = name_split { // if the filename is valid, load, preparse and record this file let text = unwrap_or!(get_source(filename)? => Loaded::Code; { - return Err(UnexpectedDirectory { path: i.extern_all(filename) }.rc()) + return Err(UnexpectedDirectory { path: filename.to_vec() }.rc()) }); let preparsed = preparse( filename.iter().map(|t| i.r(*t)).cloned().collect(), @@ -56,6 +57,9 @@ fn load_abs_path_rec( preparsed.0.visit_all_imports(&mut |modpath, _module, import| { let abs_pathv = import_abs_path(filename, modpath, &import.nonglob_path(i), i)?; + if abs_path.starts_with(&abs_pathv) { + return Ok(()); + } // recurse on imported module load_abs_path_rec( &abs_pathv, @@ -111,9 +115,11 @@ pub fn load_source<'a>( i: &Interner, get_source: &impl Fn(&[Tok]) -> IOResult, is_injected_module: &impl Fn(&[Tok]) -> bool, -) -> Result> { +) -> ProjectResult { let mut table = LoadedSourceTable::new(); + let mut any_target = false; for target in targets { + any_target |= true; load_abs_path_rec( target, &mut table, @@ -123,5 +129,5 @@ pub fn load_source<'a>( is_injected_module, )? } - Ok(table) + if any_target { Ok(table) } else { Err(NoTargets.rc()) } } diff --git a/src/pipeline/source_loader/preparse.rs b/src/pipeline/source_loader/preparse.rs index e9e826f..1628508 100644 --- a/src/pipeline/source_loader/preparse.rs +++ b/src/pipeline/source_loader/preparse.rs @@ -4,11 +4,9 @@ use std::rc::Rc; use hashbrown::HashMap; use crate::ast::Constant; +use crate::error::{ProjectError, ProjectResult, VisibilityMismatch}; use crate::interner::Interner; use crate::parse::{self, ParsingContext}; -use crate::pipeline::error::{ - ParseErrorWithPath, ProjectError, VisibilityMismatch, -}; use crate::representations::sourcefile::{ imports, normalize_namespaces, FileEntry, Member, }; @@ -38,12 +36,12 @@ fn to_module(src: &[FileEntry], prelude: &[FileEntry]) -> Module<(), ()> { let imports = imports(all_src()).cloned().collect::>(); let mut items = all_src() .filter_map(|ent| match ent { - FileEntry::Internal(Member::Namespace(ns)) => { + FileEntry::Internal(Member::Module(ns)) => { let member = ModMember::Sub(to_module(&ns.body, prelude)); let entry = ModEntry { exported: false, member }; Some((ns.name, entry)) }, - FileEntry::Exported(Member::Namespace(ns)) => { + FileEntry::Exported(Member::Module(ns)) => { let member = ModMember::Sub(to_module(&ns.body, prelude)); let entry = ModEntry { exported: true, member }; Some((ns.name, entry)) @@ -55,8 +53,8 @@ fn to_module(src: &[FileEntry], prelude: &[FileEntry]) -> Module<(), ()> { match file_entry { FileEntry::Comment(_) | FileEntry::Import(_) - | FileEntry::Internal(Member::Namespace(_)) - | FileEntry::Exported(Member::Namespace(_)) => (), + | FileEntry::Internal(Member::Module(_)) + | FileEntry::Exported(Member::Module(_)) => (), FileEntry::Export(tokv) => for tok in tokv { add_export(&mut items, *tok) @@ -89,24 +87,13 @@ pub fn preparse( source: &str, prelude: &[FileEntry], i: &Interner, -) -> Result> { +) -> ProjectResult { // Parse with no operators let ctx = ParsingContext::<&str>::new(&[], i, Rc::new(file.clone())); - let entries = parse::parse(source, ctx).map_err(|error| { - ParseErrorWithPath { - full_source: source.to_string(), - error, - path: file.clone(), - } - .rc() - })?; + let entries = parse::parse2(source, ctx)?; let normalized = normalize_namespaces(Box::new(entries.into_iter())) - .map_err(|ns| { - VisibilityMismatch { - namespace: ns.into_iter().map(|t| i.r(t)).cloned().collect(), - file: Rc::new(file.clone()), - } - .rc() + .map_err(|namespace| { + VisibilityMismatch { namespace, file: Rc::new(file.clone()) }.rc() })?; Ok(Preparsed(to_module(&normalized, prelude))) } diff --git a/src/representations/ast.rs b/src/representations/ast.rs index e5e1507..e4cfa43 100644 --- a/src/representations/ast.rs +++ b/src/representations/ast.rs @@ -6,14 +6,17 @@ 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 super::primitive::Primitive; use crate::interner::{InternedDisplay, Interner, Tok}; -use crate::utils::{map_rc, Substack}; +use crate::utils::map_rc; /// A [Clause] with associated metadata #[derive(Clone, Debug, PartialEq)] @@ -25,17 +28,6 @@ pub struct Expr { } impl Expr { - /// Obtain the contained clause - pub fn into_clause(self) -> Clause { - self.value - } - - /// Call the function on every name in this expression - pub fn visit_names(&self, binds: Substack<&N>, cb: &mut impl FnMut(&N)) { - let Expr { value, .. } = self; - value.visit_names(binds, cb); - } - /// Process all names with the given mapper. /// Return a new object if anything was processed pub fn map_names(&self, pred: &impl Fn(&N) -> Option) -> Option { @@ -49,6 +41,32 @@ impl Expr { 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 { @@ -130,7 +148,7 @@ pub enum Clause { /// eg. `(print out "hello")`, `[1, 2, 3]`, `{Some(t) => t}` S(char, Rc>>), /// A function expression, eg. `\x. x + 1` - Lambda(Rc>, Rc>>), + Lambda(Rc>>, Rc>>), /// A placeholder for macros, eg. `$name`, `...$body`, `...$lhs:1` Placeh(Placeholder), } @@ -162,7 +180,7 @@ impl Clause { if exprs.is_empty() { None } else if exprs.len() == 1 { - Some(exprs[0].clone().into_clause()) + Some(exprs[0].value.clone()) } else { Some(Self::S('(', Rc::new(exprs.to_vec()))) } @@ -176,30 +194,22 @@ impl Clause { } } - /// Recursively iterate through all "names" in an expression. - /// It also finds a lot of things that aren't names, such as all - /// bound parameters. Generally speaking, this is not a very - /// sophisticated search. - pub fn visit_names(&self, binds: Substack<&N>, cb: &mut impl FnMut(&N)) { - match self { - Clause::Lambda(arg, body) => { - arg.visit_names(binds, cb); - let new_binds = - if let Clause::Name(n) = &arg.value { binds.push(n) } else { binds }; - for x in body.iter() { - x.visit_names(new_binds, cb) - } - }, - Clause::S(_, body) => - for x in body.iter() { - x.visit_names(binds, cb) - }, - Clause::Name(name) => - if binds.iter().all(|x| x != &name) { - cb(name) - }, - _ => (), + /// 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. + 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. @@ -221,10 +231,15 @@ impl Clause { if any_some { Some(Clause::S(*c, Rc::new(new_body))) } else { None } }, Clause::Lambda(arg, body) => { - let new_arg = arg.map_names(pred); - let mut any_some = new_arg.is_some(); - let new_body = body - .iter() + 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(); @@ -232,10 +247,7 @@ impl Clause { }) .collect(); if any_some { - Some(Clause::Lambda( - new_arg.map(Rc::new).unwrap_or_else(|| arg.clone()), - Rc::new(new_body), - )) + Some(Clause::Lambda(Rc::new(new_arg), Rc::new(new_body))) } else { None } @@ -253,7 +265,7 @@ impl Clause { Self::Placeh(p) => Clause::Placeh(p), Self::P(p) => Clause::P(p), Self::Lambda(n, b) => Clause::Lambda( - map_rc(n, |n| n.transform_names(pred)), + 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( @@ -262,6 +274,32 @@ impl Clause { ), } } + + /// 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::P(_) | 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::P(_) | Clause::Placeh(_) => None, + Clause::S(_, body) => search_all_slcs(body, f), + } + } } impl Clause { @@ -319,7 +357,7 @@ impl InternedDisplay for Clause { }, Self::Lambda(arg, body) => { f.write_str("\\")?; - arg.fmt_i(f, i)?; + fmt_expr_seq(&mut arg.iter(), f, i)?; f.write_str(".")?; fmt_expr_seq(&mut body.iter(), f, i) }, @@ -360,10 +398,13 @@ impl Rule { pub fn collect_single_names(&self) -> Vec> { let mut names = Vec::new(); for e in self.pattern.iter() { - e.visit_names(Substack::Bottom, &mut |ns_name| { - if ns_name.len() == 1 { - names.push(ns_name[0]) + e.search_all(&mut |e| { + if let Clause::Name(ns_name) = &e.value { + if ns_name.len() == 1 { + names.push(ns_name[0]) + } } + None::<()> }); } names diff --git a/src/representations/ast_to_postmacro.rs b/src/representations/ast_to_postmacro.rs index d47b97a..11b0853 100644 --- a/src/representations/ast_to_postmacro.rs +++ b/src/representations/ast_to_postmacro.rs @@ -1,13 +1,14 @@ -use std::fmt::Display; use std::rc::Rc; use super::location::Location; use super::{ast, postmacro}; -use crate::utils::Substack; -use crate::Sym; +use crate::error::{ErrorPosition, ProjectError}; +use crate::utils::iter::box_once; +use crate::utils::{BoxedIter, Substack}; +use crate::{Interner, Sym}; -#[derive(Clone)] -pub enum Error { +#[derive(Debug, Clone)] +pub enum ErrorKind { /// `()` as a clause is meaningless in lambda calculus EmptyS, /// Only `(...)` may be converted to typed lambdas. `[...]` and `{...}` @@ -16,29 +17,44 @@ pub enum Error { /// Placeholders shouldn't even occur in the code during macro /// execution. Something is clearly terribly wrong Placeholder, - /// Arguments can only be [ast::Clause::Name] + /// Arguments can only be a single [ast::Clause::Name] InvalidArg, } -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::EmptyS => { - write!(f, "`()` as a clause is meaningless in lambda calculus") - }, - Error::BadGroup(_) => write!( - f, - "Only `(...)` may be converted to typed lambdas. `[...]` and \ - `{{...}}` left in the code are signs of incomplete macro execution" - ), - Error::Placeholder => write!( - f, +#[derive(Debug, Clone)] +pub struct Error { + pub location: Location, + pub kind: ErrorKind, +} +impl Error { + pub fn new(kind: ErrorKind, location: &Location) -> Self { + Self { location: location.clone(), kind } + } +} +impl ProjectError for Error { + fn description(&self) -> &str { + match self.kind { + ErrorKind::BadGroup(_) => + "Only `(...)` may be converted to typed lambdas. `[...]` 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" - ), - Error::InvalidArg => write!(f, "Arguments can only be Name nodes"), + execution,this is likely a compiler bug", } } + + fn message(&self, _i: &Interner) -> String { + match self.kind { + ErrorKind::BadGroup(char) => format!("{} block found in the code", char), + _ => self.description().to_string(), + } + } + + fn positions(&self, _i: &Interner) -> BoxedIter { + box_once(ErrorPosition { location: self.location.clone(), message: None }) + } } /// Try to convert an expression from AST format to typed lambda @@ -66,14 +82,16 @@ impl<'a> Context<'a> { /// Process an expression sequence fn exprv_rec<'a>( + location: &'a Location, v: &'a [ast::Expr], ctx: Context<'a>, ) -> Result { - let (last, rest) = v.split_last().ok_or(Error::EmptyS)?; + let (last, rest) = + (v.split_last()).ok_or_else(|| Error::new(ErrorKind::EmptyS, location))?; if rest.is_empty() { return expr_rec(&v[0], ctx); } - let f = exprv_rec(rest, ctx)?; + let f = exprv_rec(location, rest, ctx)?; let x = expr_rec(last, ctx)?; let value = postmacro::Clause::Apply(Rc::new(f), Rc::new(x)); Ok(postmacro::Expr { value, location: Location::Unknown }) @@ -86,52 +104,44 @@ fn expr_rec<'a>( ) -> Result { if let ast::Clause::S(paren, body) = value { if *paren != '(' { - return Err(Error::BadGroup(*paren)); + return Err(Error::new(ErrorKind::BadGroup(*paren), location)); } - let expr = exprv_rec(body.as_ref(), ctx)?; + let expr = exprv_rec(location, body.as_ref(), ctx)?; Ok(postmacro::Expr { value: expr.value, location: location.clone() }) } else { - let value = clause_rec(value, ctx)?; + let value = match value { + ast::Clause::P(p) => postmacro::Clause::P(p.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)), + _ => return Err(Error::new(ErrorKind::InvalidArg, location)), + }; + let body_ctx = ctx.w_name(*name); + 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), + } + }, + ast::Clause::S(paren, entries) => { + if *paren != '(' { + return Err(Error::new(ErrorKind::BadGroup(*paren), location)); + } + let expr = exprv_rec(location, entries.as_ref(), ctx)?; + expr.value + }, + ast::Clause::Placeh { .. } => + return Err(Error::new(ErrorKind::Placeholder, location)), + }; Ok(postmacro::Expr { value, location: location.clone() }) } } - -/// Process a clause -fn clause_rec<'a>( - cls: &'a ast::Clause, - ctx: Context<'a>, -) -> Result { - match cls { - ast::Clause::P(p) => Ok(postmacro::Clause::P(p.clone())), - ast::Clause::Lambda(expr, b) => { - let name = match expr.value { - ast::Clause::Name(name) => name, - ast::Clause::Placeh { .. } => return Err(Error::Placeholder), - _ => return Err(Error::InvalidArg), - }; - let body_ctx = ctx.w_name(name); - let body = exprv_rec(b.as_ref(), body_ctx)?; - Ok(postmacro::Clause::Lambda(Rc::new(body))) - }, - ast::Clause::Name(name) => { - let lvl_opt = ctx - .names - .iter() - .enumerate() - .find(|(_, n)| *n == name) - .map(|(lvl, _)| lvl); - Ok(match lvl_opt { - Some(lvl) => postmacro::Clause::LambdaArg(lvl), - None => postmacro::Clause::Constant(*name), - }) - }, - ast::Clause::S(paren, entries) => { - if *paren != '(' { - return Err(Error::BadGroup(*paren)); - } - let expr = exprv_rec(entries.as_ref(), ctx)?; - Ok(expr.value) - }, - ast::Clause::Placeh { .. } => Err(Error::Placeholder), - } -} diff --git a/src/representations/const_tree.rs b/src/representations/const_tree.rs index 0af7064..b177423 100644 --- a/src/representations/const_tree.rs +++ b/src/representations/const_tree.rs @@ -14,6 +14,7 @@ use crate::utils::{pushed, 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), @@ -40,6 +41,29 @@ impl ConstTree { pub fn tree(arr: impl IntoIterator, Self)>) -> Self { Self::Tree(arr.into_iter().collect()) } + /// Namespace the tree with the list of names + 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 + 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; diff --git a/src/representations/interpreted.rs b/src/representations/interpreted.rs index 3a0a220..731a5c7 100644 --- a/src/representations/interpreted.rs +++ b/src/representations/interpreted.rs @@ -7,6 +7,8 @@ use std::fmt::Debug; use std::ops::{Deref, DerefMut}; use std::rc::Rc; +#[allow(unused)] // for doc +use super::ast; use super::location::Location; use super::path_set::PathSet; use super::primitive::Primitive; @@ -83,9 +85,11 @@ impl ExprInst { /// across the tree. pub fn try_normalize( &self, - mapper: impl FnOnce(&Clause) -> Result<(Clause, T), E>, + mapper: impl FnOnce(&Clause, &Location) -> Result<(Clause, T), E>, ) -> Result<(Self, T), E> { - let (new_clause, extra) = mapper(&self.expr().clause)?; + let expr = self.expr(); + let (new_clause, extra) = mapper(&expr.clause, &expr.location)?; + drop(expr); self.expr_mut().clause = new_clause; Ok((self.clone(), extra)) } @@ -95,10 +99,10 @@ impl ExprInst { /// the original but is normalized independently. pub fn try_update( &self, - mapper: impl FnOnce(&Clause) -> Result<(Clause, T), E>, + mapper: impl FnOnce(&Clause, &Location) -> Result<(Clause, T), E>, ) -> Result<(Self, T), E> { let expr = self.expr(); - let (clause, extra) = mapper(&expr.clause)?; + let (clause, extra) = mapper(&expr.clause, &expr.location)?; let new_expr = Expr { clause, location: expr.location.clone() }; Ok((Self(Rc::new(RefCell::new(new_expr))), extra)) } @@ -123,6 +127,25 @@ impl ExprInst { Err(NotALiteral) } } + + /// 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::P(_) => None, + }) + } } impl Debug for ExprInst { diff --git a/src/representations/location.rs b/src/representations/location.rs index 33e0126..174f536 100644 --- a/src/representations/location.rs +++ b/src/representations/location.rs @@ -39,6 +39,36 @@ impl Location { None } } + + /// If the two locations are ranges in the same file, connect them. + /// Otherwise choose the more accurate, preferring lhs if equal. + 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: f1, range: r1 } => Location::Range { + range: match other { + Location::Range { file: f2, range: r2 } if f1 == f2 => + r1.start..r2.end, + _ => r1, + }, + file: f1, + }, + } + } + + /// Choose one of the two locations, preferring better accuracy, or lhs if + /// equal + pub fn or(self, alt: Self) -> Self { + match (&self, &alt) { + (Self::Unknown, _) => alt, + (Self::File(_), Self::Range { .. }) => alt, + _ => self, + } + } } impl Display for Location { diff --git a/src/representations/namelike.rs b/src/representations/namelike.rs index c3aec57..17ec97f 100644 --- a/src/representations/namelike.rs +++ b/src/representations/namelike.rs @@ -1,3 +1,5 @@ +use std::hash::Hash; + use crate::interner::{Interner, Tok}; /// A mutable representation of a namespaced identifier. @@ -16,7 +18,7 @@ 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 { +pub trait NameLike: 'static + Clone + Eq + Hash { /// Fully resolve the name for printing fn to_strv(&self, i: &Interner) -> Vec; } diff --git a/src/representations/sourcefile.rs b/src/representations/sourcefile.rs index 0d82a50..3dff083 100644 --- a/src/representations/sourcefile.rs +++ b/src/representations/sourcefile.rs @@ -40,7 +40,7 @@ impl Import { /// A namespace block #[derive(Debug, Clone)] -pub struct Namespace { +pub struct ModuleBlock { /// Name prefixed to all names in the block pub name: Tok, /// Prefixed entries @@ -56,7 +56,7 @@ pub enum Member { /// A constant (or function) associated with a name Constant(Constant), /// A prefixed set of other entries - Namespace(Namespace), + Module(ModuleBlock), } /// Anything we might encounter in a file @@ -94,8 +94,8 @@ pub fn normalize_namespaces( ) -> Result, Vec>> { let (mut namespaces, mut rest) = src .partition_map::, Vec<_>, _, _, _>(|ent| match ent { - FileEntry::Exported(Member::Namespace(ns)) => Either::Left((true, ns)), - FileEntry::Internal(Member::Namespace(ns)) => Either::Left((false, ns)), + FileEntry::Exported(Member::Module(ns)) => Either::Left((true, ns)), + FileEntry::Internal(Member::Module(ns)) => Either::Left((false, ns)), other => Either::Right(other), }); // Combine namespace blocks with the same name @@ -123,7 +123,7 @@ pub fn normalize_namespaces( e.push(name); e })?; - let member = Member::Namespace(Namespace { name, body }); + let member = Member::Module(ModuleBlock { name, body }); match (any_exported, any_internal) { (true, true) => Err(vec![name]), (true, false) => Ok(FileEntry::Exported(member)), diff --git a/src/rule/matcher_vectree/build.rs b/src/rule/matcher_vectree/build.rs index a19ba92..82da1de 100644 --- a/src/rule/matcher_vectree/build.rs +++ b/src/rule/matcher_vectree/build.rs @@ -112,7 +112,7 @@ fn mk_scalar(pattern: &RuleExpr) -> ScalMatcher { }, Clause::S(c, body) => ScalMatcher::S(*c, Box::new(mk_any(body))), Clause::Lambda(arg, body) => - ScalMatcher::Lambda(Box::new(mk_scalar(arg)), Box::new(mk_any(body))), + ScalMatcher::Lambda(Box::new(mk_any(arg)), Box::new(mk_any(body))), } } diff --git a/src/rule/matcher_vectree/scal_match.rs b/src/rule/matcher_vectree/scal_match.rs index 2b6c499..103c422 100644 --- a/src/rule/matcher_vectree/scal_match.rs +++ b/src/rule/matcher_vectree/scal_match.rs @@ -16,7 +16,7 @@ pub fn scal_match<'a>( (ScalMatcher::S(c1, b_mat), Clause::S(c2, body)) if c1 == c2 => any_match(b_mat, &body[..]), (ScalMatcher::Lambda(arg_mat, b_mat), Clause::Lambda(arg, body)) => { - let mut state = scal_match(arg_mat, arg)?; + let mut state = any_match(arg_mat, arg)?; state.extend(any_match(b_mat, body)?); Some(state) }, diff --git a/src/rule/matcher_vectree/shared.rs b/src/rule/matcher_vectree/shared.rs index bc67a4d..73365a2 100644 --- a/src/rule/matcher_vectree/shared.rs +++ b/src/rule/matcher_vectree/shared.rs @@ -14,7 +14,7 @@ pub enum ScalMatcher { P(Primitive), Name(Sym), S(char, Box), - Lambda(Box, Box), + Lambda(Box, Box), Placeh(Tok), } diff --git a/src/rule/prepare_rule.rs b/src/rule/prepare_rule.rs index 47b8f70..38d2fad 100644 --- a/src/rule/prepare_rule.rs +++ b/src/rule/prepare_rule.rs @@ -68,7 +68,7 @@ fn check_rec_expr( if !in_template { Err(RuleError::Multiple(*name)) } else if known != typ { - Err(RuleError::TypeMismatch(*name)) + Err(RuleError::ArityMismatch(*name)) } else { Ok(()) } @@ -79,7 +79,7 @@ fn check_rec_expr( } }, Clause::Lambda(arg, body) => { - check_rec_expr(arg.as_ref(), types, in_template)?; + check_rec_exprv(arg, types, in_template)?; check_rec_exprv(body, types, in_template) }, Clause::S(_, body) => check_rec_exprv(body, types, in_template), diff --git a/src/rule/repository.rs b/src/rule/repository.rs index c2f46df..8133777 100644 --- a/src/rule/repository.rs +++ b/src/rule/repository.rs @@ -3,6 +3,7 @@ use std::format; use std::rc::Rc; use hashbrown::HashSet; +use itertools::Itertools; use ordered_float::NotNan; use super::matcher::{Matcher, RuleExpr}; @@ -11,7 +12,6 @@ use super::state::apply_exprv; use super::{update_first_seq, RuleError, VectreeMatcher}; use crate::ast::Rule; use crate::interner::{InternedDisplay, Interner}; -use crate::utils::Substack; use crate::Sym; #[derive(Debug)] @@ -60,9 +60,7 @@ impl Repository { let rule = prepare_rule(r.clone(), i).map_err(|e| (r, e))?; let mut glossary = HashSet::new(); for e in rule.pattern.iter() { - e.visit_names(Substack::Bottom, &mut |op| { - glossary.insert(*op); - }) + glossary.extend(e.value.collect_names().into_iter()); } let matcher = M::new(Rc::new(rule.pattern.clone())); let prep = CachedRule { @@ -78,10 +76,7 @@ impl Repository { /// Attempt to run each rule in priority order once pub fn step(&self, code: &RuleExpr) -> Option { - let mut glossary = HashSet::new(); - code.visit_names(Substack::Bottom, &mut |op| { - glossary.insert(*op); - }); + let glossary = code.value.collect_names(); for (rule, deps, _) in self.cache.iter() { if !deps.is_subset(&glossary) { continue; @@ -164,8 +159,13 @@ impl InternedDisplay for Repository { i: &Interner, ) -> std::fmt::Result { writeln!(f, "Repository[")?; - for (item, _, p) in self.cache.iter() { - write!(f, "\t{}", fmt_hex(f64::from(*p)))?; + for (item, deps, p) in self.cache.iter() { + write!( + f, + " priority: {}\tdependencies: [{}]\n ", + fmt_hex(f64::from(*p)), + deps.iter().map(|t| i.extern_vec(*t).join("::")).join(", ") + )?; item.fmt_i(f, i)?; writeln!(f)?; } diff --git a/src/rule/rule_error.rs b/src/rule/rule_error.rs index d74c390..13b351b 100644 --- a/src/rule/rule_error.rs +++ b/src/rule/rule_error.rs @@ -1,6 +1,13 @@ use std::fmt; +use std::rc::Rc; +use hashbrown::HashSet; + +use crate::ast::{self, search_all_slcs, PHClass, Placeholder, Rule}; +use crate::error::{ErrorPosition, ProjectError}; use crate::interner::{InternedDisplay, Interner, Tok}; +use crate::utils::BoxedIter; +use crate::{Location, Sym}; /// Various reasons why a substitution rule may be invalid #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -8,12 +15,23 @@ pub enum RuleError { /// A key is present in the template but not the pattern Missing(Tok), /// A key uses a different arity in the template and in the pattern - TypeMismatch(Tok), + ArityMismatch(Tok), /// Multiple occurences of a placeholder in a pattern Multiple(Tok), /// Two vectorial placeholders are next to each other VecNeighbors(Tok, Tok), } +impl RuleError { + /// Convert into a unified error trait object shared by all Orchid errors + pub fn to_project_error(self, rule: &Rule) -> Rc { + match self { + RuleError::Missing(name) => Missing::new(rule, name).rc(), + RuleError::Multiple(name) => Multiple::new(rule, name).rc(), + RuleError::ArityMismatch(name) => ArityMismatch::new(rule, name).rc(), + RuleError::VecNeighbors(n1, n2) => VecNeighbors::new(rule, n1, n2).rc(), + } + } +} impl InternedDisplay for RuleError { fn fmt_i(&self, f: &mut fmt::Formatter<'_>, i: &Interner) -> fmt::Result { @@ -21,7 +39,7 @@ impl InternedDisplay for RuleError { Self::Missing(key) => { write!(f, "Key {:?} not in match pattern", i.r(key)) }, - Self::TypeMismatch(key) => write!( + Self::ArityMismatch(key) => write!( f, "Key {:?} used inconsistently with and without ellipsis", i.r(key) @@ -38,3 +56,181 @@ impl InternedDisplay for RuleError { } } } + +/// A key is present in the template but not the pattern of a rule +#[derive(Debug)] +pub struct Missing { + locations: HashSet, + name: Tok, +} +impl Missing { + pub fn new(rule: &ast::Rule, 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 ph.name == name { + locations.insert(e.location.clone()); + } + } + None::<()> + }); + } + Self { locations, name } + } +} +impl ProjectError for Missing { + fn description(&self) -> &str { + "A key appears in the template but not the pattern of a rule" + } + fn message(&self, i: &Interner) -> String { + format!( + "The key {} appears in the template but not the pattern of this rule", + i.r(self.name) + ) + } + fn positions(&self, _i: &Interner) -> BoxedIter { + Box::new( + (self.locations.iter()) + .cloned() + .map(|location| ErrorPosition { location, message: None }), + ) + } +} + +/// A key is present multiple times in the pattern of a rule +#[derive(Debug)] +pub struct Multiple { + locations: HashSet, + name: Tok, +} +impl Multiple { + pub fn new(rule: &ast::Rule, 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 ph.name == name { + locations.insert(e.location.clone()); + } + } + None::<()> + }); + } + Self { locations, name } + } +} +impl ProjectError for Multiple { + fn description(&self) -> &str { + "A key appears multiple times in the pattern of a rule" + } + fn message(&self, i: &Interner) -> String { + format!("The key {} appears multiple times in this pattern", i.r(self.name)) + } + fn positions(&self, _i: &Interner) -> BoxedIter { + Box::new( + (self.locations.iter()) + .cloned() + .map(|location| ErrorPosition { location, message: None }), + ) + } +} + +/// A key is present multiple times in the pattern of a rule +#[derive(Debug)] +pub struct ArityMismatch { + locations: HashSet<(Location, ast::PHClass)>, + name: Tok, +} +impl ArityMismatch { + pub fn new(rule: &ast::Rule, 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 ph.name == name { + locations.insert((e.location.clone(), ph.class)); + } + } + None::<()> + }); + } + Self { locations, name } + } +} +impl ProjectError for ArityMismatch { + fn description(&self) -> &str { + "A key appears with different arities in a rule" + } + fn message(&self, i: &Interner) -> String { + format!( + "The key {} appears multiple times with different arities in this rule", + i.r(self.name) + ) + } + fn positions(&self, _i: &Interner) -> 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::Vec { nonzero: true, .. } => "one or more clauses", + ast::PHClass::Vec { nonzero: false, .. } => + "any number of clauses", + }, + ), + } + })) + } +} + +/// Two vectorial placeholders appear next to each other +#[derive(Debug)] +pub struct VecNeighbors { + locations: HashSet, + n1: Tok, + n2: Tok, +} +impl VecNeighbors { + pub fn new(rule: &ast::Rule, 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( + Placeholder{ class: PHClass::Vec { .. }, name } + ) if name == n1); + let b_vec = matches!(b.value, ast::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()); + } + } + None::<()> + }); + Self { locations, n1, n2 } + } +} +impl ProjectError for VecNeighbors { + fn description(&self) -> &str { + "Two vectorial placeholders appear next to each other" + } + fn message(&self, i: &Interner) -> String { + format!( + "The keys {} and {} appear next to each other with a vectorial arity", + i.r(self.n1), + i.r(self.n2) + ) + } + fn positions(&self, _i: &Interner) -> BoxedIter { + Box::new( + (self.locations.iter()) + .cloned() + .map(|location| ErrorPosition { location, message: None }), + ) + } +} diff --git a/src/rule/state.rs b/src/rule/state.rs index ca85211..a567d12 100644 --- a/src/rule/state.rs +++ b/src/rule/state.rs @@ -1,7 +1,6 @@ use std::rc::Rc; use hashbrown::HashMap; -use itertools::Itertools; use super::matcher::RuleExpr; use crate::ast::{Clause, Expr, PHClass, Placeholder}; @@ -44,12 +43,7 @@ pub fn apply_expr(template: &RuleExpr, state: &State) -> Vec { Clause::Lambda(arg, body) => vec![Expr { location: location.clone(), value: Clause::Lambda( - Rc::new( - apply_expr(arg.as_ref(), state) - .into_iter() - .exactly_one() - .expect("Lambda arguments can only ever be scalar"), - ), + 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 fd8625a..8988804 100644 --- a/src/rule/update_first_seq.rs +++ b/src/rule/update_first_seq.rs @@ -34,8 +34,8 @@ pub fn clause>) -> Option>>>( match c { Clause::P(_) | Clause::Placeh { .. } | Clause::Name { .. } => None, Clause::Lambda(arg, body) => - if let Some(arg) = expr(arg.as_ref(), pred) { - Some(Clause::Lambda(Rc::new(arg), body.clone())) + if let Some(arg) = exprv(arg.clone(), pred) { + Some(Clause::Lambda(arg, body.clone())) } else { exprv(body.clone(), pred).map(|body| Clause::Lambda(arg.clone(), body)) }, diff --git a/src/stl/bool.orc b/src/stl/bool.orc deleted file mode 100644 index 61dc6cf..0000000 --- a/src/stl/bool.orc +++ /dev/null @@ -1,6 +0,0 @@ -export not := \bool. if bool then false else true -export ...$a != ...$b =0x3p36=> (not (...$a == ...$b)) -export ...$a == ...$b =0x3p36=> (equals (...$a) (...$b)) -export if ...$cond then ...$true else ...$false:1 =0x1p84=> ( - ifthenelse (...$cond) (...$true) (...$false) -) \ No newline at end of file diff --git a/src/stl/fn.orc b/src/stl/fn.orc deleted file mode 100644 index ce9a4b6..0000000 --- a/src/stl/fn.orc +++ /dev/null @@ -1,26 +0,0 @@ -import super::known::* - ---[ Do nothing. Especially useful as a passive cps operation ]-- -export identity := \x.x ---[ - Apply the function to the given value. Can be used to assign a - concrete value in a cps assignment statement. -]-- -export pass := \val.\cont. cont val ---[ - Apply the function to the given pair of values. Mainly useful to assign - a concrete pair of values in a cps multi-assignment statement -]-- -export pass2 := \a.\b.\cont. cont a b ---[ - A function that returns the given value for any input. Also useful as a - "break" statement in a "do" block. -]-- -export const := \a. \b.a - -export ...$prefix $ ...$suffix:1 =0x1p38=> ...$prefix (...$suffix) -export ...$prefix |> $fn ..$suffix:1 =0x2p32=> $fn (...$prefix) ..$suffix - -export ($name) => ...$body =0x2p129=> (\$name. ...$body) -export ($name, ...$argv) => ...$body =0x2p129=> (\$name. (...$argv) => ...$body) -$name => ...$body =0x1p129=> (\$name. ...$body) \ No newline at end of file diff --git a/src/stl/io/command.rs b/src/stl/io/command.rs deleted file mode 100644 index 26d4b6e..0000000 --- a/src/stl/io/command.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::io::{self, Write}; - -use super::super::runtime_error::RuntimeError; -use crate::atomic_inert; -use crate::interpreter::{HandlerParm, HandlerRes}; -use crate::representations::interpreted::{Clause, ExprInst}; -use crate::representations::{Literal, Primitive}; -use crate::utils::unwrap_or; - - - -/// An IO command to be handled by the host application. -#[derive(Clone, Debug)] -pub enum IO { - /// Print a string to standard output and resume execution - Print(String, ExprInst), - /// Read a line from standard input and pass it to the calback - Readline(ExprInst), -} -atomic_inert!(IO); - -/// Default xommand handler for IO actions -pub fn handle(effect: HandlerParm) -> HandlerRes { - // Downcast command - let io: &IO = unwrap_or!(effect.as_any().downcast_ref(); Err(effect)?); - // Interpret and execute - Ok(match io { - IO::Print(str, cont) => { - print!("{}", str); - io::stdout() - .flush() - .map_err(|e| RuntimeError::ext(e.to_string(), "writing to stdout"))?; - cont.clone() - }, - IO::Readline(cont) => { - let mut buf = String::new(); - io::stdin() - .read_line(&mut buf) - .map_err(|e| RuntimeError::ext(e.to_string(), "reading from stdin"))?; - buf.pop(); - let x = Clause::P(Primitive::Literal(Literal::Str(buf))).wrap(); - Clause::Apply { f: cont.clone(), x }.wrap() - }, - }) -} diff --git a/src/stl/io/mod.rs b/src/stl/io/mod.rs deleted file mode 100644 index bf12ab1..0000000 --- a/src/stl/io/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::interner::Interner; -use crate::ConstTree; - -mod command; -mod inspect; -mod panic; -mod print; -mod readline; - -pub use command::{handle, IO}; - -pub fn io(i: &Interner, allow_impure: bool) -> ConstTree { - let pure = ConstTree::tree([( - i.i("io"), - ConstTree::tree([ - (i.i("print"), ConstTree::xfn(print::Print)), - (i.i("readline"), ConstTree::xfn(readline::ReadLn)), - (i.i("panic"), ConstTree::xfn(panic::Panic)), - ]), - )]); - if !allow_impure { - pure - } else { - pure - + ConstTree::tree([( - i.i("io"), - ConstTree::tree([(i.i("debug"), ConstTree::xfn(inspect::Inspect))]), - )]) - } -} diff --git a/src/stl/io/print.rs b/src/stl/io/print.rs deleted file mode 100644 index 1823c65..0000000 --- a/src/stl/io/print.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::fmt::Debug; - -use super::super::inspect::with_str; -use super::command::IO; -use crate::foreign::{Atomic, AtomicResult, AtomicReturn}; -use crate::interpreter::Context; -use crate::representations::interpreted::ExprInst; -use crate::{atomic_defaults, write_fn_step}; - -write_fn_step! { - /// Wrap a string and the continuation into an [IO] event to be evaluated by - /// the embedder. - pub Print > Print1 -} -write_fn_step! { - Print1 {} - Print0 where message = x => with_str(x, |s| Ok(s.clone())); -} - -#[derive(Debug, Clone)] -struct Print0 { - message: String, - expr_inst: ExprInst, -} -impl Atomic for Print0 { - atomic_defaults!(); - fn run(&self, ctx: Context) -> AtomicResult { - let command = IO::Print(self.message.clone(), self.expr_inst.clone()); - Ok(AtomicReturn::from_data(command, ctx)) - } -} diff --git a/src/stl/io/readline.rs b/src/stl/io/readline.rs deleted file mode 100644 index 78b4d41..0000000 --- a/src/stl/io/readline.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::fmt::Debug; - -use super::command::IO; -use crate::foreign::{Atomic, AtomicResult, AtomicReturn}; -use crate::interpreter::Context; -use crate::representations::interpreted::ExprInst; -use crate::{atomic_defaults, write_fn_step}; - -write_fn_step! { - /// Create an [IO] event that reads a line form standard input and calls the - /// continuation with it. - pub ReadLn > ReadLn1 -} - -#[derive(Debug, Clone)] -struct ReadLn1 { - expr_inst: ExprInst, -} -impl Atomic for ReadLn1 { - atomic_defaults!(); - fn run(&self, ctx: Context) -> AtomicResult { - let command = IO::Readline(self.expr_inst.clone()); - Ok(AtomicReturn::from_data(command, ctx)) - } -} diff --git a/src/stl/known.orc b/src/stl/known.orc deleted file mode 100644 index f162235..0000000 --- a/src/stl/known.orc +++ /dev/null @@ -1 +0,0 @@ -export ::(,) \ No newline at end of file diff --git a/src/stl/mk_stl.rs b/src/stl/mk_stl.rs deleted file mode 100644 index 5c240f7..0000000 --- a/src/stl/mk_stl.rs +++ /dev/null @@ -1,70 +0,0 @@ -use hashbrown::HashMap; -use rust_embed::RustEmbed; - -use super::bin::bin; -use super::bool::bool; -use super::conv::conv; -use super::io::io; -use super::num::num; -use super::str::str; -use crate::interner::Interner; -use crate::pipeline::file_loader::mk_embed_cache; -use crate::pipeline::parse_layer; -use crate::representations::VName; -use crate::sourcefile::{FileEntry, Import}; -use crate::{from_const_tree, ProjectTree}; - -/// Feature flags for the STL. -#[derive(Default)] -pub struct StlOptions { - /// Whether impure functions (such as io::debug) are allowed. An embedder - /// would typically disable this flag - pub impure: bool, -} - -#[derive(RustEmbed)] -#[folder = "src/stl"] -#[prefix = "std/"] -#[include = "*.orc"] -struct StlEmbed; - -// TODO: fix all orc modules to not rely on prelude - -/// Build the standard library used by the interpreter by combining the other -/// libraries -pub fn mk_stl(i: &Interner, options: StlOptions) -> ProjectTree { - let const_tree = from_const_tree( - HashMap::from([( - i.i("std"), - io(i, options.impure) + conv(i) + bool(i) + str(i) + num(i) + bin(i), - )]), - &[i.i("std")], - ); - let ld_cache = mk_embed_cache::(".orc", i); - let targets = StlEmbed::iter() - .map(|path| { - path - .strip_suffix(".orc") - .expect("the embed is filtered for suffix") - .split('/') - .map(|segment| i.i(segment)) - .collect::>() - }) - .collect::>(); - parse_layer( - targets.iter().map(|v| &v[..]), - &|p| ld_cache.find(p), - &const_tree, - &[], - i, - ) - .expect("Parse error in STL") -} - -/// Generate prelude lines to be injected to every module compiled with the STL -pub fn mk_prelude(i: &Interner) -> Vec { - vec![FileEntry::Import(vec![Import { - path: i.i(&[i.i("std"), i.i("prelude")][..]), - name: None, - }])] -} diff --git a/src/stl/mod.rs b/src/stl/mod.rs deleted file mode 100644 index abf31a2..0000000 --- a/src/stl/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Constants exposed to usercode by the interpreter -mod arithmetic_error; -mod assertion_error; -mod bool; -mod conv; -mod io; -pub mod inspect; -mod mk_stl; -mod num; -mod runtime_error; -mod str; -pub mod codegen; -mod bin; - -pub use arithmetic_error::ArithmeticError; -pub use assertion_error::AssertionError; -pub use io::{handle as handleIO, IO}; -pub use mk_stl::{mk_prelude, mk_stl, StlOptions}; -pub use runtime_error::RuntimeError; - -pub use self::bool::Boolean; -pub use self::num::Numeric; diff --git a/src/stl/num.orc b/src/stl/num.orc deleted file mode 100644 index 5ef82d3..0000000 --- a/src/stl/num.orc +++ /dev/null @@ -1,5 +0,0 @@ -export ...$a + ...$b =0x2p36=> (add (...$a) (...$b)) -export ...$a - ...$b:1 =0x2p36=> (subtract (...$a) (...$b)) -export ...$a * ...$b =0x1p36=> (multiply (...$a) (...$b)) -export ...$a % ...$b:1 =0x1p36=> (remainder (...$a) (...$b)) -export ...$a / ...$b:1 =0x1p36=> (divide (...$a) (...$b)) diff --git a/src/stl/option.orc b/src/stl/option.orc deleted file mode 100644 index c09534f..0000000 --- a/src/stl/option.orc +++ /dev/null @@ -1,9 +0,0 @@ -import std::io::panic - -export some := \v. \d.\f. f v -export none := \d.\f. d - -export map := \option.\f. option none f -export flatten := \option. option none \opt. opt -export flatmap := \option.\f. option none \opt. map opt f -export unwrap := \option. option (panic "value expected") \x.x \ No newline at end of file diff --git a/src/stl/proc.orc b/src/stl/proc.orc deleted file mode 100644 index 1884b1a..0000000 --- a/src/stl/proc.orc +++ /dev/null @@ -1,16 +0,0 @@ -import super::fn::=> - --- remove duplicate ;-s -export do { ...$statement ; ; ...$rest:1 } =0x3p130=> do { ...$statement ; ...$rest } -export do { ...$statement ; ...$rest:1 } =0x2p130=> statement (...$statement) do { ...$rest } -export do { ...$return } =0x1p130=> ...$return - -export statement (let $name = ...$value) ...$next =0x1p230=> ( - ( \$name. ...$next) (...$value) -) -export statement (cps ...$names = ...$operation:1) ...$next =0x2p230=> ( - (...$operation) ( (...$names) => ...$next ) -) -export statement (cps ...$operation) ...$next =0x1p230=> ( - (...$operation) (...$next) -) \ No newline at end of file diff --git a/src/stl/str.orc b/src/stl/str.orc deleted file mode 100644 index 6f1f314..0000000 --- a/src/stl/str.orc +++ /dev/null @@ -1,10 +0,0 @@ -import super::(proc::*, bool::*, io::panic) - -export ...$a ++ ...$b =0x4p36=> (concat (...$a) (...$b)) - -export char_at := \s.\i. do{ - let slc = slice s i 1; - if len slc == 1 - then slc - else panic "Character index out of bounds" -} \ No newline at end of file diff --git a/src/stl/assertion_error.rs b/src/systems/assertion_error.rs similarity index 100% rename from src/stl/assertion_error.rs rename to src/systems/assertion_error.rs diff --git a/src/systems/asynch/mod.rs b/src/systems/asynch/mod.rs new file mode 100644 index 0000000..0ebfbe9 --- /dev/null +++ b/src/systems/asynch/mod.rs @@ -0,0 +1,5 @@ +mod system; +mod types; + +pub use system::{AsynchConfig, InfiniteBlock}; +pub use types::{Asynch, MessagePort}; diff --git a/src/systems/asynch/system.rs b/src/systems/asynch/system.rs new file mode 100644 index 0000000..69e86e4 --- /dev/null +++ b/src/systems/asynch/system.rs @@ -0,0 +1,183 @@ +use std::any::{type_name, Any, TypeId}; +use std::cell::RefCell; +use std::fmt::{Debug, Display}; +use std::rc::Rc; +use std::sync::mpsc::Sender; +use std::time::Duration; + +use hashbrown::HashMap; +use ordered_float::NotNan; + +use super::types::MessagePort; +use super::Asynch; +use crate::facade::{IntoSystem, System}; +use crate::foreign::cps_box::{init_cps, CPSBox}; +use crate::foreign::ExternError; +use crate::interpreted::ExprInst; +use crate::interpreter::HandlerTable; +use crate::systems::codegen::call; +use crate::systems::stl::Boolean; +use crate::utils::{unwrap_or, PollEvent, Poller}; +use crate::{atomic_inert, define_fn, ConstTree, Interner}; + +#[derive(Debug, Clone)] +struct Timer { + recurring: Boolean, + duration: NotNan, +} +define_fn! {expr=x in + SetTimer { + recurring: Boolean, + duration: NotNan + } => Ok(init_cps(2, Timer{ + recurring: *recurring, + duration: *duration + })) +} + +#[derive(Clone)] +struct CancelTimer(Rc); +impl Debug for CancelTimer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "opaque cancel operation") + } +} + +#[derive(Clone, Debug)] +struct Yield; +atomic_inert!(Yield, "a yield command"); + +/// Error indicating a yield command when all event producers and timers had +/// exited +pub struct InfiniteBlock; +impl ExternError for InfiniteBlock {} +impl Display for InfiniteBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + static MSG: &str = "User code yielded, but there are no timers or event \ + producers to wake it up in the future"; + write!(f, "{}", MSG) + } +} + +impl MessagePort for Sender> { + fn send(&mut self, message: T) { + let _ = Self::send(self, Box::new(message)); + } +} + +impl MessagePort for F +where + F: FnMut(Box) + Send + Clone + 'static, +{ + fn send(&mut self, message: T) { + self(Box::new(message)) + } +} + +type AnyHandler<'a> = Box) -> Option + 'a>; + +/// Datastructures the asynch system will eventually be constructed from +pub struct AsynchConfig<'a> { + poller: Poller, ExprInst, ExprInst>, + sender: Sender>, + handlers: HashMap>, +} +impl<'a> AsynchConfig<'a> { + /// Create a new async event loop that allows registering handlers and taking + /// references to the port before it's converted into a [System] + pub fn new() -> Self { + let (sender, poller) = Poller::new(); + Self { poller, sender, handlers: HashMap::new() } + } +} +impl<'a> Asynch for AsynchConfig<'a> { + type Port = Sender>; + + fn register( + &mut self, + mut f: impl FnMut(Box) -> Option + 'a, + ) { + let cb = move |a: Box| f(a.downcast().expect("keyed by TypeId")); + let prev = self.handlers.insert(TypeId::of::(), Box::new(cb)); + assert!( + prev.is_none(), + "Duplicate handlers for async event {}", + type_name::() + ) + } + + fn get_port(&self) -> Self::Port { + self.sender.clone() + } +} + +impl<'a> Default for AsynchConfig<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> IntoSystem<'a> for AsynchConfig<'a> { + fn into_system(self, i: &Interner) -> 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: &CPSBox| { + let mut polly = polly.borrow_mut(); + let (timeout, action, cont) = t.unpack2(); + let duration = Duration::from_secs_f64(*timeout.duration); + let cancel_timer = if timeout.recurring.0 { + CancelTimer(Rc::new(polly.set_interval(duration, action.clone()))) + } else { + CancelTimer(Rc::new(polly.set_timeout(duration, action.clone()))) + }; + Ok(call(cont.clone(), [init_cps(1, cancel_timer).wrap()]).wrap()) + } + }); + handler_table.register(move |t: &CPSBox| { + let (command, cont) = t.unpack1(); + command.0.as_ref()(); + Ok(cont.clone()) + }); + handler_table.register({ + let polly = polly.clone(); + move |_: &Yield| { + let mut polly = polly.borrow_mut(); + loop { + let next = unwrap_or!(polly.run(); + return Err(InfiniteBlock.into_extern()) + ); + match next { + PollEvent::Once(expr) => return Ok(expr), + PollEvent::Recurring(expr) => return Ok(expr), + PollEvent::Event(ev) => { + let handler = (handlers.get_mut(&ev.as_ref().type_id())) + .unwrap_or_else(|| { + panic!("Unhandled messgae type: {:?}", ev.type_id()) + }); + if let Some(expr) = handler(ev) { + return Ok(expr); + } + }, + } + } + } + }); + System { + name: vec!["system".to_string(), "asynch".to_string()], + constants: ConstTree::namespace( + [i.i("system"), i.i("async")], + ConstTree::tree([ + (i.i("set_timer"), ConstTree::xfn(SetTimer)), + (i.i("yield"), ConstTree::atom(Yield)), + ]), + ) + .unwrap_tree(), + code: HashMap::new(), + prelude: Vec::new(), + handlers: handler_table, + } + } +} diff --git a/src/systems/asynch/types.rs b/src/systems/asynch/types.rs new file mode 100644 index 0000000..8a418dc --- /dev/null +++ b/src/systems/asynch/types.rs @@ -0,0 +1,30 @@ +use crate::interpreted::ExprInst; + +/// A thread-safe handle that can be used to send events of any type +pub trait MessagePort: Send + Clone + 'static { + /// Send an event. Any type is accepted, handlers are dispatched by type ID + fn send(&mut self, message: T); +} + +pub trait Asynch { + /// A thread-safe handle that can be used to push events into the dispatcher + type Port: MessagePort; + + /// Register a function that will be called synchronously when an event of the + /// accepted type is dispatched. Only one handler may be specified for each + /// event type. The handler may choose to process the event autonomously, or + /// return an Orchid thunk for the interpreter to execute. + /// + /// # Panics + /// + /// When the function is called with an argument type it was previously called + /// with + fn register( + &mut self, + f: impl FnMut(Box) -> Option + 'static, + ); + + /// Return a handle that can be passed to worker threads and used to push + /// events onto the dispatcher + fn get_port(&self) -> Self::Port; +} diff --git a/src/stl/inspect.rs b/src/systems/cast_exprinst.rs similarity index 81% rename from src/stl/inspect.rs rename to src/systems/cast_exprinst.rs index e4e0c8e..d711b24 100644 --- a/src/stl/inspect.rs +++ b/src/systems/cast_exprinst.rs @@ -2,8 +2,10 @@ //! nature of [ExprInst], returning a reference to [Literal] is not possible. use std::rc::Rc; +use ordered_float::NotNan; + use super::assertion_error::AssertionError; -use crate::foreign::{ExternError, Atomic}; +use crate::foreign::{Atomic, ExternError}; use crate::interpreted::Clause; use crate::representations::interpreted::ExprInst; use crate::representations::Literal; @@ -48,6 +50,20 @@ pub fn with_uint( }) } +/// Like [with_lit] but also unwraps [Literal::Num] +pub fn with_num( + x: &ExprInst, + predicate: impl FnOnce(NotNan) -> Result>, +) -> Result> { + with_lit(x, |l| { + if let Literal::Num(n) = l { + predicate(*n) + } else { + AssertionError::fail(x.clone(), "a float")? + } + }) +} + /// Tries to cast the [ExprInst] into the specified atom type. Throws an /// assertion error if unsuccessful, or calls the provided function on the /// extracted atomic type. @@ -91,4 +107,12 @@ impl TryFrom<&ExprInst> for u64 { fn try_from(value: &ExprInst) -> Result { with_uint(value, Ok) } -} \ No newline at end of file +} + +impl TryFrom<&ExprInst> for NotNan { + type Error = Rc; + + fn try_from(value: &ExprInst) -> Result { + with_num(value, Ok) + } +} diff --git a/src/stl/codegen.rs b/src/systems/codegen.rs similarity index 70% rename from src/stl/codegen.rs rename to src/systems/codegen.rs index cc67388..87bdc0d 100644 --- a/src/stl/codegen.rs +++ b/src/systems/codegen.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use crate::interpreted::{Clause, ExprInst}; +use crate::utils::unwrap_or; use crate::{PathSet, Side}; /// Convert a rust Option into an Orchid Option @@ -11,7 +12,9 @@ pub fn orchid_opt(x: Option) -> Clause { } /// Constructs an instance of the orchid value Some wrapping the given -/// [ExprInst] +/// [ExprInst]. +/// +/// Takes two expressions and calls the second with the given data fn some(x: ExprInst) -> Clause { Clause::Lambda { args: None, @@ -24,6 +27,8 @@ fn some(x: ExprInst) -> Clause { } /// Constructs an instance of the orchid value None +/// +/// Takes two expressions and returns the first fn none() -> Clause { Clause::Lambda { args: Some(PathSet { steps: Rc::new(vec![]), next: None }), @@ -39,8 +44,14 @@ pub fn tuple(data: Vec) -> Clause { next: None, steps: Rc::new(data.iter().map(|_| Side::Left).collect()), }), - body: data - .into_iter() + body: (data.into_iter()) .fold(Clause::LambdaArg.wrap(), |f, x| Clause::Apply { f, x }.wrap()), } } + +/// 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 }) +} diff --git a/src/systems/io/bindings.rs b/src/systems/io/bindings.rs new file mode 100644 index 0000000..5769e00 --- /dev/null +++ b/src/systems/io/bindings.rs @@ -0,0 +1,103 @@ +use super::flow::IOCmdHandlePack; +use super::instances::{ + BRead, ReadCmd, SRead, SinkHandle, SourceHandle, WriteCmd, +}; +use crate::foreign::cps_box::init_cps; +use crate::foreign::{Atom, Atomic}; +use crate::systems::stl::Binary; +use crate::systems::RuntimeError; +use crate::{ast, define_fn, ConstTree, Interner, Primitive}; + +define_fn! { + ReadString = |x| Ok(init_cps(3, IOCmdHandlePack{ + cmd: ReadCmd::RStr(SRead::All), + handle: x.try_into()? + })) +} +define_fn! { + ReadLine = |x| Ok(init_cps(3, IOCmdHandlePack{ + cmd: ReadCmd::RStr(SRead::Line), + handle: x.try_into()? + })) +} +define_fn! { + ReadBin = |x| Ok(init_cps(3, IOCmdHandlePack{ + cmd: ReadCmd::RBytes(BRead::All), + handle: x.try_into()? + })) +} +define_fn! { + ReadBytes { + stream: SourceHandle, + n: u64 + } => Ok(init_cps(3, IOCmdHandlePack{ + cmd: ReadCmd::RBytes(BRead::N((*n).try_into().unwrap())), + handle: *stream + })) +} +define_fn! { + ReadUntil { + stream: SourceHandle, + pattern: u64 + } => { + let delim = (*pattern).try_into().map_err(|_| RuntimeError::ext( + "greater than 255".to_string(), + "converting number to byte" + ))?; + Ok(init_cps(3, IOCmdHandlePack{ + cmd: ReadCmd::RBytes(BRead::Until(delim)), + handle: *stream + })) + } +} +define_fn! { + WriteStr { + stream: SinkHandle, + string: String + } => Ok(init_cps(3, IOCmdHandlePack { + cmd: WriteCmd::WStr(string.clone()), + handle: *stream, + })) +} +define_fn! { + WriteBin { + stream: SinkHandle, + bytes: Binary + } => Ok(init_cps(3, IOCmdHandlePack { + cmd: WriteCmd::WBytes(bytes.clone()), + handle: *stream + })) +} +define_fn! { + Flush = |x| Ok(init_cps(3, IOCmdHandlePack { + cmd: WriteCmd::Flush, + handle: x.try_into()? + })) +} + +pub fn io_bindings( + i: &Interner, + std_streams: impl IntoIterator)>, +) -> ConstTree { + ConstTree::namespace( + [i.i("system"), i.i("io")], + ConstTree::tree([ + (i.i("read_string"), ConstTree::xfn(ReadString)), + (i.i("read_line"), ConstTree::xfn(ReadLine)), + (i.i("read_bin"), ConstTree::xfn(ReadBin)), + (i.i("read_n_bytes"), ConstTree::xfn(ReadBytes)), + (i.i("read_until"), ConstTree::xfn(ReadUntil)), + (i.i("write_str"), ConstTree::xfn(WriteStr)), + (i.i("write_bin"), ConstTree::xfn(WriteBin)), + (i.i("flush"), ConstTree::xfn(Flush)), + ]) + ConstTree::Tree( + std_streams + .into_iter() + .map(|(n, at)| { + let expr = ast::Clause::P(Primitive::Atom(Atom(at))).into_expr(); + (i.i(n), ConstTree::Const(expr)) + }) + .collect(), + ), + ) +} diff --git a/src/systems/io/facade.rs b/src/systems/io/facade.rs new file mode 100644 index 0000000..eb349e2 --- /dev/null +++ b/src/systems/io/facade.rs @@ -0,0 +1,154 @@ +#![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, Import}; +use crate::systems::asynch::{Asynch, MessagePort}; +use crate::Interner; + +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( + mut get_port: impl FnMut() -> P, + on_sink_close: Option>, + on_source_close: Option>, + global_streams: ST, + ) -> Self { + Self { + read_system: Rc::new(RefCell::new(IOManager::new( + get_port(), + on_source_close, + ))), + write_system: Rc::new(RefCell::new(IOManager::new( + 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 impl Asynch, + on_sink_close: Option>, + on_source_close: Option>, + std_streams: impl IntoIterator, +) -> IOSystem { + let this = IOSystem::new( + || asynch.get_port(), + on_sink_close, + on_source_close, + std_streams, + ); + let (r, w) = (this.read_system.clone(), this.write_system.clone()); + asynch.register(move |event| r.borrow_mut().dispatch(*event)); + asynch.register(move |event| w.borrow_mut().dispatch(*event)); + this +} + +impl<'a, P: MessagePort, 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::Import(vec![Import { + path: i.i(&vec![i.i("system"), i.i("io"), i.i("prelude")]), + name: None, + }])], + handlers, + } + } +} diff --git a/src/systems/io/flow.rs b/src/systems/io/flow.rs new file mode 100644 index 0000000..1a42fda --- /dev/null +++ b/src/systems/io/flow.rs @@ -0,0 +1,179 @@ +use std::collections::VecDeque; +use std::fmt::Display; + +use hashbrown::HashMap; + +use crate::foreign::ExternError; +use crate::systems::asynch::MessagePort; +use crate::utils::{take_with_output, Task}; +use crate::ThreadPool; + +pub trait StreamHandle: Clone + Send { + fn new(id: usize) -> Self; + fn id(&self) -> usize; +} + +pub trait IOHandler { + type Product; + + fn handle(self, result: Cmd::Result) -> Self::Product; +} + +pub trait IOResult: Send { + type Handler; + type HandlerProduct; + + fn handle(self, handler: Self::Handler) -> Self::HandlerProduct; +} + +pub struct IOEvent { + pub result: Cmd::Result, + pub stream: Cmd::Stream, + pub handle: Cmd::Handle, +} + +pub trait IOCmd: Send { + type Stream: Send; + type Result: Send; + type Handle: StreamHandle; + + fn execute(self, stream: &mut Self::Stream) -> Self::Result; +} + +pub struct IOTask { + pub cmd: Cmd, + pub stream: Cmd::Stream, + pub handle: Cmd::Handle, + pub port: P, +} + +impl Task for IOTask { + fn run(self) { + let Self { cmd, handle, mut port, mut stream } = self; + let result = cmd.execute(&mut stream); + port.send(IOEvent:: { handle, result, stream }) + } +} + +#[derive(Debug, Clone)] +pub struct IOCmdHandlePack { + pub cmd: Cmd, + pub handle: Cmd::Handle, +} + +enum StreamState> { + Free(Cmd::Stream), + Busy { handler: H, queue: VecDeque<(Cmd, H)>, closing: bool }, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub struct NoActiveStream(usize); +impl ExternError for NoActiveStream {} +impl Display for NoActiveStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "The stream {} had already been closed", self.0) + } +} + +pub struct IOManager> { + next_id: usize, + streams: HashMap>, + on_close: Option>, + thread_pool: ThreadPool>, + port: P, +} + +impl> IOManager { + pub fn new(port: P, on_close: Option>) -> Self { + Self { + next_id: 0, + streams: HashMap::new(), + thread_pool: ThreadPool::new(), + on_close, + port, + } + } + + pub fn add_stream(&mut self, stream: Cmd::Stream) -> Cmd::Handle { + let id = self.next_id; + self.next_id += 1; + self.streams.insert(id, StreamState::Free(stream)); + Cmd::Handle::new(id) + } + + fn dispose_stream(&mut self, stream: Cmd::Stream) { + match &mut self.on_close { + Some(f) => f(stream), + None => drop(stream), + } + } + + pub fn close_stream( + &mut self, + handle: Cmd::Handle, + ) -> Result<(), NoActiveStream> { + let state = + (self.streams.remove(&handle.id())).ok_or(NoActiveStream(handle.id()))?; + match state { + StreamState::Free(stream) => self.dispose_stream(stream), + StreamState::Busy { handler, queue, closing } => { + let new_state = StreamState::Busy { handler, queue, closing: true }; + self.streams.insert(handle.id(), new_state); + if closing { + return Err(NoActiveStream(handle.id())); + } + }, + } + Ok(()) + } + + pub fn command( + &mut self, + handle: Cmd::Handle, + cmd: Cmd, + new_handler: H, + ) -> Result<(), NoActiveStream> { + let state_mut = (self.streams.get_mut(&handle.id())) + .ok_or(NoActiveStream(handle.id()))?; + take_with_output(state_mut, |state| match state { + StreamState::Busy { closing: true, .. } => + (state, Err(NoActiveStream(handle.id()))), + StreamState::Busy { handler, mut queue, closing: false } => { + queue.push_back((cmd, new_handler)); + (StreamState::Busy { handler, queue, closing: false }, Ok(())) + }, + StreamState::Free(stream) => { + let task = IOTask { cmd, stream, handle, port: self.port.clone() }; + self.thread_pool.submit(task); + let new_state = StreamState::Busy { + handler: new_handler, + queue: VecDeque::new(), + closing: false, + }; + (new_state, Ok(())) + }, + }) + } + + pub fn dispatch(&mut self, event: IOEvent) -> Option { + let IOEvent { handle, result, stream } = event; + let id = handle.id(); + let state = + (self.streams.remove(&id)).expect("Event dispatched on unknown stream"); + let (handler, mut queue, closing) = match state { + StreamState::Busy { handler, queue, closing } => + (handler, queue, closing), + _ => panic!("Event dispatched but the source isn't locked"), + }; + if let Some((cmd, handler)) = queue.pop_front() { + let port = self.port.clone(); + self.thread_pool.submit(IOTask { handle, stream, cmd, port }); + self.streams.insert(id, StreamState::Busy { handler, queue, closing }); + } else if closing { + self.dispose_stream(stream) + } else { + self.streams.insert(id, StreamState::Free(stream)); + }; + Some(handler.handle(result)) + } +} diff --git a/src/systems/io/instances.rs b/src/systems/io/instances.rs new file mode 100644 index 0000000..0f0391a --- /dev/null +++ b/src/systems/io/instances.rs @@ -0,0 +1,160 @@ +use std::io::{self, BufRead, BufReader, Read, Write}; +use std::sync::Arc; + +use super::flow::{IOCmd, IOHandler, IOManager, StreamHandle}; +use crate::foreign::Atomic; +use crate::interpreted::ExprInst; +use crate::systems::codegen::call; +use crate::systems::stl::Binary; +use crate::{atomic_inert, Literal}; + +pub type Source = BufReader>; +pub type Sink = Box; + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct SourceHandle(usize); +atomic_inert!(SourceHandle, "an input stream handle"); +impl StreamHandle for SourceHandle { + fn new(id: usize) -> Self { + Self(id) + } + fn id(&self) -> usize { + self.0 + } +} +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct SinkHandle(usize); +atomic_inert!(SinkHandle, "an output stream handle"); +impl StreamHandle for SinkHandle { + fn new(id: usize) -> Self { + Self(id) + } + fn id(&self) -> usize { + self.0 + } +} + +/// String reading command +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum SRead { + All, + Line, +} + +/// Binary reading command +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum BRead { + All, + N(usize), + Until(u8), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ReadCmd { + RBytes(BRead), + RStr(SRead), +} + +impl IOCmd for ReadCmd { + type Stream = Source; + type Result = ReadResult; + type Handle = SourceHandle; + + // This is a buggy rule, check manually + #[allow(clippy::read_zero_byte_vec)] + fn execute(self, stream: &mut Self::Stream) -> Self::Result { + match self { + Self::RBytes(bread) => { + let mut buf = Vec::new(); + let result = match &bread { + BRead::All => stream.read_to_end(&mut buf).map(|_| ()), + BRead::Until(b) => stream.read_until(*b, &mut buf).map(|_| ()), + BRead::N(n) => { + buf.resize(*n, 0); + stream.read_exact(&mut buf) + }, + }; + ReadResult::RBin(bread, result.map(|_| buf)) + }, + Self::RStr(sread) => { + let mut buf = String::new(); + let sresult = match &sread { + SRead::All => stream.read_to_string(&mut buf), + SRead::Line => stream.read_line(&mut buf), + }; + ReadResult::RStr(sread, sresult.map(|_| buf)) + }, + } + } +} + +/// Reading command (string or binary) +pub enum ReadResult { + RStr(SRead, io::Result), + RBin(BRead, io::Result>), +} + +impl IOHandler for (ExprInst, ExprInst) { + type Product = ExprInst; + + fn handle(self, result: ReadResult) -> Self::Product { + let (succ, fail) = self; + match result { + ReadResult::RBin(_, Err(e)) | ReadResult::RStr(_, Err(e)) => + call(fail, vec![wrap_io_error(e)]).wrap(), + ReadResult::RBin(_, Ok(bytes)) => + call(succ, vec![Binary(Arc::new(bytes)).atom_cls().wrap()]).wrap(), + ReadResult::RStr(_, Ok(text)) => + call(succ, vec![Literal::Str(text).into()]).wrap(), + } + } +} + +/// Placeholder function for an eventual conversion from [io::Error] to Orchid +/// data +fn wrap_io_error(_e: io::Error) -> ExprInst { + Literal::Uint(0u64).into() +} + +pub type ReadManager

= IOManager; + +/// Writing command (string or binary) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum WriteCmd { + WBytes(Binary), + WStr(String), + Flush, +} + +impl IOCmd for WriteCmd { + type Stream = Sink; + type Handle = SinkHandle; + type Result = WriteResult; + + fn execute(self, stream: &mut Self::Stream) -> Self::Result { + let result = match &self { + Self::Flush => stream.flush(), + Self::WStr(str) => write!(stream, "{}", str).map(|_| ()), + Self::WBytes(bytes) => stream.write_all(bytes.0.as_ref()).map(|_| ()), + }; + WriteResult { result, cmd: self } + } +} + +pub struct WriteResult { + pub cmd: WriteCmd, + pub result: io::Result<()>, +} +impl IOHandler for (ExprInst, ExprInst) { + type Product = ExprInst; + + fn handle(self, result: WriteResult) -> Self::Product { + let (succ, fail) = self; + match result.result { + Ok(_) => succ, + Err(e) => call(fail, vec![wrap_io_error(e)]).wrap(), + } + } +} + +pub type WriteManager

= IOManager; diff --git a/src/systems/io/io.orc b/src/systems/io/io.orc new file mode 100644 index 0000000..07c1fbd --- /dev/null +++ b/src/systems/io/io.orc @@ -0,0 +1,31 @@ +import std::panic +import system::io +import system::async::yield + +export const print := \text.\ok. ( + io::write_str io::stdout text + (io::flush io::stdout + ok + (\e. panic "println threw on flush") + yield + ) + (\e. panic "print threw on write") + yield +) + +export const println := \line.\ok. ( + print (line ++ "\n") ok +) + +export const readln := \ok. ( + io::read_line io::stdin + ok + (\e. panic "readln threw") + yield +) + +export module prelude ( + import super::* + + export ::(print, println, readln) +) diff --git a/src/systems/io/mod.rs b/src/systems/io/mod.rs new file mode 100644 index 0000000..86fd421 --- /dev/null +++ b/src/systems/io/mod.rs @@ -0,0 +1,6 @@ +mod bindings; +mod facade; +mod flow; +mod instances; + +pub use facade::{io_system, IOStream, IOSystem}; diff --git a/src/systems/mod.rs b/src/systems/mod.rs new file mode 100644 index 0000000..3e5ca13 --- /dev/null +++ b/src/systems/mod.rs @@ -0,0 +1,13 @@ +//! Constants exposed to usercode by the interpreter +mod assertion_error; +mod asynch; +pub mod cast_exprinst; +pub mod codegen; +mod io; +mod runtime_error; +pub mod stl; + +pub use assertion_error::AssertionError; +pub use asynch::{AsynchConfig, InfiniteBlock, MessagePort}; +pub use io::{io_system, IOStream, IOSystem}; +pub use runtime_error::RuntimeError; diff --git a/src/stl/runtime_error.rs b/src/systems/runtime_error.rs similarity index 100% rename from src/stl/runtime_error.rs rename to src/systems/runtime_error.rs diff --git a/src/stl/arithmetic_error.rs b/src/systems/stl/arithmetic_error.rs similarity index 100% rename from src/stl/arithmetic_error.rs rename to src/systems/stl/arithmetic_error.rs diff --git a/src/stl/bin.rs b/src/systems/stl/bin.rs similarity index 83% rename from src/stl/bin.rs rename to src/systems/stl/bin.rs index cd323c3..9104746 100644 --- a/src/stl/bin.rs +++ b/src/systems/stl/bin.rs @@ -1,28 +1,20 @@ use std::fmt::Debug; -use std::rc::Rc; +use std::sync::Arc; use itertools::Itertools; -use super::codegen::{orchid_opt, tuple}; -use super::inspect::{with_atom, with_uint}; -use super::{RuntimeError, Boolean}; -use crate::foreign::ExternError; +use super::Boolean; use crate::interpreted::ExprInst; +use crate::systems::cast_exprinst::with_uint; +use crate::systems::codegen::{orchid_opt, tuple}; +use crate::systems::RuntimeError; use crate::utils::{iter_find, unwrap_or}; use crate::{atomic_inert, define_fn, ConstTree, Interner, Literal}; /// A block of binary data -#[derive(Clone)] -pub struct Binary(pub Rc>); -atomic_inert!(Binary); - -impl TryFrom<&ExprInst> for Binary { - type Error = Rc; - - fn try_from(value: &ExprInst) -> Result { - with_atom(value, "a blob", |a: &Binary| Ok(a.clone())) - } -} +#[derive(Clone, Hash, PartialEq, Eq)] +pub struct Binary(pub Arc>); +atomic_inert!(Binary, "a binary blob"); impl Debug for Binary { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -58,7 +50,7 @@ define_fn! {expr=x in } else { data.to_be_bytes()[8 - *size as usize..].to_vec() }; - Ok(Binary(Rc::new(bytes)).to_atom_cls()) + Ok(Binary(Arc::new(bytes)).atom_cls()) } } @@ -99,7 +91,7 @@ define_fn! {expr=x in /// Append two binary data blocks pub Concatenate { a: Binary, b: Binary } => { let data = a.0.iter().chain(b.0.iter()).copied().collect(); - Ok(Binary(Rc::new(data)).to_atom_cls()) + Ok(Binary(Arc::new(data)).atom_cls()) } } @@ -117,7 +109,7 @@ define_fn! {expr=x in )? } let data = s.0[*i as usize..*i as usize + *len as usize].to_vec(); - Ok(Binary(Rc::new(data)).to_atom_cls()) + Ok(Binary(Arc::new(data)).atom_cls()) } } @@ -144,15 +136,15 @@ define_fn! {expr=x in } let (asl, bsl) = bin.0.split_at(*i as usize); Ok(tuple(vec![ - Binary(Rc::new(asl.to_vec())).to_atom_cls().into(), - Binary(Rc::new(bsl.to_vec())).to_atom_cls().into(), + Binary(Arc::new(asl.to_vec())).atom_cls().into(), + Binary(Arc::new(bsl.to_vec())).atom_cls().into(), ])) } } define_fn! { /// Detect the number of bytes in the binary data block - pub Size = |x: &ExprInst| { + pub Size = |x| { Ok(Literal::Uint(Binary::try_from(x)?.0.len() as u64).into()) } } @@ -165,7 +157,7 @@ pub fn bin(i: &Interner) -> ConstTree { (i.i("slice"), ConstTree::xfn(Slice)), (i.i("find"), ConstTree::xfn(Find)), (i.i("split"), ConstTree::xfn(Split)), - (i.i("size"), ConstTree::xfn(Size)) + (i.i("size"), ConstTree::xfn(Size)), ]), )]) } diff --git a/src/systems/stl/bool.orc b/src/systems/stl/bool.orc new file mode 100644 index 0000000..f86f260 --- /dev/null +++ b/src/systems/stl/bool.orc @@ -0,0 +1,6 @@ +export const not := \bool. if bool then false else true +export macro ...$a != ...$b =0x3p36=> (not (...$a == ...$b)) +export macro ...$a == ...$b =0x3p36=> (equals (...$a) (...$b)) +export macro if ...$cond then ...$true else ...$false:1 =0x1p84=> ( + ifthenelse (...$cond) (...$true) (...$false) +) diff --git a/src/stl/bool.rs b/src/systems/stl/bool.rs similarity index 80% rename from src/stl/bool.rs rename to src/systems/stl/bool.rs index a7a3091..f9c28e6 100644 --- a/src/stl/bool.rs +++ b/src/systems/stl/bool.rs @@ -1,17 +1,14 @@ use std::rc::Rc; -use crate::foreign::ExternError; use crate::interner::Interner; use crate::representations::interpreted::{Clause, ExprInst}; -use crate::stl::AssertionError; +use crate::systems::AssertionError; use crate::{atomic_inert, define_fn, ConstTree, Literal, PathSet}; -use super::inspect::with_atom; - /// Booleans exposed to Orchid #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Boolean(pub bool); -atomic_inert!(Boolean); +atomic_inert!(Boolean, "a boolean"); impl From for Boolean { fn from(value: bool) -> Self { @@ -19,14 +16,6 @@ impl From for Boolean { } } -impl TryFrom<&ExprInst> for Boolean { - type Error = Rc; - - fn try_from(value: &ExprInst) -> Result { - with_atom(value, "a boolean", |b| Ok(*b)) - } -} - define_fn! {expr=x in /// Compares the inner values if /// @@ -38,11 +27,8 @@ define_fn! {expr=x in (Literal::Uint(i1), Literal::Uint(i2)) => i1 == i2, (Literal::Num(n1), Literal::Uint(u1)) => *n1 == (*u1 as f64), (Literal::Uint(u1), Literal::Num(n1)) => *n1 == (*u1 as f64), - (..) => AssertionError::fail( - b.clone().into(), - "the expected type" - )?, - }).to_atom_cls()) + (..) => AssertionError::fail(b.clone().into(), "the expected type")?, + }).atom_cls()) } // Even though it's a ternary function, IfThenElse is implemented as an unary @@ -50,7 +36,7 @@ define_fn! {expr=x in define_fn! { /// Takes a boolean and two branches, runs the first if the bool is true, the /// second if it's false. - IfThenElse = |x: &ExprInst| x.try_into() + IfThenElse = |x| x.try_into() .map_err(|_| AssertionError::ext(x.clone(), "a boolean")) .map(|b: Boolean| if b.0 {Clause::Lambda { args: Some(PathSet { steps: Rc::new(vec![]), next: None }), diff --git a/src/stl/conv.rs b/src/systems/stl/conv.rs similarity index 93% rename from src/stl/conv.rs rename to src/systems/stl/conv.rs index ea5c421..9bc6d75 100644 --- a/src/stl/conv.rs +++ b/src/systems/stl/conv.rs @@ -1,11 +1,12 @@ use chumsky::Parser; use ordered_float::NotNan; -use super::inspect::with_lit; -use super::{ArithmeticError, AssertionError}; +use super::ArithmeticError; use crate::foreign::ExternError; use crate::interner::Interner; use crate::parse::{float_parser, int_parser}; +use crate::systems::cast_exprinst::with_lit; +use crate::systems::AssertionError; use crate::{define_fn, ConstTree, Literal}; define_fn! { diff --git a/src/systems/stl/fn.orc b/src/systems/stl/fn.orc new file mode 100644 index 0000000..2e640ad --- /dev/null +++ b/src/systems/stl/fn.orc @@ -0,0 +1,26 @@ +import super::known::* + +--[ Do nothing. Especially useful as a passive cps operation ]-- +export const identity := \x.x +--[ + Apply the function to the given value. Can be used to assign a + concrete value in a cps assignment statement. +]-- +export const pass := \val.\cont. cont val +--[ + Apply the function to the given pair of values. Mainly useful to assign + a concrete pair of values in a cps multi-assignment statement +]-- +export const pass2 := \a.\b.\cont. cont a b +--[ + A function that returns the given value for any input. Also useful as a + "break" statement in a "do" block. +]-- +export const return := \a. \b.a + +export macro ...$prefix $ ...$suffix:1 =0x1p38=> ...$prefix (...$suffix) +export macro ...$prefix |> $fn ..$suffix:1 =0x2p32=> $fn (...$prefix) ..$suffix + +export macro ($name) => ...$body =0x2p129=> (\$name. ...$body) +export macro ($name, ...$argv) => ...$body =0x2p129=> (\$name. (...$argv) => ...$body) +macro $name => ...$body =0x1p129=> (\$name. ...$body) diff --git a/src/stl/io/inspect.rs b/src/systems/stl/inspect.rs similarity index 77% rename from src/stl/io/inspect.rs rename to src/systems/stl/inspect.rs index 521e1aa..9ce82c0 100644 --- a/src/stl/io/inspect.rs +++ b/src/systems/stl/inspect.rs @@ -4,12 +4,12 @@ use crate::foreign::{Atomic, AtomicReturn}; use crate::interner::InternedDisplay; use crate::interpreter::Context; use crate::representations::interpreted::ExprInst; -use crate::{atomic_defaults, write_fn_step}; +use crate::{atomic_defaults, write_fn_step, ConstTree, Interner}; write_fn_step! { /// Print and return whatever expression is in the argument without /// normalizing it. - pub Inspect > Inspect1 + Inspect > Inspect1 } #[derive(Debug, Clone)] @@ -27,3 +27,7 @@ impl Atomic for Inspect1 { }) } } + +pub fn inspect(i: &Interner) -> ConstTree { + ConstTree::tree([(i.i("inspect"), ConstTree::xfn(Inspect))]) +} diff --git a/src/systems/stl/known.orc b/src/systems/stl/known.orc new file mode 100644 index 0000000..7e4b7d2 --- /dev/null +++ b/src/systems/stl/known.orc @@ -0,0 +1 @@ +export ::[,] diff --git a/src/stl/list.orc b/src/systems/stl/list.orc similarity index 69% rename from src/stl/list.orc rename to src/systems/stl/list.orc index 7100375..6afc6b2 100644 --- a/src/stl/list.orc +++ b/src/systems/stl/list.orc @@ -1,13 +1,13 @@ import super::(option, fn::*, proc::*, loop::*, bool::*, known::*, num::*) -pair := \a.\b. \f. f a b +const pair := \a.\b. \f. f a b -- Constructors -export cons := \hd.\tl. option::some (pair hd tl) -export end := option::none +export const cons := \hd.\tl. option::some (pair hd tl) +export const end := option::none -export pop := \list.\default.\f.list default \cons.cons f +export const pop := \list.\default.\f.list default \cons.cons f -- Operators @@ -15,7 +15,7 @@ export pop := \list.\default.\f.list default \cons.cons f Fold each element into an accumulator using an `acc -> el -> acc`. This evaluates the entire list, and is always tail recursive. ]-- -export fold := \list.\acc.\f. ( +export const fold := \list.\acc.\f. ( loop_over (list, acc) { cps head, list = pop list acc; let acc = f acc head; @@ -26,7 +26,7 @@ export fold := \list.\acc.\f. ( Fold each element into an accumulator in reverse order. This evaulates the entire list, and is never tail recursive. ]-- -export rfold := \list.\acc.\f. ( +export const rfold := \list.\acc.\f. ( recursive r (list) pop list acc \head.\tail. f (r tail) head @@ -36,7 +36,7 @@ export rfold := \list.\acc.\f. ( Fold each element into a shared element with an `el -> el -> el`. This evaluates the entire list, and is never tail recursive. ]-- -export reduce := \list.\f. do{ +export const reduce := \list.\f. do{ cps head, list = pop list option::none; option::some $ fold list head f } @@ -45,7 +45,7 @@ export reduce := \list.\f. do{ Return a new list that contains only the elements from the input list for which the function returns true. This operation is lazy. ]-- -export filter := \list.\f. ( +export const filter := \list.\f. ( pop list end \head.\tail. if (f el) then cons el (filter tail f) @@ -55,7 +55,7 @@ export filter := \list.\f. ( --[ Transform each element of the list with an `el -> any`. ]-- -export map := \list.\f. ( +export const map := \list.\f. ( recursive r (list) pop list end \head.\tail. cons (f head) (r tail) @@ -65,11 +65,11 @@ export map := \list.\f. ( Skip `n` elements from the list and return the tail If `n` is not an integer, this returns `end`. ]-- -export skip := \list.\n. ( - loop_over (list, n) { - cps _head, list = if n == 0 - then const list - else pop list 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; } ) @@ -78,7 +78,7 @@ export skip := \list.\n. ( Return `n` elements from the list and discard the rest. This operation is lazy. ]-- -export take := \list.\n. ( +export const take := \list.\n. ( recursive r (list, n) if n == 0 then end @@ -90,18 +90,18 @@ export take := \list.\n. ( Return the `n`th element from the list. This operation is tail recursive. ]-- -export get := \list.\n. ( +export const get := \list.\n. ( loop_over (list, n) { cps head, list = pop list option::none; cps if n == 0 - then const (option::some head) + then return (option::some head) else identity; let n = n - 1; } ) -new[...$item, ...$rest:1] =0x2p84=> (cons (...$item) new[...$rest]) -new[...$end] =0x1p84=> (cons (...$end) end) -new[] =0x1p84=> end +macro new[...$item, ...$rest:1] =0x2p84=> (cons (...$item) new[...$rest]) +macro new[...$end] =0x1p84=> (cons (...$end) end) +macro new[] =0x1p84=> end export ::(new) diff --git a/src/stl/loop.orc b/src/systems/stl/loop.orc similarity index 60% rename from src/stl/loop.orc rename to src/systems/stl/loop.orc index 62114e9..f7b7e9b 100644 --- a/src/stl/loop.orc +++ b/src/systems/stl/loop.orc @@ -6,7 +6,7 @@ import super::known::* recommended to use one of the wrappers such as [recursive] or [loop_over] instead. ]-- -export Y := \f.(\x.f (x x))(\x.f (x x)) +export const Y := \f.(\x.f (x x))(\x.f (x x)) --[ A syntax construct that encapsulates the Y combinator and encourages @@ -16,7 +16,7 @@ export Y := \f.(\x.f (x x))(\x.f (x x)) To break out of the loop, use [std::fn::const] in a cps statement ]-- -export loop_over (..$binds) { +export macro loop_over (..$binds) { ...$body } =0x5p129=> Y (\r. def_binds parse_binds (..$binds) do{ @@ -26,38 +26,38 @@ export loop_over (..$binds) { ) init_binds parse_binds (..$binds) -- parse_binds builds a conslist -parse_binds (...$item, ...$tail:1) =0x2p250=> ( +macro parse_binds (...$item, ...$tail:1) =0x2p250=> ( parse_bind (...$item) parse_binds (...$tail) ) -parse_binds (...$item) =0x1p250=> ( +macro parse_binds (...$item) =0x1p250=> ( parse_bind (...$item) () ) -- parse_bind converts items to pairs -parse_bind ($name) =0x1p250=> ($name bind_no_value) -parse_bind ($name = ...$value) =0x1p250=> ($name (...$value)) +macro parse_bind ($name) =0x1p250=> ($name bind_no_value) +macro parse_bind ($name = ...$value) =0x1p250=> ($name (...$value)) -- def_binds creates name bindings for everything -def_binds ( ($name $value) $tail ) ...$body =0x1p250=> ( +macro def_binds ( ($name $value) $tail ) ...$body =0x1p250=> ( \$name. def_binds $tail ...$body ) -def_binds () ...$body =0x1p250=> ...$body +macro def_binds () ...$body =0x1p250=> ...$body -- init_binds passes the value for initializers -init_binds ( ($name bind_no_value) $tail ) =0x2p250=> $name init_binds $tail -init_binds ( ($name $value) $tail ) =0x1p250=> $value init_binds $tail +macro init_binds ( ($name bind_no_value) $tail ) =0x2p250=> $name init_binds $tail +macro init_binds ( ($name $value) $tail ) =0x1p250=> $value init_binds $tail -- avoid empty templates by assuming that there is a previous token -$fn init_binds () =0x1p250=> $fn +macro $fn init_binds () =0x1p250=> $fn -- apply_binds passes the name for initializers -apply_binds ( ($name $_value) $tail ) =0x1p250=> $name apply_binds $tail -$fn apply_binds () =0x1p250=> $fn +macro apply_binds ( ($name $_value) $tail ) =0x1p250=> $name apply_binds $tail +macro $fn apply_binds () =0x1p250=> $fn --[ Alias for the Y-combinator to avoid some universal pitfalls ]-- -export recursive $name (..$binds) ...$body =0x5p129=> Y (\$name. +export macro recursive $name (..$binds) ...$body =0x5p129=> Y (\$name. def_binds parse_binds (..$binds) ...$body -) init_binds parse_binds (..$binds) \ No newline at end of file +) init_binds parse_binds (..$binds) diff --git a/src/stl/map.orc b/src/systems/stl/map.orc similarity index 66% rename from src/stl/map.orc rename to src/systems/stl/map.orc index 943a0c8..b4d2c50 100644 --- a/src/stl/map.orc +++ b/src/systems/stl/map.orc @@ -1,14 +1,14 @@ import super::(bool::*, fn::*, known::*, list, option, loop::*, proc::*) -import std::io::panic +import std::panic -- utilities for using lists as pairs -export fst := \l. ( +export const fst := \l. ( list::get l 0 (panic "nonempty expected") \x.x ) -export snd := \l. ( +export const snd := \l. ( list::get l 1 (panic "2 elements expected") \x.x @@ -16,8 +16,8 @@ export snd := \l. ( -- constructors -export empty := list::end -export add := \m.\k.\v. ( +export const empty := list::end +export const add := \m.\k.\v. ( list::cons list::new[k, v] m @@ -26,11 +26,11 @@ export add := \m.\k.\v. ( -- queries -- return the last occurrence of a key if exists -export get := \m.\key. ( +export const get := \m.\key. ( loop_over (m) { cps record, m = list::pop m option::none; cps if fst record == key - then const $ option::some $ snd record + then return $ option::some $ snd record else identity; } ) @@ -38,7 +38,7 @@ export get := \m.\key. ( -- commands -- remove one occurrence of a key -export del := \m.\k. ( +export const del := \m.\k. ( recursive r (m) list::pop m list::end \head.\tail. if fst head == k then tail @@ -46,28 +46,28 @@ export del := \m.\k. ( ) -- remove all occurrences of a key -export delall := \m.\k. ( +export const delall := \m.\k. ( list::filter m \record. fst record != k ) -- replace at most one occurrence of a key -export set := \m.\k.\v. ( +export const set := \m.\k.\v. ( m |> del k |> add k v ) -- ensure that there's only one instance of each key in the map -export normalize := \m. ( +export const normalize := \m. ( recursive r (m, normal=empty) with list::pop m normal \head.\tail. r tail $ set normal (fst head) (snd head) ) -new[...$tail:2, ...$key = ...$value:1] =0x2p84=> ( +macro new[...$tail:2, ...$key = ...$value:1] =0x2p84=> ( set new[...$tail] (...$key) (...$value) ) -new[...$key = ...$value:1] =0x1p84=> (add empty (...$key) (...$value)) -new[] =0x1p84=> empty +macro new[...$key = ...$value:1] =0x1p84=> (add empty (...$key) (...$value)) +macro new[] =0x1p84=> empty -export ::(new) \ No newline at end of file +export ::(new) diff --git a/src/systems/stl/mod.rs b/src/systems/stl/mod.rs new file mode 100644 index 0000000..b5c0974 --- /dev/null +++ b/src/systems/stl/mod.rs @@ -0,0 +1,17 @@ +//! Basic types and their functions, frequently used tools with no environmental +//! dependencies. +mod arithmetic_error; +mod bin; +mod bool; +mod conv; +mod inspect; +mod num; +mod panic; +mod stl_system; +mod str; +pub use arithmetic_error::ArithmeticError; +pub use bin::Binary; +pub use num::Numeric; +pub use stl_system::StlConfig; + +pub use self::bool::Boolean; diff --git a/src/systems/stl/num.orc b/src/systems/stl/num.orc new file mode 100644 index 0000000..2abdafe --- /dev/null +++ b/src/systems/stl/num.orc @@ -0,0 +1,5 @@ +export macro ...$a + ...$b =0x2p36=> (add (...$a) (...$b)) +export macro ...$a - ...$b:1 =0x2p36=> (subtract (...$a) (...$b)) +export macro ...$a * ...$b =0x1p36=> (multiply (...$a) (...$b)) +export macro ...$a % ...$b:1 =0x1p36=> (remainder (...$a) (...$b)) +export macro ...$a / ...$b:1 =0x1p36=> (divide (...$a) (...$b)) diff --git a/src/stl/num.rs b/src/systems/stl/num.rs similarity index 97% rename from src/stl/num.rs rename to src/systems/stl/num.rs index d26f544..a60f4f9 100644 --- a/src/stl/num.rs +++ b/src/systems/stl/num.rs @@ -2,11 +2,12 @@ use std::rc::Rc; use ordered_float::NotNan; -use super::inspect::with_lit; -use super::{ArithmeticError, AssertionError}; +use super::ArithmeticError; use crate::foreign::ExternError; use crate::representations::interpreted::{Clause, ExprInst}; use crate::representations::{Literal, Primitive}; +use crate::systems::cast_exprinst::with_lit; +use crate::systems::AssertionError; use crate::{define_fn, ConstTree, Interner}; // region: Numeric, type to handle floats and uints together diff --git a/src/systems/stl/option.orc b/src/systems/stl/option.orc new file mode 100644 index 0000000..ab070eb --- /dev/null +++ b/src/systems/stl/option.orc @@ -0,0 +1,9 @@ +import std::panic + +export const some := \v. \d.\f. f v +export const none := \d.\f. d + +export const map := \option.\f. option none f +export const flatten := \option. option none \opt. opt +export const flatmap := \option.\f. option none \opt. map opt f +export const unwrap := \option. option (panic "value expected") \x.x diff --git a/src/stl/io/panic.rs b/src/systems/stl/panic.rs similarity index 65% rename from src/stl/io/panic.rs rename to src/systems/stl/panic.rs index 5eda39d..431a825 100644 --- a/src/stl/io/panic.rs +++ b/src/systems/stl/panic.rs @@ -1,13 +1,9 @@ use std::fmt::Display; -use super::super::inspect::with_str; -use crate::define_fn; use crate::foreign::ExternError; +use crate::systems::cast_exprinst::with_str; +use crate::{define_fn, ConstTree, Interner}; -define_fn! { - /// Takes a message, returns an [ExternError] unconditionally. - pub Panic = |x| with_str(x, |s| Err(OrchidPanic(s.clone()).into_extern())) -} /// An unrecoverable error in Orchid land. Because Orchid is lazy, this only /// invalidates expressions that reference the one that generated it. pub struct OrchidPanic(String); @@ -19,3 +15,12 @@ impl Display for OrchidPanic { } impl ExternError for OrchidPanic {} + +define_fn! { + /// Takes a message, returns an [ExternError] unconditionally. + Panic = |x| with_str(x, |s| Err(OrchidPanic(s.clone()).into_extern())) +} + +pub fn panic(i: &Interner) -> ConstTree { + ConstTree::tree([(i.i("panic"), ConstTree::xfn(Panic))]) +} diff --git a/src/stl/prelude.orc b/src/systems/stl/prelude.orc similarity index 79% rename from src/stl/prelude.orc rename to src/systems/stl/prelude.orc index d1bd760..18b5c33 100644 --- a/src/stl/prelude.orc +++ b/src/systems/stl/prelude.orc @@ -1,11 +1,11 @@ import std::num::* export ::(+, -, *, /, %) import std::str::* -export ::(++) +export ::[++] import std::bool::* export ::(==, if, then, else, true, false) import std::fn::* -export ::($, |>, =>, identity, pass, pass2, const) +export ::($, |>, =>, identity, pass, pass2, return) import std::list import std::map import std::option @@ -14,4 +14,4 @@ import std::loop::* export ::(loop_over, recursive) import std::known::* -export ::(,) \ No newline at end of file +export ::[,] diff --git a/src/systems/stl/proc.orc b/src/systems/stl/proc.orc new file mode 100644 index 0000000..a635f46 --- /dev/null +++ b/src/systems/stl/proc.orc @@ -0,0 +1,22 @@ +import super::fn::=> + +-- remove duplicate ;-s +export macro do { + ...$statement ; ; ...$rest:1 +} =0x3p130=> do { + ...$statement ; ...$rest +} +export macro do { + ...$statement ; ...$rest:1 +} =0x2p130=> statement (...$statement) do { ...$rest } +export macro do { ...$return } =0x1p130=> ...$return + +export macro statement (let $name = ...$value) ...$next =0x1p230=> ( + ( \$name. ...$next) (...$value) +) +export macro statement (cps ...$names = ...$operation:1) ...$next =0x2p230=> ( + (...$operation) ( (...$names) => ...$next ) +) +export macro statement (cps ...$operation) ...$next =0x1p230=> ( + (...$operation) (...$next) +) diff --git a/src/systems/stl/stl_system.rs b/src/systems/stl/stl_system.rs new file mode 100644 index 0000000..e4a6a24 --- /dev/null +++ b/src/systems/stl/stl_system.rs @@ -0,0 +1,50 @@ +#![allow(non_upper_case_globals)] +use hashbrown::HashMap; +use rust_embed::RustEmbed; + +use super::bin::bin; +use super::bool::bool; +use super::conv::conv; +use super::inspect::inspect; +use super::num::num; +use super::panic::panic; +use super::str::str; +use crate::facade::{IntoSystem, System}; +use crate::interner::Interner; +use crate::interpreter::HandlerTable; +use crate::pipeline::file_loader::embed_to_map; +use crate::sourcefile::{FileEntry, Import}; + +/// 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; + +// TODO: fix all orc modules to not rely on prelude + +impl IntoSystem<'static> for StlConfig { + fn into_system(self, i: &Interner) -> System<'static> { + let pure_fns = conv(i) + bool(i) + str(i) + num(i) + bin(i) + panic(i); + let mk_impure_fns = || inspect(i); + let fns = if self.impure { pure_fns + mk_impure_fns() } else { pure_fns }; + System { + name: vec!["std".to_string()], + constants: HashMap::from([(i.i("std"), fns)]), + code: embed_to_map::(".orc", i), + prelude: vec![FileEntry::Import(vec![Import { + path: i.i(&[i.i("std"), i.i("prelude")][..]), + name: None, + }])], + handlers: HandlerTable::new(), + } + } +} diff --git a/src/systems/stl/str.orc b/src/systems/stl/str.orc new file mode 100644 index 0000000..c04f31b --- /dev/null +++ b/src/systems/stl/str.orc @@ -0,0 +1,10 @@ +import super::(proc::*, bool::*, panic) + +export macro ...$a ++ ...$b =0x4p36=> (concat (...$a) (...$b)) + +export const char_at := \s.\i. do{ + let slc = slice s i 1; + if len slc == 1 + then slc + else panic "Character index out of bounds" +} diff --git a/src/stl/str.rs b/src/systems/stl/str.rs similarity index 94% rename from src/stl/str.rs rename to src/systems/stl/str.rs index 41d83ce..8ea267e 100644 --- a/src/stl/str.rs +++ b/src/systems/stl/str.rs @@ -1,9 +1,9 @@ use unicode_segmentation::UnicodeSegmentation; -use super::codegen::{orchid_opt, tuple}; -use super::inspect::with_str; -use super::RuntimeError; use crate::interner::Interner; +use crate::systems::cast_exprinst::with_str; +use crate::systems::codegen::{orchid_opt, tuple}; +use crate::systems::RuntimeError; use crate::utils::iter_find; use crate::{define_fn, ConstTree, Literal}; diff --git a/src/utils/delete_cell.rs b/src/utils/delete_cell.rs new file mode 100644 index 0000000..ce834be --- /dev/null +++ b/src/utils/delete_cell.rs @@ -0,0 +1,25 @@ +use std::cell::RefCell; +use std::rc::Rc; + +pub struct DeleteCell(pub Rc>>); +impl DeleteCell { + pub fn new(t: T) -> Self { + Self(Rc::new(RefCell::new(Some(t)))) + } + + pub fn take(&self) -> Option { + self.0.borrow_mut().take() + } + + pub fn clone_out(&self) -> Option + where + T: Clone, + { + self.0.borrow().clone() + } +} +impl Clone for DeleteCell { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} diff --git a/src/utils/event_poller.rs b/src/utils/event_poller.rs new file mode 100644 index 0000000..50e7da7 --- /dev/null +++ b/src/utils/event_poller.rs @@ -0,0 +1,149 @@ +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; + +enum TimerKind { + Once(DeleteCell), + Recurring { period: Duration, data_cell: DeleteCell }, +} +impl Clone for TimerKind { + fn clone(&self) -> Self { + match self { + Self::Once(c) => Self::Once(c.clone()), + Self::Recurring { period, data_cell: data } => + Self::Recurring { period: *period, data_cell: data.clone() }, + } + } +} + +/// Indicates a bit of code which is to be executed at a +/// specific point in time +/// +/// In order to work with Rust's builtin [BinaryHeap] which is a max heap, the +/// [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, +} +impl Clone for Timer { + fn clone(&self) -> Self { + Self { expires: self.expires, kind: self.kind.clone() } + } +} +impl Eq for Timer {} +impl PartialEq for Timer { + fn eq(&self, other: &Self) -> bool { + self.expires.eq(&other.expires) + } +} +impl PartialOrd for Timer { + fn partial_cmp(&self, other: &Self) -> Option { + Some(other.cmp(self)) + } +} +impl Ord for Timer { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + other.expires.cmp(&self.expires) + } +} + +pub struct Poller { + timers: BinaryHeap>, + receiver: Receiver, +} + +impl Poller { + pub fn new() -> (Sender, Self) { + let (sender, receiver) = channel(); + let this = Self { receiver, timers: BinaryHeap::new() }; + (sender, this) + } + + pub fn set_timeout( + &mut self, + duration: Duration, + data: TOnce, + ) -> impl Fn() + Clone { + 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()) + } + + pub fn set_interval( + &mut self, + period: Duration, + data: TRec, + ) -> impl Fn() + Clone { + 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()) + } + + /// Process a timer popped from the timers heap of this event loop. + /// This function returns [None] if the timer had been cancelled. **This + /// behaviour is different from [EventLoop::run] which is returns None if + /// the event loop is empty, even though the types are compatible.** + fn process_next_timer( + &mut self, + Timer { expires, kind }: Timer, + ) -> Option> { + Some(match kind { + TimerKind::Once(data) => PollEvent::Once(data.take()?), + TimerKind::Recurring { period, data_cell } => { + let data = data_cell.clone_out()?; + self.timers.push(Timer { + expires, + kind: TimerKind::Recurring { period, data_cell }, + }); + PollEvent::Recurring(data) + }, + }) + } + + /// Block until a message is received or the first timer expires + pub fn run(&mut self) -> Option> { + loop { + if let Some(expires) = self.timers.peek().map(|t| t.expires) { + return match self.receiver.recv_timeout(expires - Instant::now()) { + Ok(t) => Some(PollEvent::Event(t)), + Err(e) => { + if e == RecvTimeoutError::Disconnected { + // The receiver is now inert, but the timer must finish + sleep(expires - Instant::now()); + } + // pop and process the timer we've been waiting on + let timer = self.timers.pop().expect("checked before wait"); + let result = self.process_next_timer(timer); + // if the timer had been cancelled, repeat + if result.is_none() { + continue; + } + result + }, + }; + } else { + return match self.receiver.recv() { + Ok(t) => Some(PollEvent::Event(t)), + Err(RecvError) => None, + }; + } + } + } +} + +pub enum PollEvent { + Event(TEv), + Once(TOnce), + Recurring(TRec), +} diff --git a/src/utils/iter.rs b/src/utils/iter.rs index c3fbe64..bae1c8e 100644 --- a/src/utils/iter.rs +++ b/src/utils/iter.rs @@ -27,23 +27,3 @@ macro_rules! box_chain { } pub(crate) use box_chain; - -/// Flatten an iterator of iterators into a boxed iterator of the inner -/// nested values -pub fn box_flatten< - 'a, - T: 'a, - I: 'a + Iterator, - J: 'a + Iterator, ->( - i: I, -) -> BoxedIter<'a, T> { - Box::new(i.flatten()) -} - -/// Convert an iterator into a `Box` -pub fn into_boxed_iter<'a, T: 'a + IntoIterator>( - t: T, -) -> BoxedIter<'a, ::Item> { - Box::new(t.into_iter()) -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0961a70..b27d279 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,7 @@ mod cache; +mod delete_cell; +mod event_poller; +mod iter_find; mod print_nname; mod pushed; mod rc_to_owned; @@ -7,8 +10,9 @@ mod side; mod split_max_prefix; mod string_from_charset; mod substack; +mod take_with_output; +mod thread_pool; mod unwrap_or; -mod iter_find; pub use cache::Cache; pub use print_nname::sym2string; @@ -20,6 +24,10 @@ pub use split_max_prefix::split_max_prefix; pub use substack::{Stackframe, Substack, SubstackIterator}; pub(crate) use unwrap_or::unwrap_or; pub mod iter; +pub use delete_cell::DeleteCell; +pub use event_poller::{PollEvent, Poller}; pub use iter::BoxedIter; -pub use string_from_charset::string_from_charset; pub use iter_find::iter_find; +pub use string_from_charset::string_from_charset; +pub use take_with_output::take_with_output; +pub use thread_pool::{Query, QueryTask, Task, ThreadPool}; diff --git a/src/utils/pushed.rs b/src/utils/pushed.rs index 15aa084..d84771b 100644 --- a/src/utils/pushed.rs +++ b/src/utils/pushed.rs @@ -1,10 +1,9 @@ +use std::iter; + /// Pure version of [Vec::push] /// /// Create a new vector consisting of the provided vector with the /// element appended -pub fn pushed(vec: &Vec, t: T) -> Vec { - let mut next = Vec::with_capacity(vec.len() + 1); - next.extend_from_slice(&vec[..]); - next.push(t); - next +pub fn pushed(vec: &[T], t: T) -> Vec { + vec.iter().cloned().chain(iter::once(t)).collect() } diff --git a/src/utils/take_with_output.rs b/src/utils/take_with_output.rs new file mode 100644 index 0000000..c4778de --- /dev/null +++ b/src/utils/take_with_output.rs @@ -0,0 +1,8 @@ +pub fn take_with_output(src: &mut T, cb: impl FnOnce(T) -> (T, U)) -> U { + take_mut::scoped::scope(|scope| { + let (old, hole) = scope.take(src); + let (new, out) = cb(old); + hole.fill(new); + out + }) +} diff --git a/src/utils/thread_pool.rs b/src/utils/thread_pool.rs new file mode 100644 index 0000000..2008b90 --- /dev/null +++ b/src/utils/thread_pool.rs @@ -0,0 +1,160 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{sync_channel, SyncSender}; +use std::sync::{Arc, Mutex}; +use std::thread::spawn; + +/// A trait for a task dispatched on a [ThreadPool]. The task owns all relevant +/// data, is safe to pass between threads and is executed only once. +pub trait Task: Send + 'static { + fn run(self); +} + +impl Task for F { + fn run(self) { + self() + } +} + +pub trait Query: Send + 'static { + type Result: Send + 'static; + + fn run(self) -> Self::Result; + fn then( + self, + callback: F, + ) -> QueryTask + where + Self: Sized, + { + QueryTask { query: self, callback } + } +} +impl R + Send + 'static, R: Send + 'static> Query for F { + type Result = R; + + fn run(self) -> Self::Result { + self() + } +} + +pub struct QueryTask { + query: Q, + callback: F, +} +impl Task for QueryTask { + fn run(self) { + (self.callback)(self.query.run()) + } +} + +enum Message { + Stop, + Task(T), +} + +struct ThreadPoolData { + rdv_point: Mutex>>>, + stopping: AtomicBool, +} + +/// A thread pool to execute blocking I/O operations in parallel. +/// This thread pool is pretty inefficient for CPU-bound operations because it +/// spawns an unbounded number of concurrent threads and destroys them eagerly. +/// It is assumed that the tasks at hand are substnatially but not incomparably +/// more expensive than spawning a new thread. +/// +/// If multiple threads finish their tasks, one waiting thread is kept, the +/// rest exit. If all threads are busy, new threads are spawned when tasks +/// arrive. To get rid of the last waiting thread, drop the thread pool. +/// +/// ``` +/// use orchidlang::ThreadPool; +/// +/// let pool = ThreadPool::new(|s: String, _| println!("{}", s)); +/// +/// // spawns first thread +/// pool.submit("foo".to_string()); +/// // probably spawns second thread +/// pool.submit("bar".to_string()); +/// // either spawns third thread or reuses first +/// pool.submit("baz".to_string()); +/// ``` +pub struct ThreadPool { + data: Arc>, +} +impl ThreadPool { + /// Create a new thread pool. This just initializes the threadsafe + /// datastructures used to synchronize tasks and doesn't spawn any threads. + /// The first submission spawns the first thread. + pub fn new() -> Self { + Self { + data: Arc::new(ThreadPoolData { + rdv_point: Mutex::new(None), + stopping: AtomicBool::new(false), + }), + } + } + + /// Submit a task to the thread pool. This tries to send the task to the + /// waiting thread, or spawn a new one. If a thread is done with its task + /// and finds that it another thread is already waiting, it exits. + pub fn submit(&self, task: T) { + let mut standby = self.data.rdv_point.lock().unwrap(); + if let Some(port) = standby.take() { + (port.try_send(Message::Task(task))).expect( + "This channel cannot be disconnected unless the receiver crashes + between registering the sender and blocking for receive, and it cannot + be full because it's taken before insertion", + ); + } else { + drop(standby); + let data = self.data.clone(); + // worker thread created if all current ones are busy + spawn(move || { + let mut cur_task = task; + loop { + // Handle the task + cur_task.run(); + // Apply for a new task if no other thread is doing so already + let mut standby_spot = data.rdv_point.lock().unwrap(); + if standby_spot.is_some() { + return; // exit if we would be the second in line + } + let (sender, receiver) = sync_channel(1); + *standby_spot = Some(sender); + drop(standby_spot); + if data.stopping.load(Ordering::SeqCst) { + return; // exit if the pool was dropped before we applied + } + // Wait for the next event on the pool + let msg = (receiver.recv()).expect("We are holding a reference"); + match msg { + // repeat with next task + Message::Task(task) => cur_task = task, + // exit if the pool is dropped + Message::Stop => return, + } + } + }); + } + } +} + +impl Default for ThreadPool { + fn default() -> Self { + Self::new() + } +} + +impl Drop for ThreadPool { + // Ensure all threads exit properly + fn drop(&mut self) { + self.data.stopping.store(true, Ordering::SeqCst); + let mut rdv_point = self.data.rdv_point.lock().unwrap(); + if let Some(pending) = rdv_point.take() { + pending + .try_send(Message::Stop) + .expect("The channel is always removed before push") + } + } +}