rcl/src/runtime.rs
Ruud van Asseldonk 80ba83e678 Add build target parsing
Wow, writing a proper deserializer for an RCL value is still a lot of
work, even in Rust! I should write a #[derive] macro for it. But having
record types would make this a lot easier already, so let's wait with
that. For now it's hand-written with a dozen error messages that I will
need to add goldens for ...
2024-07-27 23:03:05 +02:00

369 lines
12 KiB
Rust

// RCL -- A reasonable configuration language.
// Copyright 2023 Ruud van Asseldonk
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// A copy of the License has been included in the root of the repository.
//! Representations of values and scopes at runtime.
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use crate::ast::{CallArg, Expr};
use crate::error::{IntoError, PathElement, Result};
use crate::eval::Evaluator;
use crate::fmt_rcl::format_rcl;
use crate::fmt_type::format_type;
use crate::pprint::{concat, indent, Doc};
use crate::source::Span;
use crate::type_diff::{Mismatch, TypeDiff};
use crate::type_source::Source;
use crate::types;
use crate::types::{Side, SourcedType, Type};
/// The arguments to a function call at runtime.
pub struct FunctionCall<'a> {
/// The opening paren for the call.
pub call_open: Span,
/// The closing paren for the call.
pub call_close: Span,
/// The arguments and their spans in the source code.
pub args: &'a [CallArg<Value>],
}
/// The arguments to a method call at runtime.
pub struct MethodCall<'a> {
/// The source code span of the receiver of the method call.
///
/// In `widget.len()`, the receiver is `widget`.
pub receiver_span: Span,
/// The receiver of the call.
pub receiver: &'a Value,
/// The span of the method being called.
///
/// In `widget.len()`, the method is `len`.
pub method_span: Span,
/// Arguments to the call.
pub call: FunctionCall<'a>,
}
/// A built-in function.
#[derive(Eq, Ord, PartialEq, PartialOrd)]
pub struct BuiltinFunction {
pub name: &'static str,
pub type_: fn() -> types::Function,
pub f: for<'a> fn(&'a mut Evaluator, FunctionCall<'a>) -> Result<Value>,
}
/// A built-in method.
#[derive(Eq, Ord, PartialEq, PartialOrd)]
pub struct BuiltinMethod {
pub name: &'static str,
pub type_: fn() -> types::Function,
pub f: for<'a> fn(&'a mut Evaluator, MethodCall<'a>) -> Result<Value>,
}
/// A method and its receiver.
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct MethodInstance {
/// The span where we refer to the method, e.g. the `keys` in `{}.keys()`.
pub method_span: Span,
/// The method to be called.
pub method: &'static BuiltinMethod,
/// The span of the receiving expression, e.g. the `{}` in `{}.keys()`.
pub receiver_span: Span,
/// The receiver of the call.
pub receiver: Value,
}
impl std::fmt::Debug for BuiltinFunction {
// coverage:off -- Debug is needed for assert, not expected to be called.
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}@{:p}", self.name, self.f)
}
// coverage:on
}
impl std::fmt::Debug for BuiltinMethod {
// coverage:off -- Debug is needed for assert, not expected to be called.
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}@{:p}", self.name, self.f)
}
// coverage:on
}
#[derive(Debug)]
pub struct Function {
/// Source location of lambda, including args, `=>`, and body.
///
/// This span is used to identify the function for comparison and equality,
/// so we don't have to inspect its AST.
pub span: Span,
/// Captured environment at the time of the call.
///
/// TODO: It might be nicer to capture only the variables that are needed,
/// but then we need to inspect the body AST when the lambda is produced.
pub env: Env,
pub body: Rc<Expr>,
/// The type of this function, including its arguments.
pub type_: Rc<types::Function>,
}
impl PartialEq for Function {
fn eq(&self, other: &Function) -> bool {
// What matters for the identity of the lambda is where in the source
// code it was produced. If that is the same, then the args and body are
// necessarily the same. But the captured environment could be different,
// so we take that into account too.
(self.span, &self.env) == (other.span, &other.env)
}
}
impl Eq for Function {}
impl PartialOrd for Function {
// coverage:off -- All callers use `Ord`, not `PartialOrd`.
fn partial_cmp(&self, other: &Function) -> Option<Ordering> {
Some(self.cmp(other))
}
// coverage:on
}
impl Ord for Function {
fn cmp(&self, other: &Function) -> Ordering {
let lhs = (self.span, &self.env);
let rhs = (other.span, &other.env);
lhs.cmp(&rhs)
}
}
/// A value.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum Value {
Null,
Bool(bool),
// TODO: Should be a bigint.
Int(i64),
String(Rc<str>),
List(Rc<Vec<Value>>),
// TODO: Should preserve insertion order.
Set(Rc<BTreeSet<Value>>),
// TODO: Should preserve insertion order.
Dict(Rc<BTreeMap<Value, Value>>),
Function(Rc<Function>),
BuiltinFunction(&'static BuiltinFunction),
BuiltinMethod(Rc<MethodInstance>),
}
impl Value {
/// Extract the dict if it is one, panic otherwise.
#[inline]
pub fn expect_dict(&self) -> &BTreeMap<Value, Value> {
match self {
Value::Dict(inner) => inner,
other => panic!("Expected Dict but got {other:?}."),
}
}
/// Extract the list if it is one, panic otherwise.
#[inline]
pub fn expect_list(&self) -> &[Value] {
match self {
Value::List(inner) => inner.as_ref(),
other => panic!("Expected List but got {other:?}."),
}
}
/// Extract the list if it is one, panic otherwise.
#[inline]
pub fn expect_set(&self) -> &BTreeSet<Value> {
match self {
Value::Set(inner) => inner,
other => panic!("Expected Set but got {other:?}."),
}
}
/// Extract the string if it is one, panic otherwise.
#[inline]
pub fn expect_string(&self) -> &str {
match self {
Value::String(inner) => inner.as_ref(),
other => panic!("Expected String but got {other:?}."),
}
}
/// As [`expect_string`], but make an owned `Rc` copy instead of borrowing.
#[inline]
pub fn expect_string_clone(&self) -> Rc<str> {
match self {
Value::String(inner) => inner.clone(),
other => panic!("Expected String but got {other:?}."),
}
}
/// Dynamically check that the value fits the required type.
pub fn is_instance_of(&self, at: Span, type_: &SourcedType) -> Result<()> {
let req_type = match &type_.type_ {
Type::Any => return Ok(()),
t => t,
};
match (req_type, self) {
// For the primitive types, we just check for matching values.
(Type::Null, Value::Null) => return Ok(()),
(Type::Bool, Value::Bool(..)) => return Ok(()),
(Type::Int, Value::Int(..)) => return Ok(()),
(Type::String, Value::String(..)) => return Ok(()),
// For compound types, we descend into them to check.
(Type::List(elem_type), Value::List(elems)) => {
for (i, elem) in elems.iter().enumerate() {
elem.is_instance_of(at, elem_type)
.map_err(|err| err.with_path_element(PathElement::Index(i)))?;
}
return Ok(());
}
(Type::Set(elem_type), Value::Set(elems)) => {
for (i, elem) in elems.iter().enumerate() {
elem.is_instance_of(at, elem_type).map_err(|err|
// Even though sets don't strictly have indexes,
// they do have an order, so report the index to
// clarify that this is a nested error.
err.with_path_element(PathElement::Index(i)))?;
}
return Ok(());
}
(Type::Dict(dict), Value::Dict(kvs)) => {
for (k, v) in kvs.iter() {
k.is_instance_of(at, &dict.key)
.map_err(|err| err.with_path_element(PathElement::Key(k.clone())))?;
v.is_instance_of(at, &dict.value)
.map_err(|err| err.with_path_element(PathElement::Key(k.clone())))?;
}
return Ok(());
}
(Type::Union(types), value) => {
// For a union, if it's an instance of any member, then it's
// okay, if not, we fall through to the generic error at the end.
for member in types.members.iter() {
match value.is_instance_of(at, member) {
Ok(()) => return Ok(()),
Err(..) => continue,
}
}
}
(Type::Function(fn_type), Value::Function(fn_val)) => {
let error = match fn_val.type_.is_subtype_of(fn_type) {
TypeDiff::Ok(..) => return Ok(()),
// If we encounter a defer, if that happens statically at
// typecheck time then we can insert a runtime check. But
// now we are at runtime, and we can't guarantee that these
// types are compatible, so treat that as an error.
TypeDiff::Defer(..) => TypeDiff::Error(Mismatch::Atom {
actual: SourcedType {
type_: Type::Function(fn_val.type_.clone()),
source: Source::None,
},
expected: SourcedType {
type_: Type::Function(fn_type.clone()),
source: Source::None,
},
}),
error => error,
};
error.check(at)?;
unreachable!("The above ? fails.")
}
_ => {}
}
let mut error = at.error("Type mismatch.").with_body(concat! {
"Expected a value that fits this type:"
Doc::HardBreak Doc::HardBreak
indent! { format_type(req_type).into_owned() }
Doc::HardBreak Doc::HardBreak
"But got this value:"
Doc::HardBreak Doc::HardBreak
indent! { format_rcl(self).into_owned() }
});
type_.explain_error(Side::Expected, &mut error);
error.err()
}
}
impl<'a> From<&'a str> for Value {
#[inline]
fn from(value: &'a str) -> Self {
Value::String(value.into())
}
}
/// An environment binds names to values.
pub type Env = crate::env::Env<Value>;
/// Create a new environment with an initialized standard library.
pub fn prelude() -> Env {
let mut env = Env::new();
env.push("std".into(), crate::stdlib::initialize());
env
}
macro_rules! builtin_function {
(
$rcl_name:expr,
( $( $arg_name:ident: $arg_type:tt ),* ) -> $result:tt,
const $rust_const:ident,
$rust_name:ident
) => {
pub const $rust_const: crate::runtime::BuiltinFunction = crate::runtime::BuiltinFunction {
name: $rcl_name,
type_: || {
#[allow(unused_imports)]
use crate::types::{Type, Dict, Function, FunctionArg, builtin, make_function, make_type};
crate::types::make_function!( ($( $arg_name: $arg_type ),*) -> $result)
},
f: $rust_name,
};
};
}
pub(crate) use builtin_function;
macro_rules! builtin_method {
(
$rcl_name:expr,
( $( $arg_name:ident: $arg_type:tt ),* ) -> $result:tt,
const $rust_const:ident,
$rust_name:ident
) => {
pub const $rust_const: crate::runtime::BuiltinMethod = crate::runtime::BuiltinMethod {
name: $rcl_name,
type_: || {
#[allow(unused_imports)]
use crate::types::{Type, Dict, Function, FunctionArg, builtin, make_function, make_type};
crate::types::make_function!( ($( $arg_name: $arg_type ),*) -> $result)
},
f: $rust_name,
};
};
}
pub(crate) use builtin_method;