use std::{any::Any, sync::Arc};
use crate::{
callable::Callable,
env::{Cx, Env},
error::{Diagnostic, Error, Result},
expr::Expr,
hint::{HintMetadata, diagnostic_hints_value},
id::{CORE_SHAPE_CLASS_ID, ShapeId, Symbol},
object::{Args, ClassRef, Object, RawArgs, ShapeRef},
value::Value,
};
pub trait Shape: Callable {
fn id(&self) -> Option<ShapeId> {
None
}
fn symbol(&self) -> Option<Symbol> {
None
}
fn parents(&self, _cx: &mut Cx) -> Result<Vec<ShapeRef>> {
Ok(Vec::new())
}
fn is_effectful(&self) -> bool {
false
}
fn is_total(&self) -> bool {
false
}
fn is_subshape_of(&self, _cx: &mut Cx, _parent: &dyn Shape) -> Result<Option<bool>> {
Ok(None)
}
fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch>;
fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch>;
fn describe(&self, cx: &mut Cx) -> Result<ShapeDoc>;
}
impl<T> Object for T
where
T: Shape + Any,
{
fn display(&self, cx: &mut Cx) -> Result<String> {
let doc = self.describe(cx)?;
match self.symbol() {
Some(symbol) => Ok(format!("#<shape {} {}>", symbol, doc.name)),
None => Ok(format!("#<shape {}>", doc.name)),
}
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl<T> crate::ObjectCompat for T
where
T: Shape + Any,
{
fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
let symbol = Symbol::qualified("core", "Shape");
if let Some(value) = cx.registry().class_by_symbol(&symbol) {
return Ok(value.clone());
}
cx.factory().class_stub(CORE_SHAPE_CLASS_ID, symbol)
}
fn as_table(&self, cx: &mut Cx) -> Result<Value> {
let doc = self.describe(cx)?;
let mut entries = vec![
(Symbol::new("name"), cx.factory().string(doc.name)?),
(
Symbol::new("effectful"),
cx.factory().bool(self.is_effectful())?,
),
(Symbol::new("total"), cx.factory().bool(self.is_total())?),
];
if let Some(symbol) = self.symbol() {
entries.push((
Symbol::new("symbol"),
cx.factory().string(symbol.to_string())?,
));
}
for (index, detail) in doc.details.into_iter().enumerate() {
entries.push((
Symbol::qualified("detail", index.to_string()),
cx.factory().string(detail)?,
));
}
cx.factory().table(entries)
}
fn as_shape(&self) -> Option<&dyn Shape> {
Some(self)
}
}
impl<T> Callable for T
where
T: Shape,
{
fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
let [value] = args.values() else {
return Err(Error::Eval("shape call expects 1 argument".to_owned()));
};
call_shape(cx, self, ShapeCallTarget::Value(value.clone()))
}
fn call_exprs(&self, cx: &mut Cx, args: RawArgs) -> Result<Value> {
let [expr] = args.exprs() else {
return Err(Error::Eval("shape call expects 1 expression".to_owned()));
};
call_shape(cx, self, ShapeCallTarget::Expr(expr.clone()))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ShapeDoc {
pub name: String,
pub details: Vec<String>,
}
impl ShapeDoc {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
details: Vec::new(),
}
}
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.details.push(detail.into());
self
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct MatchScore(i32);
impl MatchScore {
pub fn exact(value: i32) -> Self {
Self(value)
}
pub fn reject() -> Self {
Self(i32::MIN / 2)
}
pub fn value(self) -> i32 {
self.0
}
}
impl core::ops::AddAssign for MatchScore {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
#[derive(Clone, Debug, Default)]
pub struct ShapeBindings {
values: Vec<(Symbol, Value)>,
exprs: Vec<(Symbol, Expr)>,
}
impl ShapeBindings {
pub fn new() -> Self {
Self::default()
}
pub fn bind_value(&mut self, name: Symbol, value: Value) {
self.values.push((name, value));
}
pub fn bind_expr(&mut self, name: Symbol, expr: Expr) {
self.exprs.push((name, expr));
}
pub fn extend(&mut self, other: ShapeBindings) {
self.values.extend(other.values);
self.exprs.extend(other.exprs);
}
pub fn values(&self) -> &[(Symbol, Value)] {
&self.values
}
pub fn exprs(&self) -> &[(Symbol, Expr)] {
&self.exprs
}
pub fn into_env(self, cx: &mut Cx) -> Result<()> {
let env = self.into_child_env(cx)?;
*cx.env_mut() = env;
Ok(())
}
pub fn into_child_env(self, cx: &mut Cx) -> Result<Env> {
let mut env = Env::child(Arc::new(cx.env().clone()));
for (name, value) in self.values {
env.define(name, value);
}
for (name, expr) in self.exprs {
let value = cx.factory().expr(expr)?;
env.define(name, value);
}
Ok(env)
}
}
#[derive(Clone, Debug)]
pub struct ShapeMatch {
pub accepted: bool,
pub captures: ShapeBindings,
pub score: MatchScore,
pub diagnostics: Vec<Diagnostic>,
}
impl ShapeMatch {
pub fn accept(score: MatchScore) -> Self {
Self {
accepted: true,
captures: ShapeBindings::new(),
score,
diagnostics: Vec::new(),
}
}
pub fn reject(message: impl Into<String>) -> Self {
Self {
accepted: false,
captures: ShapeBindings::new(),
score: MatchScore::reject(),
diagnostics: vec![Diagnostic::error(message)],
}
}
pub fn reject_with_diagnostic(diagnostic: Diagnostic) -> Self {
Self {
accepted: false,
captures: ShapeBindings::new(),
score: MatchScore::reject(),
diagnostics: vec![diagnostic],
}
}
}
#[derive(Clone, Debug)]
pub struct ShapeMatchObject {
matched: ShapeMatch,
}
impl ShapeMatchObject {
pub fn new(matched: ShapeMatch) -> Self {
Self { matched }
}
pub fn matched(&self) -> &ShapeMatch {
&self.matched
}
}
impl Object for ShapeMatchObject {
fn display(&self, _cx: &mut Cx) -> Result<String> {
Ok(format!(
"#<shape-match {} score={}>",
if self.matched.accepted {
"accepted"
} else {
"rejected"
},
self.matched.score.value()
))
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl crate::ObjectCompat for ShapeMatchObject {
fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
let symbol = Symbol::qualified("core", "ShapeMatch");
if let Some(value) = cx.registry().class_by_symbol(&symbol) {
return Ok(value.clone());
}
cx.factory()
.class_stub(crate::id::CORE_SHAPE_MATCH_CLASS_ID, symbol)
}
fn truth(&self, _cx: &mut Cx) -> Result<bool> {
Ok(self.matched.accepted)
}
fn as_table(&self, cx: &mut Cx) -> Result<Value> {
shape_match_table(cx, &self.matched)
}
fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
self.as_table(cx)?.object().as_expr(cx)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExprKind {
Nil,
Bool,
Number,
Symbol,
String,
Bytes,
List,
Vector,
Map,
Set,
Call,
Infix,
Prefix,
Postfix,
Block,
Quote,
Annotated,
Extension,
}
impl ExprKind {
pub fn matches(&self, expr: &Expr) -> bool {
matches!(
(self, expr),
(Self::Nil, Expr::Nil)
| (Self::Bool, Expr::Bool(_))
| (Self::Number, Expr::Number(_))
| (Self::Symbol, Expr::Symbol(_))
| (Self::String, Expr::String(_))
| (Self::Bytes, Expr::Bytes(_))
| (Self::List, Expr::List(_))
| (Self::Vector, Expr::Vector(_))
| (Self::Map, Expr::Map(_))
| (Self::Set, Expr::Set(_))
| (Self::Call, Expr::Call { .. })
| (Self::Infix, Expr::Infix { .. })
| (Self::Prefix, Expr::Prefix { .. })
| (Self::Postfix, Expr::Postfix { .. })
| (Self::Block, Expr::Block(_))
| (Self::Quote, Expr::Quote { .. })
| (Self::Annotated, Expr::Annotated { .. })
| (Self::Extension, Expr::Extension { .. })
)
}
pub fn name(&self) -> &'static str {
match self {
Self::Nil => "nil",
Self::Bool => "bool",
Self::Number => "number",
Self::Symbol => "symbol",
Self::String => "string",
Self::Bytes => "bytes",
Self::List => "list",
Self::Vector => "vector",
Self::Map => "map",
Self::Set => "set",
Self::Call => "call",
Self::Infix => "infix",
Self::Prefix => "prefix",
Self::Postfix => "postfix",
Self::Block => "block",
Self::Quote => "quote",
Self::Annotated => "annotated",
Self::Extension => "extension",
}
}
}
#[derive(Clone, Debug)]
pub enum ShapeCallTarget {
Value(Value),
Expr(Expr),
}
pub fn call_shape(cx: &mut Cx, shape: &dyn Shape, target: ShapeCallTarget) -> Result<Value> {
let matched = match target {
ShapeCallTarget::Value(value) => shape.check_value(cx, value)?,
ShapeCallTarget::Expr(expr) => shape.check_expr(cx, &expr)?,
};
shape_match_value(cx, matched)
}
pub fn shape_match_value(cx: &mut Cx, matched: ShapeMatch) -> Result<Value> {
cx.factory()
.opaque(Arc::new(ShapeMatchObject::new(matched)))
}
pub fn shape_is_subshape_of(cx: &mut Cx, child: &dyn Shape, parent: &dyn Shape) -> Result<bool> {
if let (Some(child_id), Some(parent_id)) = (child.id(), parent.id())
&& child_id == parent_id
{
return Ok(true);
}
if let (Some(child_symbol), Some(parent_symbol)) = (child.symbol(), parent.symbol())
&& child_symbol == parent_symbol
{
return Ok(true);
}
if let Some(answer) = child.is_subshape_of(cx, parent)? {
return Ok(answer);
}
if matches!(
parent.symbol(),
Some(symbol)
if symbol == Symbol::qualified("core", "Any")
|| symbol == Symbol::qualified("core", "AnyShape")
) && !child.is_effectful()
{
return Ok(true);
}
for candidate in child.parents(cx)? {
let Some(candidate) = candidate.object().as_shape() else {
continue;
};
if shape_is_subshape_of(cx, candidate, parent)? {
return Ok(true);
}
}
Ok(false)
}
fn shape_match_table(cx: &mut Cx, matched: &ShapeMatch) -> Result<Value> {
let value_captures = cx.factory().table(matched.captures.values().to_vec())?;
let expr_captures = cx.factory().table(
matched
.captures
.exprs()
.iter()
.map(|(symbol, expr)| Ok((symbol.clone(), cx.factory().expr(expr.clone())?)))
.collect::<Result<Vec<_>>>()?,
)?;
let diagnostics = matched
.diagnostics
.clone()
.into_iter()
.map(|diagnostic| diagnostic_value(cx, diagnostic))
.collect::<Result<Vec<_>>>()?;
let diagnostics = cx.factory().list(diagnostics)?;
cx.factory().table(vec![
(
Symbol::new("accepted"),
cx.factory().bool(matched.accepted)?,
),
(
Symbol::new("score"),
cx.factory().number_literal(
Symbol::qualified("numbers", "f64"),
matched.score.value().to_string(),
)?,
),
(Symbol::qualified("captures", "value"), value_captures),
(Symbol::qualified("captures", "expr"), expr_captures),
(Symbol::new("diagnostics"), diagnostics),
])
}
fn diagnostic_value(cx: &mut Cx, diagnostic: Diagnostic) -> Result<Value> {
let hints = diagnostic_hints_value(cx, &diagnostic)?;
let severity = match diagnostic.severity {
crate::error::Severity::Error => "error",
crate::error::Severity::Warning => "warning",
crate::error::Severity::Info => "info",
crate::error::Severity::Note => "note",
};
let related = diagnostic
.related
.into_iter()
.filter(|related| !HintMetadata::is_hint_diagnostic(related))
.map(|related| diagnostic_value(cx, related))
.collect::<Result<Vec<_>>>()?;
let related = cx.factory().list(related)?;
let mut entries = vec![
(
Symbol::new("severity"),
cx.factory().symbol(Symbol::new(severity))?,
),
(
Symbol::new("message"),
cx.factory().string(diagnostic.message)?,
),
(Symbol::new("related"), related),
(Symbol::new("hints"), hints),
];
if let Some(code) = diagnostic.code {
entries.push((Symbol::new("code"), cx.factory().symbol(code)?));
}
cx.factory().table(entries)
}
#[cfg(test)]
#[path = "shape_tests.rs"]
mod tests;