Skip to content

Commit

Permalink
feat: named and unnamed type assignment 3 of 3 (gnolang#2367)
Browse files Browse the repository at this point in the history
This is the last part of the solution for issue gnolang#1141. 
The  1 of 3 of the solution can be found in PR  gnolang#1143.
The  2 of 3 of the solution can be found in PR gnolang#1246 

It decomposes function calls that return multiple values in the
preprocess.

### Here is the problem to solve: 

`  u1, n2 = x() `

How do we ensure that the returned multiple values from a function call
adhere to named and unnamed type assignment specifications?
Additionally, we want to solve this problem during preprocessing instead
of at runtime to minimize the impact on runtime performance.

### The main ideas:

    u1, n2 = x()  << decompose the statement to the following two lines 
    // .tmp_1, .tmp_2 := x() 
    // u1, n2 = .tmp_1, .tmp_2

then we can apply name and unname type conversion specs to the second
line.
    u1, n2 = _tmp_1, _tmp_2


### Here are the example code and the explanation

```
// decompose_filetest.gno
package main

  type nat []int

  func x() (nat, []int) {
    a := nat{1}
    b := []int{2}
    return a, b
  }

  func main() {
    var u1 []int
    var n2 nat

    u1, n2 = x() 
    // .tmp_1, .tmp_2 := x() 
    // u1, n2 = .tmp_1, .tmp_2

    println(u1)
    println(n2)

  }

  // Output:
  // slice[(1 int)]
  // (slice[(2 int)] main.nat)

```

### Here is the simplified recursive tree of the transformation in the
preprocess

<img width="1336" alt="image"
src="https://github.com/gnolang/gno/assets/90544084/306a4770-457d-4131-a82a-2de5c6b0dadf">

### Here are the major steps involved in this decomposition during
preprocessing:

- Create hidden temporary name expressions .tmp1, .tmp2. In Go, a
leading dot is not valid in variable names, ensuring that users cannot
create names that clash with these hidden variables.


- Create two statements in the block: one for defining and one for
assigning.

```
 .tmp1, .tmp2 := x() 
 u1, n2 = .tmp_1, .tmp_2
```

- Preprocess each newly created statements

- Replace the original statement with the two newly created statements. 


### Here are two additional changes to facilitate above.

- Update the FuncValue's body in `updates := pn.PrepareNewValues(pv)
`since its source Body has been changed during preprocessing.

- Replace all ` for index := range Body` with `for i:=0; i < len(Body);
i++` in transcribe.go since the body length might change due to the
decomposition.


<!-- please provide a detailed description of the changes made in this
pull request. -->

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [x] No breaking changes were made
- [x] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Co-authored-by: Miloš Živković <milos.zivkovic@tendermint.com>
Co-authored-by: Morgan <git@howl.moe>
  • Loading branch information
3 people authored and gfanton committed Jul 23, 2024
1 parent e55f939 commit bff435e
Show file tree
Hide file tree
Showing 20 changed files with 793 additions and 12 deletions.
2 changes: 1 addition & 1 deletion gnovm/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ lint:
$(rundep) github.com/golangci/golangci-lint/cmd/golangci-lint run --config ../.github/golangci.yml ./...

.PHONY: fmt
fmt:
fmt:
go run ./cmd/gno fmt $(GNOFMT_FLAGS) ./stdlibs/...
$(rundep) mvdan.cc/gofumpt $(GOFMT_FLAGS) .

Expand Down
28 changes: 28 additions & 0 deletions gnovm/pkg/gnolang/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,10 @@ func (ss Body) GetBody() Body {
return ss
}

func (ss *Body) SetBody(nb Body) {
*ss = nb
}

func (ss Body) GetLabeledStmt(label Name) (stmt Stmt, idx int) {
for idx, stmt = range ss {
if label == stmt.GetLabel() {
Expand Down Expand Up @@ -1375,6 +1379,13 @@ func (x *PackageNode) PrepareNewValues(pv *PackageValue) []TypedValue {
panic("PackageNode.PrepareNewValues() package mismatch")
}
}
// The FuncValue Body may have been altered during the preprocessing.
// We need to update body field from the source in the FuncValue accordingly.
for _, tv := range x.Values {
if fv, ok := tv.V.(*FuncValue); ok {
fv.UpdateBodyFromSource()
}
}
pvl := len(block.Values)
pnl := len(x.Values)
// copy new top-level defined values/types.
Expand Down Expand Up @@ -1480,6 +1491,7 @@ type BlockNode interface {
Define(Name, TypedValue)
Define2(bool, Name, Type, TypedValue)
GetBody() Body
SetBody(Body)
}

// ----------------------------------------
Expand Down Expand Up @@ -1873,18 +1885,34 @@ func (x *IfStmt) GetBody() Body {
panic("IfStmt has no body (but .Then and .Else do)")
}

func (x *IfStmt) SetBody(b Body) {
panic("IfStmt has no body (but .Then and .Else do)")
}

func (x *SwitchStmt) GetBody() Body {
panic("SwitchStmt has no body (but its cases do)")
}

func (x *SwitchStmt) SetBody(b Body) {
panic("SwitchStmt has no body (but its cases do)")
}

func (x *FileNode) GetBody() Body {
panic("FileNode has no body (but it does have .Decls)")
}

func (x *FileNode) SetBody(b Body) {
panic("FileNode has no body (but it does have .Decls)")
}

func (x *PackageNode) GetBody() Body {
panic("PackageNode has no body")
}

func (x *PackageNode) SetBody(b Body) {
panic("PackageNode has no body")
}

// ----------------------------------------
// Value Path

Expand Down
105 changes: 103 additions & 2 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"math/big"
"reflect"
"strings"
"sync/atomic"

"github.com/gnolang/gno/tm2/pkg/errors"
Expand Down Expand Up @@ -426,6 +427,24 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node {
switch n := n.(type) {
// TRANS_ENTER -----------------------
case *AssignStmt:
if n.Op == DEFINE {
for _, lx := range n.Lhs {
ln := lx.(*NameExpr).Name
if ln == blankIdentifier {
// ignore.
} else if strings.HasPrefix(string(ln), ".decompose_") {
_, ok := last.GetLocalIndex(ln)
if !ok {
// initial declaration to be re-defined.
last.Predefine(false, ln)
} else {
// do not redeclare.
}
}
}
} else {
// nothing defined.
}

// TRANS_ENTER -----------------------
case *ImportDecl, *ValueDecl, *TypeDecl, *FuncDecl:
Expand Down Expand Up @@ -1872,7 +1891,88 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node {
} else { // ASSIGN, or assignment operation (+=, -=, <<=, etc.)
// NOTE: Keep in sync with DEFINE above.
if len(n.Lhs) > len(n.Rhs) {
// check is done in assertCompatible
// check is done in assertCompatible where we also
// asserted we have at lease one element in Rhs
if cx, ok := n.Rhs[0].(*CallExpr); ok {
// we decompose the a,b = x(...) for named and unamed
// type value return in an assignments
// Call case: a, b = x(...)
ift := evalStaticTypeOf(store, last, cx.Func)
cft := getGnoFuncTypeOf(store, ift)
// check if we we need to decompose for named typed conversion in the function return results
var decompose bool

for i, rhsType := range cft.Results {
lt := evalStaticTypeOf(store, last, n.Lhs[i])
if lt != nil && isNamedConversion(rhsType.Type, lt) {
decompose = true
break
}
}
if decompose {
// only enter this section if cft.Results to be converted to Lhs type for named type conversion.
// decompose a,b = x()
// .decompose1, .decompose2 := x() assignment statement expression (Op=DEFINE)
// a,b = .decompose1, .decompose2 assignment statement expression ( Op=ASSIGN )
// add the new statement to last.Body

// step1:
// create a hidden var with leading . (dot) the curBodyLen increase every time when there is a decomposition
// because there could be multiple decomposition happens
// we use both stmt index and return result number to differentiate the .decompose variables created in each assignment decompostion
// ex. .decompose_3_2: this variable is created as the 3rd statement in the block, the 2nd parameter returned from x(),
// create .decompose_1_1, .decompose_1_2 .... based on number of result from x()
tmpExprs := make(Exprs, 0, len(cft.Results))
for i := range cft.Results {
rn := fmt.Sprintf(".decompose_%d_%d", index, i)
tmpExprs = append(tmpExprs, Nx(rn))
}
// step2:
// .decompose1, .decompose2 := x()
dsx := &AssignStmt{
Lhs: tmpExprs,
Op: DEFINE,
Rhs: n.Rhs,
}
dsx.SetLine(n.Line)
dsx = Preprocess(store, last, dsx).(*AssignStmt)

// step3:

// a,b = .decompose1, .decompose2
// assign stmt expression
// The right-hand side will be converted to a call expression for named/unnamed conversion.
// tmpExprs is a []Expr; we make a copy of tmpExprs to prevent dsx.Lhs in the previous statement (dsx) from being changed by side effects.
// If we don't copy tmpExprs, when asx.Rhs is converted to a const call expression during the preprocessing of the AssignStmt asx,
// dsx.Lhs will change from []NameExpr to []CallExpr.
// This side effect would cause a panic when the machine executes the dsx statement, as it expects Lhs to be []NameExpr.

asx := &AssignStmt{
Lhs: n.Lhs,
Op: ASSIGN,
Rhs: copyExprs(tmpExprs),
}
asx.SetLine(n.Line)
asx = Preprocess(store, last, asx).(*AssignStmt)

// step4:
// replace the original stmt with two new stmts
body := last.GetBody()
// we need to do an in-place replacement while leaving the current node
n.Attributes = dsx.Attributes
n.Lhs = dsx.Lhs
n.Op = dsx.Op
n.Rhs = dsx.Rhs

// insert a assignment statement a,b = .decompose1,.decompose2 AFTER the current statement in the last.Body.
body = append(body[:index+1], append(Body{asx}, body[index+1:]...)...)
last.SetBody(body)
} // end of the decomposition

// Last step: we need to insert the statements to FuncValue.body of PackageNopde.Values[i].V
// updating FuncValue.body=FuncValue.Source.Body in updates := pn.PrepareNewValues(pv) during preprocess.
// we updated FuncValue from source.
}
} else { // len(Lhs) == len(Rhs)
if n.Op == SHL_ASSIGN || n.Op == SHR_ASSIGN {
if len(n.Lhs) != 1 || len(n.Rhs) != 1 {
Expand Down Expand Up @@ -3160,7 +3260,7 @@ func predefineNow2(store Store, last BlockNode, d Decl, m map[Name]struct{}) (De
} else {
panic("should not happen")
}

// The body may get altered during preprocessing later.
if !dt.TryDefineMethod(&FuncValue{
Type: ft,
IsMethod: true,
Expand Down Expand Up @@ -3388,6 +3488,7 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) {
pkg := skipFile(last).(*PackageNode)
// define a FuncValue w/ above type as d.Name.
// fill in later during *FuncDecl:BLOCK.
// The body may get altered during preprocessing later.
fv := &FuncValue{
Type: ft,
IsMethod: false,
Expand Down
27 changes: 18 additions & 9 deletions gnovm/pkg/gnolang/transcribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
} else {
cnn = cnn2.(*FuncLitExpr)
}
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_FUNCLIT_BODY, idx, cnn.Body[idx], &c).(Stmt)
if isBreak(c) {
break
Expand Down Expand Up @@ -383,7 +384,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
} else {
cnn = cnn2.(*BlockStmt)
}
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_BLOCK_BODY, idx, cnn.Body[idx], &c).(Stmt)
if isBreak(c) {
break
Expand All @@ -393,7 +395,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
}
case *BranchStmt:
case *DeclStmt:
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_DECL_BODY, idx, cnn.Body[idx], &c).(SimpleDeclStmt)
if isBreak(c) {
break
Expand Down Expand Up @@ -438,7 +441,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
return
}
}
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_FOR_BODY, idx, cnn.Body[idx], &c).(Stmt)
if isBreak(c) {
break
Expand Down Expand Up @@ -488,7 +492,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
} else {
cnn = cnn2.(*IfCaseStmt)
}
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_IF_CASE_BODY, idx, cnn.Body[idx], &c).(Stmt)
if isBreak(c) {
break
Expand Down Expand Up @@ -525,7 +530,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
return
}
}
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_RANGE_BODY, idx, cnn.Body[idx], &c).(Stmt)
if isBreak(c) {
break
Expand Down Expand Up @@ -565,7 +571,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
if isStopOrSkip(nc, c) {
return
}
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_SELECTCASE_BODY, idx, cnn.Body[idx], &c).(Stmt)
if isBreak(c) {
break
Expand Down Expand Up @@ -640,7 +647,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
return
}
}
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_SWITCHCASE_BODY, idx, cnn.Body[idx], &c).(Stmt)
if isBreak(c) {
break
Expand All @@ -666,7 +674,8 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc
} else {
cnn = cnn2.(*FuncDecl)
}
for idx := range cnn.Body {
// iterate over Body; its length can change if a statement is decomposed.
for idx := 0; idx < len(cnn.Body); idx++ {
cnn.Body[idx] = transcribe(t, nns, TRANS_FUNC_BODY, idx, cnn.Body[idx], &c).(Stmt)
if isBreak(c) {
break
Expand Down
9 changes: 9 additions & 0 deletions gnovm/pkg/gnolang/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,15 @@ func (fv *FuncValue) GetBodyFromSource(store Store) []Stmt {
return fv.body
}

func (fv *FuncValue) UpdateBodyFromSource() {
if fv.Source == nil {
panic(fmt.Sprintf(
"Source is missing for FuncValue %q",
fv.Name))
}
fv.body = fv.Source.GetBody()
}

func (fv *FuncValue) GetSource(store Store) BlockNode {
if rn, ok := fv.Source.(RefNode); ok {
source := store.GetBlockNode(rn.GetLocation())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

type nat []int

func x() (nat, []int) {
a := nat{1}
b := []int{2}
return a, b
}

func main() {
var u1 []int
var n2 nat

_, n2 = x()
// .tmp1, .tmp_2 := x()
// _, u2 = .tmp1, .tmp_2

println(u1)
println(n2)

}

// Output:
// (nil []int)
// (slice[(2 int)] main.nat)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

type nat []int

func x() (nat, []int) {
a := nat{1}
b := []int{2}
return a, b
}

func main() {
var u1 []int
var n2 nat

u1, _ = x()
// .tmp1, .tmp_2 := x()
// u1, _ = .tmp1, .tmp_2

println(u1)
println(n2)

}

// Output:
// slice[(1 int)]
// (nil main.nat)
Loading

0 comments on commit bff435e

Please sign in to comment.