diff --git a/GUIDE.md b/GUIDE.md new file mode 100644 index 0000000..f53cb3d --- /dev/null +++ b/GUIDE.md @@ -0,0 +1,197 @@ +# Tealang guide + +Tealang translates all (except `mulw`) own constructions to corresponding **TEAL** instructions +so that there is almost one-to-one mapping between statements in both languages. + +Refer to [TEAL documentation](https://developer.algorand.org/docs/teal) for details. + +*Note, all code snippets below only for Tealang features demonstration.* + +## Program structure + +* imports +* variable and constant declarations and function definitions +* logic function + +## Types + +* uint64 (unsigned integer) +* []byte (byte array) + +## Statements vs expressions + +Statement is a standalone unit of execution that does not return any value. +In opposite, expression is evaluated to some value. + +## Declarations, definitions and assignments + +Constants, variables and function are supported: +``` +const a = 1 +const b = "abc\x01" +let x = b +function test(x) { return x; } +``` + +Declarations, definitions and assignments are statement + +## Functions + +Unlike **TEAL** functions are supported and inlined at the calling point. +Functions must return some value and can not be recursive. + +``` +function inc(x) { return x+1; } +function logic() { return inc(0); } +``` + +## Logic function + +Must exist in every program and return integer. The return value (zero/non-zero) is **TRUE** or **FALSE** return code for entire **TEAL** program (smart contract). +``` +function logic() { + if txn.Sender == "ABC" { + return 1 + } + return 0 +} +``` + +## Flow control statements + +### if-else + +Consist of `if` keyword, conditional expression (must evaluate to integer), `if-block` and optional `else-block`. +``` +if x == 1 { + return 1 +} else { + let x = txn.Receiver +} +``` + +### return + +`return` forces current function to exit and return a value. For the special `logic` function it would be entire program return value. + +### error + +`error` forces program to exit with an error. + +## Comments + +Single line comments are supported, commenting sequence is `//` + +## Expressions + +### Conditional expression + +Similar to conditional statement but `else-block` is required, and both blocks must evaluate to an expression. +``` +let sender = if global.GroupSize > 1 { txn.Sender } else { gtxn[1].Sender } +``` + +### Arithmetic, Logic, and Cryptographic Operations + +All operations like +, -, *, ==, !=, <, >, >=, etc. +See [TEAL documentation](https://github.com/algorand/go-algorand/blob/master/data/transactions/logic/README.md#arithmetic-logic-and-cryptographic-operations) for the full list. + +## Builtin objects + +There are 4 builtin objects: `txn`, `gtxn`, `global`, `args`. Accessing them is an expression. + +| Object and Syntax | Description | +| --- | --- | +| `args[N]` | returns Args[N] value | +| `txn.FIELD` | retrieves field from current transaction | +| `gtxn[N].FIELD` | retrieves field from a transaction N in the current transaction group | +| `global.FIELD` | returns globals | + +#### Transaction fields +| Index | Name | Type | Notes | +| --- | --- | --- | --- | +| 0 | Sender | []byte | 32 byte address | +| 1 | Fee | uint64 | micro-Algos | +| 2 | FirstValid | uint64 | round number | +| 3 | FirstValidTime | uint64 | Causes program to fail; reserved for future use. | +| 4 | LastValid | uint64 | round number | +| 5 | Note | []byte | | +| 6 | Lease | []byte | | +| 7 | Receiver | []byte | 32 byte address | +| 8 | Amount | uint64 | micro-Algos | +| 9 | CloseRemainderTo | []byte | 32 byte address | +| 10 | VotePK | []byte | 32 byte address | +| 11 | SelectionPK | []byte | 32 byte address | +| 12 | VoteFirst | uint64 | | +| 13 | VoteLast | uint64 | | +| 14 | VoteKeyDilution | uint64 | | +| 15 | Type | []byte | | +| 16 | TypeEnum | uint64 | See table below | +| 17 | XferAsset | uint64 | Asset ID | +| 18 | AssetAmount | uint64 | value in Asset's units | +| 19 | AssetSender | []byte | 32 byte address. Causes clawback of all value of asset from AssetSender if Sender is the Clawback address of the asset. | +| 20 | AssetReceiver | []byte | 32 byte address | +| 21 | AssetCloseTo | []byte | 32 byte address | +| 22 | GroupIndex | uint64 | Position of this transaction within an atomic transaction group. A stand-alone transaction is implicitly element 0 in a group of 1. | +| 23 | TxID | []byte | The computed ID for this transaction. 32 bytes. | + +#### Global fields + +| Index | Name | Type | Notes | +| --- | --- | --- | --- | +| 0 | MinTxnFee | uint64 | micro Algos | +| 1 | MinBalance | uint64 | micro Algos | +| 2 | MaxTxnLife | uint64 | rounds | +| 3 | ZeroAddress | []byte | 32 byte address of all zero bytes | +| 4 | GroupSize | uint64 | Number of transactions in this atomic transaction group. At least 1. | + +## Scopes + +Tealang maintains a single global scope (shared between main program and imported modules) and nested scopes for every execution block. +Blocks are created for functions and if-else branches. +Parent scope is accessible from nested blocks. If a variable declared in nested block, it might shadow variable with the same name from parent scope. + +``` +let x = 1 +function logic() { + let x = 2 // shadows 1 in logic block + if 1 { + let x = 3 // shadows 2 in if-block + } + return x // 2 +} +``` + +## Imports + +Unlike **TEAL**, a tealang program can be split to modules. There is a standard library `stdlib` containing some constants and **TEAL** templates as tealang functions. + +### Module structure + +* imports +* declarations and definitions + +``` +const myconst = 1 +function myfunction() { return 0; } +``` + +## Standard library + +At the moment consist of 2 files: +1. const.tl +2. template.tl + +``` +import stdlib.const + +function logic() { + let ret = TxTypePayment + return ret +} +``` + +## More examples + +* [examples directory](https://github.com/pzbitskiy/tealang/tree/master/examples) +* [stdlib directory](https://github.com/pzbitskiy/tealang/tree/master/stdlib) \ No newline at end of file diff --git a/README.md b/README.md index d2fc8c2..ce9fb5f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Teal Language -High-level language for Algorand Smart Contracts at Layer-1 and it's low-level TEAL language. -The goal is to abstract the stack-based TEAL VM and provide imperative Go/JS/Python-like syntax. +High-level language for Algorand Smart Contracts at Layer-1 and it's low-level **TEAL** language. +The goal is to abstract the stack-based **TEAL** VM and provide imperative Go/JS/Python-like syntax. ## Language Features @@ -13,7 +13,7 @@ let variable1 = 1 const myaddr = "XYZ" ``` -* All binary and unary operations from TEAL +* All binary and unary operations from **TEAL** ``` let a = (1 + 2) / 3 let b = ~a @@ -55,8 +55,17 @@ function logic() { } ``` +* Modules +``` +import stdlib.const +``` + * Antlr-based parser +## Language guide + +[Documentation](GUIDE.md) + ## Usage * Tealang to bytecode @@ -79,8 +88,6 @@ function logic() { Checkout [syntax highlighter](https://github.com/pzbitskiy/tealang-syntax-highlighter) for vscode. -TODO: Tealang guide - ## Build from sources ### Prerequisites @@ -109,3 +116,15 @@ make go ```sh make java-gui ``` + +## Roadmap + +1. Constant folding. +2. Strings concatenation. +3. Fix known limitations. +4. Code generation for return at the end of function + +## Limitations + +* No `mulw` **TEAL** opcode support. +* No check that all branches `return` or `error`. diff --git a/compiler/codegen_test.go b/compiler/codegen_test.go index 7774492..8260d7b 100644 --- a/compiler/codegen_test.go +++ b/compiler/codegen_test.go @@ -299,3 +299,66 @@ func TestCodegenOneLineCond(t *testing.T) { a.Equal("==", lines[9]) a.Equal("&&", lines[10]) } + +func TestCodegenShadow(t *testing.T) { + a := require.New(t) + + source := ` +let x = 1 +function logic() { + let x = 2 // shadows 1 in logic block + if 1 { + let x = 3 // shadows 2 in if-block + } + return x // 2 +} +` + result, errors := Parse(source) + a.NotEmpty(result, errors) + a.Empty(errors) + prog := Codegen(result) + lines := strings.Split(prog, "\n") + a.Equal("intcblock 0 1 2 3", lines[0]) + a.Equal("intc 1", lines[1]) + a.Equal("store 0", lines[2]) + a.Equal("intc 2", lines[3]) + a.Equal("store 1", lines[4]) + a.Equal("intc 1", lines[5]) + a.Equal("!", lines[6]) + a.Equal("bnz if_stmt_end_", lines[7][:len("bnz if_stmt_end_")]) + a.Equal("intc 3", lines[8]) + a.Equal("store 2", lines[9]) + a.Equal("if_stmt_end_", lines[10][:len("if_stmt_end_")]) + a.Equal("load 1", lines[11]) + a.Equal("intc 1", lines[12]) + a.Equal("bnz end_logic", lines[13]) + a.Equal("end_logic:", lines[14]) +} + +func TestCodegenNestedFun(t *testing.T) { + a := require.New(t) + + source := ` +function test1() { return 1; } +function test2() { return test1(); } +function logic() { + return test2() +} +` + result, errors := Parse(source) + a.NotEmpty(result, errors) + a.Empty(errors) + prog := Codegen(result) + lines := strings.Split(prog, "\n") + a.Equal("intcblock 0 1", lines[0]) + a.Equal("intc 1", lines[1]) + a.Equal("intc 1", lines[2]) + a.Equal("bnz end_test1", lines[3]) + a.Equal("end_test1:", lines[4]) + a.Equal("intc 1", lines[5]) + a.Equal("bnz end_test2", lines[6]) + a.Equal("end_test2:", lines[7]) + a.Equal("intc 1", lines[8]) + a.Equal("bnz end_logic", lines[9]) + a.Equal("end_logic:", lines[10]) +} diff --git a/compiler/example_test.go b/compiler/example_test.go index 651c722..f71fdd5 100644 --- a/compiler/example_test.go +++ b/compiler/example_test.go @@ -40,3 +40,48 @@ function logic() { prog := Codegen(result) a.NotEmpty(prog) } + +func TestGuideCompile(t *testing.T) { + a := require.New(t) + source := ` +import stdlib.const + +const a = 1 +const b = "abc\x01" +let x = b +function test(x) { return x; } +let sender = if global.GroupSize > 1 { txn.Sender } else { gtxn[1].Sender } + +function inc(x) { return x+1; } +const myconst = 1 +function myfunction() { return 0; } + +function logic() { + if txn.Sender == "ABC" { + return 1 + } + + let x = 2 // shadows 1 in logic block + if 1 { + let x = 3 // shadows 2 in if-block + } + return x // 2 + + if x == 1 { + return 1 + } else { + let x = txn.Receiver + } + + return inc(0); + + let ret = TxTypePayment + return ret +} +` + result, errors := Parse(source) + a.NotEmpty(result, errors) + a.Empty(errors) + prog := Codegen(result) + a.NotEmpty(prog) +} diff --git a/examples/fee-reimburse.tl b/examples/fee-reimburse.tl deleted file mode 100644 index ca581fe..0000000 --- a/examples/fee-reimburse.tl +++ /dev/null @@ -1,25 +0,0 @@ -if global.GroupSize != 2 { - error -} - -let myFee = txn.Fee -let mySender = txn.Sender -let reimburseTxIndex = if txn.GroupIndex == 0 { 1 } else { 0 } - -let reimburseTo = "" -let reimburseAmount = 0 - -// depending on reimbursement transaction index (0 or 1) determine values to check -if reimburseTxIndex == 1 { - reimburseTo = gtxn[1].Receiver - reimburseAmount = gtxn[1].Amount -} else { - reimburseTo = gtxn[0].Receiver - reimburseAmount = gtxn[0].Amount -} - -if mySender != reimburseTo || myFee != reimburseAmount { - return 0 -} - -return 1 diff --git a/examples/syntax-errors.tl b/examples/syntax-errors.tl index 629aebc..cd2858a 100644 --- a/examples/syntax-errors.tl +++ b/examples/syntax-errors.tl @@ -3,26 +3,26 @@ let d = 1 + 2 ; let e = if a > 0 {1} else {2} function test(x, y) { - return x+y/2 + return x+y/2 } function logic() { - if e == 1 { - let x = a + b; - error - } + if e == 1 { + let x = a + b; + error + } - if a == 1 { - return 0 - } + if a == 1 { + return 0 + } - let x = 2; - x = global.GroupSize - x = gtxn[1].Sender + let x = 2; + x = global.GroupSize + x = gtxn[1].Sender - test(1+1, 2) - sha256(x) - ed25519verify("\x01\x02", c, x) + test(1+1, 2) + sha256(x) + ed25519verify("\x01\x02", c, x) - return 1 + return 1 } diff --git a/stdlib/noop.tl b/stdlib/noop.tl index 95e854a..e39c61f 100644 --- a/stdlib/noop.tl +++ b/stdlib/noop.tl @@ -1,3 +1,3 @@ function NoOp() { - return 0 + return 0 } diff --git a/stdlib/templates.tl b/stdlib/templates.tl index 059eea9..abf9536 100644 --- a/stdlib/templates.tl +++ b/stdlib/templates.tl @@ -1,6 +1,5 @@ import stdlib.const - // Suppose the owner of account A wants to send a payment to account 'to', but does not want to pay a transaction fee. // If account A signs the following contract with the appropriate parameters, // then anyone can cover a fee for that payment on account A's behalf. @@ -8,41 +7,41 @@ import stdlib.const // The first transaction must spend the transaction fee into account A, // and the second transaction must be the specified payment transaction from account A to account 'to'. function DynamicFee(to, amt, closeTo, firstValid, lastValid, lease) { - const expectedGroupSize = 2 - const reimburseTxIndex = 0 - const myTxIndex = 1 - - // ensure group is layout properly - if global.GroupSize != expectedGroupSize || txn.GroupIndex != myTxIndex { - error - } - - // Check that the first transaction (reimbursement) is a payment, - // which is required since the first transaction should be paying the fee for the second - if gtxn[reimburseTxIndex].TypeEnum != TxTypePayment { - return 0 - } - - // Check that the second transaction is a payment as well - if txn.TypeEnum != TxTypePayment { - return 0 - } - - // specify that the receiver of funds from the first transaction (reimbursement) - // is equal to the sender of the second transaction - // and funds are equal to fee of the second transaction - let reimburseReceiver = gtxn[reimburseTxIndex].Receiver - let reimburseAmount = gtxn[reimburseTxIndex].Amount - let sender = txn.Sender - let fee = txn.Fee - if sender != reimburseReceiver || fee != reimburseAmount { - return 0 - } - - // verify that all other tx fields match to the contract parameters - if txn.Receiver == to && txn.CloseRemainderTo == closeTo && txn.Amount == amt && txn.FirstValid == firstValid && txn.LastValid == lastValid && txn.Lease == lease { - return 1 - } - - return 0 + const expectedGroupSize = 2 + const reimburseTxIndex = 0 + const myTxIndex = 1 + + // ensure group is layout properly + if global.GroupSize != expectedGroupSize || txn.GroupIndex != myTxIndex { + error + } + + // Check that the first transaction (reimbursement) is a payment, + // which is required since the first transaction should be paying the fee for the second + if gtxn[reimburseTxIndex].TypeEnum != TxTypePayment { + return 0 + } + + // Check that the second transaction is a payment as well + if txn.TypeEnum != TxTypePayment { + return 0 + } + + // specify that the receiver of funds from the first transaction (reimbursement) + // is equal to the sender of the second transaction + // and funds are equal to fee of the second transaction + let reimburseReceiver = gtxn[reimburseTxIndex].Receiver + let reimburseAmount = gtxn[reimburseTxIndex].Amount + let sender = txn.Sender + let fee = txn.Fee + if sender != reimburseReceiver || fee != reimburseAmount { + return 0 + } + + // verify that all other tx fields match to the contract parameters + if txn.Receiver == to && txn.CloseRemainderTo == closeTo && txn.Amount == amt && txn.FirstValid == firstValid && txn.LastValid == lastValid && txn.Lease == lease { + return 1 + } + + return 0 } \ No newline at end of file