Skip to content
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

Open
novusnota opened this issue Dec 11, 2024 · 2 comments
Open

Tuple type support #1127

novusnota opened this issue Dec 11, 2024 · 2 comments
Assignees
Labels
discussion Ideas that are not fully formed and require discussion language design language feature question Further information is requested
Milestone

Comments

@novusnota
Copy link
Member

novusnota commented Dec 11, 2024

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:

// Creates a tuple out of two passed values
asm fun tuple2(a: Int, b: Int): (a: Int, b: Int) { TUPLE }

fun example1() {
    let t = tuple2(5, 6);
    t.a; // because we know the type of this tuple,
         // we know how to make the indexed access under the hood with
         // 0 INDEX
    dump(t.a);
    dump(t.b);
}

// Adds a tuple into the tuple
asm extends fun push(
    self: (a: Int, b: Int),
    t: (c: Int),
): (a: Int, b: Int, c: Int) { TPUSH }

asm fun tuple1(c: Int): (c: Int) { TUPLE }

fun example2() {
   let t: (a: Int, b: Int, c: Int) = tuple2(5, 6).push(tuple1());
}

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:

struct Tuple1 { t: (c: Int) }
struct Tuple2 { t: (a: Int, b: Int) }
struct Tuple3 { t: (a: Int, b: Int, c: Int) }

asm fun tuple1(c: Int): Tuple1 { TUPLE }
asm fun tuple2(a: Int, b: Int): Tuple2 { TUPLE }
asm extends fun push(self: Tuple2, t: Tuple1): Tuple3 { TPUSH }

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 a tuple keyword and combine it with current syntax of Structs to make tuple creation explicit. Additionally, we might introduce something like an Array / Tuple type, which would accomodate tuples of any allowed length (up to 255 elements).

Related:

@novusnota novusnota added language feature language design question Further information is requested discussion Ideas that are not fully formed and require discussion labels Dec 11, 2024
@novusnota
Copy link
Member Author

novusnota commented Dec 12, 2024

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 asm functions, but rather inconvenient. I've debugged this 3 hours straight because of [] placed by FunC on the stack when pushing SingletonArray255{} there, and occasional DROPs all of the sudden:

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)

@novusnota
Copy link
Member Author

novusnota commented Dec 17, 2024

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:

#DEBUG#: File example.tact:5:9:
#DEBUG#: dumpStack()
#DEBUG#: stack(4 values) : 500000000 C{96...C7} 0 ()

#DEBUG#: File example.tact:7:9:
#DEBUG#: dumpStack()
#DEBUG#: stack(5 values) : 500000000 C{96...C7} 0 () (())

#DEBUG#: File example.tact:9:9:
#DEBUG#: dumpStack()
#DEBUG#: stack(6 values) : 500000000 C{96...C7} 0 () (()) (())

#DEBUG#: File example.tact:11:9:
#DEBUG#: dumpStack()
#DEBUG#: stack(7 values) : 500000000 C{96...C7} 0 () (()) (()) [5 5]

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();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Ideas that are not fully formed and require discussion language design language feature question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants