-
Notifications
You must be signed in to change notification settings - Fork 118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tuple type support #1127
Comments
TL;DR: Working with values that aren't possible to express in the current type system of Tact is possible, but messy because of the implicit DROPs of unused arguments in non-asm functions, which one inevitably has to use alongside asm ones to work with, say, tuples. It's possible to work with the tuple values in Tact through the A hacky attempt to work with tuples without expressing them in Tact types directly// Up to 255 entries
message SingletonArray255 {}
// NOTE:
// SingletonArray255 can be made growable and shrinkable to any number of elements
// with some modifications of extension functions shown below
// NOTE:
// Since tuples can hold values of any TVM type, it doesn't have to be an array of integers.
// But for the simplicity, applicability and the the sake of this example I chose an array of ints.
//
// Lifecycle
//
// Initializes the array/tuple
fun singletonArray255(): SingletonArray255 {
__SingletonArray255();
let arr = SingletonArray255{};
__SingletonArray255Post();
return arr;
}
asm fun __SingletonArray255() { NIL }
asm fun __SingletonArray255Post() { DROP }
// Checks if the top of the stack is a tuple
extends fun check(self: SingletonArray255): Bool { return __check(); }
asm fun __check(): Bool { DUP ISTUPLE }
// Duplicates the tuple on the stack
extends fun dup(self: SingletonArray255) { __dup(); }
asm fun __dup() { DUP }
// FIXME: Don't forget to call this once done with the array/tuple to clear it from the stack
extends fun drop(self: SingletonArray255) { __drop(); }
asm fun __drop() { DUP DROP } // this DUP is here because the empty message tuple gets in the way otherwise
//
// Accessing items
//
extends fun first(self: SingletonArray255): Int { return __first(); }
asm fun __first(): Int { DUP FIRST }
extends fun second(self: SingletonArray255): Int { return __second() }
asm fun __second(): Int { DUP SECOND }
extends fun third(self: SingletonArray255): Int { return __third() }
asm fun __third(): Int { DUP THIRD }
extends fun last(self: SingletonArray255): Int { return __last() }
asm fun __last(): Int { DUP LAST }
extends fun get(self: SingletonArray255, idx: Int): Int { return __get(idx) }
asm fun __get(idx: Int): Int { s1 PUSH SWAP INDEXVAR }
extends fun set(self: SingletonArray255, idx: Int, item: Int) { __set(idx, item) }
asm fun __set(idx: Int, item: Int) { SWAP SETINDEXVAR }
extends fun push(self: SingletonArray255, item: Int) { __push(item) }
asm fun __push(item: Int) { TPUSH }
extends fun pop(self: SingletonArray255): Int { return __pop() }
asm fun __pop(): Int { TPOP }
extends fun len(self: SingletonArray255): Int { return __len() }
asm fun __len(): Int { DUP TLEN }
// NOTE:
// I'd also write a map(), filter(), reduce(), etc.,
// but we don't have a simple way of working with continuations too, so that will wait
// For now, here's the sum() function:
extends fun sum(self: SingletonArray255): Int {
// dumpStack();
let times = __sumExplode();
// dumpStack();
repeat (times) { __sumStep() }
// dumpStack();
return __sumResult();
}
asm fun __sumExplode(): Int { DUP DUP TLEN EXPLODEVAR ZERO SWAP }
asm fun __sumStep() { ADD }
asm fun __sumResult(): Int { }
//
// Usage:
//
fun showcase() {
// Now we have our singleton array:
let arr = singletonArray255();
// Pushing couple of items to it (super cheap compared to maps)
arr.push(5);
arr.push(6);
// Things
dump(arr.first());
dump(arr.len());
// Sum!
dump(arr.sum());
// Mandatory thing once you've done with the array
// Think of it as free() in C
arr.drop();
} The better way right now is to write all the same functions in FunC and then make all the nice bindings from Tact. The even better (?) way is to recall that optional Structs are, in fact, represented as tuples when Tact is compiled to FunC. See: #1127 (comment) |
More experiments: struct Opt { a: Int?; b: Int? }
asm fun opt(structure: Opt?) {}
// ... somewhere in receiver below ...
opt(null);
dumpStack();
opt(Opt{});
dumpStack();
opt(Opt{ a: null, b: null });
dumpStack();
opt(Opt{ a: 5, b: 5 });
dumpStack(); Lead to the following discoveries:
Which means, that all one needs to have to express tuples via Structs is to make them optional! By the way, fields don't have to be optional for that, so here's the minimal example: struct Two { a: Int; b: Int }
asm fun tupleTwo(structure: Two?) {}
// ... somewhere in receiver below ...
tupleTwo(Two{ a: 5, b: 6 }); // will place [5 6] onto the stack! That said, local variable assignments are impossible still, so the following won't compile: // "Struct-tuple"
struct Tuple2 { a: Int; b: Int }
// In FunC, tuple2 looks like: [int, int] tuple2() { return [5, 6]; }
@name(tuple2)
native tuple2(): Tuple2?;
fun showcase() {
// This won't compile:
let t = tuple2();
// This won't compile either:
let t2: Tuple2? = tuple2();
// And this results in the type mismatch: "Tuple2?" is not assignable to "Tuple2"
let t3: Tuple2 = tuple2();
} |
There was a proposal with custom syntax just until I realized that `Struct?` is represented as tuple already
Idea
The rough idea is to name it
Tuple
internally, while not allowing any declarations like Struct and Message have, and instead only using it via the(...)
literal (unlike FunC, but alternative syntax with[...]
is proposed below too). They could behave similarly to either data classes in Kotlin, records in Dart or tuples in OCaml.Draft
A very rough initial draft:
The approach of not adding any explicit declarations seems fine because a) the tuple seem to be needed only for advanced-ish use cases by knowledgeable users, and b) we can always just wrap them in Structs and do the following, because Structs spread their values:
Alternative syntax
Since
DUMPSTK
shows the tuples with[...]
literal syntax, we too might go with it and align the syntax with FunC in that regard. So, instead of(a: Int, b: Int)
example shown above we'd have[a: Int, b: Int]
.Notes
Once we have tuples, we can implement arrays for Tact much more efficiently compared to their current map-based alternatives.
The current implementation of tuples in Tact uses optional Structs, see: #1127 (comment). They allow mapping onto values, but disallow local variable assignments — if one tries to work with tuples in this form, it's only possible to do a series of continuous function calls with no variable assignments in between. Still somewhat useful for throwaway-like calculations of some things, but rather dissapointing.
However, putting aside all the limitations, the
Struct?
being a tuple feels like a foot gun that can fire in unsuspecting users, so we should probably introduce atuple
keyword and combine it with current syntax of Structs to make tuple creation explicit. Additionally, we might introduce something like anArray
/Tuple
type, which would accomodate tuples of any allowed length (up to 255 elements).Related:
The text was updated successfully, but these errors were encountered: