///|
pub fn eval(
  source : String,
  environment~ : @eval.Environment,
  filename? : String = "",
) -> Map[String, @value.Value] raise Error {
  let tokens = @lexer.tokenize(source, file=filename)
  let stmts = @parser.parse(tokens, file=filename)
  let scope = @eval.Scope::new(env=environment)
  // Populate scope with predeclared builtins
  for name, val in environment.predeclared {
    scope.set(name, val)
  }
  @eval.exec_module(scope, stmts)
  // Extract all non-predeclared, non-underscore bindings as exports
  let result : Map[String, @value.Value] = {}
  for name, val in scope.bindings {
    if not(environment.predeclared.contains(name)) && not(name.has_prefix("_")) {
      result[name] = val
    }
  }
  result
}

///|
pub fn eval_expr(
  source : String,
  environment~ : @eval.Environment,
  filename? : String = "",
) -> @value.Value raise Error {
  let tokens = @lexer.tokenize(source, file=filename)
  let expr = @parser.parse_expression(tokens, file=filename)
  let scope = @eval.Scope::new(env=environment)
  for name, val in environment.predeclared {
    scope.set(name, val)
  }
  @eval.eval_expr(scope, expr)
}

///|
pub fn new_environment(
  loader? : ((String) -> String raise Error)? = None,
) -> @eval.Environment {
  @eval.Environment::new(loader~)
}

///|
pub fn set_builtin(
  env : @eval.Environment,
  name : String,
  func : (Array[@value.Value], Map[String, @value.Value]) -> @value.Value raise Error,
) -> Unit {
  env.predeclared[name] = @value.Value::BuiltinFunction(name, func)
}