Skip to content

Commit

Permalink
feat(gnovm): align Gno constant handling with Go specifications (#2828)
Browse files Browse the repository at this point in the history
Related Issues:
Closes #2628 

This PR aims to align Gno's constant handling with the Go specification
regarding constant expressions (see [Go Specification on
Constants](https://go.dev/ref/spec#Constant_expressions)).

1. **Primitive Type Requirement**  
   - **Should Work:**  
     ```go
     const t = 1
     ```
   - **Should Return an Error:**  
     ```go
     const t = []string{"1"}
     ```
     **Error:**  
     ```
(const (slice[("1" string)] []string)) (value of type []string) is not
constant
     ```

2. **Function Calls Disallowed**  
   Only built-in functions should be allowed.  
   - **Should Work:**  
     ```go
     const t = len("s")
     ```
   - **Should Return an Error:**  
     ```go
     func v() string {
         return ""
     }
     const t = v()
     ```
     **Error:**  
     ```
     v<VPBlock(3,0)>() (value of type string) is not constant
     ```

3. **Constant Operands Requirement**  
Constant expressions may contain only constant operands and are
evaluated at compile time.
   - **Should Work:**  
     ```go
     const t = 1
     const v = t
     ```
   - **Should Raise an Error:**  
     ```go
     t := 1
     const v = t
     ```
     **Error:**  
     ```
     t (variable of type int) is not constant
     ```

4. **Type Assertion Forbidden**  
   - This code:  
     ```go
     var i interface{} = 1
     const t, ok = i.(int)
     ```
   - **Should Raise This Error:**  
     ```
     i.(int) (comma, ok expression of type int) is not constant
     ```

5. **Index Expression Forbidden**  
   - This code:  
     ```go
     var i = []string{}
     const t, ok = i[0]
     ```
   - **Should Return This Error:**  
     ```
     i[0] (variable of type string) is not constant
     ```


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

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

- [ ] Added new tests, or not needed, or not feasible
- [ ] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] 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>
  • Loading branch information
omarsy authored Jan 14, 2025
1 parent c79c16d commit 3adb2ac
Show file tree
Hide file tree
Showing 33 changed files with 567 additions and 4 deletions.
4 changes: 0 additions & 4 deletions examples/gno.land/r/demo/keystore/keystore_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ import (
)

func TestRender(t *testing.T) {
// https://github.com/gnolang/gno/pull/3375 changed const -> var :
// For some reason non native functions fails on constants with
// constant overflows (code=2), this does not happens when using a variable
// TODO: check this issue after and if this PR is merged
var (
author1 std.Address = testutils.TestAddress("author1")
author2 std.Address = testutils.TestAddress("author2")
Expand Down
1 change: 1 addition & 0 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -2294,6 +2294,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node {
// NOTE: may or may not be a *ConstExpr,
// but if not, make one now.
for i, vx := range n.Values {
assertValidConstExpr(store, last, n, vx)
n.Values[i] = evalConst(store, last, vx)
}
} else {
Expand Down
145 changes: 145 additions & 0 deletions gnovm/pkg/gnolang/type_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,151 @@ func assertAssignableTo(n Node, xt, dt Type, autoNative bool) {
}
}

func assertValidConstExpr(store Store, last BlockNode, n *ValueDecl, expr Expr) {
if n.Type != nil {
nt := evalStaticType(store, last, n.Type)
if xnt, ok := nt.(*NativeType); ok {
nt = go2GnoBaseType(xnt.Type)
}

if _, ok := baseOf(nt).(PrimitiveType); !ok {
panic(fmt.Sprintf("invalid constant type %s", nt.String()))
}
}

nt := evalStaticTypeOf(store, last, expr)
if xnt, ok := nt.(*NativeType); ok {
nt = go2GnoBaseType(xnt.Type)
}

if nt == nil {
panic(fmt.Sprintf("%s (variable of type nil) is not constant", expr))
}

if _, ok := baseOf(nt).(PrimitiveType); !ok {
panic(fmt.Sprintf("%s (variable of type %s) is not constant", expr, nt))
}

assertValidConstValue(store, last, expr)
}

func assertValidConstValue(store Store, last BlockNode, currExpr Expr) {
Main:
switch currExpr := currExpr.(type) {
case *ConstExpr:
case *UnaryExpr:
// *, & is filter out previously since they are not primitive
assertValidConstValue(store, last, currExpr.X)
case *TypeAssertExpr:
ty := evalStaticTypeOf(store, last, currExpr)
if _, ok := ty.(*TypeType); ok {
ty = evalStaticType(store, last, currExpr)
}
panic(fmt.Sprintf("%s (comma, ok expression of type %s) is not constant", currExpr.String(), currExpr.Type))
case *CallExpr:
ift := evalStaticTypeOf(store, last, currExpr.Func)
switch baseOf(ift).(type) {
case *FuncType:
tup := evalStaticTypeOfRaw(store, last, currExpr).(*tupleType)

// check for built-in functions
if cx, ok := currExpr.Func.(*ConstExpr); ok {
if fv, ok := cx.V.(*FuncValue); ok {
if fv.PkgPath == uversePkgPath {
// TODO: should support min, max, real, imag
switch {
case fv.Name == "len":
at := evalStaticTypeOf(store, last, currExpr.Args[0])
if _, ok := baseOf(at).(*ArrayType); ok {
// ok
break Main
}
assertValidConstValue(store, last, currExpr.Args[0])
break Main
case fv.Name == "cap":
at := evalStaticTypeOf(store, last, currExpr.Args[0])
if _, ok := baseOf(at).(*ArrayType); ok {
// ok
break Main
}
assertValidConstValue(store, last, currExpr.Args[0])
break Main
}
}
}
}

switch {
case len(tup.Elts) == 0:
panic(fmt.Sprintf("%s (no value) used as value", currExpr.String()))
case len(tup.Elts) == 1:
panic(fmt.Sprintf("%s (value of type %s) is not constant", currExpr.String(), tup.Elts[0]))
default:
panic(fmt.Sprintf("multiple-value %s (value of type %s) in single-value context", currExpr.String(), tup.Elts))
}
case *TypeType:
for _, arg := range currExpr.Args {
assertValidConstValue(store, last, arg)
}
case *NativeType:
// Todo: should add a test after the fix of https://github.com/gnolang/gno/issues/3006
ty := evalStaticType(store, last, currExpr.Func)
panic(fmt.Sprintf("%s (variable of type %s) is not constant", currExpr.String(), ty))
default:
panic(fmt.Sprintf(
"unexpected func type %v (%v)",
ift, reflect.TypeOf(ift)))
}
case *BinaryExpr:
assertValidConstValue(store, last, currExpr.Left)
assertValidConstValue(store, last, currExpr.Right)
case *SelectorExpr:
xt := evalStaticTypeOf(store, last, currExpr.X)
switch xt := xt.(type) {
case *PackageType:
var pv *PackageValue
if cx, ok := currExpr.X.(*ConstExpr); ok {
// NOTE: *Machine.TestMemPackage() needs this
// to pass in an imported package as *ConstEzpr.
pv = cx.V.(*PackageValue)
} else {
// otherwise, packages can only be referred to by
// *NameExprs, and cannot be copied.
pvc := evalConst(store, last, currExpr.X)
pv_, ok := pvc.V.(*PackageValue)
if !ok {
panic(fmt.Sprintf(
"missing package in selector expr %s",
currExpr.String()))
}
pv = pv_
}
if pv.GetBlock(store).Source.GetIsConst(store, currExpr.Sel) {
break Main
}

tt := pv.GetBlock(store).Source.GetStaticTypeOf(store, currExpr.Sel)
panic(fmt.Sprintf("%s (variable of type %s) is not constant", currExpr.String(), tt))
case *PointerType, *DeclaredType, *StructType, *InterfaceType, *TypeType, *NativeType:
ty := evalStaticTypeOf(store, last, currExpr)
if _, ok := ty.(*TypeType); ok {
ty = evalStaticType(store, last, currExpr)
}
panic(fmt.Sprintf("%s (variable of type %s) is not constant", currExpr.String(), ty))
default:
panic(fmt.Sprintf(
"unexpected selector expression type %v",
reflect.TypeOf(xt)))
}
default:
ift := evalStaticTypeOf(store, last, currExpr)
if _, ok := ift.(*TypeType); ok {
ift = evalStaticType(store, last, currExpr)
}
panic(fmt.Sprintf("%s (variable of type %s) is not constant", currExpr.String(), ift))
}
}

// checkValDefineMismatch checks for mismatch between the number of variables and values in a ValueDecl or AssignStmt.
func checkValDefineMismatch(n Node) {
var (
Expand Down
11 changes: 11 additions & 0 deletions gnovm/tests/files/const23.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package main

import "fmt"

func main() {
const t []string = []string{}
fmt.Println(t)
}

// Error:
// main/files/const23.gno:6:8: invalid constant type []string
82 changes: 82 additions & 0 deletions gnovm/tests/files/const24.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"fmt"
"time"
)

func main() {
const a int = 1_000_000
const b byte = byte(1)
const c float64 = 1_000_000.000
const d string = "Hello, World!"
const e rune = 'a'
const g bool = true
const h uint = 1_000
const i int8 = 1
const j int16 = 1
const k int32 = 1
const l int64 = 1
const m uint8 = 1
const n uint16 = 1
const o uint32 = 1
const p uint64 = 1
const r float32 = 1_000_000.000
const s = r
const t = len("s")
const u = 1 + len("s") + 3
ars := [10]string{}
const v = len(ars)
const w = cap(ars)
const x = time.Second
const y = +len("ay")

fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
fmt.Println(e)
fmt.Println(g)
fmt.Println(h)
fmt.Println(i)
fmt.Println(j)
fmt.Println(k)
fmt.Println(l)
fmt.Println(m)
fmt.Println(n)
fmt.Println(o)
fmt.Println(p)
fmt.Println(r)
fmt.Println(s)
fmt.Println(t)
fmt.Println(u)
fmt.Println(v)
fmt.Println(w)
println(x)
fmt.Println(y)
}

// Output:
// 1000000
// 1
// 1e+06
// Hello, World!
// 97
// true
// 1000
// 1
// 1
// 1
// 1
// 1
// 1
// 1
// 1
// 1e+06
// 1e+06
// 1
// 5
// 10
// 10
// 1s
// 2
11 changes: 11 additions & 0 deletions gnovm/tests/files/const25.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package main

import "fmt"

func main() {
const t = []string{"1"}
fmt.Println(t)
}

// Error:
// main/files/const25.gno:6:8: [](const-type string){(const ("1" string))} (variable of type []string) is not constant
15 changes: 15 additions & 0 deletions gnovm/tests/files/const26.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import "fmt"

func v() string {
return ""
}

func main() {
const t = v()
fmt.Println(t)
}

// Error:
// main/files/const26.gno:10:8: v<VPBlock(3,0)>() (value of type string) is not constant
16 changes: 16 additions & 0 deletions gnovm/tests/files/const27.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import "fmt"

func v() string {
return ""
}

func main() {
var i interface{} = 1
const t, ok = i.(int)
fmt.Println(t, ok)
}

// Error:
// main/files/const27.gno:11:8: i<VPBlock(1,0)>.((const-type int)) (comma, ok expression of type (const-type int)) is not constant
12 changes: 12 additions & 0 deletions gnovm/tests/files/const28.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

import "fmt"

func main() {
var s []string = []string{"1"}
const t, ok = s[0]
fmt.Println(t, ok)
}

// Error:
// main/files/const28.gno:7:8: s<VPBlock(1,0)>[(const (0 int))] (variable of type string) is not constant
12 changes: 12 additions & 0 deletions gnovm/tests/files/const29.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

import "fmt"

func main() {
s := "1"
const t = s
fmt.Println(t)
}

// Error:
// main/files/const29.gno:7:8: s<VPBlock(1,0)> (variable of type string) is not constant
15 changes: 15 additions & 0 deletions gnovm/tests/files/const30.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import "fmt"

func v() {
return
}

func main() {
const t = v()
fmt.Println(t)
}

// Error:
// main/files/const30.gno:10:8: v<VPBlock(3,0)> (no value) used as value
15 changes: 15 additions & 0 deletions gnovm/tests/files/const31.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import "fmt"

func v() (string, string) {
return "", ""
}

func main() {
const t, v = v()
fmt.Println(t)
}

// Error:
// main/files/const31.gno:10:8: (const (v func()( string, string)))() (variable of type (string,string)) is not constant
11 changes: 11 additions & 0 deletions gnovm/tests/files/const32.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package main

import "fmt"

func main() {
const t = 1 + 2 + len([]string{})
fmt.Println(t)
}

// Error:
// main/files/const32.gno:6:8: [](const-type string){} (variable of type []string) is not constant
12 changes: 12 additions & 0 deletions gnovm/tests/files/const33.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

var x = 1
var y = 1

const b = x == y

func main() {
println("ok")
}
// Error:
// main/files/const33.gno:6:7: x<VPBlock(2,0)> (variable of type int) is not constant
Loading

0 comments on commit 3adb2ac

Please sign in to comment.