Skip to content

Commit 3765697

Browse files
committed
[Tolk] Built-in map<K,V> as a high-level wrapper over TVM dicts
1 parent 4d59949 commit 3765697

37 files changed

+2506
-62
lines changed

crypto/smartcont/tolk-stdlib/common.tolk

Lines changed: 293 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
// Standard library for Tolk (LGPL licence).
22
// It contains common functions that are available out of the box, the user doesn't have to import anything.
3-
// More specific functions are required to be imported explicitly, like "@stdlib/tvm-dicts".
3+
// More specific functions are required to be imported explicitly, like "@stdlib/gas-payments".
44
tolk 1.0
55

6-
/// In Tolk v1.x there would be a type `map<K,V>`.
7-
/// Currently, working with dictionaries is still low-level, with raw cells.
8-
/// But just for clarity, we use "dict" instead of a "cell?" where a cell-dictionary is assumed.
9-
/// Every dictionary object can be null. TVM NULL is essentially "empty dictionary".
10-
type dict = cell?
11-
126
/**
137
Tuple manipulation primitives.
148
Elements of a tuple can be of arbitrary type.
@@ -150,11 +144,9 @@ fun contract.getAddress(): address
150144
fun contract.getOriginalBalance(): coins
151145
asm "BALANCE" "FIRST"
152146

153-
/// Same as [contract.getOriginalBalance], but returns a tuple:
154-
/// `int` — balance in nanotoncoins;
155-
/// `dict` — a dictionary with 32-bit keys representing the balance of "extra currencies".
147+
/// Same as [contract.getOriginalBalance], but returns a tuple: balance in nanotoncoins and extra currencies
156148
@pure
157-
fun contract.getOriginalBalanceWithExtraCurrencies(): [coins, dict]
149+
fun contract.getOriginalBalanceWithExtraCurrencies(): [coins, ExtraCurrenciesMap]
158150
asm "BALANCE"
159151

160152
/// Returns the persistent contract storage cell. It can be parsed or modified with slice and builder primitives later.
@@ -614,6 +606,289 @@ fun debug.dumpStack(): void
614606
builtin
615607

616608

609+
/*
610+
Implementation of `map<K, V>` and methods over it.
611+
612+
Note: maps in TVM are called "dictionaries". They are stored as nested cells
613+
(each value in a separate cell, probably with intermediate cells-nodes).
614+
Constructing even small dicts is gas-expensive, because cell creation costs a lot.
615+
616+
`map<K, V>` is a "human-readable interface" over low-level dictionaries. The compiler,
617+
knowing types of K and V, effectively generates asm instructions for storing/loading values.
618+
But still, of course, it's a tree of cells, it's just a TVM dictionary.
619+
*/
620+
621+
/// `map<K, V>` is a built-in high-level type. Internally, it's an optional cell.
622+
/// - an empty map is represented as `null`.
623+
/// - a non-empty map points to a root cell.
624+
/// Note about some restrictions for K and V types:
625+
/// - a key must be fixed-with; valid: int32, uint64, address, bits256, Point; invalid: int, coins
626+
/// - a value must be serializable; valid: int32, coins, AnyStruct, Cell<AnyStruct>; invalid: int, builder
627+
struct map<K, V> {
628+
tvmDict: cell?
629+
}
630+
631+
/// A low-level TVM dictionary is called "dict".
632+
/// Think of it as "a map with unknown keys and unknown values".
633+
type dict = cell?
634+
635+
/// Returns an empty typed map. It's essentially "PUSHNULL", since TVM NULL represents an empty map.
636+
/// Example:
637+
/// ```
638+
/// var m: map<int8, int32> = createEmptyMap();
639+
/// ```
640+
@pure
641+
fun createEmptyMap<K, V>(): map<K, V>
642+
builtin
643+
644+
/// Converts a low-level TVM dictionary to a typed map.
645+
/// Actually, does nothing: accepts an "optional cell" and returns the same "optional cell",
646+
/// so if you specify key/value types incorrectly, it will fail later, at [map.get] and similar.
647+
@pure
648+
fun createMapFromLowLevelDict<K, V>(d: dict): map<K, V>
649+
builtin
650+
651+
/// Converts a high-level map to a low-level TVM dictionary.
652+
/// Actually, does nothing: returns the same "optional cell".
653+
@pure
654+
fun map<K, V>.toLowLevelDict(self): dict
655+
asm "NOP"
656+
657+
/// Checks whether a map is empty (whether a cell is null).
658+
/// Note: a check `m == null` will not work, use `m.isEmpty()`.
659+
@pure
660+
fun map<K, V>.isEmpty(self): bool
661+
asm "DICTEMPTY"
662+
663+
/// Checks whether a key exists in a map.
664+
@pure
665+
fun map<K, V>.exists(self, key: K): bool
666+
builtin
667+
668+
/// Gets an element by key. If not found, does NOT throw, just returns isFound = false.
669+
/// Example:
670+
/// ```
671+
/// val r = m.get(123);
672+
/// if (r.isFound) {
673+
/// r.loadValue()
674+
/// }
675+
/// ```
676+
@pure
677+
fun map<K, V>.get(self, key: K): MapLookupResult<V>
678+
builtin
679+
680+
/// Gets an element by key and throws if it doesn't exist.
681+
@pure
682+
fun map<K, V>.mustGet(self, key: K, throwIfNotFound: int = 9): V
683+
builtin
684+
685+
/// Sets an element by key.
686+
/// Example:
687+
/// ```
688+
/// m.set(k, 3);
689+
/// ```
690+
/// Since it returns `self`, calls may be chained.
691+
@pure
692+
fun map<K, V>.set(mutate self, key: K, value: V): self
693+
builtin
694+
695+
/// Sets an element and returns the previous element at that key. If no previous, isFound = false.
696+
/// Example:
697+
/// ```
698+
/// val prev = m.setAndGetPrevious(k, 3);
699+
/// if (prev.isFound) {
700+
/// prev.loadValue()
701+
/// }
702+
/// ```
703+
@pure
704+
fun map<K, V>.setAndGetPrevious(mutate self, key: K, value: V): MapLookupResult<V>
705+
builtin
706+
707+
/// Sets an element only if the key already exists. Returns whether an element was replaced.
708+
@pure
709+
fun map<K, V>.replaceIfExists(mutate self, key: K, value: V): bool
710+
builtin
711+
712+
/// Sets an element only if the key already exists and returns the previous element at that key.
713+
@pure
714+
fun map<K, V>.replaceAndGetPrevious(mutate self, key: K, value: V): MapLookupResult<V>
715+
builtin
716+
717+
/// Sets an element only if the key does not exist. Returns whether an element was added.
718+
@pure
719+
fun map<K, V>.addIfNotExists(mutate self, key: K, value: V): bool
720+
builtin
721+
722+
/// Sets an element only if the key does not exist. If exists, returns an old value.
723+
@pure
724+
fun map<K, V>.addOrGetExisting(mutate self, key: K, value: V): MapLookupResult<V>
725+
builtin
726+
727+
/// Delete an element at the key. Returns whether an element was deleted.
728+
@pure
729+
fun map<K, V>.delete(mutate self, key: K): bool
730+
builtin
731+
732+
/// Delete an element at the key and returns the deleted element. If not exists, isFound = false.
733+
/// Example:
734+
/// ```
735+
/// val prev = m.deleteAndGetDeleted(k);
736+
/// if (prev.isFound) {
737+
/// prev.loadValue()
738+
/// }
739+
/// ```
740+
@pure
741+
fun map<K, V>.deleteAndGetDeleted(mutate self, key: K): MapLookupResult<V>
742+
builtin
743+
744+
/// Finds the first (minimal) element in a map. If key are integers, it's the minimal integer.
745+
/// If keys are addresses or complex structures (represented as slices), it's lexicographically smallest.
746+
/// For an empty map, just returns isFound = false.
747+
/// Useful for iterating over a map:
748+
/// ```
749+
/// var r = m.findFirst();
750+
/// while (r.isFound) {
751+
/// ... use r.getKey() and r.loadValue()
752+
/// r = m.iterateNext(r)
753+
/// }
754+
/// ```
755+
@pure
756+
fun map<K, V>.findFirst(self): MapEntry<K, V>
757+
builtin
758+
759+
/// Finds the last (maximal) element in a map. If key are integers, it's the maximal integer.
760+
/// If keys are addresses or complex structures (represented as slices), it's lexicographically largest.
761+
/// For an empty map, just returns isFound = false.
762+
/// Useful for iterating over a map:
763+
/// ```
764+
/// var r = m.findLast();
765+
/// while (r.isFound) {
766+
/// ... use r.getKey() and r.loadValue()
767+
/// r = m.iteratePrev(r)
768+
/// }
769+
/// ```
770+
@pure
771+
fun map<K, V>.findLast(self): MapEntry<K, V>
772+
builtin
773+
774+
/// Finds an element with key > pivotKey.
775+
/// Don't forget to check `isFound` before using `getKey()` and `loadValue()` of the result.
776+
@pure
777+
fun map<K, V>.findKeyGreater(self, pivotKey: K): MapEntry<K, V>
778+
builtin
779+
780+
/// Finds an element with key >= pivotKey.
781+
/// Don't forget to check `isFound` before using `getKey()` and `loadValue()` of the result.
782+
@pure
783+
fun map<K, V>.findKeyGreaterOrEqual(self, pivotKey: K): MapEntry<K, V>
784+
builtin
785+
786+
/// Finds an element with key < pivotKey.
787+
/// Don't forget to check `isFound` before using `getKey()` and `loadValue()` of the result.
788+
@pure
789+
fun map<K, V>.findKeyLess(self, pivotKey: K): MapEntry<K, V>
790+
builtin
791+
792+
/// Finds an element with key <= pivotKey.
793+
/// Don't forget to check `isFound` before using `getKey()` and `loadValue()` of the result.
794+
@pure
795+
fun map<K, V>.findKeyLessOrEqual(self, pivotKey: K): MapEntry<K, V>
796+
builtin
797+
798+
/// Iterate over a map in ascending order.
799+
/// Example:
800+
/// ```
801+
/// // iterate for all keys >= 10 up to the end
802+
/// var r = m.findKeyGreaterOrEqual(10);
803+
/// while (r.isFound) {
804+
/// ... use r.getKey() and r.loadValue()
805+
/// r = m.iterateNext(r)
806+
/// }
807+
/// ```
808+
@pure
809+
fun map<K, V>.iterateNext(self, current: MapEntry<K, V>): MapEntry<K, V>
810+
builtin
811+
812+
/// Iterate over a map in reverse order.
813+
/// Example:
814+
/// ```
815+
/// // iterate for all keys < 10 down lo lowest
816+
/// var r = m.findKeyLess(10);
817+
/// while (r.isFound) {
818+
/// ... use r.getKey() and r.loadValue()
819+
/// r = m.iteratePrev(r)
820+
/// }
821+
/// ```@pure
822+
fun map<K, V>.iteratePrev(self, current: MapEntry<K, V>): MapEntry<K, V>
823+
builtin
824+
825+
/// MapLookupResult is a return value of [map.get], [map.setAndGetPrevious], and similar.
826+
/// Instead of returning a nullable value (that you'd check on null before usage),
827+
/// this struct is returned (and you check on `isFound` before usage).
828+
/// Example:
829+
/// ```
830+
/// val r = m.get(key);
831+
/// if (r.isFound) {
832+
/// r.loadValue() // unpacks a returned slice
833+
/// }
834+
/// ```
835+
struct MapLookupResult<TValue> {
836+
rawSlice: slice? // holds encoded value, present if isFound
837+
isFound: bool
838+
}
839+
840+
@pure
841+
fun MapLookupResult<TValue>.loadValue(self): TValue {
842+
// it's assumed that you check for `isFound` before calling `loadValue()`;
843+
// note that `assertEnd` is called to ensure no remaining data left besides TValue
844+
return TValue.fromSlice(self.rawSlice!, {
845+
assertEndAfterReading: true
846+
})
847+
}
848+
849+
@pure
850+
fun MapLookupResult<slice>.loadValue(self): slice {
851+
return self.rawSlice!
852+
}
853+
854+
/// MapEntry is a return value of [map.findFirst], [map.iterateNext], and similar.
855+
/// You should check for `isFound` before calling `getKey()` and `loadValue()`.
856+
/// Example:
857+
/// ```
858+
/// var r = m.findFirst();
859+
/// while (r.isFound) {
860+
/// ... use r.getKey() and r.loadValue()
861+
/// r = m.iterateNext(r)
862+
/// }
863+
/// ```
864+
struct MapEntry<K, V> {
865+
rawValue: slice? // holds encoded value, present if isFound
866+
key: K
867+
isFound: bool
868+
}
869+
870+
@pure
871+
fun MapEntry<K, V>.getKey(self): K {
872+
// it's assumed that you check for `isFound` before calling `getKey()`;
873+
// (otherwise, it will contain an incorrect stack slot with `null` value, not K)
874+
return self.key
875+
}
876+
877+
@pure
878+
fun MapEntry<K, V>.loadValue(self): V {
879+
// it's assumed that you check for `isFound` before calling `loadValue()`;
880+
// note that `assertEnd` is inserted to ensure no remaining data left besides TValue
881+
return V.fromSlice(self.rawValue!, {
882+
assertEndAfterReading: true
883+
})
884+
}
885+
886+
@pure
887+
fun MapEntry<K, slice>.loadValue(self): slice {
888+
return self.rawValue!
889+
}
890+
891+
617892
/**
618893
Slice primitives: parsing cells.
619894
When you _load_ some data, you mutate the slice (shifting an internal pointer on the stack).
@@ -715,7 +990,6 @@ fun slice.getMiddleBits(self, offset: int, len: int): slice
715990
asm "SDSUBSTR"
716991

717992
/// Loads a dictionary (TL HashMapE structure, represented as TVM cell) from a slice.
718-
/// Returns `null` if `nothing` constructor is used.
719993
@pure
720994
fun slice.loadDict(mutate self): dict
721995
asm( -> 1 0) "LDDICT"
@@ -993,7 +1267,10 @@ const SEND_MODE_ESTIMATE_FEE_ONLY = 1024
9931267
/// +64 substitutes the entire balance of the incoming message as an outcoming value (slightly inaccurate, gas expenses that cannot be estimated before the computation is completed are not taken into account).
9941268
/// +128 substitutes the value of the entire balance of the contract before the start of the computation phase (slightly inaccurate, since gas expenses that cannot be estimated before the completion of the computation phase are not taken into account).
9951269

996-
type ExtraCurrenciesDict = dict
1270+
/// `ExtraCurrenciesMap` represents a dictionary of "extra currencies" other than TON coin.
1271+
/// They can be attached to a message (incoming and outgoing) and stored on contract's balance.
1272+
/// It's a dictionary `[currencyID => amount]` (amount is like `coins`, but encoded with a high precision).
1273+
type ExtraCurrenciesMap = map<int32, varuint32>
9971274

9981275
/// ContractState is "code + data" of a contract.
9991276
/// Used in outgoing messages (StateInit) to initialize a destination contract.
@@ -1062,7 +1339,7 @@ struct CreateMessageOptions<TBody = never> {
10621339
/// whether a message will bounce back on error
10631340
bounce: bool
10641341
/// message value: attached tons (or tons + extra currencies)
1065-
value: coins | (coins, ExtraCurrenciesDict)
1342+
value: coins | (coins, ExtraCurrenciesMap)
10661343
/// destination is either a provided address, or is auto-calculated by stateInit
10671344
dest: address // either just send a message to some address
10681345
| builder // ... or a manually constructed builder with a valid address
@@ -1312,7 +1589,7 @@ fun sendRawMessage(msg: cell, mode: int): void
13121589
struct InMessage {
13131590
senderAddress: address // an internal address from which the message arrived
13141591
valueCoins: coins // ton amount attached to an incoming message
1315-
valueExtra: dict // extra currencies attached to an incoming message
1592+
valueExtra: ExtraCurrenciesMap // extra currencies attached to an incoming message
13161593
originalForwardFee: coins // fee that was paid by the sender
13171594
createdLt: uint64 // logical time when a message was created
13181595
createdAt: uint32 // unixtime when a message was created
@@ -1332,7 +1609,7 @@ struct InMessage {
13321609
struct InMessageBounced {
13331610
senderAddress: address // an internal address from which the message was bounced
13341611
valueCoins: coins // ton amount attached to a message
1335-
valueExtra: dict // extra currencies attached to a message
1612+
valueExtra: ExtraCurrenciesMap // extra currencies attached to a message
13361613
originalForwardFee: coins // comission that the sender has payed to send this message
13371614
createdLt: uint64 // logical time when a message was created (and bounced)
13381615
createdAt: uint32 // unixtime when a message was created (and bounced)

0 commit comments

Comments
 (0)