Skip to content

Commit 2bb89b4

Browse files
committed
[Tolk] Correctly handle variadic ints at serialization
Occasionally, `varint32` was not handled at packing to/from cells and worked like a regular `int32`. This MR fixes the issue, and also: - adds missing varintN types - fix intN/uintN limits - rename bytesN to bitsN, since "bits" are more frequent in practice
1 parent 72e0ae2 commit 2bb89b4

15 files changed

+223
-79
lines changed

crypto/smartcont/tolk-stdlib/common.tolk

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ struct PackOptions {
227227
/// however, if you guarantee that a slice is valid (for example, it comes from trusted sources),
228228
/// set this option to true to disable runtime checks;
229229
/// note: `int32` and other are always validated for overflow without any extra gas,
230-
/// so this flag controls only rarely used `bytesN` / `bitsN` types
231-
skipBitsNFieldsValidation: bool = false,
230+
/// so this flag controls only rarely used `bitsN` type
231+
skipBitsNValidation: bool = false,
232232
}
233233

234234
/// UnpackOptions allows you to control behavior of `MyStruct.fromCell(c)` and similar functions.

tolk-tester/tests/intN-tests.tolk

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ fun test3(op: int32, qid: uint64) {
8080
if (op == qid) {}
8181
if ((op as int32?)! == qid) {}
8282
if (op == (qid as uint64)) {}
83-
if ((op as uint257?)! != (qid as int256?)!) {}
83+
if ((op as int257?)! != (qid as uint256?)!) {}
8484
__expect_type(op << qid, "int");
8585

8686
takeAnyInt(op);
@@ -227,7 +227,7 @@ fun main() {
227227
assign0(mutate t.1 as uint16);
228228
assign0(mutate t.2 as int);
229229
__expect_type(t.0 as int1, "int1");
230-
__expect_type(t.0 as uint257, "uint257");
230+
__expect_type(t.0 as int257, "int257");
231231
__expect_type(0 as int32, "int32");
232232
__expect_type((0 as int32) as uint64?, "uint64?");
233233
__expect_type(null as (int8, [uint16?])?, "(int8, [uint16?])?");

tolk-tester/tests/lazy-load-tests.tolk

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ struct OneIntOneRef {
217217
r: cell;
218218
}
219219

220+
struct WithVariadicInts {
221+
i1: varuint32;
222+
i2: varuint32;
223+
}
224+
220225

221226
@noinline
222227
fun contract.getFakeData(seqno: int): Cell<WalletStorage> {
@@ -655,7 +660,7 @@ fun demo135(skip: bool) {
655660
cc.bin16 = stringHexToSlice("00000000") as bits16;
656661
try {
657662
return cc.toCell({
658-
skipBitsNFieldsValidation: skip
663+
skipBitsNValidation: skip
659664
}).beginParse().remainingBitsCount();
660665
} catch (excno) {
661666
return excno;
@@ -939,6 +944,13 @@ fun demo156(s: slice): int {
939944
}
940945
}
941946

947+
@method_id(157)
948+
fun demo157() {
949+
var c: Cell<WithVariadicInts> = WithVariadicInts { i1: 1 << 100, i2: 1 << 200 }.toCell();
950+
val d = lazy c.load();
951+
return d.i2 == (1 << 200);
952+
}
953+
942954

943955
@method_id(200)
944956
fun demo200() {
@@ -1280,7 +1292,7 @@ fun demo310() {
12801292
f9: "" as bits200,
12811293
f10: ("" as bits400, ()),
12821294
f11: 0,
1283-
}.toCell({skipBitsNFieldsValidation: true});
1295+
}.toCell({skipBitsNValidation: true});
12841296
var st = lazy cell.load();
12851297
var sm = lazy cell.load();
12861298
var sk = lazy cell.load();
@@ -1625,6 +1637,7 @@ fun main() {
16251637
@testcase | 155 | | 555
16261638
@testcase | 156 | x{0110} | 16
16271639
@testcase | 156 | x{FF10} | -100
1640+
@testcase | 157 | | -1
16281641

16291642
@testcase | 200 | | 1
16301643
@testcase | 201 | | 1 2 -1

tolk-tester/tests/pack-unpack-2.tolk

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,30 @@ struct DifferentMix3 {
334334
pairm: (int32, int64)?;
335335
}
336336

337+
/*
338+
ui16: VarUInteger 16
339+
i16: VarInteger 16
340+
ui32: VarUInteger 32
341+
i32: VarInteger 32
342+
*/
343+
344+
struct WithVariadicInts {
345+
ui16: varuint16;
346+
i16: varint16;
347+
ui32: varuint32;
348+
i32: varint32;
349+
}
350+
351+
/*
352+
Test MIN_INT and MAX_INT
353+
*/
354+
355+
struct EdgeCaseInts {
356+
maxUint: uint256 = +115792089237316195423570985008687907853269984665640564039457584007913129639935;
357+
maxInt: int257 = +115792089237316195423570985008687907853269984665640564039457584007913129639935;
358+
minInt: int257 = -115792089237316195423570985008687907853269984665640564039457584007913129639935 - 1;
359+
}
360+
337361
/*
338362
Test: write with builder, read with other struct
339363
(type `builder` is available for writing, but not for reading)
@@ -670,6 +694,27 @@ fun test_mutatingRemainder() {
670694
return obj.rest.remainingBitsCount();
671695
}
672696

697+
@method_id(228)
698+
fun test_VariadicIntegers() {
699+
var t: WithVariadicInts = {
700+
ui16: (1 << 120) - 1,
701+
i16: -(1 << 119),
702+
ui32: (1 << 248) - 1,
703+
i32: -(1 << 247),
704+
};
705+
run<WithVariadicInts>(t, stringHexToSlice("ffffffffffffffffffffffffffffffff800000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe000000000000000000000000000000000000000000000000000000000000020_"));
706+
run<WithVariadicInts>({ui16: 0, i16: 0, ui32: 0, i32: 0}, stringHexToSlice("000020_"));
707+
708+
var c = t.toCell().load();
709+
return (c.ui16 == t.ui16) & (c.i16 == t.i16) & (c.ui32 == t.ui32) & (c.i32 == t.i32)
710+
}
711+
712+
@method_id(229)
713+
fun test_EdgeCaseIntegers() {
714+
var edge: EdgeCaseInts = {};
715+
var manual = beginCell().storeUint(edge.maxUint, 256).storeInt(edge.maxInt, 257).storeInt(edge.minInt, 257);
716+
return edge.toCell().hash() == manual.endCell().hash()
717+
}
673718

674719
fun main() {
675720
var t: JustInt32 = { value: 10 };
@@ -709,4 +754,6 @@ fun main() {
709754
@testcase | 225 | | 5 40 65535 8 50000000
710755
@testcase | 226 | | 1 16 0
711756
@testcase | 227 | | 16
757+
@testcase | 228 | | -1
758+
@testcase | 229 | | -1
712759
*/

tolk-tester/tests/pack-unpack-5.tolk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ fun test1() {
2222
coins.estimatePackSize(),
2323
bits6.estimatePackSize(),
2424
bytes8.estimatePackSize(),
25+
varint32.estimatePackSize(),
2526
);
2627
}
2728

@@ -204,7 +205,7 @@ fun main() {
204205
}
205206

206207
/**
207-
@testcase | 101 | | [ 32 32 0 0 ] [ 64 64 0 0 ] [ 1 1 0 0 ] [ 1 1 0 0 ] [ 0 9999 0 4 ] [ 4 124 0 0 ] [ 6 6 0 0 ] [ 64 64 0 0 ]
208+
@testcase | 101 | | [ 32 32 0 0 ] [ 64 64 0 0 ] [ 1 1 0 0 ] [ 1 1 0 0 ] [ 0 9999 0 4 ] [ 4 124 0 0 ] [ 6 6 0 0 ] [ 64 64 0 0 ] [ 5 253 0 0 ]
208209
@testcase | 102 | | [ 32 32 0 0 ] [ 1 33 0 0 ] [ 1 33 0 0 ] [ 17 33 0 0 ]
209210
@testcase | 103 | | [ 67 199 0 0 ] [ 69 599 0 0 ]
210211
@testcase | 104 | | [ 4 534 0 0 ] [ 2 536 0 0 ] [ 6 1070 0 0 ]

tolk-tester/tests/pack-unpack-6.tolk

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,13 @@ fun test5() {
5050

5151
@method_id(106)
5252
fun test6() {
53-
return SomeBytesFields { f1: stringHexToSlice("11") as bytes1 }.toCell({skipBitsNFieldsValidation: true}).hash() & 0xFFFF;
53+
return SomeBytesFields { f1: stringHexToSlice("11") as bytes1 }.toCell({skipBitsNValidation: true}).hash() & 0xFFFF;
5454
}
5555

5656
@method_id(107)
5757
fun test7(believe: bool) {
5858
try {
59-
return SomeBytesFields { f1: stringHexToSlice("ffff") as bytes1 }.toCell({skipBitsNFieldsValidation: believe}).hash() & 0xFFFF;
59+
return SomeBytesFields { f1: stringHexToSlice("ffff") as bytes1 }.toCell({skipBitsNValidation: believe}).hash() & 0xFFFF;
6060
} catch (excno) {
6161
return excno;
6262
}

tolk/builtins.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,25 @@ static AsmOp compile_fetch_int(std::vector<VarDescr>& res, std::vector<VarDescr>
10361036
return exec_op(loc, (fetch ? "LD"s : "PLD"s) + (sgnd ? "IX" : "UX"), 2, 1 + (unsigned)fetch);
10371037
}
10381038

1039+
// fun slice.__loadVarInt(mutate self, bits: int, unsigned: bool): int
1040+
static AsmOp compile_fetch_varint(std::vector<VarDescr>& res, std::vector<VarDescr>& args, SrcLocation loc) {
1041+
tolk_assert(args.size() == 3 && res.size() == 2);
1042+
// it's a hidden function for auto-serialization (not exposed to stdlib), to bits/unsigned are not dynamic
1043+
tolk_assert(args[1].is_int_const() && args[2].is_int_const());
1044+
long n_bits = args[1].int_const->to_long();
1045+
long is_unsigned = args[2].int_const->to_long();
1046+
1047+
args[1].unused();
1048+
args[2].unused();
1049+
if (n_bits == 16) {
1050+
return exec_op(loc, is_unsigned ? "LDVARUINT16" : "LDVARINT16", 1, 2);
1051+
}
1052+
if (n_bits == 32) {
1053+
return exec_op(loc, is_unsigned ? "LDVARUINT32" : "LDVARINT32", 1, 2);
1054+
}
1055+
tolk_assert(false);
1056+
}
1057+
10391058
// fun builder.storeInt (mutate self, x: int, len: int): self asm(x b len) "STIX";
10401059
// fun builder.storeUint (mutate self, x: int, len: int): self asm(x b len) "STUX";
10411060
static AsmOp compile_store_int(std::vector<VarDescr>& res, std::vector<VarDescr>& args, SrcLocation loc, bool sgnd) {
@@ -1063,6 +1082,25 @@ static AsmOp compile_store_int(std::vector<VarDescr>& res, std::vector<VarDescr>
10631082
return exec_op(loc, sgnd ? "STIX" : "STUX", 3, 1);
10641083
}
10651084

1085+
// fun builder.__storeVarInt (mutate self, x: int, bits: int, unsigned: bool): self
1086+
static AsmOp compile_store_varint(std::vector<VarDescr>& res, std::vector<VarDescr>& args, SrcLocation loc) {
1087+
tolk_assert(args.size() == 4 && res.size() == 1);
1088+
// it's a hidden function for auto-serialization (not exposed to stdlib), to bits/unsigned are not dynamic
1089+
tolk_assert(args[2].is_int_const() && args[3].is_int_const());
1090+
long n_bits = args[2].int_const->to_long();
1091+
long is_unsigned = args[3].int_const->to_long();
1092+
1093+
args[2].unused();
1094+
args[3].unused();
1095+
if (n_bits == 16) {
1096+
return exec_op(loc, is_unsigned ? "STVARUINT16" : "STVARINT16", 2, 1);
1097+
}
1098+
if (n_bits == 32) {
1099+
return exec_op(loc, is_unsigned ? "STVARUINT32" : "STVARINT32", 2, 1);
1100+
}
1101+
tolk_assert(false);
1102+
}
1103+
10661104
// fun builder.storeBool(mutate self, value: bool): self asm( -> 1 0) "1 STI";
10671105
static AsmOp compile_store_bool(std::vector<VarDescr>& res, std::vector<VarDescr>& args, SrcLocation loc) {
10681106
tolk_assert(args.size() == 2 && res.size() == 1);
@@ -1388,6 +1426,13 @@ void define_builtins() {
13881426
define_builtin_func("__InMessage.getInMsgParam", ParamsInt1, Int, nullptr,
13891427
compile_calc_InMessage_getInMsgParam,
13901428
0);
1429+
define_builtin_method("builder.__storeVarInt", Builder, {Builder, Int, Int, Bool}, Unit, nullptr,
1430+
compile_store_varint, // not exposed to stdlib, used in auto-serialization
1431+
FunctionData::flagMarkedAsPure | FunctionData::flagHasMutateParams | FunctionData::flagAcceptsSelf | FunctionData::flagReturnsSelf);
1432+
define_builtin_method("slice.__loadVarInt", Slice, {Slice, Int, Bool}, Int, nullptr,
1433+
compile_fetch_varint, // not exposed to stdlib, used in auto-serialization
1434+
FunctionData::flagMarkedAsPure | FunctionData::flagHasMutateParams | FunctionData::flagAcceptsSelf,
1435+
{}, {1, 0});
13911436

13921437
// compile-time only functions, evaluated essentially at compile-time, no runtime implementation
13931438
// they are placed in stdlib and marked as `builtin`

tolk/pack-unpack-api.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class PackUnpackAvailabilityChecker {
5858
if (any_type->try_as<TypeDataIntN>()) {
5959
return {};
6060
}
61-
if (any_type->try_as<TypeDataBytesN>()) {
61+
if (any_type->try_as<TypeDataBitsN>()) {
6262
return {};
6363
}
6464
if (any_type == TypeDataCoins::create()) {
@@ -345,7 +345,7 @@ std::vector<var_idx_t> generate_lazy_struct_to_cell(CodeBlob& code, SrcLocation
345345
ctx.generate_pack_any(hidden_field->declared_type, std::move(ir_field));
346346
} else {
347347
std::vector ir_gap_or_tail = loaded_state->get_ir_loaded_aside_field(hidden_field);
348-
if (hidden_field->declared_type->unwrap_alias()->try_as<TypeDataBytesN>()) {
348+
if (hidden_field->declared_type->unwrap_alias()->try_as<TypeDataBitsN>()) {
349349
ctx.storeSlice(ir_gap_or_tail[0]);
350350
} else {
351351
ctx.generate_pack_any(hidden_field->declared_type, std::move(ir_gap_or_tail));

tolk/pack-unpack-serializers.cpp

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ PackContext::PackContext(CodeBlob& code, SrcLocation loc, std::vector<var_idx_t>
6565
, f_storeUint(lookup_function("builder.storeUint"))
6666
, ir_builder(std::move(ir_builder))
6767
, ir_builder0(this->ir_builder[0])
68-
, option_skipBitsNFieldsValidation(ir_options[0]) {
68+
, option_skipBitsNValidation(ir_options[0]) {
6969
}
7070

7171
void PackContext::storeInt(var_idx_t ir_idx, int len) const {
@@ -264,16 +264,51 @@ struct S_IntN final : ISerializer {
264264
}
265265
};
266266

267-
struct S_BytesN final : ISerializer {
267+
struct S_VariadicIntN final : ISerializer {
268+
const int n_bits; // only 16 and 32 available
269+
const bool is_unsigned;
270+
271+
explicit S_VariadicIntN(int n_bits, bool is_unsigned)
272+
: n_bits(n_bits), is_unsigned(is_unsigned) {}
273+
274+
void pack(const PackContext* ctx, CodeBlob& code, SrcLocation loc, std::vector<var_idx_t>&& rvect) override {
275+
FunctionPtr f_storeVarInt = lookup_function("builder.__storeVarInt");
276+
std::vector args = { ctx->ir_builder0, rvect[0], code.create_int(loc, n_bits, "(n-bits)"), code.create_int(loc, is_unsigned, "(is-unsigned)") };
277+
code.emplace_back(loc, Op::_Call, ctx->ir_builder, std::move(args), f_storeVarInt);
278+
}
279+
280+
std::vector<var_idx_t> unpack(const UnpackContext* ctx, CodeBlob& code, SrcLocation loc) override {
281+
FunctionPtr f_loadVarInt = lookup_function("slice.__loadVarInt");
282+
std::vector args = { ctx->ir_slice0, code.create_int(loc, n_bits, "(n-bits)"), code.create_int(loc, is_unsigned, "(is-unsigned)") };
283+
std::vector result = code.create_tmp_var(TypeDataInt::create(), loc, "(loaded-varint)");
284+
code.emplace_back(loc, Op::_Call, std::vector{ctx->ir_slice0, result[0]}, std::move(args), f_loadVarInt);
285+
return result;
286+
}
287+
288+
void skip(const UnpackContext* ctx, CodeBlob& code, SrcLocation loc) override {
289+
// no TVM instruction to skip, just load but don't use the result
290+
unpack(ctx, code, loc);
291+
}
292+
293+
PackSize estimate(const EstimateContext* ctx) override {
294+
if (n_bits == 32) {
295+
return PackSize(5, 253);
296+
} else {
297+
return PackSize(4, 124); // same as `coins`
298+
}
299+
}
300+
};
301+
302+
struct S_BitsN final : ISerializer {
268303
const int n_bits;
269304

270-
explicit S_BytesN(int n_width, bool is_bits)
305+
explicit S_BitsN(int n_width, bool is_bits)
271306
: n_bits(is_bits ? n_width : n_width * 8) {}
272307

273308
void pack(const PackContext* ctx, CodeBlob& code, SrcLocation loc, std::vector<var_idx_t>&& rvect) override {
274309
tolk_assert(rvect.size() == 1);
275310

276-
Op& if_disabled_by_user = code.emplace_back(loc, Op::_If, std::vector{ctx->option_skipBitsNFieldsValidation});
311+
Op& if_disabled_by_user = code.emplace_back(loc, Op::_If, std::vector{ctx->option_skipBitsNValidation});
277312
{
278313
code.push_set_cur(if_disabled_by_user.block0);
279314
code.close_pop_cur(loc);
@@ -400,10 +435,8 @@ struct S_Coins final : ISerializer {
400435
}
401436

402437
void skip(const UnpackContext* ctx, CodeBlob& code, SrcLocation loc) override {
403-
FunctionPtr f_loadCoins = lookup_function("slice.loadCoins");
404-
std::vector args = ctx->ir_slice;
405-
std::vector dummy_loaded = code.create_tmp_var(TypeDataInt::create(), loc, "(loaded-coins)");
406-
code.emplace_back(loc, Op::_Call, std::vector{ctx->ir_slice0, dummy_loaded[0]}, std::move(args), f_loadCoins);
438+
// no TVM instruction to skip, just load but don't use the result
439+
unpack(ctx, code, loc);
407440
}
408441

409442
PackSize estimate(const EstimateContext* ctx) override {
@@ -428,9 +461,7 @@ struct S_Address final : ISerializer {
428461
// we can't do just
429462
// ctx->skipBits(2 + 1 + 8 + 256);
430463
// because it may be addr_none or addr_extern; there is no "skip address" in TVM, so just load it
431-
FunctionPtr f_loadAddress = lookup_function("slice.loadAddress");
432-
std::vector ir_address = code.create_tmp_var(TypeDataSlice::create(), loc, "(tmp-addr)");
433-
code.emplace_back(loc, Op::_Call, std::vector{ctx->ir_slice0, ir_address[0]}, ctx->ir_slice, f_loadAddress);
464+
unpack(ctx, code, loc);
434465
}
435466

436467
PackSize estimate(const EstimateContext* ctx) override {
@@ -1088,10 +1119,13 @@ std::vector<PackOpcode> auto_generate_opcodes_for_union(TypePtr union_type, std:
10881119

10891120
static std::unique_ptr<ISerializer> get_serializer_for_type(TypePtr any_type) {
10901121
if (const auto* t_intN = any_type->try_as<TypeDataIntN>()) {
1122+
if (t_intN->is_variadic) {
1123+
return std::make_unique<S_VariadicIntN>(t_intN->n_bits, t_intN->is_unsigned);
1124+
}
10911125
return std::make_unique<S_IntN>(t_intN->n_bits, t_intN->is_unsigned);
10921126
}
1093-
if (const auto* t_bytesN = any_type->try_as<TypeDataBytesN>()) {
1094-
return std::make_unique<S_BytesN>(t_bytesN->n_width, t_bytesN->is_bits);
1127+
if (const auto* t_bitsN = any_type->try_as<TypeDataBitsN>()) {
1128+
return std::make_unique<S_BitsN>(t_bitsN->n_width, t_bitsN->is_bits);
10951129
}
10961130
if (any_type == TypeDataCoins::create()) {
10971131
return std::make_unique<S_Coins>();

tolk/pack-unpack-serializers.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class PackContext {
6464
public:
6565
const std::vector<var_idx_t> ir_builder;
6666
const var_idx_t ir_builder0;
67-
const var_idx_t option_skipBitsNFieldsValidation;
67+
const var_idx_t option_skipBitsNValidation;
6868

6969
PackContext(CodeBlob& code, SrcLocation loc, std::vector<var_idx_t> ir_builder, const std::vector<var_idx_t>& ir_options);
7070

0 commit comments

Comments
 (0)