diff --git a/crates/tx3-lang/src/analyzing.rs b/crates/tx3-lang/src/analyzing.rs index 633de87..f11ffb6 100644 --- a/crates/tx3-lang/src/analyzing.rs +++ b/crates/tx3-lang/src/analyzing.rs @@ -169,7 +169,7 @@ impl Error { Symbol::AssetDef(asset) => format!("AssetDef({})", asset.name.value), Symbol::EnvVar(name, _) => format!("EnvVar({})", name), Symbol::ParamVar(name, _) => format!("ParamVar({})", name), - Symbol::Function(name) => format!("Function({})", name), + Symbol::FunctionDef(fn_def) => format!("FunctionDef({})", fn_def.name.value), Symbol::Input(block) => format!("Input({})", block.name), Symbol::Reference(block) => format!("Reference({})", block.name), Symbol::Output(idx) => format!("Output({})", idx), @@ -305,7 +305,70 @@ macro_rules! bail_report { }; } -const BUILTIN_FUNCTIONS: &[&str] = &["min_utxo", "tip_slot", "slot_to_time", "time_to_slot"]; +fn builtin_fn_defs() -> Vec { + vec![ + FnDef { + name: Identifier::new("min_utxo"), + parameters: ParameterList { + parameters: vec![ParamDef { + name: Identifier::new("output"), + r#type: Type::Int, + docstring: None, + }], + span: Span::DUMMY, + }, + return_type: Type::AnyAsset, + body: None, + builtin: Some(BuiltinFn::MinUtxo), + span: Span::DUMMY, + scope: None, + }, + FnDef { + name: Identifier::new("tip_slot"), + parameters: ParameterList { + parameters: vec![], + span: Span::DUMMY, + }, + return_type: Type::Int, + body: None, + builtin: Some(BuiltinFn::TipSlot), + span: Span::DUMMY, + scope: None, + }, + FnDef { + name: Identifier::new("slot_to_time"), + parameters: ParameterList { + parameters: vec![ParamDef { + name: Identifier::new("slot"), + r#type: Type::Int, + docstring: None, + }], + span: Span::DUMMY, + }, + return_type: Type::Int, + body: None, + builtin: Some(BuiltinFn::SlotToTime), + span: Span::DUMMY, + scope: None, + }, + FnDef { + name: Identifier::new("time_to_slot"), + parameters: ParameterList { + parameters: vec![ParamDef { + name: Identifier::new("time"), + r#type: Type::Int, + docstring: None, + }], + span: Span::DUMMY, + }, + return_type: Type::Int, + body: None, + builtin: Some(BuiltinFn::TimeToSlot), + span: Span::DUMMY, + scope: None, + }, + ] +} impl Scope { pub fn new(parent: Option>) -> Self { @@ -378,6 +441,13 @@ impl Scope { ); } + pub fn track_fn_def(&mut self, fn_def: &FnDef) { + self.symbols.insert( + fn_def.name.value.clone(), + Symbol::FunctionDef(Box::new(fn_def.clone())), + ); + } + pub fn track_local_expr(&mut self, name: &str, expr: DataExpr) { self.symbols .insert(name.to_string(), Symbol::LocalExpr(Box::new(expr))); @@ -416,8 +486,6 @@ impl Scope { Some(symbol.clone()) } else if let Some(parent) = &self.parent { parent.resolve(name) - } else if BUILTIN_FUNCTIONS.contains(&name) { - Some(Symbol::Function(name.to_string())) } else { None } @@ -719,9 +787,6 @@ impl Analyzable for DataExpr { DataExpr::PropertyOp(x) => x.analyze(parent), DataExpr::AnyAssetConstructor(x) => x.analyze(parent), DataExpr::FnCall(x) => x.analyze(parent), - DataExpr::MinUtxo(x) => x.analyze(parent), - DataExpr::SlotToTime(x) => x.analyze(parent), - DataExpr::TimeToSlot(x) => x.analyze(parent), DataExpr::ConcatOp(x) => x.analyze(parent), _ => AnalyzeReport::default(), } @@ -739,9 +804,6 @@ impl Analyzable for DataExpr { DataExpr::PropertyOp(x) => x.is_resolved(), DataExpr::AnyAssetConstructor(x) => x.is_resolved(), DataExpr::FnCall(x) => x.is_resolved(), - DataExpr::MinUtxo(x) => x.is_resolved(), - DataExpr::SlotToTime(x) => x.is_resolved(), - DataExpr::TimeToSlot(x) => x.is_resolved(), DataExpr::ConcatOp(x) => x.is_resolved(), _ => true, } @@ -1225,6 +1287,77 @@ impl Analyzable for LocalsBlock { } } +impl Analyzable for LetBinding { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.value.analyze(parent) + } + + fn is_resolved(&self) -> bool { + self.value.is_resolved() + } +} + +impl Analyzable for FnBody { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let mut report = AnalyzeReport::default(); + + for binding in &mut self.let_bindings { + report = report + binding.analyze(parent.clone()); + } + + report = report + self.result.analyze(parent); + + report + } + + fn is_resolved(&self) -> bool { + self.let_bindings.is_resolved() && self.result.is_resolved() + } +} + +impl Analyzable for FnDef { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let params_report = self.parameters.analyze(parent.clone()); + let return_type_report = self.return_type.analyze(parent.clone()); + + // Built-in functions have no body — nothing more to analyze + let body = match &mut self.body { + Some(body) => body, + None => return params_report + return_type_report, + }; + + let mut scope = Scope::new(parent); + + for param in self.parameters.parameters.iter() { + scope.track_param_var(¶m.name.value, param.r#type.clone()); + } + + // Add let-bindings sequentially - each binding creates a new scope layer + let mut current_scope = Rc::new(scope); + + let mut bindings_report = AnalyzeReport::default(); + + for binding in &mut body.let_bindings { + bindings_report = bindings_report + binding.analyze(Some(current_scope.clone())); + // Create a new scope layer with this binding available + let mut next_scope = Scope::new(Some(current_scope)); + next_scope.track_local_expr(&binding.name.value, binding.value.clone()); + current_scope = Rc::new(next_scope); + } + + let result_report = body.result.analyze(Some(current_scope.clone())); + + self.scope = Some(current_scope); + + params_report + return_type_report + bindings_report + result_report + } + + fn is_resolved(&self) -> bool { + self.parameters.is_resolved() + && self.body.as_ref().map_or(true, |b| b.is_resolved()) + } +} + impl Analyzable for ParamDef { fn analyze(&mut self, parent: Option>) -> AnalyzeReport { self.r#type.analyze(parent) @@ -1414,6 +1547,14 @@ impl Analyzable for Program { scope.track_alias_def(alias_def); } + for builtin in builtin_fn_defs().iter() { + scope.track_fn_def(builtin); + } + + for fn_def in self.functions.iter() { + scope.track_fn_def(fn_def); + } + self.scope = Some(Rc::new(scope)); let parties = self.parties.analyze(self.scope.clone()); @@ -1429,15 +1570,41 @@ impl Analyzable for Program { let (types, aliases) = resolve_types_and_aliases(scope_rc, &mut types, &mut aliases); + // Functions may call other functions, and lowering inlines a callee's + // *analyzed* body. A single analysis pass leaves each call site holding + // a pre-analysis clone of its callee (whose own body is unresolved), so + // we analyze to a fixed point: each pass re-registers the + // progressively-analyzed definitions and re-resolves call sites against + // them. Functions are non-recursive (the call graph is acyclic), so the + // number of definitions is a sufficient upper bound on the longest call + // chain and the iteration terminates. + let program_scope = self.scope.clone(); + let mut functions = AnalyzeReport::default(); + for _ in 0..self.functions.len() { + let mut fn_scope = Scope::new(program_scope.clone()); + for fn_def in self.functions.iter() { + fn_scope.track_fn_def(fn_def); + } + functions = self.functions.analyze(Some(Rc::new(fn_scope))); + } + + // Final scope: txs resolve calls to the fully-analyzed definitions. + let mut fn_scope = Scope::new(program_scope); + for fn_def in self.functions.iter() { + fn_scope.track_fn_def(fn_def); + } + self.scope = Some(Rc::new(fn_scope)); + let txs = self.txs.analyze(self.scope.clone()); - parties + policies + types + aliases + txs + assets + parties + policies + types + aliases + functions + txs + assets } fn is_resolved(&self) -> bool { self.policies.is_resolved() && self.types.is_resolved() && self.aliases.is_resolved() + && self.functions.is_resolved() && self.txs.is_resolved() && self.assets.is_resolved() } diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index cf7cdec..dfdeb6d 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -32,7 +32,7 @@ pub enum Symbol { AliasDef(Box), RecordField(Box), VariantCase(Box), - Function(String), + FunctionDef(Box), Fees, } @@ -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 { match self { Symbol::ParamVar(_, ty) => Some(ty.as_ref().clone()), @@ -182,6 +189,8 @@ pub struct Program { pub assets: Vec, pub parties: Vec, pub policies: Vec, + #[serde(default)] + pub functions: Vec, pub span: Span, // analysis @@ -743,10 +752,6 @@ pub enum DataExpr { MapConstructor(MapConstructor), AnyAssetConstructor(AnyAssetConstructor), Identifier(Identifier), - MinUtxo(Identifier), - ComputeTipSlot, - SlotToTime(Box), - TimeToSlot(Box), AddOp(AddOp), SubOp(SubOp), ConcatOp(ConcatOp), @@ -786,11 +791,12 @@ 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(call) => call + .callee + .symbol + .as_ref() + .and_then(|s| s.as_fn_def()) + .map(|fn_def| fn_def.return_type.clone()), } } } @@ -974,6 +980,50 @@ 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, + pub result: Box, + pub span: Span, +} + +/// A function provided by the compiler rather than declared in source. Each +/// variant lowers to a dedicated compiler operation (§7); see +/// `analyzing::builtin_fn_defs` for their signatures. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum BuiltinFn { + MinUtxo, + TipSlot, + SlotToTime, + 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, + /// 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, + pub span: Span, + + // analysis + #[serde(skip)] + pub(crate) scope: Option>, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum ChainSpecificBlock { Cardano(crate::cardano::CardanoBlock), diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index 22754f0..05ff3e8 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -413,79 +413,97 @@ impl IntoLower for ast::FnCall { fn into_lower(&self, ctx: &Context) -> Result { let function_name = &self.callee.value; - match function_name.as_str() { - "min_utxo" => { - if self.args.len() != 1 { - return Err(Error::InvalidAst(format!( - "min_utxo expects 1 argument, got {}", - self.args.len() - ))); + // Check if callee resolves to a function definition + if let Some(symbol) = self.callee.symbol.as_ref() { + if let Some(fn_def) = symbol.as_fn_def() { + if let Some(builtin) = fn_def.builtin { + return lower_builtin_fn_call(builtin, &self.args, ctx); } - let arg = self.args[0].into_lower(ctx)?; - Ok(ir::Expression::EvalCompiler(Box::new( - ir::CompilerOp::ComputeMinUtxo(arg), - ))) + return inline_fn_call(fn_def, &self.args, ctx); } - "tip_slot" => { - if !self.args.is_empty() { - return Err(Error::InvalidAst(format!( - "tip_slot expects 0 arguments, got {}", - self.args.len() - ))); - } - Ok(ir::Expression::EvalCompiler(Box::new( - ir::CompilerOp::ComputeTipSlot, - ))) - } - "slot_to_time" => { - if self.args.len() != 1 { - return Err(Error::InvalidAst(format!( - "slot_to_time expects 1 argument, got {}", - self.args.len() - ))); - } - let arg = self.args[0].into_lower(ctx)?; - Ok(ir::Expression::EvalCompiler(Box::new( - ir::CompilerOp::ComputeSlotToTime(arg), - ))) + } + + // Try to coerce as asset - if it fails, we'll get an error + match coerce_identifier_into_asset_def(&self.callee) { + Ok(asset_def) => { + let policy = asset_def.policy.into_lower(ctx)?; + let asset_name = asset_def.asset_name.into_lower(ctx)?; + let amount = self.args[0].into_lower(ctx)?; + + Ok(ir::Expression::Assets(vec![ir::AssetExpr { + policy, + asset_name, + amount, + }])) } - "time_to_slot" => { - if self.args.len() != 1 { - return Err(Error::InvalidAst(format!( - "time_to_slot expects 1 argument, got {}", - self.args.len() - ))); - } + Err(_) => Err(Error::InvalidAst(format!( + "unknown function: {}", + function_name + ))), + } + } +} - let arg = self.args[0].into_lower(ctx)?; +fn lower_builtin_fn_call( + builtin: ast::BuiltinFn, + args: &[ast::DataExpr], + ctx: &Context, +) -> Result { + let op = match builtin { + ast::BuiltinFn::MinUtxo => ir::CompilerOp::ComputeMinUtxo(args[0].into_lower(ctx)?), + ast::BuiltinFn::TipSlot => ir::CompilerOp::ComputeTipSlot, + ast::BuiltinFn::SlotToTime => ir::CompilerOp::ComputeSlotToTime(args[0].into_lower(ctx)?), + ast::BuiltinFn::TimeToSlot => ir::CompilerOp::ComputeTimeToSlot(args[0].into_lower(ctx)?), + }; + + Ok(ir::Expression::EvalCompiler(Box::new(op))) +} - Ok(ir::Expression::EvalCompiler(Box::new( - ir::CompilerOp::ComputeTimeToSlot(arg), - ))) - } - _ => { - // Try to coerce as asset - if it fails, we'll get an error - - match coerce_identifier_into_asset_def(&self.callee) { - Ok(asset_def) => { - let policy = asset_def.policy.into_lower(ctx)?; - let asset_name = asset_def.asset_name.into_lower(ctx)?; - let amount = self.args[0].into_lower(ctx)?; - - Ok(ir::Expression::Assets(vec![ir::AssetExpr { - policy, - asset_name, - amount, - }])) - } - Err(_) => Err(Error::InvalidAst(format!( - "unknown function: {}", - function_name - ))), +struct ParamSubstituter<'a> { + subs: &'a std::collections::HashMap, +} + +impl tx3_tir::Visitor for ParamSubstituter<'_> { + fn reduce( + &mut self, + expr: ir::Expression, + ) -> Result { + if let ir::Expression::EvalParam(ref param) = expr { + if let ir::Param::ExpectValue(name, _) = param.as_ref() { + if let Some(replacement) = self.subs.get(name) { + return Ok(replacement.clone()); } } } + Ok(expr) + } +} + +fn inline_fn_call( + fn_def: &ast::FnDef, + args: &[ast::DataExpr], + ctx: &Context, +) -> Result { + let body = fn_def.body.as_ref().ok_or_else(|| { + Error::InvalidAst(format!( + "cannot inline built-in function '{}'", + fn_def.name.value + )) + })?; + + let lowered_body = body.result.into_lower(ctx)?; + + // Build substitution map: lowercased param name → lowered arg + let mut subs = std::collections::HashMap::new(); + for (param, arg) in fn_def.parameters.parameters.iter().zip(args) { + subs.insert(param.name.value.to_lowercase(), arg.into_lower(ctx)?); } + + use tx3_tir::Node; + let mut visitor = ParamSubstituter { subs: &subs }; + lowered_body + .apply(&mut visitor) + .map_err(|e| Error::InvalidAst(e.to_string())) } impl IntoLower for ast::PropertyOp { @@ -567,18 +585,6 @@ impl IntoLower for ast::DataExpr { ast::DataExpr::PropertyOp(x) => x.into_lower(ctx)?, ast::DataExpr::UtxoRef(x) => x.into_lower(ctx)?, ast::DataExpr::FnCall(x) => x.into_lower(ctx)?, - ast::DataExpr::MinUtxo(x) => ir::Expression::EvalCompiler(Box::new( - ir::CompilerOp::ComputeMinUtxo(x.into_lower(ctx)?), - )), - ast::DataExpr::ComputeTipSlot => { - ir::Expression::EvalCompiler(Box::new(ir::CompilerOp::ComputeTipSlot)) - } - ast::DataExpr::SlotToTime(x) => ir::Expression::EvalCompiler(Box::new( - ir::CompilerOp::ComputeSlotToTime(x.into_lower(ctx)?), - )), - ast::DataExpr::TimeToSlot(x) => ir::Expression::EvalCompiler(Box::new( - ir::CompilerOp::ComputeTimeToSlot(x.into_lower(ctx)?), - )), }; Ok(out) @@ -1048,12 +1054,20 @@ mod tests { test_lowering!(min_utxo); + test_lowering!(tip_slot); + + test_lowering!(posix_time); + test_lowering!(donation); test_lowering!(list_concat); test_lowering!(buidler_fest_2026); + test_lowering!(functions); + + test_lowering!(nested_functions); + test_lowering!(param_field_shadow); test_lowering!(oracle_reference_datum); diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 29e392b..09c8cae 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -12,10 +12,7 @@ use pest::{ }; use pest_derive::Parser; -use crate::{ - ast::*, - cardano::{PlutusWitnessBlock, PlutusWitnessField}, -}; +use crate::ast::*; #[derive(Parser)] #[grammar = "tx3.pest"] @@ -122,6 +119,7 @@ impl AstNode for Program { aliases: Vec::new(), parties: Vec::new(), policies: Vec::new(), + functions: Vec::new(), scope: None, span, }; @@ -136,6 +134,7 @@ impl AstNode for Program { Rule::alias_def => program.aliases.push(AliasDef::parse(pair)?), Rule::party_def => program.parties.push(PartyDef::parse(pair)?), Rule::policy_def => program.policies.push(PolicyDef::parse(pair)?), + Rule::fn_def => program.functions.push(FnDef::parse(pair)?), Rule::EOI => break, x => unreachable!("Unexpected rule in program: {:?}", x), } @@ -964,6 +963,82 @@ impl AstNode for crate::ast::FnCall { } } +impl AstNode for LetBinding { + const RULE: Rule = Rule::let_binding; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + let name = Identifier::parse(inner.next().unwrap())?; + let value = DataExpr::parse(inner.next().unwrap())?; + + Ok(LetBinding { name, value, span }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for FnBody { + const RULE: Rule = Rule::fn_body; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let inner = pair.into_inner(); + + let mut let_bindings = Vec::new(); + let mut result_expr = None; + + for pair in inner { + match pair.as_rule() { + Rule::let_binding => let_bindings.push(LetBinding::parse(pair)?), + Rule::data_expr => result_expr = Some(DataExpr::parse(pair)?), + x => unreachable!("Unexpected rule in fn_body: {:?}", x), + } + } + + Ok(FnBody { + let_bindings, + result: Box::new(result_expr.expect("fn_body must have a result expression")), + span, + }) + } + + fn span(&self) -> &Span { + &self.span + } +} + +impl AstNode for FnDef { + const RULE: Rule = Rule::fn_def; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + let name = Identifier::parse(inner.next().unwrap())?; + let parameters = ParameterList::parse(inner.next().unwrap())?; + let return_type = Type::parse(inner.next().unwrap())?; + let body = FnBody::parse(inner.next().unwrap())?; + + Ok(FnDef { + name, + parameters, + return_type, + body: Some(body), + builtin: None, + span, + scope: None, + }) + } + + fn span(&self) -> &Span { + &self.span + } +} + impl AstNode for RecordConstructorField { const RULE: Rule = Rule::record_constructor_field; @@ -1321,10 +1396,6 @@ impl AstNode for DataExpr { DataExpr::NegateOp(x) => &x.span, DataExpr::PropertyOp(x) => &x.span, DataExpr::UtxoRef(x) => x.span(), - DataExpr::MinUtxo(x) => x.span(), - DataExpr::SlotToTime(x) => x.span(), - DataExpr::TimeToSlot(x) => x.span(), - DataExpr::ComputeTipSlot => &Span::DUMMY, // TODO DataExpr::FnCall(x) => &x.span, } } @@ -2677,6 +2748,7 @@ mod tests { env: None, assets: vec![], policies: vec![], + functions: vec![], span: Span::DUMMY, scope: None, } @@ -2973,4 +3045,8 @@ mod tests { test_parsing!(list_concat); test_parsing!(buidler_fest_2026); + + test_parsing!(functions); + + test_parsing!(nested_functions); } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index fbe198d..7010bf2 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -451,6 +451,13 @@ env_def = { "env" ~ "{" ~ (env_field ~ ",")+ ~ "}" } +// Function definitions +let_binding = { "let" ~ identifier ~ "=" ~ data_expr ~ ";" } +fn_body = { let_binding* ~ data_expr } +fn_def = { + "fn" ~ identifier ~ parameter_list ~ "->" ~ type ~ "{" ~ fn_body ~ "}" +} + // Transaction definition tx_def = { docstring? ~ "tx" ~ identifier ~ parameter_list ~ "{" ~ tx_body_block* ~ "}" @@ -459,6 +466,6 @@ tx_def = { // Program program = { SOI ~ - (env_def | asset_def | party_def | policy_def | type_def | tx_def)* ~ + (env_def | asset_def | party_def | policy_def | type_def | fn_def | tx_def)* ~ EOI } diff --git a/examples/functions.ast b/examples/functions.ast new file mode 100644 index 0000000..10ea4ee --- /dev/null +++ b/examples/functions.ast @@ -0,0 +1,1301 @@ +{ + "env": null, + "txs": [ + { + "name": { + "value": "use_double", + "span": { + "dummy": false, + "start": 529, + "end": 539 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "amount", + "span": { + "dummy": false, + "start": 540, + "end": 546 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 539, + "end": 552 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "source", + "many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 588, + "end": 594 + } + } + } + }, + { + "MinAmount": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 616, + "end": 619 + } + }, + "args": [ + { + "Identifier": { + "value": "amount", + "span": { + "dummy": false, + "start": 620, + "end": 626 + } + } + } + ], + "span": { + "dummy": false, + "start": 616, + "end": 627 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 559, + "end": 634 + } + } + ], + "outputs": [ + { + "name": null, + "optional": false, + "fields": [ + { + "To": { + "Identifier": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 660, + "end": 668 + } + } + } + }, + { + "Amount": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 686, + "end": 689 + } + }, + "args": [ + { + "FnCall": { + "callee": { + "value": "double", + "span": { + "dummy": false, + "start": 690, + "end": 696 + } + }, + "args": [ + { + "Identifier": { + "value": "amount", + "span": { + "dummy": false, + "start": 697, + "end": 703 + } + } + } + ], + "span": { + "dummy": false, + "start": 690, + "end": 704 + } + } + } + ], + "span": { + "dummy": false, + "start": 686, + "end": 705 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 639, + "end": 712 + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 526, + "end": 714 + }, + "collateral": [], + "metadata": null + }, + { + "name": { + "value": "use_complex", + "span": { + "dummy": false, + "start": 719, + "end": 730 + } + }, + "parameters": { + "parameters": [], + "span": { + "dummy": false, + "start": 730, + "end": 732 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "source", + "many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 768, + "end": 774 + } + } + } + }, + { + "MinAmount": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 796, + "end": 799 + } + }, + "args": [ + { + "Number": 2000000 + } + ], + "span": { + "dummy": false, + "start": 796, + "end": 808 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 739, + "end": 815 + } + } + ], + "outputs": [ + { + "name": null, + "optional": false, + "fields": [ + { + "To": { + "Identifier": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 841, + "end": 849 + } + } + } + }, + { + "Amount": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 867, + "end": 870 + } + }, + "args": [ + { + "FnCall": { + "callee": { + "value": "complex", + "span": { + "dummy": false, + "start": 871, + "end": 878 + } + }, + "args": [ + { + "Number": 50 + }, + { + "Number": 75 + } + ], + "span": { + "dummy": false, + "start": 871, + "end": 886 + } + } + } + ], + "span": { + "dummy": false, + "start": 867, + "end": 887 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 820, + "end": 894 + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 716, + "end": 896 + }, + "collateral": [], + "metadata": null + }, + { + "name": { + "value": "use_pool", + "span": { + "dummy": false, + "start": 901, + "end": 909 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "delta_a", + "span": { + "dummy": false, + "start": 910, + "end": 917 + } + }, + "type": "Int" + }, + { + "name": { + "value": "delta_b", + "span": { + "dummy": false, + "start": 924, + "end": 931 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 909, + "end": 937 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "source", + "many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 973, + "end": 979 + } + } + } + }, + { + "MinAmount": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 1001, + "end": 1004 + } + }, + "args": [ + { + "Number": 2000000 + } + ], + "span": { + "dummy": false, + "start": 1001, + "end": 1013 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 944, + "end": 1020 + } + } + ], + "outputs": [ + { + "name": null, + "optional": false, + "fields": [ + { + "To": { + "Identifier": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 1046, + "end": 1054 + } + } + } + }, + { + "Amount": { + "SubOp": { + "lhs": { + "Identifier": { + "value": "source", + "span": { + "dummy": false, + "start": 1072, + "end": 1078 + } + } + }, + "rhs": { + "Identifier": { + "value": "fees", + "span": { + "dummy": false, + "start": 1081, + "end": 1085 + } + } + }, + "span": { + "dummy": false, + "start": 1079, + "end": 1080 + } + } + } + }, + { + "Datum": { + "FnCall": { + "callee": { + "value": "adjust_pool", + "span": { + "dummy": false, + "start": 1102, + "end": 1113 + } + }, + "args": [ + { + "FnCall": { + "callee": { + "value": "make_pool", + "span": { + "dummy": false, + "start": 1114, + "end": 1123 + } + }, + "args": [ + { + "Number": 1000 + }, + { + "Number": 2000 + } + ], + "span": { + "dummy": false, + "start": 1114, + "end": 1135 + } + } + }, + { + "Identifier": { + "value": "delta_a", + "span": { + "dummy": false, + "start": 1137, + "end": 1144 + } + } + }, + { + "Identifier": { + "value": "delta_b", + "span": { + "dummy": false, + "start": 1146, + "end": 1153 + } + } + } + ], + "span": { + "dummy": false, + "start": 1102, + "end": 1154 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 1025, + "end": 1161 + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 898, + "end": 1163 + }, + "collateral": [], + "metadata": null + } + ], + "types": [ + { + "name": { + "value": "PoolState", + "span": { + "dummy": false, + "start": 36, + "end": 45 + } + }, + "cases": [ + { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "pair_a", + "span": { + "dummy": false, + "start": 52, + "end": 58 + } + }, + "type": "Int", + "span": { + "dummy": false, + "start": 52, + "end": 63 + } + }, + { + "name": { + "value": "pair_b", + "span": { + "dummy": false, + "start": 69, + "end": 75 + } + }, + "type": "Int", + "span": { + "dummy": false, + "start": 69, + "end": 80 + } + } + ], + "span": { + "dummy": false, + "start": 31, + "end": 83 + } + } + ], + "span": { + "dummy": false, + "start": 31, + "end": 83 + } + } + ], + "aliases": [], + "assets": [], + "parties": [ + { + "name": { + "value": "Sender", + "span": { + "dummy": false, + "start": 6, + "end": 12 + } + }, + "span": { + "dummy": false, + "start": 0, + "end": 13 + } + }, + { + "name": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 20, + "end": 28 + } + }, + "span": { + "dummy": false, + "start": 14, + "end": 29 + } + } + ], + "policies": [], + "functions": [ + { + "name": { + "value": "double", + "span": { + "dummy": false, + "start": 88, + "end": 94 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "x", + "span": { + "dummy": false, + "start": 95, + "end": 96 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 94, + "end": 102 + } + }, + "return_type": "Int", + "body": { + "let_bindings": [], + "result": { + "AddOp": { + "lhs": { + "Identifier": { + "value": "x", + "span": { + "dummy": false, + "start": 116, + "end": 117 + } + } + }, + "rhs": { + "Identifier": { + "value": "x", + "span": { + "dummy": false, + "start": 120, + "end": 121 + } + } + }, + "span": { + "dummy": false, + "start": 118, + "end": 119 + } + } + }, + "span": { + "dummy": false, + "start": 116, + "end": 122 + } + }, + "span": { + "dummy": false, + "start": 85, + "end": 123 + } + }, + { + "name": { + "value": "complex", + "span": { + "dummy": false, + "start": 128, + "end": 135 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "a", + "span": { + "dummy": false, + "start": 136, + "end": 137 + } + }, + "type": "Int" + }, + { + "name": { + "value": "b", + "span": { + "dummy": false, + "start": 144, + "end": 145 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 135, + "end": 151 + } + }, + "return_type": "Int", + "body": { + "let_bindings": [ + { + "name": { + "value": "base", + "span": { + "dummy": false, + "start": 169, + "end": 173 + } + }, + "value": { + "AddOp": { + "lhs": { + "Identifier": { + "value": "a", + "span": { + "dummy": false, + "start": 176, + "end": 177 + } + } + }, + "rhs": { + "Identifier": { + "value": "b", + "span": { + "dummy": false, + "start": 180, + "end": 181 + } + } + }, + "span": { + "dummy": false, + "start": 178, + "end": 179 + } + } + }, + "span": { + "dummy": false, + "start": 165, + "end": 182 + } + }, + { + "name": { + "value": "adjusted", + "span": { + "dummy": false, + "start": 191, + "end": 199 + } + }, + "value": { + "SubOp": { + "lhs": { + "Identifier": { + "value": "base", + "span": { + "dummy": false, + "start": 202, + "end": 206 + } + } + }, + "rhs": { + "Number": 100 + }, + "span": { + "dummy": false, + "start": 207, + "end": 208 + } + } + }, + "span": { + "dummy": false, + "start": 187, + "end": 213 + } + } + ], + "result": { + "AddOp": { + "lhs": { + "Identifier": { + "value": "adjusted", + "span": { + "dummy": false, + "start": 218, + "end": 226 + } + } + }, + "rhs": { + "Identifier": { + "value": "adjusted", + "span": { + "dummy": false, + "start": 229, + "end": 237 + } + } + }, + "span": { + "dummy": false, + "start": 227, + "end": 228 + } + } + }, + "span": { + "dummy": false, + "start": 165, + "end": 238 + } + }, + "span": { + "dummy": false, + "start": 125, + "end": 239 + } + }, + { + "name": { + "value": "make_pool", + "span": { + "dummy": false, + "start": 244, + "end": 253 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "a", + "span": { + "dummy": false, + "start": 254, + "end": 255 + } + }, + "type": "Int" + }, + { + "name": { + "value": "b", + "span": { + "dummy": false, + "start": 262, + "end": 263 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 253, + "end": 269 + } + }, + "return_type": { + "Custom": { + "value": "PoolState", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + } + }, + "body": { + "let_bindings": [], + "result": { + "StructConstructor": { + "type": { + "value": "PoolState", + "span": { + "dummy": false, + "start": 289, + "end": 298 + } + }, + "case": { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "pair_a", + "span": { + "dummy": false, + "start": 309, + "end": 315 + } + }, + "value": { + "Identifier": { + "value": "a", + "span": { + "dummy": false, + "start": 317, + "end": 318 + } + } + }, + "span": { + "dummy": false, + "start": 309, + "end": 318 + } + }, + { + "name": { + "value": "pair_b", + "span": { + "dummy": false, + "start": 328, + "end": 334 + } + }, + "value": { + "Identifier": { + "value": "b", + "span": { + "dummy": false, + "start": 336, + "end": 337 + } + } + }, + "span": { + "dummy": false, + "start": 328, + "end": 337 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 299, + "end": 344 + } + }, + "span": { + "dummy": false, + "start": 289, + "end": 344 + } + } + }, + "span": { + "dummy": false, + "start": 289, + "end": 345 + } + }, + "span": { + "dummy": false, + "start": 241, + "end": 346 + } + }, + { + "name": { + "value": "adjust_pool", + "span": { + "dummy": false, + "start": 351, + "end": 362 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "pool", + "span": { + "dummy": false, + "start": 363, + "end": 367 + } + }, + "type": { + "Custom": { + "value": "PoolState", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + } + } + }, + { + "name": { + "value": "delta_a", + "span": { + "dummy": false, + "start": 380, + "end": 387 + } + }, + "type": "Int" + }, + { + "name": { + "value": "delta_b", + "span": { + "dummy": false, + "start": 394, + "end": 401 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 362, + "end": 407 + } + }, + "return_type": { + "Custom": { + "value": "PoolState", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + } + }, + "body": { + "let_bindings": [], + "result": { + "StructConstructor": { + "type": { + "value": "PoolState", + "span": { + "dummy": false, + "start": 427, + "end": 436 + } + }, + "case": { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "pair_a", + "span": { + "dummy": false, + "start": 447, + "end": 453 + } + }, + "value": { + "AddOp": { + "lhs": { + "PropertyOp": { + "operand": { + "Identifier": { + "value": "pool", + "span": { + "dummy": false, + "start": 455, + "end": 459 + } + } + }, + "property": { + "Identifier": { + "value": "pair_a", + "span": { + "dummy": false, + "start": 460, + "end": 466 + } + } + }, + "span": { + "dummy": false, + "start": 459, + "end": 466 + } + } + }, + "rhs": { + "Identifier": { + "value": "delta_a", + "span": { + "dummy": false, + "start": 469, + "end": 476 + } + } + }, + "span": { + "dummy": false, + "start": 467, + "end": 468 + } + } + }, + "span": { + "dummy": false, + "start": 447, + "end": 476 + } + }, + { + "name": { + "value": "pair_b", + "span": { + "dummy": false, + "start": 486, + "end": 492 + } + }, + "value": { + "SubOp": { + "lhs": { + "PropertyOp": { + "operand": { + "Identifier": { + "value": "pool", + "span": { + "dummy": false, + "start": 494, + "end": 498 + } + } + }, + "property": { + "Identifier": { + "value": "pair_b", + "span": { + "dummy": false, + "start": 499, + "end": 505 + } + } + }, + "span": { + "dummy": false, + "start": 498, + "end": 505 + } + } + }, + "rhs": { + "Identifier": { + "value": "delta_b", + "span": { + "dummy": false, + "start": 508, + "end": 515 + } + } + }, + "span": { + "dummy": false, + "start": 506, + "end": 507 + } + } + }, + "span": { + "dummy": false, + "start": 486, + "end": 515 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 437, + "end": 522 + } + }, + "span": { + "dummy": false, + "start": 427, + "end": 522 + } + } + }, + "span": { + "dummy": false, + "start": 427, + "end": 523 + } + }, + "span": { + "dummy": false, + "start": 348, + "end": 524 + } + } + ], + "span": { + "dummy": false, + "start": 0, + "end": 1164 + } +} \ No newline at end of file diff --git a/examples/functions.tx3 b/examples/functions.tx3 new file mode 100644 index 0000000..86f4db4 --- /dev/null +++ b/examples/functions.tx3 @@ -0,0 +1,65 @@ +party Sender; +party Receiver; + +type PoolState { + pair_a: Int, + pair_b: Int, +} + +fn double(x: Int) -> Int { + x + x +} + +fn complex(a: Int, b: Int) -> Int { + let base = a + b; + let adjusted = base - 100; + adjusted + adjusted +} + +fn make_pool(a: Int, b: Int) -> PoolState { + PoolState { + pair_a: a, + pair_b: b, + } +} + +fn adjust_pool(pool: PoolState, delta_a: Int, delta_b: Int) -> PoolState { + PoolState { + pair_a: pool.pair_a + delta_a, + pair_b: pool.pair_b - delta_b, + } +} + +tx use_double(amount: Int) { + input source { + from: Sender, + min_amount: Ada(amount), + } + output { + to: Receiver, + amount: Ada(double(amount)), + } +} + +tx use_complex() { + input source { + from: Sender, + min_amount: Ada(2000000), + } + output { + to: Receiver, + amount: Ada(complex(50, 75)), + } +} + +tx use_pool(delta_a: Int, delta_b: Int) { + input source { + from: Sender, + min_amount: Ada(2000000), + } + output { + to: Receiver, + amount: source - fees, + datum: adjust_pool(make_pool(1000, 2000), delta_a, delta_b), + } +} diff --git a/examples/functions.use_complex.tir b/examples/functions.use_complex.tir new file mode 100644 index 0000000..e1ee1e7 --- /dev/null +++ b/examples/functions.use_complex.tir @@ -0,0 +1,120 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "utxos": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "Number": 2000000 + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + }, + "redeemer": "None" + } + ], + "outputs": [ + { + "address": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, + "datum": "None", + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalBuiltIn": { + "Add": [ + { + "EvalBuiltIn": { + "Sub": [ + { + "EvalBuiltIn": { + "Add": [ + { + "Number": 50 + }, + { + "Number": 75 + } + ] + } + }, + { + "Number": 100 + } + ] + } + }, + { + "EvalBuiltIn": { + "Sub": [ + { + "EvalBuiltIn": { + "Add": [ + { + "Number": 50 + }, + { + "Number": 75 + } + ] + } + }, + { + "Number": 100 + } + ] + } + } + ] + } + } + } + ] + }, + "optional": false + } + ], + "validity": null, + "mints": [], + "burns": [], + "adhoc": [], + "collateral": [], + "signers": null, + "metadata": [] +} \ No newline at end of file diff --git a/examples/functions.use_double.tir b/examples/functions.use_double.tir new file mode 100644 index 0000000..32eb07b --- /dev/null +++ b/examples/functions.use_double.tir @@ -0,0 +1,99 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "utxos": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "amount", + "Int" + ] + } + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + }, + "redeemer": "None" + } + ], + "outputs": [ + { + "address": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, + "datum": "None", + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalBuiltIn": { + "Add": [ + { + "EvalParam": { + "ExpectValue": [ + "amount", + "Int" + ] + } + }, + { + "EvalParam": { + "ExpectValue": [ + "amount", + "Int" + ] + } + } + ] + } + } + } + ] + }, + "optional": false + } + ], + "validity": null, + "mints": [], + "burns": [], + "adhoc": [], + "collateral": [], + "signers": null, + "metadata": [] +} \ No newline at end of file diff --git a/examples/functions.use_pool.tir b/examples/functions.use_pool.tir new file mode 100644 index 0000000..f0a1bfb --- /dev/null +++ b/examples/functions.use_pool.tir @@ -0,0 +1,186 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "utxos": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "Number": 2000000 + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + }, + "redeemer": "None" + } + ], + "outputs": [ + { + "address": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, + "datum": { + "Struct": { + "constructor": 0, + "fields": [ + { + "EvalBuiltIn": { + "Add": [ + { + "EvalBuiltIn": { + "Property": [ + { + "Struct": { + "constructor": 0, + "fields": [ + { + "Number": 1000 + }, + { + "Number": 2000 + } + ] + } + }, + { + "Number": 0 + } + ] + } + }, + { + "EvalParam": { + "ExpectValue": [ + "delta_a", + "Int" + ] + } + } + ] + } + }, + { + "EvalBuiltIn": { + "Sub": [ + { + "EvalBuiltIn": { + "Property": [ + { + "Struct": { + "constructor": 0, + "fields": [ + { + "Number": 1000 + }, + { + "Number": 2000 + } + ] + } + }, + { + "Number": 1 + } + ] + } + }, + { + "EvalParam": { + "ExpectValue": [ + "delta_b", + "Int" + ] + } + } + ] + } + } + ] + } + }, + "amount": { + "EvalBuiltIn": { + "Sub": [ + { + "EvalCoerce": { + "IntoAssets": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "Number": 2000000 + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + } + } + }, + { + "EvalParam": "ExpectFees" + } + ] + } + }, + "optional": false + } + ], + "validity": null, + "mints": [], + "burns": [], + "adhoc": [], + "collateral": [], + "signers": null, + "metadata": [] +} \ No newline at end of file diff --git a/examples/nested_functions.ast b/examples/nested_functions.ast new file mode 100644 index 0000000..6b90394 --- /dev/null +++ b/examples/nested_functions.ast @@ -0,0 +1,473 @@ +{ + "env": null, + "txs": [ + { + "name": { + "value": "run", + "span": { + "dummy": false, + "start": 161, + "end": 164 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "amount", + "span": { + "dummy": false, + "start": 165, + "end": 171 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 164, + "end": 177 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "source", + "many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 213, + "end": 219 + } + } + } + }, + { + "MinAmount": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 241, + "end": 244 + } + }, + "args": [ + { + "Identifier": { + "value": "amount", + "span": { + "dummy": false, + "start": 245, + "end": 251 + } + } + } + ], + "span": { + "dummy": false, + "start": 241, + "end": 252 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 184, + "end": 259 + } + } + ], + "outputs": [ + { + "name": null, + "optional": false, + "fields": [ + { + "To": { + "Identifier": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 285, + "end": 293 + } + } + } + }, + { + "Amount": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 311, + "end": 314 + } + }, + "args": [ + { + "FnCall": { + "callee": { + "value": "inc4", + "span": { + "dummy": false, + "start": 315, + "end": 319 + } + }, + "args": [ + { + "Identifier": { + "value": "amount", + "span": { + "dummy": false, + "start": 320, + "end": 326 + } + } + } + ], + "span": { + "dummy": false, + "start": 315, + "end": 327 + } + } + } + ], + "span": { + "dummy": false, + "start": 311, + "end": 328 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 264, + "end": 335 + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 158, + "end": 337 + }, + "collateral": [], + "metadata": null + } + ], + "types": [], + "aliases": [], + "assets": [], + "parties": [ + { + "name": { + "value": "Sender", + "span": { + "dummy": false, + "start": 6, + "end": 12 + } + }, + "span": { + "dummy": false, + "start": 0, + "end": 13 + } + }, + { + "name": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 20, + "end": 28 + } + }, + "span": { + "dummy": false, + "start": 14, + "end": 29 + } + } + ], + "policies": [], + "functions": [ + { + "name": { + "value": "inc", + "span": { + "dummy": false, + "start": 34, + "end": 37 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "x", + "span": { + "dummy": false, + "start": 38, + "end": 39 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 37, + "end": 45 + } + }, + "return_type": "Int", + "body": { + "let_bindings": [], + "result": { + "AddOp": { + "lhs": { + "Identifier": { + "value": "x", + "span": { + "dummy": false, + "start": 59, + "end": 60 + } + } + }, + "rhs": { + "Number": 1 + }, + "span": { + "dummy": false, + "start": 61, + "end": 62 + } + } + }, + "span": { + "dummy": false, + "start": 59, + "end": 65 + } + }, + "span": { + "dummy": false, + "start": 31, + "end": 66 + } + }, + { + "name": { + "value": "inc2", + "span": { + "dummy": false, + "start": 71, + "end": 75 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "x", + "span": { + "dummy": false, + "start": 76, + "end": 77 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 75, + "end": 83 + } + }, + "return_type": "Int", + "body": { + "let_bindings": [], + "result": { + "FnCall": { + "callee": { + "value": "inc", + "span": { + "dummy": false, + "start": 97, + "end": 100 + } + }, + "args": [ + { + "FnCall": { + "callee": { + "value": "inc", + "span": { + "dummy": false, + "start": 101, + "end": 104 + } + }, + "args": [ + { + "Identifier": { + "value": "x", + "span": { + "dummy": false, + "start": 105, + "end": 106 + } + } + } + ], + "span": { + "dummy": false, + "start": 101, + "end": 107 + } + } + } + ], + "span": { + "dummy": false, + "start": 97, + "end": 108 + } + } + }, + "span": { + "dummy": false, + "start": 97, + "end": 109 + } + }, + "span": { + "dummy": false, + "start": 68, + "end": 110 + } + }, + { + "name": { + "value": "inc4", + "span": { + "dummy": false, + "start": 115, + "end": 119 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "x", + "span": { + "dummy": false, + "start": 120, + "end": 121 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 119, + "end": 127 + } + }, + "return_type": "Int", + "body": { + "let_bindings": [], + "result": { + "FnCall": { + "callee": { + "value": "inc2", + "span": { + "dummy": false, + "start": 141, + "end": 145 + } + }, + "args": [ + { + "FnCall": { + "callee": { + "value": "inc2", + "span": { + "dummy": false, + "start": 146, + "end": 150 + } + }, + "args": [ + { + "Identifier": { + "value": "x", + "span": { + "dummy": false, + "start": 151, + "end": 152 + } + } + } + ], + "span": { + "dummy": false, + "start": 146, + "end": 153 + } + } + } + ], + "span": { + "dummy": false, + "start": 141, + "end": 154 + } + } + }, + "span": { + "dummy": false, + "start": 141, + "end": 155 + } + }, + "span": { + "dummy": false, + "start": 112, + "end": 156 + } + } + ], + "span": { + "dummy": false, + "start": 0, + "end": 338 + } +} \ No newline at end of file diff --git a/examples/nested_functions.run.tir b/examples/nested_functions.run.tir new file mode 100644 index 0000000..1041b9f --- /dev/null +++ b/examples/nested_functions.run.tir @@ -0,0 +1,121 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "utxos": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "amount", + "Int" + ] + } + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + }, + "redeemer": "None" + } + ], + "outputs": [ + { + "address": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, + "datum": "None", + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalBuiltIn": { + "Add": [ + { + "EvalBuiltIn": { + "Add": [ + { + "EvalBuiltIn": { + "Add": [ + { + "EvalBuiltIn": { + "Add": [ + { + "EvalParam": { + "ExpectValue": [ + "amount", + "Int" + ] + } + }, + { + "Number": 1 + } + ] + } + }, + { + "Number": 1 + } + ] + } + }, + { + "Number": 1 + } + ] + } + }, + { + "Number": 1 + } + ] + } + } + } + ] + }, + "optional": false + } + ], + "validity": null, + "mints": [], + "burns": [], + "adhoc": [], + "collateral": [], + "signers": null, + "metadata": [] +} \ No newline at end of file diff --git a/examples/nested_functions.tx3 b/examples/nested_functions.tx3 new file mode 100644 index 0000000..81a3c79 --- /dev/null +++ b/examples/nested_functions.tx3 @@ -0,0 +1,25 @@ +party Sender; +party Receiver; + +fn inc(x: Int) -> Int { + x + 1 +} + +fn inc2(x: Int) -> Int { + inc(inc(x)) +} + +fn inc4(x: Int) -> Int { + inc2(inc2(x)) +} + +tx run(amount: Int) { + input source { + from: Sender, + min_amount: Ada(amount), + } + output { + to: Receiver, + amount: Ada(inc4(amount)), + } +} diff --git a/examples/posix_time.create_timestamp_tx.tir b/examples/posix_time.create_timestamp_tx.tir new file mode 100644 index 0000000..32c4c68 --- /dev/null +++ b/examples/posix_time.create_timestamp_tx.tir @@ -0,0 +1,133 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "utxos": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "Number": 2000000 + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + }, + "redeemer": "None" + } + ], + "outputs": [ + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "datum": { + "Struct": { + "constructor": 0, + "fields": [ + { + "EvalCompiler": { + "ComputeSlotToTime": { + "EvalCompiler": "ComputeTipSlot" + } + } + }, + { + "EvalCompiler": { + "ComputeTimeToSlot": { + "EvalParam": { + "ExpectValue": [ + "deadline", + "Int" + ] + } + } + } + } + ] + } + }, + "amount": { + "EvalBuiltIn": { + "Sub": [ + { + "EvalCoerce": { + "IntoAssets": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "Number": 2000000 + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + } + } + }, + { + "EvalParam": "ExpectFees" + } + ] + } + }, + "optional": false + } + ], + "validity": null, + "mints": [], + "burns": [], + "adhoc": [], + "collateral": [], + "signers": null, + "metadata": [] +} \ No newline at end of file diff --git a/examples/tip_slot.create_timestamp_tx.tir b/examples/tip_slot.create_timestamp_tx.tir new file mode 100644 index 0000000..3b607c5 --- /dev/null +++ b/examples/tip_slot.create_timestamp_tx.tir @@ -0,0 +1,134 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "utxos": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "Number": 2000000 + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + }, + "redeemer": "None" + } + ], + "outputs": [ + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "datum": { + "Struct": { + "constructor": 0, + "fields": [ + { + "EvalCompiler": "ComputeTipSlot" + }, + { + "EvalBuiltIn": { + "Add": [ + { + "EvalCompiler": "ComputeTipSlot" + }, + { + "EvalParam": { + "ExpectValue": [ + "validity_period", + "Int" + ] + } + } + ] + } + } + ] + } + }, + "amount": { + "EvalBuiltIn": { + "Sub": [ + { + "EvalCoerce": { + "IntoAssets": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "Number": 2000000 + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + } + } + }, + { + "EvalParam": "ExpectFees" + } + ] + } + }, + "optional": false + } + ], + "validity": null, + "mints": [], + "burns": [], + "adhoc": [], + "collateral": [], + "signers": null, + "metadata": [] +} \ No newline at end of file diff --git a/specs/v1beta0/01-introduction.md b/specs/v1beta0/01-introduction.md index 4acbbdf..7f168cf 100644 --- a/specs/v1beta0/01-introduction.md +++ b/specs/v1beta0/01-introduction.md @@ -4,8 +4,8 @@ Tx3 is a domain-specific language for describing **transaction templates** on UTxO-based blockchains. A Tx3 program declares the shape of one or more -transactions together with the parties, assets, policies, types, and -environment values they refer to. It is not a general-purpose programming +transactions together with the parties, assets, policies, types, functions, +and environment values they refer to. It is not a general-purpose programming language and contains no constructs for unbounded loops, recursion, or side-effects. diff --git a/specs/v1beta0/02-domain-model.md b/specs/v1beta0/02-domain-model.md index 482e002..5cf3157 100644 --- a/specs/v1beta0/02-domain-model.md +++ b/specs/v1beta0/02-domain-model.md @@ -16,6 +16,8 @@ source files. The top-level definitions, in their declared kinds, are: - *policy definitions* (`policy`): named minting/spending policies, either declared by hash literal or constructed from constituent fields. - *type definitions* (`type`): records, sum types (variants), and aliases. +- *function definitions* (`fn`): named, pure helpers that compute a data value + from typed parameters; see §2.5. - *transaction templates* (`tx`): parameterised templates for transactions. A program declares at most one `env` block. All other top-level kinds may @@ -54,13 +56,16 @@ a *data expression*. Data expressions cover: built-ins; - a restricted set of operators: addition (`+`), subtraction (`-`), prefix negation (`!`), property access (`.`), and indexing (`[…]`); -- a small set of built-in functions: `min_utxo`, `tip_slot`, `slot_to_time`, - `time_to_slot`, plus `concat` and the `AnyAsset` constructor. +- calls to *functions* — both the built-ins `min_utxo`, `tip_slot`, + `slot_to_time`, `time_to_slot` (plus `concat` and the `AnyAsset` constructor) + and user-defined functions (`fn`, §2.5) — using the same call syntax. -The expression language is intentionally restricted. It has no control flow, -no user-defined functions, no comparison operators, and no boolean -combinators. Everything that requires computation beyond simple arithmetic -and aggregation is the job of the resolver, not the source language. +The expression language is intentionally restricted. It has no control flow, no +comparison operators, and no boolean combinators. User-defined functions +(§2.5) are pure and non-recursive, and are eliminated by inlining before +resolution (§7); they add naming and reuse, not new runtime computation. +Everything that requires computation beyond simple arithmetic and aggregation +is the job of the resolver, not the source language. ## 2.4 Inputs, outputs, references, and named values @@ -82,7 +87,27 @@ Within the body of a `tx`, these names live in a single shared scope (see §6.1). Names need not be declared before use; in particular, output names may be referenced from other blocks regardless of textual order. -## 2.5 The compilation pipeline (informative) +## 2.5 Functions + +A *function* is a named, pure helper that maps typed parameters to a single +data value. A function declares a parameter list, an explicit return type, and +a body consisting of zero or more `let`-bindings followed by a result +expression (§4.2.6). Functions exist purely to name and reuse data-expression +fragments; they do not introduce new runtime behaviour. + +The language exposes a fixed set of *built-in* functions (`min_utxo`, +`tip_slot`, `slot_to_time`, `time_to_slot`) whose results are computed by the +resolver. *User-defined* functions are written with `fn` and are eliminated by +the compiler through inlining: each call site is replaced by the function's +result expression with the arguments substituted for the parameters (§7). As a +consequence: + +- functions MUST NOT be recursive, directly or indirectly; +- a function body is a data expression and MUST NOT contain transaction blocks; +- built-in and user-defined functions share one call syntax and one namespace + (§6.1), and are invoked wherever a data expression is allowed. + +## 2.6 The compilation pipeline (informative) A conforming compiler processes a Tx3 program in the following phases. The spec is normative only over the first two; lowering is described informatively @@ -99,7 +124,7 @@ Errors detected in phase 1 are *syntax errors*. Errors detected in phase 2 are *semantic errors*. A conforming compiler MUST emit at least one diagnostic for any program that violates a rule from §3–§8 (see §9). -## 2.6 Chain genericity +## 2.7 Chain genericity The core of the language (§3–§7) is intended to be chain-agnostic. Constructs that are specific to a particular target chain live under a dedicated diff --git a/specs/v1beta0/03-lexical-structure.md b/specs/v1beta0/03-lexical-structure.md index 82410c8..703e33b 100644 --- a/specs/v1beta0/03-lexical-structure.md +++ b/specs/v1beta0/03-lexical-structure.md @@ -60,8 +60,8 @@ where it becomes the *documentation string* of that construct: - a `party_def` (§4.2.3); - an `env_field` (§4.2.1); -- a `parameter` of a `tx_def` (§4.2.6); -- a `tx_def` (§4.2.6). +- a `parameter` of a `tx_def` (§4.2.7); +- a `tx_def` (§4.2.7). A `///` line that does not immediately precede one of the constructs above is a syntax error. Conforming compilers MAY relax this restriction as an @@ -182,9 +182,9 @@ user-defined identifiers: ``` env asset party policy type tx -input output mint burn reference collateral -validity signers metadata locals cardano bitcoin -true false +fn let input output mint burn +reference collateral validity signers metadata locals +cardano bitcoin true false Int Bool Bytes AnyAsset Address UtxoRef List Map ``` diff --git a/specs/v1beta0/04-syntactic-grammar.md b/specs/v1beta0/04-syntactic-grammar.md index c7bd8b4..62f7c14 100644 --- a/specs/v1beta0/04-syntactic-grammar.md +++ b/specs/v1beta0/04-syntactic-grammar.md @@ -24,6 +24,7 @@ top_level_def ::= | party_def | policy_def | type_def + | fn_def | tx_def ``` @@ -111,7 +112,25 @@ Notes: variant-case field is required by the grammar above and is conformant Tx3 style. -### 4.2.6 Transaction +### 4.2.6 Function + +``` +fn_def ::= "fn" identifier parameter_list "->" type "{" fn_body "}" +fn_body ::= let_binding* data_expr +let_binding ::= "let" identifier "=" data_expr ";" +``` + +A *function definition* introduces a named, pure helper that computes a data +value. The `parameter_list` (§4.2.7) MAY be empty. The `type` after `->` is the +declared return type. A function body is an optional sequence of `let`-bindings +followed by a single result `data_expr`, whose static type MUST match the +declared return type (§5.7). Functions are top-level only: a function body MUST +NOT contain transaction blocks, and functions MUST NOT be nested. Function calls +use the `fn_call` production (§4.4) and MAY appear anywhere a `data_expr` is +valid. Static-semantic rules for functions are given in §6, and their +evaluation by inlining is specified in §7. + +### 4.2.7 Transaction ``` tx_def ::= docstring? "tx" identifier parameter_list "{" tx_body_block* "}" @@ -120,7 +139,8 @@ parameter ::= docstring? identifier ":" type ``` A *transaction definition* declares a parameterised template. The -`parameter_list` MAY be empty. Trailing commas are permitted. +`parameter_list` MAY be empty. Trailing commas are permitted. The +`parameter_list` production above is shared by `fn_def` (§4.2.6). ## 4.3 Types diff --git a/specs/v1beta0/05-type-system.md b/specs/v1beta0/05-type-system.md index d07908a..2558603 100644 --- a/specs/v1beta0/05-type-system.md +++ b/specs/v1beta0/05-type-system.md @@ -197,10 +197,18 @@ The built-in `AnyAsset(policy, asset_name, amount)` yields a value of type `AnyAsset`. Its arguments MUST have types `Bytes`, `Bytes`, and `Int` respectively. -## 5.6 Built-in functions +## 5.6 Functions -The following identifiers, when used in `fn_call` position (§4.4), denote -built-in functions provided by every conforming compiler: +A *function* is invoked in `fn_call` position (§4.4). The result type of a call +is the function's declared (or, for built-ins, defined) return type. Each +argument MUST be assignable (§5.9) to the type of the corresponding parameter, +and the number of arguments MUST equal the number of parameters. Both built-in +and user-defined functions live in the single program-global namespace (§6.1). + +### 5.6.1 Built-in functions + +The following identifiers denote built-in functions provided by every +conforming compiler: | Name | Argument types | Result type | Notes | | ---------------- | ------------------------------- | ----------- | ----- | @@ -210,13 +218,29 @@ built-in functions provided by every conforming compiler: | `time_to_slot` | `(time : Int)` | `Int` | Converts a POSIX time to a slot number. | `min_utxo` is special: its single argument is treated as an identifier -reference rather than a general data expression. `tip_slot` MAY be invoked -either as `tip_slot()` or as the bare identifier `tip_slot` (the latter -form is accepted but using the call form is RECOMMENDED). +reference rather than a general data expression. Built-in functions MUST be +invoked using call syntax (e.g. `tip_slot()`); a bare identifier that resolves +to a built-in function is not a valid data expression. These names occupy the program-global namespace and SHOULD NOT be shadowed by user-defined identifiers. +### 5.6.2 User-defined functions + +A `fn_def` (§4.2.6) introduces a function whose signature is fully explicit: +each parameter is typed, and the return type follows `->`. Within the body, the +parameters are in scope (§6.1), and each `let`-binding introduces a name bound +to the static type of its initializer, visible to subsequent bindings and to +the result expression. The static type of the result expression MUST be +assignable (§5.9) to the declared return type; otherwise the program is +ill-typed. + +A function body is a pure data expression: it MUST NOT reference transaction +inputs, outputs, references, `fees`, or any `tx`-local symbol, and MUST NOT +contain transaction blocks. Functions MUST NOT be recursive (§6). Because +user-defined functions are eliminated by inlining (§7), they introduce no new +values or types beyond those expressible by their bodies. + ## 5.7 Built-in symbols In addition to the built-in functions above, the following bare identifiers diff --git a/specs/v1beta0/06-static-semantics.md b/specs/v1beta0/06-static-semantics.md index a3cb181..a82f071 100644 --- a/specs/v1beta0/06-static-semantics.md +++ b/specs/v1beta0/06-static-semantics.md @@ -17,6 +17,8 @@ The scope tree for a program has the following shape: ``` program scope +├── (per fn_def) function scope +│ └── (per let_binding) binding scope ├── (per tx_def) tx scope │ ├── (per struct/property/variant access) inner scope (§6.3) ``` @@ -30,8 +32,16 @@ The *program scope* contains: kind *asset*; - one symbol per `record_def` or `variant_def`, of kind *type*; - one symbol per `alias_def`, of kind *alias*; +- one symbol per `fn_def`, of kind *function*; - the built-in functions `min_utxo`, `tip_slot`, `slot_to_time`, - `time_to_slot` (§5.6). + `time_to_slot`, also of kind *function* (§5.6). + +A *function scope* (one per `fn_def`) extends the program scope with one symbol +per parameter, of kind *param-var*. The body's `let`-bindings are introduced +sequentially: each `let_binding` adds a *local-expr* symbol that is visible to +later bindings and to the result expression, but not to earlier bindings. A +function scope does not extend any `tx` scope, so a function body cannot +reference inputs, outputs, references, locals, or `fees` of any transaction. A *tx scope* (one per `tx_def`) extends the program scope with: @@ -70,13 +80,21 @@ the compiler MUST report a *not-in-scope* error. Within a single scope, an identifier MUST be bound by at most one definition. Multiple `record_def`/`variant_def`/`alias_def`/`party_def`/ -`policy_def`/`asset_def`/`env`-field/parameter/local/input/reference/named -output with the same name are *duplicate definitions* and MUST be -rejected. The error MAY name only the first or last occurrence. +`policy_def`/`asset_def`/`fn_def`/`env`-field/parameter/local/input/reference/ +named output with the same name are *duplicate definitions* and MUST be +rejected. The error MAY name only the first or last occurrence. A user-defined +`fn_def` that reuses the name of a built-in function (§5.6.1) is likewise a +duplicate definition. The implicit `Ada` asset definition is added by the compiler; redeclaring `Ada` at the top level is a duplicate-definition error (§5.8). +Functions MUST NOT be recursive: the call graph induced by `fn_call` +expressions in function bodies MUST be acyclic. A program containing a function +that calls itself, directly or through a cycle of other functions, is not +conforming; a conforming compiler SHOULD reject it with a diagnostic rather +than attempt the non-terminating inlining of §7. + ## 6.3 Type checking Every `data_expr` has a *static type* assigned by the analyzer. The static @@ -103,7 +121,8 @@ type is computed bottom-up as follows: static types of the first entry's key and value. - An `any_asset_constructor` has type `AnyAsset`. - A `concat_constructor` has the static type of its first argument. -- A `fn_call` to a built-in has the type listed in §5.6. +- A `fn_call` to a built-in has the type listed in §5.6.1; a `fn_call` to a + user-defined function has that function's declared return type (§5.6.2). - An `add_op` or `sub_op` has the static type of its left operand. - A `negate_op` has the static type of its operand. - A `property_op` `e.p` has the type of the property `p` on `e`'s type diff --git a/specs/v1beta0/07-transaction-semantics.md b/specs/v1beta0/07-transaction-semantics.md index 59c0ced..dae629b 100644 --- a/specs/v1beta0/07-transaction-semantics.md +++ b/specs/v1beta0/07-transaction-semantics.md @@ -309,6 +309,39 @@ implied by the policy. The semantics of `script` and `ref` fields are chain-specific and are described in §8 for Cardano targets. +### 7.13.5 `fn` + +```tx3 +fn double(x: Int) -> Int { + x + x +} + +fn discounted(base: Int, off: Int) -> Int { + let net = base - off; + net +} +``` + +A `fn_def` declares a pure, non-recursive helper (§2.5, §5.6.2). It contributes +no blocks or structure to any transaction; instead, every call to a +user-defined function is **inlined** during lowering: + +1. The function's result expression is taken as a template, with its + `let`-bindings substituted into it (each binding's name replaced by its + initializer, in declaration order). +2. Each parameter occurrence in that template is replaced by the corresponding + call argument expression, evaluated in the caller's scope. +3. The resulting expression is spliced in at the call site, as though the + programmer had written it inline. + +Because arguments are substituted positionally and functions are non-recursive +(§6.2), inlining terminates and yields an ordinary data expression. A call +therefore has no observable effect beyond that of its expansion: user-defined +functions are a source-level convenience and leave no trace in the lowered TIR. +Calls to built-in functions (§5.6.1) are not inlined; they lower to the +corresponding resolver operation (e.g. `min_utxo` → minimum-UTxO computation, +§7.5). + ## 7.14 Cross-block constraints Within a single `tx_def`, the following requirements apply across blocks: diff --git a/specs/v1beta0/09-conformance.md b/specs/v1beta0/09-conformance.md index d29072d..2aa1eca 100644 --- a/specs/v1beta0/09-conformance.md +++ b/specs/v1beta0/09-conformance.md @@ -27,7 +27,7 @@ source file: 1. **MUST** report no diagnostics, beyond optional warnings, when given a conforming program. The program MUST be processed up to and including - the static-semantic phase (§2.5) without rejection. + the static-semantic phase (§2.6) without rejection. 2. **MUST** emit at least one diagnostic in the appropriate category (§6.9) for any program that violates a MUST or MUST NOT from §3–§8. 3. **MUST** preserve the meaning of every conforming program when it