diff --git a/server/src/e2e/suite/folding.test.ts b/server/src/e2e/suite/folding.test.ts new file mode 100644 index 00000000..c8e93a6b --- /dev/null +++ b/server/src/e2e/suite/folding.test.ts @@ -0,0 +1,60 @@ +import * as vscode from "vscode" +import * as assert from "node:assert" +import {BaseTestSuite} from "./BaseTestSuite" +import type {TestCase} from "./TestParser" + +suite("Folding Test Suite", () => { + const testSuite = new (class extends BaseTestSuite { + protected runTest(testFile: string, testCase: TestCase): void { + test(`Folding: ${testCase.name}`, async () => { + await this.replaceDocumentText(testCase.input) + + const foldingRanges = await vscode.commands.executeCommand<vscode.FoldingRange[]>( + "vscode.executeFoldingRangeProvider", + this.document.uri, + ) + + if (foldingRanges.length <= 0) { + throw new Error("No folding ranges found") + } + + const rangesInfo = foldingRanges + .map(range => `[${range.start + 1}, ${range.end + 1}]`) + .join(", ") + + const lines = this.document.getText().split("\n") + const startLines = new Set(foldingRanges.map(range => range.start)) + + lines.forEach((lineContent, index) => { + if (startLines.has(index)) { + lines[index] = lineContent.replace(/\s*$/, "...") + } + }) + + const actualText = `${rangesInfo}\n${lines.join("\n")}`.trimEnd() + const expectedText = testCase.expected.trimEnd() + + if (BaseTestSuite.UPDATE_SNAPSHOTS) { + this.updates.push({ + filePath: testFile, + testName: testCase.name, + actual: actualText, + }) + } else { + assert.deepStrictEqual(actualText, expectedText) + } + }) + } + })() + + suiteSetup(async function () { + this.timeout(10_000) + await testSuite.suiteSetup() + }) + + setup(async () => testSuite.setup()) + teardown(async () => testSuite.teardown()) + suiteTeardown(() => testSuite.suiteTeardown()) + + testSuite.runTestsFromDirectory("folding") +}) diff --git a/server/src/e2e/suite/testcases/folding/assembly.test b/server/src/e2e/suite/testcases/folding/assembly.test new file mode 100644 index 00000000..e0f557cd --- /dev/null +++ b/server/src/e2e/suite/testcases/folding/assembly.test @@ -0,0 +1,215 @@ +======================================================================== +Assembly function with unknown instruction +======================================================================== +asm fun foo() { + 10 PUSHINT2 +} +------------------------------------------------------------------------ +[1, 2] +asm fun foo() {... + 10 PUSHINT2 +} + +======================================================================== +Big assembly function with PUSHCONT +======================================================================== +asm fun send(params: SendParameters) { + // Instructions are grouped, and the stack states they produce as a group are shown right after. + // In the end, our message Cell should have the following TL-B structure: + // message$_ {X:Type} + // info:CommonMsgInfoRelaxed + // init:(Maybe (Either StateInit ^StateInit)) + // body:(Either X ^X) + // = MessageRelaxed X; + + // Group 1: Rearrangements + 3 4 BLKSWAP + s2 XCHG0 + // → Stack state + // s0: `params.bounce` + // s1: `params.to` + // s2: `params.value` + // s3: `params.data` + // s4: `params.code` + // s5: `params.body` + // s6: `params.mode` + // For brevity, the "params" prefix will be omitted from now on. + + // Group 2: Storing the `bounce`, `to` and `value` into a Builder + NEWC + b{01} STSLICECONST // store tag = $0 and ihr_disabled = true + 1 STI // store `bounce` + b{000} STSLICECONST // store bounced = false and src = addr_none + STSLICE // store `to` + SWAP + STGRAMS // store `value` + 105 PUSHINT // 1 + 4 + 4 + 64 + 32 + STZEROES // store currency_collection, ihr_fee, fwd_fee, created_lt and created_at + // → Stack state + // s0: Builder + // s1: `data` + // s2: `code` + // s3: `body` + // s4: `mode` + + // Group 3: Placing the Builder after code and data, then checking those for nullability + s2 XCHG0 + DUP2 + ISNULL + SWAP + ISNULL + MUL // note that -1 * -1 wraps back to -1 + // → Stack state + // s0: -1 (true) if `data` and `code` are both null, 0 (false) otherwise + // s1: `code` + // s2: `data` + // s3: Builder + // s4: `body` + // s5: `mode` + + // Group 4: Left branch of the IFELSE, executed if s0 is -1 (true) + <{ + DROP2 // drop `data` and `code`, since either of those is null + NULL // push a single null + SWAP // place Builder on top + }> PUSHCONT + + // Group 4: Right branch of the IFELSE, executed if s0 is 0 (false) + <{ + // _ split_depth:(Maybe (## 5)) + // special:(Maybe TickTock) + // code:(Maybe ^Cell) + // data:(Maybe ^Cell) + // library:(Maybe ^Cell) + // = StateInit; + NEWC // start composing StateInit + b{00} STSLICECONST // split_depth and special + STDICT // store code + STDICT // store data + b{0} STSLICECONST // store library + ENDC // end composing StateInit + SWAP // place Builder on top + b{1} STSLICECONST // an extra bit for storing StateInit as ref + }> PUSHCONT + + // Group 4: IFELSE that does the branching shown above + IFELSE + // → Stack state + // s0: Builder + // s1: null or StateInit + // s2: `body` + // s3: `mode` + + // Group 5: Finalizing the message + STDICT // store StateInit + STDICT // store `body` as ref with an extra Maybe bit, since `body` might be null + ENDC + // → Stack state + // s0: Cell + // s1: `mode` + + // Group 6: Sending the message, with `mode` on top + SWAP + SENDRAWMSG // https://github.com/tact-lang/tact/issues/1558 +} +------------------------------------------------------------------------ +[1, 98], [56, 59], [63, 77] +asm fun send(params: SendParameters) {... + // Instructions are grouped, and the stack states they produce as a group are shown right after. + // In the end, our message Cell should have the following TL-B structure: + // message$_ {X:Type} + // info:CommonMsgInfoRelaxed + // init:(Maybe (Either StateInit ^StateInit)) + // body:(Either X ^X) + // = MessageRelaxed X; + + // Group 1: Rearrangements + 3 4 BLKSWAP + s2 XCHG0 + // → Stack state + // s0: `params.bounce` + // s1: `params.to` + // s2: `params.value` + // s3: `params.data` + // s4: `params.code` + // s5: `params.body` + // s6: `params.mode` + // For brevity, the "params" prefix will be omitted from now on. + + // Group 2: Storing the `bounce`, `to` and `value` into a Builder + NEWC + b{01} STSLICECONST // store tag = $0 and ihr_disabled = true + 1 STI // store `bounce` + b{000} STSLICECONST // store bounced = false and src = addr_none + STSLICE // store `to` + SWAP + STGRAMS // store `value` + 105 PUSHINT // 1 + 4 + 4 + 64 + 32 + STZEROES // store currency_collection, ihr_fee, fwd_fee, created_lt and created_at + // → Stack state + // s0: Builder + // s1: `data` + // s2: `code` + // s3: `body` + // s4: `mode` + + // Group 3: Placing the Builder after code and data, then checking those for nullability + s2 XCHG0 + DUP2 + ISNULL + SWAP + ISNULL + MUL // note that -1 * -1 wraps back to -1 + // → Stack state + // s0: -1 (true) if `data` and `code` are both null, 0 (false) otherwise + // s1: `code` + // s2: `data` + // s3: Builder + // s4: `body` + // s5: `mode` + + // Group 4: Left branch of the IFELSE, executed if s0 is -1 (true) + <{... + DROP2 // drop `data` and `code`, since either of those is null + NULL // push a single null + SWAP // place Builder on top + }> PUSHCONT + + // Group 4: Right branch of the IFELSE, executed if s0 is 0 (false) + <{... + // _ split_depth:(Maybe (## 5)) + // special:(Maybe TickTock) + // code:(Maybe ^Cell) + // data:(Maybe ^Cell) + // library:(Maybe ^Cell) + // = StateInit; + NEWC // start composing StateInit + b{00} STSLICECONST // split_depth and special + STDICT // store code + STDICT // store data + b{0} STSLICECONST // store library + ENDC // end composing StateInit + SWAP // place Builder on top + b{1} STSLICECONST // an extra bit for storing StateInit as ref + }> PUSHCONT + + // Group 4: IFELSE that does the branching shown above + IFELSE + // → Stack state + // s0: Builder + // s1: null or StateInit + // s2: `body` + // s3: `mode` + + // Group 5: Finalizing the message + STDICT // store StateInit + STDICT // store `body` as ref with an extra Maybe bit, since `body` might be null + ENDC + // → Stack state + // s0: Cell + // s1: `mode` + + // Group 6: Sending the message, with `mode` on top + SWAP + SENDRAWMSG // https://github.com/tact-lang/tact/issues/1558 +} diff --git a/server/src/e2e/suite/testcases/folding/contract.test b/server/src/e2e/suite/testcases/folding/contract.test new file mode 100644 index 00000000..f5652763 --- /dev/null +++ b/server/src/e2e/suite/testcases/folding/contract.test @@ -0,0 +1,169 @@ +======================================================================== +Simple Contract +======================================================================== +primitive Int; + +contract A { + +} +------------------------------------------------------------------------ +[3, 4] +primitive Int; + +contract A {... + +} + +======================================================================== +Contract with receiver +======================================================================== +primitive Int; + +contract A { + receive("Test") { + let a: Int = 32; + } +} +------------------------------------------------------------------------ +[3, 6], [4, 5] +primitive Int; + +contract A {... + receive("Test") {... + let a: Int = 32; + } +} + +======================================================================== +Contract with external +======================================================================== +contract Name { + external("external") { + // some code + } +} +------------------------------------------------------------------------ +[1, 4], [2, 3] +contract Name {... + external("external") {... + // some code + } +} + +======================================================================== +Contract with bounced and filled body +======================================================================== +contract A { + bounced(msg: Int) { + while (true) { + let start = false; + } + } +} +------------------------------------------------------------------------ +[1, 6], [2, 5], [3, 4] +contract A {... + bounced(msg: Int) {... + while (true) {... + let start = false; + } + } +} + +======================================================================== +Contract with nested blocks +======================================================================== +contract Nested { + fun calculate() { + { + let x = 10; + { + let y = x * 2; + log(y); + } + } + } +} +------------------------------------------------------------------------ +[1, 10], [2, 9], [3, 8], [5, 7] +contract Nested {... + fun calculate() {... + {... + let x = 10; + {... + let y = x * 2; + log(y); + } + } + } +} + +======================================================================== +Contract with message handlers +======================================================================== +contract Wallet { + receive("Deposit") { + balance += msg.amount; + } + + receive("Withdraw") { + if (balance >= msg.amount) { + send(msg.amount); + balance -= msg.amount; + } + } +} +------------------------------------------------------------------------ +[1, 11], [2, 3], [6, 10], [7, 9] +contract Wallet {... + receive("Deposit") {... + balance += msg.amount; + } + + receive("Withdraw") {... + if (balance >= msg.amount) {... + send(msg.amount); + balance -= msg.amount; + } + } +} + +======================================================================== +Contract with if-else +======================================================================== +contract Counter { + fun increment() { + if (value < 10) { + value += 1; + } else { + value = 0; + } + } +} +------------------------------------------------------------------------ +[1, 8], [2, 7], [3, 4], [5, 6] +contract Counter {... + fun increment() {... + if (value < 10) {... + value += 1; + } else {... + value = 0; + } + } +} + +======================================================================== +Contract with empty init +======================================================================== +contract A { + init() { + + } +} +------------------------------------------------------------------------ +[1, 4], [2, 3] +contract A {... + init() {... + + } +} diff --git a/server/src/e2e/suite/testcases/folding/functions.test b/server/src/e2e/suite/testcases/folding/functions.test new file mode 100644 index 00000000..cce14379 --- /dev/null +++ b/server/src/e2e/suite/testcases/folding/functions.test @@ -0,0 +1,63 @@ +======================================================================== +Global function +======================================================================== +primitive Int; + +fun foo(size: Int) { + // some code +} + +fun test() { + foo(42); +} +------------------------------------------------------------------------ +[3, 4], [7, 8] +primitive Int; + +fun foo(size: Int) {... + // some code +} + +fun test() {... + foo(42); +} + +======================================================================== +Global function with Cell +======================================================================== +fun foo() { + const cell = TokenNotification{ + query_id: 10, + amount: 20, + from: address(""), + }.toCell()/* Size: 367 bits plus up to 120 bits */ +} +------------------------------------------------------------------------ +[1, 6], [2, 5] +fun foo() {... + const cell = TokenNotification{... + query_id: 10, + amount: 20, + from: address(""), + }.toCell()/* Size: 367 bits plus up to 120 bits */ +} + +======================================================================== +Global function with foreach +======================================================================== +fun foo() { + let a: map<Int, String> = emptyMap(); + foreach (key, value in a) { + dump(key) + dump(value) + } +} +------------------------------------------------------------------------ +[1, 6], [3, 5] +fun foo() {... + let a: map<Int, String> = emptyMap(); + foreach (key, value in a) {... + dump(key) + dump(value) + } +} diff --git a/server/src/e2e/suite/testcases/folding/struct.test b/server/src/e2e/suite/testcases/folding/struct.test new file mode 100644 index 00000000..0e45061e --- /dev/null +++ b/server/src/e2e/suite/testcases/folding/struct.test @@ -0,0 +1,13 @@ +======================================================================== +Single Struct +======================================================================== +primitive Int; +struct Foo { + a: Int; +} +------------------------------------------------------------------------ +[2, 3] +primitive Int; +struct Foo {... + a: Int; +} diff --git a/server/src/e2e/suite/testcases/folding/trait.test b/server/src/e2e/suite/testcases/folding/trait.test new file mode 100644 index 00000000..4d48155d --- /dev/null +++ b/server/src/e2e/suite/testcases/folding/trait.test @@ -0,0 +1,11 @@ +======================================================================== +Simple Trait +======================================================================== +trait Foo { + Bar: Int; +} +------------------------------------------------------------------------ +[1, 2] +trait Foo {... + Bar: Int; +}