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
100 changes: 74 additions & 26 deletions docs/TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,18 +330,12 @@ A contract must have at least one entrypoint function. Contracts with multiple e

### Function Parameters and Return Types

Functions can have multiple parameters and return values:
Functions can have multiple parameters. A function with one plain return value writes the
type directly after `:`:

```javascript
// Function with return type
function add(int a, int b): (int) {
return (a + b);
}

// Multiple return values
function split(byte[32] data): (byte[16], byte[16]) {
byte[16] left, byte[16] right = data.split(16);
return (left, right);
function add(int a, int b): int {
return a + b;
}

// Using the return value
Expand All @@ -351,6 +345,34 @@ entrypoint function example() {
}
```

Tuple return types are written in parentheses. A tuple with more than one value
can be destructured into typed bindings:

```javascript
function getPair(): (int, int) {
return (10, 20);
}

entrypoint function example() {
(int left, int right) = getPair();
require(left + right == 30);
}
```

A parenthesized single return type is a one-element tuple, not the same as a
plain scalar return:

```javascript
function getWrapped(): (int) {
return (7);
}

entrypoint function example() {
int value = getWrapped().0;
require(value == 7);
}
```

---

## Operators
Expand Down Expand Up @@ -726,12 +748,20 @@ byte[] combined = a + b; // 0x12345678

**Split:**

Split byte[] at a specific index:
`split(int)` divides a byte array at a specific index and returns a two-value
tuple `(byte[], byte[])`. Use `.0` for the left part and `.1` for the right part:

```javascript
byte[] data = 0x1234567890abcdef;
byte[] left = data.split(4)[0]; // 0x12345678
byte[] right = data.split(4)[1]; // 0x90abcdef
byte[] left = data.split(4).0; // 0x12345678
byte[] right = data.split(4).1; // 0x90abcdef
```

You can also destructure both parts at once:

```javascript
byte[] data = 0x1234567890abcdef;
(byte[4] left, byte[4] right) = data.split(4);
```

**Slice:**
Expand Down Expand Up @@ -1109,45 +1139,63 @@ entrypoint function example() {

### Tuple Unpacking

Unpack multiple values from function returns or split operations:
Unpack multiple values from tuple-returning functions or tuple-returning
built-ins such as `split(int)`:

```javascript
// Function with multiple returns
function getPair(): (int, int) {
return (10, 20);
}

// Unpack split results and function results
entrypoint function example(byte[32] data) {
byte[16] left, byte[16] right = data.split(16);
(byte[16] left, byte[16] right) = data.split(16);
(int x, int y) = getPair();
}
```

**In Function Parameters:**
Tuple fields can also be accessed directly with numeric field access:

```javascript
entrypoint function example(byte[32] data) {
byte[16] x, byte[16] y = data.split(16);
require(x == y);
function getPair(): (int, int) {
return (10, 20);
}

entrypoint function example() {
int first = getPair().0;
int second = getPair().1;
require(first + second == 30);
}
```

A one-element tuple uses the same field access:

```javascript
function getOnly(): (int) {
return (5);
}

entrypoint function example() {
require(getOnly().0 == 5);
}
```

### Split and Slice Operations

**Split:**

Divide byte[] into two parts at a given index:
Divide `byte[]` into two parts at a given index. The built-in has the shape
`split(int): (byte[], byte[])`, so the result is accessed like other tuple
returns:

```javascript
byte[] data = 0x1122334455667788;

// Split at byte 4
byte[] left = data.split(4)[0]; // 0x11223344
byte[] right = data.split(4)[1]; // 0x55667788
byte[] left = data.split(4).0; // 0x11223344
byte[] right = data.split(4).1; // 0x55667788

// Direct tuple unpacking with types
byte[4] a, byte[4] b = data.split(4);
// Destructure both parts with types
(byte[4] a, byte[4] b) = data.split(4);
```

**Slice:**
Expand Down
79 changes: 58 additions & 21 deletions silverscript-lang/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ pub struct FunctionAst<'i> {
pub entrypoint: bool,
#[serde(default)]
pub return_types: Vec<TypeRef>,
#[serde(default)]
pub returns_tuple: bool,
pub body: Vec<Statement<'i>>,
#[serde(skip_deserializing)]
pub return_type_spans: Vec<Span<'i>>,
Expand Down Expand Up @@ -777,9 +779,14 @@ impl SourceFormatter {
signature.push_str(&format_params(&function.params));
signature.push(')');
if !function.return_types.is_empty() {
signature.push_str(": (");
signature.push_str(&function.return_types.iter().map(TypeRef::type_name).collect::<Vec<_>>().join(", "));
signature.push(')');
if function.returns_tuple {
signature.push_str(": (");
signature.push_str(&function.return_types.iter().map(TypeRef::type_name).collect::<Vec<_>>().join(", "));
signature.push(')');
} else {
signature.push_str(": ");
signature.push_str(&function.return_types[0].type_name());
}
}
signature.push_str(" {");

Expand All @@ -801,7 +808,7 @@ impl SourceFormatter {
}
Statement::TupleAssignment { left_type_ref, left_name, right_type_ref, right_name, expr, .. } => {
self.line(&format!(
"{} {}, {} {} = {};",
"({} {}, {} {}) = {};",
left_type_ref.type_name(),
left_name,
right_type_ref.type_name(),
Expand Down Expand Up @@ -964,7 +971,7 @@ fn format_expr_with_prec(expr: &Expr<'_>, parent_prec: u8, right_child: bool) ->
ExprKind::New { name, args, .. } => format!("new {}({})", name, format_expr_list(args)),
ExprKind::Split { source, index, part, .. } => {
format!(
"{}.split({})[{}]",
"{}.split({}).{}",
format_expr_with_prec(source, PREC_POSTFIX, false),
format_expr(index),
match part {
Expand Down Expand Up @@ -1352,10 +1359,12 @@ fn parse_function_definition<'i>(pair: Pair<'i, Rule>) -> Result<FunctionAst<'i>
let params = parse_typed_parameter_list(params_pair)?;

let mut return_types = Vec::new();
let mut returns_tuple = false;
let mut return_type_spans = Vec::new();
if let Some(next) = inner.peek() {
if next.as_rule() == Rule::return_type_list {
let return_pair = inner.next().expect("checked");
returns_tuple = return_pair.as_str().trim_start_matches(':').trim_start().starts_with('(');
let (types, spans) = parse_return_type_list(return_pair)?;
return_types = types;
return_type_spans = spans;
Expand All @@ -1377,7 +1386,19 @@ fn parse_function_definition<'i>(pair: Pair<'i, Rule>) -> Result<FunctionAst<'i>
}
let body_span = body_span.unwrap_or(span);

Ok(FunctionAst { name, attributes, entrypoint, params, return_types, return_type_spans, body, span, name_span, body_span })
Ok(FunctionAst {
name,
attributes,
entrypoint,
params,
return_types,
returns_tuple,
return_type_spans,
body,
span,
name_span,
body_span,
})
}

fn parse_function_attribute<'i>(pair: Pair<'i, Rule>) -> Result<FunctionAttributeAst<'i>, CompilerError> {
Expand Down Expand Up @@ -1924,23 +1945,12 @@ fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result<Expr<'i>, CompilerError> {
let mut index_inner = postfix.into_inner();
let index_pair = index_inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple index".to_string()))?;
let index_expr = parse_expression(index_pair)?;
let index_span = index_expr.span;
let span = expr.span.join(&postfix_span);
if let ExprKind::Split { source, index: split_index, span: split_span, .. } = &expr.kind {
let part = match index_expr.kind {
ExprKind::Int(0) => SplitPart::Left,
ExprKind::Int(1) => SplitPart::Right,
_ => {
return Err(CompilerError::Unsupported("split() index must be 0 or 1".to_string()).with_span(&index_span));
}
};
expr = Expr::new(
ExprKind::Split { source: source.clone(), index: split_index.clone(), part, span: *split_span },
span,
);
} else {
expr = Expr::new(ExprKind::ArrayIndex { source: Box::new(expr), index: Box::new(index_expr) }, span);
if matches!(&expr.kind, ExprKind::Split { .. }) {
return Err(CompilerError::Unsupported("split() results must be accessed with .0 or .1".to_string())
.with_span(&postfix_span));
}
expr = Expr::new(ExprKind::ArrayIndex { source: Box::new(expr), index: Box::new(index_expr) }, span);
}
Rule::unary_suffix => {
let kind = match postfix.as_str() {
Expand All @@ -1951,6 +1961,33 @@ fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result<Expr<'i>, CompilerError> {
let span = expr.span.join(&postfix_span);
expr = Expr::new(ExprKind::UnarySuffix { source: Box::new(expr), kind, span: postfix_span }, span);
}
Rule::tuple_field_access => {
let raw = postfix.as_str().trim().trim_start_matches('.');
let index = raw
.parse::<usize>()
.map_err(|_| CompilerError::Unsupported(format!("invalid tuple field index '{raw}'")).with_span(&postfix_span))?;
let span = expr.span.join(&postfix_span);
if let ExprKind::Split { source, index: split_index, span: split_span, .. } = &expr.kind {
let part = match index {
0 => SplitPart::Left,
1 => SplitPart::Right,
_ => {
return Err(
CompilerError::Unsupported("split() index must be 0 or 1".to_string()).with_span(&postfix_span)
);
}
};
expr = Expr::new(
ExprKind::Split { source: source.clone(), index: split_index.clone(), part, span: *split_span },
span,
);
} else {
expr = Expr::new(
ExprKind::FieldAccess { source: Box::new(expr), field: index.to_string(), field_span: postfix_span },
span,
);
}
}
Rule::field_access => {
if matches!(&expr.kind, ExprKind::Introspection { .. }) || expr_root_identifier(&expr).as_deref() == Some("tx") {
return Err(CompilerError::Unsupported("field access on transaction introspection is not supported".to_string()));
Expand Down
1 change: 1 addition & 0 deletions silverscript-lang/src/compiler/covenant_declarations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,7 @@ fn generated_entrypoint<'i>(
params,
entrypoint: true,
return_types: Vec::new(),
returns_tuple: false,
body,
return_type_spans: Vec::new(),
span: policy.span,
Expand Down
41 changes: 41 additions & 0 deletions silverscript-lang/src/compiler/inline_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,10 @@ impl<'i, 'd> Inliner<'i, 'd> {
self.functions.get(name).cloned().filter(|function| !function.entrypoint) // TODO: Store this information in a separate set for efficiency
}

fn tuple_field_index(field: &str) -> Option<usize> {
(!field.is_empty() && field.chars().all(|ch| ch.is_ascii_digit())).then(|| field.parse().ok()).flatten()
}

fn inline_call(
&mut self,
function: &FunctionAst<'i>,
Expand Down Expand Up @@ -477,6 +481,12 @@ impl<'i, 'd> Inliner<'i, 'd> {
ExprKind::Call { name, args, name_span } => {
let (mut prelude, args) = self.lower_exprs(args, scope, visited_functions)?;
if let Some(function) = self.inline_target(name) {
if function.returns_tuple {
return Err(CompilerError::Unsupported(format!(
"function '{}' returns a tuple and cannot be used directly in expressions; access a tuple field instead",
function.name
)));
}
if function.return_types.len() != 1 {
return Err(CompilerError::Unsupported(format!(
"function '{}' with multiple return values cannot be used in expressions",
Expand Down Expand Up @@ -591,6 +601,37 @@ impl<'i, 'd> Inliner<'i, 'd> {
Ok((prelude, Expr::new(ExprKind::StateObject(lowered_fields), span)))
}
ExprKind::FieldAccess { source, field, field_span } => {
if let Some(index) = Self::tuple_field_index(field)
&& let ExprKind::Call { name, args, name_span } = &source.kind
&& let Some(function) = self.inline_target(name)
{
if !function.returns_tuple {
return Err(CompilerError::Unsupported(format!("function '{}' does not return a tuple", function.name)));
}
if index >= function.return_types.len() {
return Err(CompilerError::Unsupported(format!(
"tuple index {index} out of bounds for function '{}'",
function.name
)));
}
let temp_names = function.return_types.iter().map(|_| self.fresh_name(name)).collect::<Vec<_>>();
let bindings = function
.return_types
.iter()
.zip(temp_names.iter())
.map(|(type_ref, temp_name)| ParamAst {
type_ref: type_ref.clone(),
name: temp_name.clone(),
span,
type_span: *name_span,
name_span: *name_span,
})
.collect::<Vec<_>>();
let prelude = self.inline_call(&function, args, Some(&bindings), scope, visited_functions, span)?;
let selected_name = temp_names[index].clone();
self.debug_recorder.record_visible_name(&selected_name, &format!("{}.{}", function.name, index));
return Ok((prelude, Expr::identifier(selected_name)));
}
let (prelude, source) = self.lower_expr(source, scope, visited_functions)?;
Ok((
prelude,
Expand Down
Loading