Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 290 additions & 13 deletions crates/tx3-lang/src/analyzing.rs

Large diffs are not rendered by default.

86 changes: 76 additions & 10 deletions crates/tx3-lang/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub enum Symbol {
AliasDef(Box<AliasDef>),
RecordField(Box<RecordField>),
VariantCase(Box<VariantCase>),
Function(String),
FunctionDef(Box<FnDef>),
Fees,
}

Expand Down Expand Up @@ -120,6 +120,13 @@ impl Symbol {
}
}

pub fn as_fn_def(&self) -> Option<&FnDef> {
match self {
Symbol::FunctionDef(x) => Some(x.as_ref()),
_ => None,
}
}

pub fn target_type(&self) -> Option<Type> {
match self {
Symbol::ParamVar(_, ty) => Some(ty.as_ref().clone()),
Expand Down Expand Up @@ -182,6 +189,8 @@ pub struct Program {
pub assets: Vec<AssetDef>,
pub parties: Vec<PartyDef>,
pub policies: Vec<PolicyDef>,
#[serde(default)]
pub functions: Vec<FnDef>,
pub span: Span,

// analysis
Expand Down Expand Up @@ -730,6 +739,15 @@ pub struct FnCall {
pub span: Span,
}

impl FnCall {
/// The static type of the call: the callee function's declared return type,
/// or `None` until the callee is resolved (§6.3).
pub fn target_type(&self) -> Option<Type> {
let fn_def = self.callee.symbol.as_ref()?.as_fn_def()?;
Some(fn_def.return_type.clone())
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DataExpr {
None,
Expand All @@ -743,10 +761,6 @@ pub enum DataExpr {
MapConstructor(MapConstructor),
AnyAssetConstructor(AnyAssetConstructor),
Identifier(Identifier),
MinUtxo(Identifier),
ComputeTipSlot,
SlotToTime(Box<DataExpr>),
TimeToSlot(Box<DataExpr>),
AddOp(AddOp),
SubOp(SubOp),
ConcatOp(ConcatOp),
Expand Down Expand Up @@ -786,11 +800,7 @@ impl DataExpr {
DataExpr::PropertyOp(x) => x.target_type(),
DataExpr::AnyAssetConstructor(x) => x.target_type(),
DataExpr::UtxoRef(_) => Some(Type::UtxoRef),
DataExpr::MinUtxo(_) => Some(Type::AnyAsset),
DataExpr::ComputeTipSlot => Some(Type::Int),
DataExpr::SlotToTime(_) => Some(Type::Int),
DataExpr::TimeToSlot(_) => Some(Type::Int),
DataExpr::FnCall(_) => None, // Function call return type determined by symbol resolution
DataExpr::FnCall(x) => x.target_type(),
}
}
}
Expand Down Expand Up @@ -974,6 +984,62 @@ pub struct AssetDef {
pub span: Span,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LetBinding {
pub name: Identifier,
pub value: DataExpr,
pub span: Span,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FnBody {
pub let_bindings: Vec<LetBinding>,
pub result: Box<DataExpr>,
pub span: Span,
}

/// A function provided by the compiler rather than declared in source. This is
/// only the serializable *key* carried on `FnDef`; each variant's signature,
/// analysis, and lowering live with its [`crate::builtins::Builtin`] impl.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum BuiltinFn {
MinUtxo,
TipSlot,
SlotToTime,
TimeToSlot,
}

impl BuiltinFn {
/// Every built-in key. `crate::builtins::resolve` maps each to its
/// implementation (exhaustively, so this list and the registry stay in
/// sync at compile time).
pub const ALL: [BuiltinFn; 4] = [
BuiltinFn::MinUtxo,
BuiltinFn::TipSlot,
BuiltinFn::SlotToTime,
BuiltinFn::TimeToSlot,
];
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FnDef {
pub name: Identifier,
pub parameters: ParameterList,
pub return_type: Type,
/// The inline body of a user-defined function. `None` for built-ins, which
/// carry a `builtin` kind instead.
pub body: Option<FnBody>,
/// Set when this is a compiler-provided function; mutually exclusive with
/// `body`. User-defined functions always parse with `builtin: None`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub builtin: Option<BuiltinFn>,
pub span: Span,

// analysis
#[serde(skip)]
pub(crate) scope: Option<Rc<Scope>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ChainSpecificBlock {
Cardano(crate::cardano::CardanoBlock),
Expand Down
177 changes: 177 additions & 0 deletions crates/tx3-lang/src/builtins/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//! Compiler-provided built-in functions.
//!
//! Each built-in is a zero-sized type implementing [`Builtin`], which gathers
//! its signature, its synthetic [`ast::FnDef`], and its lowering in one place.
//! [`ast::BuiltinFn`] is the serializable key carried on `FnDef`; [`resolve`]
//! maps a key to its implementation and is exhaustive, so a new `BuiltinFn`
//! variant will not compile until it is registered here.
//!
//! Built-ins whose lowering is "lower the arguments and feed them to a
//! [`ir::CompilerOp`]" are declared with the [`builtin_boilerplate!`] table
//! below. A built-in that needs custom analysis or lowering can instead be a
//! hand-written `impl Builtin`; both kinds coexist behind [`resolve`].

use crate::ast::{self, BuiltinFn, FnDef, Identifier, ParamDef, ParameterList, Span, Type};
use crate::lowering::{Context, Error as LowerError, IntoLower};

use tx3_tir::model::v1beta0 as ir;

/// The type signature of a built-in: named parameters and a return type.
pub struct Signature {
pub params: Vec<(&'static str, Type)>,
pub returns: Type,
}

/// A compiler-provided function. One implementor per [`BuiltinFn`] variant.
pub trait Builtin: Sync {
/// The serializable key this built-in is registered and resolved under.
fn kind(&self) -> BuiltinFn;

/// The call-site name (e.g. `"min_utxo"`).
fn name(&self) -> &'static str;

/// Parameter and return types.
fn signature(&self) -> Signature;

/// Lower a call to this built-in into its compiler operation. Built-in
/// calls are evaluated by the resolver, not inlined like user functions.
fn lower_call(
&self,
args: &[ast::DataExpr],
ctx: &Context,
) -> Result<ir::Expression, LowerError>;

/// The synthetic, body-less `FnDef` registered into the program scope so
/// that calls resolve against a real signature.
fn definition(&self) -> FnDef {
let sig = self.signature();

FnDef {
name: Identifier::new(self.name()),
parameters: ParameterList {
parameters: sig
.params
.into_iter()
.map(|(name, r#type)| ParamDef {
name: Identifier::new(name),
r#type,
docstring: None,
})
.collect(),
span: Span::DUMMY,
},
return_type: sig.returns,
body: None,
builtin: Some(self.kind()),
span: Span::DUMMY,
scope: None,
}
}
}

/// Declare a built-in whose lowering is "bind each named parameter to its
/// lowered argument, then construct an [`ir::CompilerOp`]".
///
/// ```text
/// builtin_boilerplate!(Variant, "name", (param: Type, …) -> ReturnType => CompilerOp(param, …));
/// ```
///
/// The operation references the parameters by name, so the names are checked
/// against the signature at compile time. Parameter and return types must be
/// bare [`ast::Type`] variant idents (e.g. `Int`, `AnyAsset`); a built-in that
/// needs a richer type or non-`CompilerOp` lowering is written by hand instead.
macro_rules! builtin_boilerplate {
(
$variant:ident,
$name:literal,
( $( $param:ident : $pty:ident ),* $(,)? ) -> $ret:ident
=> $op:ident $(( $( $oparg:ident ),* ))?
) => {
pub struct $variant;

impl Builtin for $variant {
fn kind(&self) -> BuiltinFn {
BuiltinFn::$variant
}

fn name(&self) -> &'static str {
$name
}

fn signature(&self) -> Signature {
Signature {
params: vec![ $( (stringify!($param), Type::$pty) ),* ],
returns: Type::$ret,
}
}

// A nullary built-in (e.g. `tip_slot`) uses neither `args` nor
// `ctx`; allow that so every invocation expands uniformly.
#[allow(unused_mut, unused_variables)]
fn lower_call(
&self,
args: &[ast::DataExpr],
ctx: &Context,
) -> Result<ir::Expression, LowerError> {
let mut args = args.iter();
$(
let $param = args
.next()
.ok_or_else(|| LowerError::InvalidAst(format!(
"built-in '{}' expects argument '{}'",
$name,
stringify!($param),
)))?
.into_lower(ctx)?;
)*
Ok(ir::Expression::EvalCompiler(Box::new(
ir::CompilerOp::$op $(( $( $oparg ),* ))?
)))
}
}
};
}

builtin_boilerplate!(MinUtxo, "min_utxo", (output: Int) -> AnyAsset => ComputeMinUtxo(output));
builtin_boilerplate!(TipSlot, "tip_slot", () -> Int => ComputeTipSlot);
builtin_boilerplate!(SlotToTime, "slot_to_time", (slot: Int) -> Int => ComputeSlotToTime(slot));
builtin_boilerplate!(TimeToSlot, "time_to_slot", (time: Int) -> Int => ComputeTimeToSlot(time));

/// Map a built-in key to its implementation. Exhaustive by construction.
pub fn resolve(kind: BuiltinFn) -> &'static dyn Builtin {
match kind {
BuiltinFn::MinUtxo => &MinUtxo,
BuiltinFn::TipSlot => &TipSlot,
BuiltinFn::SlotToTime => &SlotToTime,
BuiltinFn::TimeToSlot => &TimeToSlot,
}
}

/// Every built-in, in [`BuiltinFn::ALL`] order.
pub fn all() -> impl Iterator<Item = &'static dyn Builtin> {
BuiltinFn::ALL.into_iter().map(resolve)
}

#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;

#[test]
fn registry_is_consistent() {
let mut names = HashSet::new();
for kind in BuiltinFn::ALL {
let b = resolve(kind);
// `resolve` returns the implementation for the requested key.
assert_eq!(b.kind(), kind);
// The synthetic definition is tagged with the same key and matches
// the declared signature.
let def = b.definition();
assert_eq!(def.builtin, Some(kind));
assert_eq!(def.name.value, b.name());
assert_eq!(def.parameters.parameters.len(), b.signature().params.len());
// Names are unique across the set.
assert!(names.insert(b.name()), "duplicate built-in name: {}", b.name());
}
}
}
1 change: 1 addition & 0 deletions crates/tx3-lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

pub mod analyzing;
pub mod ast;
pub(crate) mod builtins;
pub mod lowering;
pub mod parsing;

Expand Down
Loading
Loading