diff --git a/crates/tx3-lang/src/analyzing.rs b/crates/tx3-lang/src/analyzing.rs index d3e4d3a..7c884d3 100644 --- a/crates/tx3-lang/src/analyzing.rs +++ b/crates/tx3-lang/src/analyzing.rs @@ -608,6 +608,19 @@ impl Analyzable for SubOp { } } +impl Analyzable for MulOp { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + let left = self.lhs.analyze(parent.clone()); + let right = self.rhs.analyze(parent.clone()); + + left + right + } + + fn is_resolved(&self) -> bool { + self.lhs.is_resolved() && self.rhs.is_resolved() + } +} + impl Analyzable for NegateOp { fn analyze(&mut self, parent: Option>) -> AnalyzeReport { self.operand.analyze(parent) @@ -753,6 +766,7 @@ impl Analyzable for DataExpr { DataExpr::Identifier(x) => x.analyze(parent), DataExpr::AddOp(x) => x.analyze(parent), DataExpr::SubOp(x) => x.analyze(parent), + DataExpr::MulOp(x) => x.analyze(parent), DataExpr::NegateOp(x) => x.analyze(parent), DataExpr::PropertyOp(x) => x.analyze(parent), DataExpr::AnyAssetConstructor(x) => x.analyze(parent), @@ -770,6 +784,7 @@ impl Analyzable for DataExpr { DataExpr::Identifier(x) => x.is_resolved(), DataExpr::AddOp(x) => x.is_resolved(), DataExpr::SubOp(x) => x.is_resolved(), + DataExpr::MulOp(x) => x.is_resolved(), DataExpr::NegateOp(x) => x.is_resolved(), DataExpr::PropertyOp(x) => x.is_resolved(), DataExpr::AnyAssetConstructor(x) => x.is_resolved(), diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 4003882..752b9da 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -719,6 +719,19 @@ impl SubOp { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct MulOp { + pub lhs: Box, + pub rhs: Box, + pub span: Span, +} + +impl MulOp { + pub fn target_type(&self) -> Option { + self.lhs.target_type() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ConcatOp { pub lhs: Box, @@ -763,6 +776,7 @@ pub enum DataExpr { Identifier(Identifier), AddOp(AddOp), SubOp(SubOp), + MulOp(MulOp), ConcatOp(ConcatOp), NegateOp(NegateOp), PropertyOp(PropertyOp), @@ -795,6 +809,7 @@ impl DataExpr { }, DataExpr::AddOp(x) => x.target_type(), DataExpr::SubOp(x) => x.target_type(), + DataExpr::MulOp(x) => x.target_type(), DataExpr::ConcatOp(x) => x.target_type(), DataExpr::NegateOp(x) => x.target_type(), DataExpr::PropertyOp(x) => x.target_type(), diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index ca67800..13492d6 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -443,6 +443,19 @@ impl IntoLower for ast::SubOp { } } +impl IntoLower for ast::MulOp { + type Output = ir::Expression; + + fn into_lower(&self, ctx: &Context) -> Result { + let left = self.lhs.into_lower(ctx)?; + let right = self.rhs.into_lower(ctx)?; + + Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Mul( + left, right, + )))) + } +} + impl IntoLower for ast::ConcatOp { type Output = ir::Expression; @@ -621,6 +634,7 @@ impl IntoLower for ast::DataExpr { ast::DataExpr::Identifier(x) => x.into_lower(ctx)?, ast::DataExpr::AddOp(x) => x.into_lower(ctx)?, ast::DataExpr::SubOp(x) => x.into_lower(ctx)?, + ast::DataExpr::MulOp(x) => x.into_lower(ctx)?, ast::DataExpr::ConcatOp(x) => x.into_lower(ctx)?, ast::DataExpr::NegateOp(x) => x.into_lower(ctx)?, ast::DataExpr::PropertyOp(x) => x.into_lower(ctx)?, diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 09c8cae..41e9c9b 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -1327,11 +1327,22 @@ impl DataExpr { span, })) } + + fn mul_op_parse(left: DataExpr, pair: Pair, right: DataExpr) -> Result { + let span = pair.as_span().into(); + + Ok(DataExpr::MulOp(MulOp { + lhs: Box::new(left), + rhs: Box::new(right), + span, + })) + } } static DATA_EXPR_PRATT_PARSER: LazyLock> = LazyLock::new(|| { PrattParser::new() .op(Op::infix(Rule::data_add, Assoc::Left) | Op::infix(Rule::data_sub, Assoc::Left)) + .op(Op::infix(Rule::data_mul, Assoc::Left)) .op(Op::prefix(Rule::data_negate)) .op(Op::postfix(Rule::data_property) | Op::postfix(Rule::data_index)) }); @@ -1372,6 +1383,7 @@ impl AstNode for DataExpr { .map_infix(|left, op, right| match op.as_rule() { Rule::data_add => DataExpr::add_op_parse(left?, op, right?), Rule::data_sub => DataExpr::sub_op_parse(left?, op, right?), + Rule::data_mul => DataExpr::mul_op_parse(left?, op, right?), x => unreachable!("Unexpected rule as data infix: {:?}", x), }) .parse(inner) @@ -1392,6 +1404,7 @@ impl AstNode for DataExpr { DataExpr::Identifier(x) => x.span(), DataExpr::AddOp(x) => &x.span, DataExpr::SubOp(x) => &x.span, + DataExpr::MulOp(x) => &x.span, DataExpr::ConcatOp(x) => &x.span, DataExpr::NegateOp(x) => &x.span, DataExpr::PropertyOp(x) => &x.span, @@ -2147,6 +2160,49 @@ mod tests { }) ); + input_to_ast_check!( + DataExpr, + "mul_op", + "5 * var1", + DataExpr::MulOp(MulOp { + lhs: Box::new(DataExpr::Number(5)), + rhs: Box::new(DataExpr::Identifier(Identifier::new("var1"))), + span: Span::DUMMY, + }) + ); + + // `*` binds tighter than `+`, so `2 + 3 * 4` parses as `2 + (3 * 4)`. + input_to_ast_check!( + DataExpr, + "mul_binds_tighter_than_add", + "2 + 3 * 4", + DataExpr::AddOp(AddOp { + lhs: Box::new(DataExpr::Number(2)), + rhs: Box::new(DataExpr::MulOp(MulOp { + lhs: Box::new(DataExpr::Number(3)), + rhs: Box::new(DataExpr::Number(4)), + span: Span::DUMMY, + })), + span: Span::DUMMY, + }) + ); + + // ... and on the left, `3 * 4 + 2` parses as `(3 * 4) + 2`. + input_to_ast_check!( + DataExpr, + "mul_left_binds_tighter_than_add", + "3 * 4 + 2", + DataExpr::AddOp(AddOp { + lhs: Box::new(DataExpr::MulOp(MulOp { + lhs: Box::new(DataExpr::Number(3)), + rhs: Box::new(DataExpr::Number(4)), + span: Span::DUMMY, + })), + rhs: Box::new(DataExpr::Number(2)), + span: Span::DUMMY, + }) + ); + input_to_ast_check!( DataExpr, "concat_op", diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index 7010bf2..1024226 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -138,9 +138,10 @@ fn_call = { data_expr = { data_prefix* ~ data_primary ~ data_postfix* ~ (data_infix ~ data_prefix* ~ data_primary ~ data_postfix* )* } - data_infix = _{ data_add | data_sub } + data_infix = _{ data_add | data_sub | data_mul } data_add = { "+" } data_sub = { "-" } + data_mul = { "*" } data_prefix = _{ data_negate } data_negate = { "!" } diff --git a/specs/v1beta0/03-lexical-structure.md b/specs/v1beta0/03-lexical-structure.md index 703e33b..1050e64 100644 --- a/specs/v1beta0/03-lexical-structure.md +++ b/specs/v1beta0/03-lexical-structure.md @@ -221,8 +221,11 @@ The following tokens are operators or punctuation: Operator precedence and associativity are given in §4.5. -The token `*` is used both as a flag on `input` blocks (`input*`) and is -otherwise reserved; it is **not** a multiplication operator. +The token `*` is used both as a flag on `input` blocks (`input*`) and as the +infix multiplication operator (§5.5.1). The two roles are disambiguated by +grammatical context: `*` is a block flag only when it immediately follows the +`input` keyword in a transaction block; in a data expression it is the +multiplication operator. The token `?` is used as a flag on `output` blocks (`output?`); it is otherwise reserved. diff --git a/specs/v1beta0/04-syntactic-grammar.md b/specs/v1beta0/04-syntactic-grammar.md index c9b3b6e..9688a27 100644 --- a/specs/v1beta0/04-syntactic-grammar.md +++ b/specs/v1beta0/04-syntactic-grammar.md @@ -164,7 +164,7 @@ A `custom_type` is an identifier that MUST resolve to a `record_def`, data_expr ::= data_prefix* data_primary data_postfix* ( data_infix data_prefix* data_primary data_postfix* )* -data_infix ::= "+" | "-" +data_infix ::= "+" | "-" | "*" data_prefix ::= "!" data_postfix ::= "." identifier | "[" data_expr "]" @@ -255,13 +255,14 @@ tightest binding (highest precedence) to loosest: | ----- | -------------------------- | ------------- | | 1 | `.` `[…]` (postfix) | left | | 2 | `!` (prefix) | non-assoc | -| 3 | `+` `-` (infix) | left | +| 3 | `*` (infix) | left | +| 4 | `+` `-` (infix) | left | Parentheses `( data_expr )` may be used to override precedence. There are no further operators in `v1beta0`. In particular, there are no -comparison operators, no boolean combinators, no multiplicative or -shift operators, and no ternary form. +comparison operators, no boolean combinators, no division or shift +operators, and no ternary form. ## 4.6 Transaction-body blocks diff --git a/specs/v1beta0/05-type-system.md b/specs/v1beta0/05-type-system.md index 2558603..23a7079 100644 --- a/specs/v1beta0/05-type-system.md +++ b/specs/v1beta0/05-type-system.md @@ -152,10 +152,21 @@ pairs, both arities binary, left-associative: | `Int` | `Int` | `Int` | Integer addition / subtraction. | | `AnyAsset` | `AnyAsset` | `AnyAsset` | Asset-quantity aggregation. | +The infix operator `*` is binary and left-associative, and binds tighter than +`+` and `-` (§4.5). It is defined for the following operand-type pairs: + +| Left type | Right type | Result type | Meaning | +| ----------- | ----------- | ----------- | -------------------------------- | +| `Int` | `Int` | `Int` | Integer multiplication. | +| `AnyAsset` | `Int` | `AnyAsset` | Scale every asset quantity. | +| `Int` | `AnyAsset` | `AnyAsset` | Scale every asset quantity. | + +Multiplication of one `AnyAsset` by another `AnyAsset` is **not** defined. + Where the left and right operands have compatible compound types that contain `Int` or `AnyAsset` components (such as types representing UTxO -values), implementations MAY extend `+` and `-` to those types provided the -extension is consistent across operand pairs. Such extensions are out of +values), implementations MAY extend `+`, `-`, and `*` to those types provided +the extension is consistent across operand pairs. Such extensions are out of the scope of this version of the specification. The prefix operator `!` is defined as **arithmetic negation** when applied