mirror of
https://github.com/ruuda/rcl.git
synced 2025-12-23 04:47:19 +00:00
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 ...
369 lines
12 KiB
Rust
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;
|