#![allow(incomplete_features)]
#![feature(adt_const_params)]
#![feature(iter_intersperse)]
#![deny(rust_2018_idioms)]
#![warn(clippy::pedantic)]
#![allow(clippy::upper_case_acronyms)]
#![allow(clippy::similar_names)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::type_complexity)]
#![allow(clippy::unnested_or_patterns)]
#![allow(clippy::if_then_panic)]
#[cfg(not(target_env = "msvc"))]
#[cfg(feature = "jemallocator")]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
use std::{fmt, fs, io};
use std::collections::{HashMap, hash_map::DefaultHasher};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::ffi::OsStr;
#[cfg(target_env = "msvc")]
use std::io::{Write, BufRead};
use lazy_static::lazy_static;
use maplit::hashmap;
mod hashablemap;
use hashablemap::HashableMap;
mod threadhandle;
pub use threadhandle::{ThreadConfig, ThreadHandle};
mod thrio;
mod internaldata;
use internaldata::InternalData;
mod parsing;
use parsing::{ENUM_VALUE, Expr, Exprs, Line, Lines, Tokens, VecMap, unescape, match_pat, parse_number};
pub use parsing::{Pos, RefTVal, TVal, Token, AST, Callable, FnArgs, FnReturn, TokenType, Type, Value};
mod runnable;
pub use runnable::*;
mod ops;
use ops::{Op, OpTrait, Precedence};
mod umcore;
mod umstd;
mod ummod;
#[derive(Debug, Clone)]
pub enum Error {
Argument(String),
Parse(String, Option<Pos>),
Script(String, Option<Pos>),
Control(String, Option<Box<TVal>>),
Custom(TVal),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Argument(m) => write!(f, "Argument Error: {}", m),
Error::Parse(m, pos) => match pos {
Some(pos) => write!(f, "Parse Error at {}: {}", pos, m),
None => write!(f, "Parse Error: {}", m),
},
Error::Script(m, pos) => match pos {
Some(pos) => write!(f, "Script Error at {}: {}", pos, m),
None => write!(f, "Script Error: {}", m),
},
Error::Control(m, v) => {
match v {
Some(v) if m == "panic" => {
if let Value::Enum(_, e) = &v.val {
if let Value::List(l) = e.1[0].clone_out().val {
if let Some(pos) = Pos::from_value(&l[0].clone_out().val) {
if let Value::String(s) = l[1].clone_out().val {
return write!(f, "Panic at {}: {}", pos, s);
}
}
}
}
write!(f, "Unknown panic: {}", v.val)
},
_ => write!(f, "Control Error: {}: {:?}", m, v),
}
},
Error::Custom(v) => write!(f, "Other Error: {}", v.val),
}
}
}
impl std::error::Error for Error {}
pub struct Args {
pub longflags: Vec<String>,
pub script: Vec<String>,
}
impl Args {
pub fn handle() -> Result<Args, Error> {
let mut args = Args {
longflags: vec![],
script: vec![],
};
let eargs: Vec<String> = std::env::args().collect();
let mut i = 1;
let mut sa = false;
while i < eargs.len() {
if sa {
args.script.push(eargs[i].clone());
} else if eargs[i].starts_with('-') {
if eargs[i].starts_with("--") {
args.longflags.push(eargs[i][2..].into());
} else {
return Err(Error::Argument(format!("Unsupported arg: {}", eargs[i])))
}
} else {
sa = true;
continue;
}
i += 1;
}
Ok(args)
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Env<'a> {
parent: Option<&'a Env<'a>>,
vars: HashMap<String, RefTVal>,
}
lazy_static! {
static ref CORE: Env<'static> = {
log::debug!("Initializing core...");
let mut env = Env::from(hashmap!{
"_".into() => Value::Type(Type::any(), HashableMap::arc()).into(),
});
umcore::init(&mut env);
env
};
static ref PRELUDE: Env<'static> = {
let mut env = Env::child(&CORE);
log::debug!("Initializing prelude...");
umstd::init(&mut env);
env
};
}
impl<'a> Env<'a> {
#[must_use]
pub fn core() -> Env<'a> {
CORE.clone()
}
#[must_use]
pub fn prelude() -> Env<'a> {
PRELUDE.clone()
}
#[must_use]
pub fn empty() -> Env<'a> {
Env {
parent: None,
vars: hashmap!{},
}
}
#[must_use]
pub fn child(parent: &'a Env<'a>) -> Env<'a> {
Env {
parent: Some(parent),
vars: hashmap!{},
}
}
#[must_use]
pub fn iter_flatten<'b>(it: &'b Env<'a>) -> Box<dyn Iterator<Item = (&'b String, &'b RefTVal)> + 'b> {
if let Some (p) = it.parent {
return Box::new(Self::iter_flatten(p).chain(it.vars.iter()));
}
return Box::new(it.vars.iter());
}
#[must_use]
pub fn flatten(old: &Env<'a>) -> Env<'a> {
Env::from(
Self::iter_flatten(old)
.map(|(k, v)| {
(k.clone(), v.clone())
}).collect::<HashMap<String, RefTVal>>()
)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&RefTVal> {
match self.vars.get(name) {
Some(v) => Some(v),
None => match &self.parent {
Some(p) => p.get(name), None => None,
},
}
}
#[must_use]
pub fn has(&self, name: &str) -> bool {
if self.vars.contains_key(name) {
true
} else {
match &self.parent {
Some(p) => p.has(name),
None => false,
}
}
}
pub fn set(&mut self, name: &str, rv: &RefTVal) {
self.vars.insert(name.into(), RefTVal::clone(rv));
}
pub fn unset(&mut self, name: &str) {
self.vars.remove(name);
}
pub fn update(&mut self, vars: HashMap<String, RefTVal>) {
self.vars.extend(vars);
}
#[must_use]
pub fn diff(&self, mut nenv: Env<'a>) -> HashMap<String, RefTVal> {
let outer = self;
let mut vars = hashmap!{};
for (k, v) in nenv.vars.drain() {
if let Some(ov) = outer.get(&k) {
if ov != &v {
vars.insert(k, v);
}
}
}
vars
}
}
impl<'a> From<HashableMap<String, RefTVal>> for Env<'a> {
fn from(vars: HashableMap<String, RefTVal>) -> Env<'a> {
Env {
parent: None,
vars: vars.map,
}
}
}
impl<'a> From<HashMap<String, RefTVal>> for Env<'a> {
fn from(vars: HashMap<String, RefTVal>) -> Env<'a> {
Env {
parent: None,
vars,
}
}
}
impl<'a> fmt::Debug for Env<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parent = match self.parent {
Some(_) => "Some(Env {...})",
None => "None",
};
write!(f, "Env {{ parent: {}, vars: {{{:?}}} }}", parent, self.vars)
}
}
pub fn compile<'a>(scriptname: &str, script: &str, env: &'a Env<'a>, pos: Option<Pos>) -> Result<Runnable<'a>, Error> {
let lines: Vec<Line> = Lines::split(scriptname, script, pos)
.inspect(|l| {
if !l.data.starts_with("//") {
log::trace!("{}", l);
}
}).collect();
let exprs: Vec<Expr> = lines.iter().flat_map(Exprs::split).collect();
let mut tokens: Vec<Tokens> = exprs.iter()
.map(Tokens::tokenize)
.collect();
let asts: Result<Vec<AST>, Error> = tokens.drain(0..).map(|ts| {
AST::parse(ts, env)
}).collect();
let mut shash = DefaultHasher::new();
env!("CARGO_PKG_VERSION").hash(&mut shash);
script.hash(&mut shash);
Ok(Runnable {
hash: shash.finish(),
env: Env::child(env),
ast: AST::Container {
token: Token {
ttype: TokenType::Container,
pos: Pos {
filename: scriptname.into(),
line: 0,
col: 0,
},
data: "{".into(),
},
children: asts?,
},
})
}
pub fn run(scriptname: &str, script: &str, env: &Env<'_>, pos: Option<Pos>, printout: bool) -> FnReturn {
let run = match compile(scriptname, script, env, pos) {
Ok(r) => r,
Err(m) => {
if printout {
log::info!("{}\n", m);
}
return (None, Err(m));
},
};
match run.ast {
AST::Container { children, .. } => {
let mut nenv = Env::child(env);
let vals: Result<Vec<RefTVal>, Error> = children.iter()
.filter_map(|ast| {
if let AST::Container { token, .. } = &ast {
if token.data.starts_with("//") {
return None;
}
}
let (vars, val) = ast.run(&nenv);
if let Some(vars) = vars {
nenv.update(vars);
}
if printout {
match val {
Ok(ref v) => log::info!("==> {}\n", v),
Err(ref m) => log::error!("{}\n", m),
}
}
Some(val)
}).collect();
match vals {
Ok(mut vals) => match vals.pop() {
Some(v) => (Some(nenv.vars), Ok(v)),
None => (None, Ok(Value::none().into())),
},
Err(m) => (None, Err(m)),
}
},
_ => (None, Err(Error::Script("expected script container AST".into(), None))),
}
}
pub fn run_path<P: AsRef<Path>>(path: &P, env: &Env<'_>, run_main: bool) -> FnReturn {
let path = path.as_ref();
let path: PathBuf = if path.is_dir() {
path.join("main.um")
} else if !path.is_file() && path.extension() != Some(OsStr::new("um")) {
path.with_extension("um")
} else {
PathBuf::from(path)
};
let script = match fs::read_to_string(&path) {
Ok(s) => s,
Err(m) => return (None, Err(Error::Argument(format!("Failed to load {}: {}", path.display(), m)))),
};
let (vars, val) = match run(&path.to_string_lossy(), &script, env, None, false) {
(vars, Ok(v)) => (vars, v),
(vars, Err(m)) => return (vars, Err(m)),
};
if run_main {
if let Some(vars) = &vars {
if let Some(main) = vars.get("main") {
if let Value::Function { body, .. } = main.clone_out().val {
let mut nenv = Env::child(env);
nenv.update(vars.clone());
let (_, val) = body.call(&nenv, FnArgs::Normal {
this: Box::new(main.clone()),
pos: None,
args: TVal {
ttype: Type::none(),
attr: hashmap!{}.into(),
val: Value::none(),
}.into(),
});
return (Some(nenv.vars), val);
}
}
}
}
(vars, Ok(val))
}
#[cfg(not(target_env = "msvc"))]
struct EmptyCompleter;
#[cfg(not(target_env = "msvc"))]
impl liner::Completer for EmptyCompleter {
fn completions(&mut self, _start: &str) -> Vec<String> {
vec![]
}
}
#[cfg(not(target_env = "msvc"))]
struct EnvCompleter<'a> (Env<'a>);
#[cfg(not(target_env = "msvc"))]
impl<'a> liner::Completer for EnvCompleter<'a> {
fn completions(&mut self, start: &str) -> Vec<String> {
if start.is_empty() {
Env::flatten(&self.0).vars.keys()
.cloned()
.collect()
} else {
Env::flatten(&self.0).vars.keys()
.filter(|k| k.starts_with(start))
.cloned()
.collect()
}
}
}
fn interactive_init(env: &mut Env<'_>) {
env.set("help", &Value::String(
"\n
Welcome to the Umbra interpreter!
Try pressing TAB to see variables that are available in the current
environment. TAB can also be used to complete initial variable names.
For examples of language usage take a look at the test module, and for
instructions on usage as a library check out https://docs.rs/umbra-lang/
To exit, either press Ctrl-C or run `exit()` without quotes.
\n".into()).into());
}
pub fn run_interactive(name: &str, env: &Env<'_>) {
#[cfg(target_env = "msvc")]
return run_interactive_basic(name, env);
#[cfg(not(target_env = "msvc"))]
{
let mut nenv = Env::child(env);
interactive_init(&mut nenv);
let mut pos = Pos::start(&format!("<stdin/{}>", name));
let mut ctx = liner::Context::new();
loop {
match ctx.read_line(&format!("{}:{}> ", name, pos.line), None, &mut EnvCompleter(nenv.clone())) {
Ok(line) => {
if line.trim().is_empty() {
eprintln!();
continue;
}
if let Err(m) = ctx.history.push(line.clone().into()) {
log::error!("Failed to write line to history: {}", m);
break;
};
let (vars, _) = run(&format!("<stdin/{}>", name), &line, &nenv, Some(pos.clone()), true);
if let Some(vars) = vars {
nenv.update(vars);
}
pos.line += 1;
},
Err(e @ io::Error { .. }) if e.kind() == io::ErrorKind::Interrupted => {
eprintln!("^C");
return;
},
Err(e @ io::Error { .. }) if e.kind() == io::ErrorKind::UnexpectedEof => {
continue;
},
Err(m) => {
log::error!("Error: {:?}", m);
continue;
},
}
}
}
}
#[cfg(target_env = "msvc")]
fn run_interactive_basic(name: &str, env: &Env<'_>) {
let mut nenv = Env::child(env);
interactive_init(&mut nenv);
let mut pos = Pos::start(&format!("<stdin/{}", name));
let mut line = String::new();
loop {
line.clear();
eprint!("{}:{}> ", name, pos.line);
io::stdout().flush().unwrap();
io::stdin().lock().read_line(&mut line).unwrap();
if line.trim().is_empty() {
eprintln!();
continue;
}
let (vars, _) = run(&format!("<stdin/{}", name), &line, &nenv, Some(pos.clone()), true);
if let Some(vars) = vars {
nenv.update(vars);
}
pos.line += 1;
}
}