Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: extending const behavior #919

Open
peter7891 opened this issue Jun 20, 2023 · 2 comments
Open

proposal: extending const behavior #919

peter7891 opened this issue Jun 20, 2023 · 2 comments
Assignees
Labels
gno-proposal Gnolang change proposals 🌱 feature New update to Gno

Comments

@peter7891
Copy link
Contributor

Description

Right now, we have constants implemented as described by the Go language spec.
I think, it would be a worthwhile discussion on whether we should extend that functionality.
For example, adding constant data structures, arrays, slices etc.
Additional questions on this might be if the const behavior should propagate to members and
if we should allow function results to be assignable to constants and how that would look like.

Why

I think, it would be of significant benefit of users, as they won't need to create special functions as in Go
to simulate this behavior. Also, it would catch bugs when there is mutation.

@peter7891 peter7891 self-assigned this Jun 20, 2023
@peter7891 peter7891 added the 🌱 feature New update to Gno label Jun 20, 2023
@thehowl
Copy link
Member

thehowl commented Jul 21, 2023

I'm sort of against on significantly changing the behaviour of the language from Go without good reason or to enable a specific functionality which makes sense in our context.

But in any case, let me try to tackle why I don't think this is a good idea.

Aside from numeric, boolean, and string data types, in Gno we currently have slices, structs, maps, pointers, arrays, interfaces, function values. Let's run through each one of them:

Slices

Slices have the semantic in Go where, in essence, the underlying array is always heap allocated and modifiable.
So adding slices as constant data structure would probably implicitly mean adding the "mutable" or "immutable" heap pointer semantics. In other words, we'd need to do this:

package main

const a = []byte{1, 2, 3, 4, 5, 6, 7, 8}

func main() {
	b := a[:4] // allowed operation
	toOnes(a) // preprocess-time panic: cannot modify constant underlying array of a
	toOnes(b) // preprocess-time panic: cannot modify constant underlying array of b
}

func toOnes(s []byte) {
	for i := range s {
		s[i] = 1
	}
}

So in other words, what is written out as a []byte may not always be a slice of bytes, but may hold a reference to an underlying immutable array. Of course, this can make sense in a language like Rust which provides first-class support for specifying whether a value is mutable or immutable. But doing this in Gno would probably mean expanding the language further than just adding support for "immutable" arrays; and use cases where the underlying array is mutated would probably have to be guarded by a copy() into a mutable slice.

Note, on top of everything, that aside perhaps some preprocess-time optimizations that could be added when referencing the slice, this does not necessarily add performance improvements; the array still needs to be "heap-allocated", and I can see little optimizations in the above example, or any examples off top of my head, where computations would be reduced.

Pointers

Please no. I think I elaborated enough in the slice example why I think having pointers where the underlying data is immutable is a bad feature.

Structs

I would not necessarily take too much issue with this, were it not for the fact that the structs we could specify as constants probably couldn't have in the type any of the data types we don't support for constant values.

Maps

Same problem as slices. Operations like assignment of a key would have to runtime or preprocess-time panic. Would imply adding concept of im/mutability of variables.

Interfaces

No point having them as constants if you ask me.

Arrays

Probably the only ones that make some degree of sense. The only additional things we would need to add, compared to the disallowed operations of a constant string, are slicing arr[:] (implicit reference) and assignment of index arr[0] = "hello". Both of these would be possible if arr was instead, in the local block, arr := constArr, as here we're copying by value on the stack.

I still see little advantage than what we could do with simple static analysis, optimizing an array-typed variable to be implicitly a constant.

Function values

A constant function value is just a function definition, and would only be useful for aliasing. I think this is already a clear and preprocessor-optimizable pattern for function aliases:

func X() string { return Y() }

Previous language design discussions, and conclusion

Looking up on google golang constant arrays site:github.com/golang in an attempt to look up previous implementation discussions on the go repo turned these results up:

golang/go#6386 provides the following useful commentary, which I tend to agree on:

I'm against this. If it would have to have constant semantics then its run time costs
are the same as today, only hidden.
        const c = []byte{1}
        a := c
        a[0] = 42
        b := c
        fmt.Println(b[0] == 1)
The above can print 'true' only if the c's backing array is copied in assignment to 'a',
however the const declaration gives an illusion of always using the same backing array -
like is the case of a string's backing array.
IOW: 1. nothing is gained by const []T and 2. run time costs get hidden and thus
potentially confusing.
The implications of such a change are much more far-fetching than meets the eye:
there are numerous open questions that would have to be answered _satisfactorily_; and I
don't think we are there yet.
For instance, if we allow such constants, where is the limit? Do we allow constant maps?
What about constant channels? Constant pointers? Is it just a special case for slices?
etc.
A first step might be to allow constant arrays and structs as long as they are only
composed of fields that can have constant types themselves.
An even smaller step (which I do think we should do) is to make "foo"[i] a constant if i
is a constant (right now it's a non-constant byte).
Finally, note that it's often not very hard for a compiler to detect that a
package-level variable is never modified. Thus, an implementation may choose to optimize
the variable initialization and possibly compute it compile time. At this point, the
const declaration only serves as documentation and for safety; there's no performance
loss anymore.
But again, we have tried to keep the type system (incl. what constants are) relatively
simple so that it doesn't get into the way. It's not clear the benefits are worth the
price in additional complexity.

Russ Cox: Evaluation of read-only slices, found through golang/go#20443.

Extra: on errors

As far as I know, in Go there is one big case of exported variables which really should be constants to avoid a ""malicious"" package being able to modify the values: errors. It is not uncommon in Go to have variables such as var ErrXxx = errors.New("hello!"). As a commenter on golang/go#6386 rightly points out, this does pose an issue whereby a func init() could change the value of this error, and potentially make it something whereby calling the Error() method causes side effects, or even making its value nil, causing infinite headaches.

I think in Go this is less of an issue than the commenter writes; not reading through a dependency's source code before using its code is a bad practice IMO, and if you allow code like that to enter your project that's on you.

Of course, this is different for Gno, where we have realms.

It might be the case that the better pattern for implementing errors in Gno might have to be this -- and as a consequence replace most global variable declarations also in the stdlibs to be constants:

type Error string

func (e Error) Error() string {
  return string(e)
}

const ErrVerification = Error("crypto/rsa: verification error")

Although I think this warrants a separate issue.

Extra: using a function's output as a constant

If the function is pure (ie. no dependency on global variables), this can be determined by the preprocessor, and the instances where its called "pre-compiled". Similarly, if it's used in an unexported global variable or a local variable which is never changed, its value can in turn be changed by the processor to be a constant value. So, pointless to me.

@moul
Copy link
Member

moul commented Aug 9, 2023

Related with gnolang/hackerspace#15

@moul moul added this to the 💡Someday/Maybe milestone Sep 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
gno-proposal Gnolang change proposals 🌱 feature New update to Gno
Projects
Status: 🔵 Not Needed for Launch
Development

No branches or pull requests

3 participants