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
14 changes: 14 additions & 0 deletions crates/tx3-lang/src/analyzing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,16 @@ impl Analyzable for ListConstructor {
}
}

impl Analyzable for TupleConstructor {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.elements.analyze(parent)
}

fn is_resolved(&self) -> bool {
self.elements.is_resolved()
}
}

impl Analyzable for MapField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.key.analyze(parent.clone()) + self.value.analyze(parent.clone())
Expand All @@ -776,6 +786,7 @@ impl Analyzable for DataExpr {
DataExpr::StructConstructor(x) => x.analyze(parent),
DataExpr::ListConstructor(x) => x.analyze(parent),
DataExpr::MapConstructor(x) => x.analyze(parent),
DataExpr::TupleConstructor(x) => x.analyze(parent),
DataExpr::Identifier(x) => x.analyze(parent),
DataExpr::AddOp(x) => x.analyze(parent),
DataExpr::SubOp(x) => x.analyze(parent),
Expand All @@ -795,6 +806,7 @@ impl Analyzable for DataExpr {
DataExpr::StructConstructor(x) => x.is_resolved(),
DataExpr::ListConstructor(x) => x.is_resolved(),
DataExpr::MapConstructor(x) => x.is_resolved(),
DataExpr::TupleConstructor(x) => x.is_resolved(),
DataExpr::Identifier(x) => x.is_resolved(),
DataExpr::AddOp(x) => x.is_resolved(),
DataExpr::SubOp(x) => x.is_resolved(),
Expand Down Expand Up @@ -942,6 +954,7 @@ impl Analyzable for Type {
Type::Map(key_type, value_type) => {
key_type.analyze(parent.clone()) + value_type.analyze(parent)
}
Type::Tuple(elements) => elements.analyze(parent),
_ => AnalyzeReport::default(),
}
}
Expand All @@ -951,6 +964,7 @@ impl Analyzable for Type {
Type::Custom(x) => x.is_resolved(),
Type::List(x) => x.is_resolved(),
Type::Map(key_type, value_type) => key_type.is_resolved() && value_type.is_resolved(),
Type::Tuple(elements) => elements.iter().all(|t| t.is_resolved()),
_ => true,
}
}
Expand Down
46 changes: 46 additions & 0 deletions crates/tx3-lang/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,25 @@ impl ListConstructor {
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TupleConstructor {
pub elements: Vec<DataExpr>,
pub span: Span,
}

impl TupleConstructor {
pub fn target_type(&self) -> Option<Type> {
// A tuple's type is known only when every element's type is known.
let elements = self
.elements
.iter()
.map(|x| x.target_type())
.collect::<Option<Vec<_>>>()?;

Some(Type::Tuple(elements))
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MapField {
pub key: DataExpr,
Expand Down Expand Up @@ -689,6 +708,14 @@ pub struct PropertyOp {

impl PropertyOp {
pub fn target_type(&self) -> Option<Type> {
// Positional tuple access (`t.0`) resolves to the element type at that
// index; for every other operand the property carries its own type.
if let (Some(Type::Tuple(elements)), DataExpr::Number(index)) =
(self.operand.target_type(), self.property.as_ref())
{
return elements.get(*index as usize).cloned();
}

self.property.target_type()
}
}
Expand Down Expand Up @@ -785,6 +812,7 @@ pub enum DataExpr {
StructConstructor(StructConstructor),
ListConstructor(ListConstructor),
MapConstructor(MapConstructor),
TupleConstructor(TupleConstructor),
AnyAssetConstructor(AnyAssetConstructor),
Identifier(Identifier),
AddOp(AddOp),
Expand Down Expand Up @@ -821,6 +849,7 @@ impl DataExpr {
Some(inner) => Some(Type::List(Box::new(inner))),
None => None,
},
DataExpr::TupleConstructor(x) => x.target_type(),
DataExpr::AddOp(x) => x.target_type(),
DataExpr::SubOp(x) => x.target_type(),
DataExpr::MulOp(x) => x.target_type(),
Expand Down Expand Up @@ -864,6 +893,7 @@ pub enum Type {
AnyAsset,
List(Box<Type>),
Map(Box<Type>, Box<Type>),
Tuple(Vec<Type>),
Custom(Identifier),
}

Expand All @@ -881,6 +911,14 @@ impl std::fmt::Display for Type {
Type::Utxo => write!(f, "Utxo"),
Type::Map(key, value) => write!(f, "Map<{}, {}>", key, value),
Type::List(inner) => write!(f, "List<{inner}>"),
Type::Tuple(elements) => {
let inner = elements
.iter()
.map(|t| t.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "Tuple<{inner}>")
}
Type::Custom(id) => write!(f, "{}", id.value),
}
}
Expand Down Expand Up @@ -932,6 +970,14 @@ impl Type {
.target_type()
.filter(|ty| *ty == Type::Int)
.map(|_| property),
// Positional tuple access (`t.0`): the property is a literal index,
// valid only when it falls within the tuple's arity.
Type::Tuple(elements) => match property {
DataExpr::Number(index) if (0..elements.len() as i64).contains(&index) => {
Some(DataExpr::Number(index))
}
_ => None,
},
_ => None,
}
}
Expand Down
41 changes: 41 additions & 0 deletions crates/tx3-lang/src/facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,45 @@ pub mod tests {
workspace.analyze().unwrap();
workspace.lower().unwrap();
}

// End-to-end: a `Tuple<..>` param type, a `(..)` tuple literal, and `[i]`
// positional access all flow through parse -> analyze -> lower.
#[test]
fn tuple_end_to_end() {
use tx3_tir::model::v1beta0::Expression;

let src = r#"
party Sender;
party Receiver;

tx swap(
pair: Tuple<Int, Bytes>
) {
input source {
from: Sender,
min_amount: Ada(pair[0]),
}

output {
to: Receiver,
amount: Ada(pair[0]),
datum: (pair[0], pair[1]),
}
}
"#;

let mut workspace = Workspace::from_string(src.to_string());
workspace.parse().unwrap();
workspace.analyze().unwrap();
workspace.lower().unwrap();

let tir = workspace.tir("swap").unwrap();

// The output datum lowered to an N-element tuple expression.
let datum = &tir.outputs[0].datum;
assert!(
matches!(datum, Expression::Tuple(elements) if elements.len() == 2),
"expected a 2-element tuple datum, got {datum:?}"
);
}
}
18 changes: 18 additions & 0 deletions crates/tx3-lang/src/lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ impl IntoLower for ast::Type {
ast::Type::AnyAsset => Ok(Type::AnyAsset),
ast::Type::List(_) => Ok(Type::List),
ast::Type::Map(_, _) => Ok(Type::Map),
ast::Type::Tuple(_) => Ok(Type::Tuple),
ast::Type::Custom(x) => Ok(Type::Custom(x.value.clone())),
}
}
Expand Down Expand Up @@ -611,6 +612,20 @@ impl IntoLower for ast::ListConstructor {
}
}

impl IntoLower for ast::TupleConstructor {
type Output = ir::Expression;

fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
let elements = self
.elements
.iter()
.map(|x| x.into_lower(ctx))
.collect::<Result<Vec<_>, _>>()?;

Ok(ir::Expression::Tuple(elements))
}
}

impl IntoLower for ast::MapConstructor {
type Output = ir::Expression;

Expand Down Expand Up @@ -642,6 +657,7 @@ impl IntoLower for ast::DataExpr {
ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower(ctx)?),
ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower(ctx)?),
ast::DataExpr::MapConstructor(x) => x.into_lower(ctx)?,
ast::DataExpr::TupleConstructor(x) => x.into_lower(ctx)?,
ast::DataExpr::AnyAssetConstructor(x) => x.into_lower(ctx)?,
ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
ast::DataExpr::Identifier(x) => x.into_lower(ctx)?,
Expand Down Expand Up @@ -1129,6 +1145,8 @@ mod tests {

test_lowering!(lang_tour);

test_lowering!(tuples);

test_lowering!(transfer);

test_lowering!(swap);
Expand Down
103 changes: 103 additions & 0 deletions crates/tx3-lang/src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,23 @@ impl AstNode for MapConstructor {
}
}

impl AstNode for TupleConstructor {
const RULE: Rule = Rule::tuple_constructor;

fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
let span = pair.as_span().into();
let inner = pair.into_inner();

let elements = inner.map(DataExpr::parse).collect::<Result<Vec<_>, _>>()?;

Ok(TupleConstructor { elements, span })
}

fn span(&self) -> &Span {
&self.span
}
}

impl DataExpr {
fn number_parse(pair: Pair<Rule>) -> Result<Self, Error> {
Ok(DataExpr::Number(pair.as_str().parse().unwrap()))
Expand All @@ -1257,6 +1274,10 @@ impl DataExpr {
Ok(DataExpr::MapConstructor(MapConstructor::parse(pair)?))
}

fn tuple_constructor_parse(pair: Pair<Rule>) -> Result<Self, Error> {
Ok(DataExpr::TupleConstructor(TupleConstructor::parse(pair)?))
}

fn utxo_ref_parse(pair: Pair<Rule>) -> Result<Self, Error> {
Ok(DataExpr::UtxoRef(UtxoRef::parse(pair)?))
}
Expand Down Expand Up @@ -1372,6 +1393,7 @@ impl AstNode for DataExpr {
Rule::struct_constructor => DataExpr::struct_constructor_parse(x),
Rule::list_constructor => DataExpr::list_constructor_parse(x),
Rule::map_constructor => DataExpr::map_constructor_parse(x),
Rule::tuple_constructor => DataExpr::tuple_constructor_parse(x),
Rule::unit => Ok(DataExpr::Unit),
Rule::identifier => DataExpr::identifier_parse(x),
Rule::utxo_ref => DataExpr::utxo_ref_parse(x),
Expand Down Expand Up @@ -1411,6 +1433,7 @@ impl AstNode for DataExpr {
DataExpr::StructConstructor(x) => x.span(),
DataExpr::ListConstructor(x) => x.span(),
DataExpr::MapConstructor(x) => x.span(),
DataExpr::TupleConstructor(x) => x.span(),
DataExpr::AnyAssetConstructor(x) => x.span(),
DataExpr::Identifier(x) => x.span(),
DataExpr::AddOp(x) => &x.span,
Expand Down Expand Up @@ -1452,6 +1475,13 @@ impl AstNode for Type {
let value_type = Type::parse(inner.next().unwrap())?;
Ok(Type::Map(Box::new(key_type), Box::new(value_type)))
}
Rule::tuple_type => {
let elements = inner
.into_inner()
.map(Type::parse)
.collect::<Result<Vec<_>, _>>()?;
Ok(Type::Tuple(elements))
}
Rule::custom_type => Ok(Type::Custom(Identifier::new(inner.as_str().to_owned()))),
x => unreachable!("Unexpected rule in type: {:?}", x),
}
Expand Down Expand Up @@ -1753,6 +1783,34 @@ mod tests {
Type::List(Box::new(Type::List(Box::new(Type::Int))))
);

input_to_ast_check!(
Type,
"tuple",
"Tuple<Int, Bytes>",
Type::Tuple(vec![Type::Int, Type::Bytes])
);

input_to_ast_check!(
Type,
"tuple_three_with_nested",
"Tuple<Int, Bytes, List<Bool>>",
Type::Tuple(vec![
Type::Int,
Type::Bytes,
Type::List(Box::new(Type::Bool)),
])
);

input_to_ast_check!(
Type,
"tuple_nested",
"Tuple<Tuple<Int, Int>, Bytes>",
Type::Tuple(vec![
Type::Tuple(vec![Type::Int, Type::Int]),
Type::Bytes,
])
);

input_to_ast_check!(
TypeDef,
"type_def_record",
Expand Down Expand Up @@ -2015,6 +2073,49 @@ mod tests {
}
);

// A two-or-more element parenthesized list is a tuple literal.
input_to_ast_check!(
DataExpr,
"tuple_literal",
"(1, 0xFF, true)",
DataExpr::TupleConstructor(TupleConstructor {
elements: vec![
DataExpr::Number(1),
DataExpr::HexString(HexStringLiteral::new("FF".to_string())),
DataExpr::Bool(true),
],
span: Span::DUMMY,
})
);

// Trailing comma is permitted in a tuple literal.
input_to_ast_check!(
DataExpr,
"tuple_literal_trailing_comma",
"(1, 2,)",
DataExpr::TupleConstructor(TupleConstructor {
elements: vec![DataExpr::Number(1), DataExpr::Number(2)],
span: Span::DUMMY,
})
);

// A single parenthesized expression is grouping, NOT a one-tuple.
input_to_ast_check!(DataExpr, "grouping_not_tuple", "(42)", DataExpr::Number(42));

// Positional tuple access uses bracket indexing with a literal index; it
// lowers through the same PropertyOp path as struct/list access.
input_to_ast_check!(
DataExpr,
"tuple_index_access",
"my_tuple[0]",
DataExpr::PropertyOp(PropertyOp {
operand: Box::new(DataExpr::Identifier(Identifier::new("my_tuple"))),
property: Box::new(DataExpr::Number(0)),
span: Span::DUMMY,
scope: None,
})
);

input_to_ast_check!(DataExpr, "literal_bool_true", "true", DataExpr::Bool(true));

input_to_ast_check!(
Expand Down Expand Up @@ -3124,6 +3225,8 @@ mod tests {

test_parsing!(lang_tour);

test_parsing!(tuples);

test_parsing!(transfer);

test_parsing!(swap);
Expand Down
Loading
Loading