golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.1k stars 17.68k forks source link

proposal: spec: allow constants of arbitrary data structure type #6386

Open gopherbot opened 11 years ago

gopherbot commented 11 years ago

by RickySeltzer:

var each1 = []byte{'e', 'a', 'c', 'h'}
    const each2 = []byte{'e', 'a', 'c', 'h'}

The 'var' is accepted, the 'const' is not. This is a defect in the language spec and
design.

1. What is a short input program that triggers the error?
http://play.golang.org/p/Jbo9waCn_h

2. What is the full compiler output?
prog.go:7: const initializer []byte literal is not a constant
 [process exited with non-zero status]
robpike commented 11 years ago

Comment 1:

Issue #6388 has been merged into this issue.

robpike commented 11 years ago

Comment 2:

Labels changed: added priority-someday, languagechange, removed priority-triage, go1.2maybe.

Status changed to Accepted.

cznic commented 11 years ago

Comment 3:

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.
gopherbot commented 11 years ago

Comment 4 by RickySeltzer:

If slice constants have this hidden cost problem, could we at least have constant arrays
and arrays of arrays?
 const sentence = [...][...]byte{"a", "series", "of", "pieces", "of", "text"}
1. This should be sufficiently const that it could, in the right environment, go into
rom or the code segment.  That is, be immutable, ideally.
2. I should be able to type it without getting RSI from repeating '[]byte' for every
word.
3. For byte structures like this, it isn't absolutely necessary that we would be allowed
to enter arbitrary runes, although that would be technically feasible, and useful. 
Especially for those who aren't English-language programmers.  And it would be
consistent with the rest of Go.
gopherbot commented 11 years ago

Comment 5 by RickySeltzer:

Change "arbitrary runes" ==> "arbitrary Unicode characters with large code points".
griesemer commented 11 years ago

Comment 6:

Some comments:
1) This is neither a defect of the language nor the design. The language was
_deliberately_ designed to only permit constant of basic types.
2) 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.

Status changed to LongTerm.

gopherbot commented 11 years ago

Comment 7 by RickySeltzer:

In many projects I have programmed, there is a need for non-writable initialized data
larger than a single variable.  This is analogous to an 'asm' directive that initializes
bits in the code segment.  Something that the compiler has to do anyway.
Also, embedded systems often need to put data into read-only-memory (ROM).  
This has more to do with storage class than the type system.  Declarations rather like
the following might be simplest:
const (
   data =  []byte("The quick brown fox jumped over the lazy dog.")
   π = float64(3.14159)
   bits = []uint32{0x12345678, 0xBabeAbed}
)
I don't see any additional complexity here.  But it might be easier to introduce a new
modifier, "readonly" to supplement "const", if for some reason the above chokes the
grammar. 
Putting data in the code segment is just my way of saying that it's simple.  Actually,
it would be a security risk.  Anything marked "const" or "readonly" should be NX (Not
eXecutable).
gopherbot commented 11 years ago

Comment 8 by RickySeltzer:

Also, the language and runtime currently make strings immutable.  So the "Hidden cost"
problem in #4, above, already exists.  Just extend this to const.
cznic commented 11 years ago

Comment 9:

@8: No, you cannot do &str[expr], nor you can do str[expr] = expr, but you can of all of
that with a []byte, for example.
rsc commented 10 years ago

Comment 10:

Labels changed: added go1.3maybe.

gopherbot commented 10 years ago

Comment 11 by RickySeltzer:

Ah.  It was a typo on my part to use slice notation []byte, instead of array notation,
[...]byte.  I don't propose constant slices.  I propose constant (initialized) arrays. 
See here: http://play.golang.org/p/eCn6ip--w0.
rsc commented 10 years ago

Comment 12:

Labels changed: added release-none, removed go1.3maybe.

rsc commented 10 years ago

Comment 13:

Labels changed: added repo-main.

OneOfOne commented 8 years ago

any news about this?

It'd be nice to have something like const keys = [...]string{"a", "b", ... }

griesemer commented 8 years ago

This will not change in the foreseeable future.

The bar for language changes is extremely high at this point. "It'd be nice" is certainly not sufficient even as a starting point. To have a chance of even just being considered, there would need to be a full proposal together with a detailed analysis of cost and benefit.

In this specific case, extending the concept of constants to other than just basic types would be a significant change with all kinds of repercussions. I like to add also to the comment in the initial issue report that the current situation is not a "defect" in the spec - it was conceived as is pretty much from day one, for very good reasons.

Leaving open for a future Go 2 if there will ever be one.

ianlancetaylor commented 6 years ago

This is about whether Go should have immutable values. There are a number of things to consider.

One thing to consider is how to handle

const s = [...]int{ f() }

That is, do the elements of an const array have to be const themselves? Is it possible to set up such an array in an init function?

People have already raised questions about const slices. Another question is whether const values are addressable. If I can take a pointer to a const value, such as a field in a const struct, presumably I can't modify the value through that pointer, but what stops me from doing that? This proposal doesn't have any language mechanism for distinguishing a normal pointer from a pointer to a const (which I think is a good thing). So something has to catch erroneous writes and, presumably, panic. If we can put the initializer in read-only memory then I guess that will happen automatically, but then it's hard to initialize the elements in a function.

One idea I've mentioned elsewhere is a freeze function, that would do a shallow copy of a value into memory that is then made read-only (somehow). I don't know if that can be implemented efficiently but it is an approach to this general problem that does not involve a language change.

jaekwon commented 6 years ago

Update: If you follow the link I pasted below, you'll see how I realized that module-level state is bad(tm). Please check @wora's proposal thread.

This discussion is pretty open ended, because we're talking about at least two concerns... one about deep immutability, and one about shallow immutability.

I propose that we introduce shallow immutability only. See the proposal for the const-mutable type.

In the above proposal, const is just another kind of var except you can't change what it points to, e.g. it's not addressable. It's got nothing to do with deep immutability, and I imagine it would be fairly easy to implement even for Go1. Combined with the proposal for read-only slices, you get a sufficiently complete set of orthogonal language rules that allow you to program safe and performant code.

bcmills commented 6 years ago

That is, do the elements of an const array have to be const themselves? Is it possible to set up such an array in an init function?

In the compile-time functions proposal, I proposed that package-level functions could themselves be marked const (i.e., made available at compile time), where the arguments to those functions could be:

The focus of that proposal was on computing types, but you could fairly easily strip out the first-class types and I think end up with a reasonable proposal for generalizing const to other value types (but not slices or pointers).

It would, however, be a lot of work to implement.

jimmyfrasche commented 6 years ago

I wanted to think through what this would mean:

Note: This is not an argument against constant slices, maps, etc.; just an exploration of what allowing only the above to be constants would mean.

To answer @ianlancetaylor's questions, these, like other constants, are not addressable (nor are their indices/fields), cannot be created in init() functions, and const s [...]int{ f() } would be illegal unless f() is a constant expression. Each element or field is itself constant (so ca[0] = v and cs.f = v are both illegal).

This would make

const (
  x int = iota
  y
  z
)

and

const vec = [...]int{0, 1, 2}

and

const vec = struct {
  x, y, z int
}{1, 2, 3}

all equivalent ways of organizing and labeling the same constants.

Constant arrays

Constant arrays being inaddressable has a major downside: they cannot be sliced (unless constant slices are also allowed). The primary use cases for constant arrays are, as far as I am aware, tables of precomputed values and file/network signatures. None of these especially require slicing. It would still be very inconvenient, though.

It would be possible to define slicing a constant array to always duplicate the backing array but this would make an expensive operation look like an expensive one. No go.

Another option would be to extend the copy builtin to take arrays, so that copy(s, a) behaved like copy(s, a[:]) does now. This would make the expense of the operation clear.

That's still a bit awkward. It would be nice, but not essential, to also define a new built in, dup, which takes a slice or an array and returns a slice with a copy of the (backing) array. (µ-experience report: I've written similar for byte slices at least a dozen times). This would make it easier to convert a constant array into a slice as part of an expression and allow the compiler to optimize the copy out when possible, like for _, v := range dup(ca)[1:] {. Limited to slices, it could be written as a generic function but would need to be a builtin to operate on arrays directly so it only makes sense to consider it if there are constant arrays.

iota in a constant array should behave the same way as iota in any other const block so

const (
  a, x = [2]int{iota, iota + 1}, iota
  b, y
  c, z
)

would result in a = [2]int{0, 1}, x = 0, b = [2]int{1, 2}, y = 1, c = [2]int{2, 3}, and z = 2.

It is unclear to me whether the below, in whole or in part, should be allowed:

const (
  a = [1+iota]int{iota: iota}
  b
  c
)

However, if it is, it should result in a = [1]int{0}, b = [2]int{0, 1}, and c = [3]int{0, 0, 2}. While logical, the utility is unclear.

Constant structs

Since constant structs are free of pointers, they can always be passed by value without aliasing. Passing a constant struct to a function duplicates the original and the duplicate is then mutable (thought the compiler would be free to elide the duplication when it can prove an absence of mutation).

Given

type T struct {
  a, b, c int
}
var X = T{1, 2, 3}
const Y = T{1, 2, 3}

the only differences between X and Y is that the initial definition of Y cannot be changed and pointer methods cannot be called directly on Y.

A rough equivalent that can be done today would be to define Y as

func Y() T {
  return T{1, 2, 3}
}

That offers the same safety guarantees and, I'm sure, some of the optimization opportunities, but it's more verbose and makes it awkward to define large sets of pseudo-constants without code generation.

Allowing constant structs would be useful for sentinel/named values that should never be changed or grouping related constants into a single complex.

(Everything said about constant structs would apply to constant discriminated unions, should those be added).

Discussion

I can certainly see better why constants were limited to basic types. The gains of extending constants to the product types—without adding immutability to the language—are not mind blowing. If these were allowed, I'd certainly use them with carefree abandon, but they don't seem to quite clear the bar by themselves.

Combined with something like https://github.com/golang/go/issues/23637#issuecomment-376002346 I could see it being worth the effort, since it provides a more concrete need and additional value than they have on their own.

Being able to use iota when defining a lot of named struct values would be useful, though @griesemer proposed allowing iota in var blocks in #21473. That would not disallow allowing another package to change the value, but, as disquieting as that prospect is, it doesn't seem to have caused any serious issues in practice.

networkimprov commented 6 years ago

See also #21130

zigo101 commented 5 years ago

An alternative proposal for the same goal.

zigo101 commented 4 years ago

(This post is moved here per @ianlancetaylor's suggestion.)

I like this proposal and I made a proposal which specifies more detailed rules for all kinds of operations and provides a possible implementation.

The following proposal should keep compatibility if it re-uses the const keyword instead of using a new keyword final.

(The following is moved from https://github.com/golang/go/issues/36528)

=====================

The main intention of this proposal is to let Go support package-level immutable values of any composite types, though local immutable values are also supported.

A new keyword final is introduced to declare final values. (Maybe we can re-use the const keyword. Both choices have their respective benefits and drawbacks.)

Basic values can't be declared as final values (they can be declared as constants already), though basic values might be presented as immutable elements of some composite values. (So maybe it is not bad to re-use the const keyword to declare final composite values.)

Final values can't be modified and should not be taken addresses. Taking addresses of final values doesn't compile or panics at run time.

The value referenced by a pointer, whether the pointer is final or not, is always modifiable.

If a final value is bound with a literal in its declaration, all the values presenting as sub-literals (or final identifiers) in the bound literal are also final values, except the composite literals being taken addresses, either explicitly or implicitly. (See below for examples.)

For convenience, final values can be assigned to variables, so that final values can be passed as function arguments.

Some modifications of final values can be detected at compile time, some others can only be detected at run time. Compile-time modifications of final values don't compile. Run-time modifications of final values cause panics (unrecoverable panics are recommended).

The modifications to final array elements and final struct fields can be detected and rejected at compile time.

The modifications to directly declared finals (the values bound to identifiers) can be detected and rejected at compile time.

The elements of final slices and maps might be final or not, depends on whether or not the elements are presented in the literals bound to declared final values. The elements of a slice or map, whether or not the slice or map is final, must be either all immutable (final) or all mutable (variable).

The elements of a slice derived from final arrays or a final slice whose elements are finals are also finals.

The memory blocks allocated for final elements of final slices and maps (and final arrays) will be tagged as immutable. An additional run-time condition check is needed when modifying slice/map elements or taking addresses of slice elements. The additional check will not cause an obvious efficiency loss for map element modifications (and address taking), but might cause a small but non-ignorable efficiency loss (about 25% more overhead) for slice elements modifications (and address taking). This is a drawback of this proposal,

Example:

var s = []int{1, 2, 3}

func _() {
    //===== slice
    final x = [][]int{
        s, 
        []int{7, 8, 9},
    }

    x[0] = nil   // panic
    x[0][0] = 5  // ok, elements of x[0] are mutable
    x[1][0] = 6  // panic, elements of x[1] are immutable
    _ = &x[1][0] // panic
    x = nil      // not compile

    x2 := x       // ok, x2 is a variable.
    x2[0][0] = 5  // ok
    x2[1][0] = 6  // panic
    _ = &x2[1][0] // panic
    x2 = nil      // ok

    // append calls should not modify immutable elements.
    _ = append(x[1][:1], 9) // panic
    _ = append(x[1], 9)     // ok, for a new memory block is allocated.

    //===== map
    type T struct{n int}
    final y = map[T][]int{
        T{1}: s,
        T{5}: x[1],
        T{3}: []int{7, 8, 9},
    }

    y[T{1}] = nil   // panic
    y[T{1}][1] = 5  // ok
    y[T{5}] = nil   // panic
    _ = &y[T{5}][0] // panic
    y[T{5}][0] = 5  // panic
    y[T{3}] = nil   // panic
    y[T{3}][2] = 5  // panic
    y = nil         // not compile

    y2 := y          // ok, y2 is a variable
    y2[T{1}] = nil   // panic
    y2[T{3}] = nil   // panic
    _ = &y2[T{5}][0] // panic
    y2[T{5}] = nil   // panic
    y2 = nil         // ok

    // Entries of final maps may not change.
    y[T{9}] = []int{} // panic
    delete(y, T{9})   // panic

    //===== function
    final f = func() {
        // f can call itself if it is a final (might be not a good idea).
    }
    f = nil  // not compile
    f2 := f  // ok, f2 is a variable
    f2 = nil // ok

    //===== channel
    final c = make(chan int, 10)
    c = nil // not compile

    // Sends to and receives from final channels are ok.
    c <- 1 // ok
    <-c    // ok

    //===== interface
    final ia interface{} = s
    final ib interface{} = []int{7, 8, 9}

    ia = nil // not compile
    ib = nil // not compile
    ia.([]int)[1] = 5 // ok
    ib.([]int)[1] = 5 // panic

    //===== pointer
    final p1 = new(int)
    final p2 = &struct{}

    p1 == nil // not compile
    p2 == nil // not compile

    // The values referenced by final pointers are always modifiable.
    *p = 5        // ok
    *p = struct{} // ok
}
func _ () {
    type T struct {
        x int
    }

    final ts = []*T {
        {1}, // <=> &T{1}
        {2}, // <=> &T{2}
    }

    // Both are ok.
    ts[0].x = 9
    ts[1].x = 9

    // Both are ok.
    *ts[0] = T{}
    *ts[1] = T{}

    // Both fail to compile.
    ts[0] = &T{}
    ts[1] = &T{}
}
func _ () {
    final ss = []*[]int {
        &[]int{1, 2, 3},
        {7, 8, 9}, // <=> &[]int{7, 8, 9}
    }

    // Both are ok.
    (*ss[0])[1] = 5
    (*ss[1])[1] = 5

    // Both are ok.
    *ss[0] = []int{}
    *ss[1] = []int{}

    // Both fail to compile.
    ss[0] = &[]int{}
    ss[1] = &[]int{}
}

[Update]: some possible solutions to detect final elements of slices

Possible solution 1

It looks the current slice element modification operations have already checked whether or not the target element is allocated on an immutable zone. See the last example in the wiki page for evidence. But I'm not sure whether or not this is true. If this is true, then final elements of slices can also be allocated in immutable zones so that the above mentioned "25% more overhead" will not exist.

[update]: Ralph Corderoy explained that constant strings are allocated on read-only memory pages. So I think final strings can also be allocated on read-only memory pages. This is so great that the above mentioned "25% more overhead" can be avoided! ([update:] there is still a problem. Values allocated on read-only memory pages can be taken addresses, which is disallowed by this proposal. So maybe it is not a bad idea to merge this solution with the following solution 2 into one: taking addresses will check cap but modifying elements will not. After all, taking element addresses is a rare operation in practice.)

Possible solution 2

Maybe it is not needed to tag map, slice and array element memory blocks as immutable. The internal structure of a map records whether or not the map is a final. For slices, maybe the final info can be recorded in the cap field, a negative cap means the elements of the corresponding slice are finals. So the slice element modification operation is like:

// Assume to modify slice x at index i with value v.
if i < 0 || i >= x.len {
   panic("index out of range")
}
// This condition judgement is the new overhead.
// This overhead could be saved by cooperating with solution 1.
if x.cap < 0 {
   panic("final value can not be modified")
}
*(x.ptr+ i * elemSize) = v 
// or 
// return x.ptr+ i * elemSize
// for address taking.

Possible solution 3

The slice internal structure can be modified with one more indirection:

type slice struct {
    len, cap int
    elements *sturct {
        ptr       unsafe.Pointer
        immutable bool
    }
}
ianlancetaylor commented 4 years ago

The language already has constants, of course. So arguably this is just extending the existing concept of a constant to additional types. The suggestion, then, is to permit a const value of any type for which we can use a composite literal: struct, slice, map, array types. But we can't permit these values to be addressable, which implies 1) we can not permit const values that are or contain pointer types; 2) we can not permit slicing a const slice or array. The composite literal would itself have to contain only constant values (which could in turn be constant struct, etc. types).

This seems implementable. Since the values are not addressable, the compiler could easily detect and prevent any attempt to change a const value.

The question is: is this worth it? Does it provide enough additional functionality to the language that the change is worth making?

changkun commented 4 years ago

Just one comment on "is this worth it?":

It is unbelievable that we cannot write constant errors:

- const ErrVerification = errors.New("crypto/rsa: verification error")
+ var ErrVerification = errors.New("crypto/rsa: verification error")

Seriously? If the following code appears in one of the project dependencies, then it destroys the whole systems:

import "cropto/rsa"
func init() {
    rsa.ErrVerification = nil
}
networkimprov commented 4 years ago

The only reason listed above to bar addressability is that it precludes initialization by a function; see https://github.com/golang/go/issues/6386#issuecomment-349786380. Why is that a problematic limitation?

CAFxX commented 4 years ago

The only reason listed above to bar addressability is that it precludes initialization by a function; see #6386 (comment). Why is that a problematic limitation?

I may be missing something here, but even initialization by a function does not sound impossible. Excluding partial evaluation a-la graalvm, it should be possible to do initialization at runtime, taking care to store const values in pages that will be marked read-only as soon as initialization is over. It wouldn't be trivial to implement, but it shouldn't necessarily prevent addressability either.

griesemer commented 4 years ago

@CAFxX Marking memory holding constants as read-only after initialization would certainly provide the most flexibility. But w/o evaluation at compile-time, the compiler won't know the values of those constants, and transitively, the values of any constant expressions depending on those constants. A variety of compile-time checks would need to become runtime checks. Not impossible (and perhaps still backward-compatible), but something to consider.

Hence the question whether the much simpler approach would provide enough benefit.

networkimprov commented 4 years ago

As structs are typically passed by pointer, the inability to use a constant struct where a pointer is required is a rather severe limitation.

To support compile-time evaluation, could the compiler generate offsets into a table for constant addresses that gets populated at runtime?

ianlancetaylor commented 4 years ago

@changkun It's a good point that we should consider permitting const for values of interface type. But it's still hard to permit initializing them with a function while retaining safety.

@networkimprov The reason to ban addressability is that if a program can take the address of a value, then we have no completely reliable way of preventing that program from using that pointer to change the value. A const value that can be changed during program execution cannot really be described as a constant. Yes, this is a severe restriction.

The idea of making the memory read-only after it has been initialized is certainly tempting, but it's hard to see how to make that completely reliable in all cases.

Generating offsets into a table would imply that different values of the same type have different kinds of values. That seems difficult to implement.

There are all serious concerns and may well mean that the simple approach suggested in https://github.com/golang/go/issues/6386#issuecomment-613699012 is not really useful.

networkimprov commented 4 years ago

offsets into a table would imply that different values of the same type have different kinds of values

Sorry, I didn't follow; could you elaborate? (I meant that the compiler would replace pointers to constants with a table lookup yielding a pointer.)

making the memory read-only after it has been initialized is certainly tempting, but it's hard to see how to make that completely reliable

In what cases would that not work?

ianlancetaylor commented 4 years ago

@networkimprov Perhaps I don't understand your suggestion. The issue is how to handle a composite literal with an embedded pointer, where that pointer might point to some package-scope variable that can in turn by changed while the program is running. The problem is that the struct field (say) has a pointer type. I think you are suggesting that the pointer type could be represented not as a pointer, but as an index into a table. But then we have a pointer type that could be either a normal pointer value or an index into a table. How does arbitrary code, which may be in a different package, know how to interpret that value? As I say, perhaps I misunderstand your suggestion, so can you clarify? Thanks.

Making memory read-only after it has been initialized would not work if we can't reliably identify and collect all the memory in question. We can only mark entire pages as read-only, so we have to be sure that all read-only memory is in one set of pages while all modifiable memory is in a different set of pages. Suppose now that we initialize a constant value with a slice of some non-constant variable, where the values being slices themselves have pointers. What should we do and how can we make it completely reliable?

networkimprov commented 4 years ago

Suppose now we initialize a constant value with a slice of some non-constant variable ... ... a composite literal with an embedded pointer, where that pointer might point to some package-scope variable

Wouldn't those fail as initialization by non-constant expression? Shouldn't pointers that are constant only reference constants?

you are suggesting that the pointer type could be represented not as a pointer, but as an index into a table. But then we have a pointer type that could be either a normal pointer value or an index into a table.

Apologies if this is naive, but I'm suggesting the compiler produce a complete table lookup at any site taking the address of a constant. It can't just substitute an offset for an address.

const kVa = 1 // address placed in const_addrs at startup
const kVb = 2 // ditto
const kVc = 3    // not in const_addrs
const kPa = &kVa // not in const_addrs
...
f(&kVa, &kVb) // compiles to f((*int)(const_addrs + 0), (*int)(const_addrs + 8))

func f(a, b *int) { *a = 9 } // runtime error for above call

*kPa = 9 // compiler error
ianlancetaylor commented 4 years ago

OK, even if we only permit pointers to constant values, still other code can pull out those pointers and modify the values to which they point, so that can only be permitted if we can be absolutely certain that we can always put those values in read-only memory.

I think I now understand what you are getting at with your constant offset, but I don't understand how that solves the problem. That is an implementation that we could use if we assume that we can always reliably identify which memory has to be read-only, and if we assume that we have a way to initialize that memory and then make it read-only. I find it hard to convince myself that both of those are true on all the platforms for which Go works.

networkimprov commented 4 years ago

The offsets into a table of addresses are meant to support compile-time evaluation, where each address of a constant needs a compile-time value, raised above by @griesemer.

On which platforms do you think read-only memory could be difficult to achieve, and why? Maybe we could pull in domain experts to comment on them...

In what cases would it be difficult to determine that an object should be read-only?

griesemer commented 4 years ago

@networkimprov Let's look at a concrete example:

type List struct {
    next *List
    data int
}

const c = List{next: &List{next: &List{}}}
var v = List{next: &List{next: &List{}}}

Are you suggesting that the next field behaves differently when we have a "constant list" vs in the normal case (offset vs regular pointer)?

ianlancetaylor commented 4 years ago

Consider wasm, for example. Also, the bare metal tamago port that is under discussion.

CAFxX commented 4 years ago

I completely agree that there may be simpler options with a narrower scope. But just to ensure I am getting the right picture:

But w/o evaluation at compile-time, the compiler won't know the values of those constants, and transitively, the values of any constant expressions depending on those constants.

The compiler wouldn't know the values of those constants(-at-runtime), but would know their addresses, so other constants expressions that depend on those values would become constant(-at-runtime) as well, i.e. transitevely be initialized at startup, and then marked read-only.

The compiler would need to track that these are constants(-at-runtime) and report error if code attempts to modify their value.

In a sense, they would behave like const for writes, and like var for reads.

As compilers improve and are progressively able to partially evaluate more at compile-time, more of these constant-at-runtime instances would turn into regular constants and not require initialization at runtime.

A variety of compile-time checks would need to become runtime checks.

Even in the model described just now? Wouldn't marking read-only the memory used to store the constants at runtime sidestep additional runtime checks?

griesemer commented 4 years ago

@CAFxX No, it wouldn't. For instance, given an array a and a constant expression x, right now a[x] will not require a runtime index bounds check. But if we don't know the exact value of x, it will need an index bound check at runtime. The compiler may become smarter over time, but we could still not guarantee the constant evaluation in general.

I am not saying this is a show-stopper, but it is something that would have to be taken into account and which would change existing behavior. What now leads to a compile-time error may lead to a runtime panic if the compiler cannot evaluate all constant values at compile time.

networkimprov commented 4 years ago

Are you suggesting that the next field behaves differently when we have a "constant list" ...?

EDIT: there would be no difference to the user between the two List chains you defined, except that for the const one: a) c.next.data = v gives a compiler error, b) var p = &c ... p.next.data = v gives a runtime error.

If you printed a .next field, you'd see an address from the const_addrs table I described in https://github.com/golang/go/issues/6386#issuecomment-614311625.

Note that I am not advocating "constant-at-runtime" semantics.

Consider wasm, for example. Also, the bare metal tamago port that is under discussion.

@ianlancetaylor wasm lacks rather a lot of features; should a nascent world limit Go progress on mature platforms? And it appears this issue has been raised: https://github.com/WebAssembly/design/issues/1278.

Re tamago, I'd imagine they'll happily evolve as Go does; cc @abarisani.

griesemer commented 4 years ago

@networkimprov What happens if one assigns that const list c to variable v in my example?

networkimprov commented 4 years ago

That copies the outermost List into a variable, with same effect as:

const kL = List{next: &List{}}
var   vL = List{next: &kL}
...
vL.data = v      // ok
kL.data = v      // compiler error
kL.next.data = v // compiler error
vL.next.data = v // runtime error
abarisani commented 4 years ago

Re tamago, I'd imagine they'll happily evolve as Go does; cc @abarisani.

I can confirm.

marcel commented 4 years ago

It is unbelievable that we cannot write constant errors:

- const ErrVerification = errors.New("crypto/rsa: verification error")
+ var ErrVerification = errors.New("crypto/rsa: verification error")

You can though @changkun...

type Error string

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

const ErrVerification = Error("crypto/rsa: verification error")
changkun commented 4 years ago

You can though @changkun...

type Error string

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

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

No. It does not solve the problem generally, because this approach cannot be applied to const errChain = wrap(errArgs). The fundamental problem is that we are missing immutable data, whereas typecasting here is just a verbose workaround for unpacked error.

ValarDragon commented 3 years ago

Just wanted to bump / voice immense support for this. As @changkun notes, this is honestly critical for security of the code. Additionally, its huge for API safety. Theres a large class of situations where you want publicly exported structs that other modules should be able to Read, but not accidentally set.

(I mean these can still be gotten around via many unsafe hacks, but thats not the point. The points to get people to not accidentally mess up, due to inability to provide a Safe API)

zigo101 commented 2 years ago

Just a remainder reminder that, if arrays could be declared as constants, then there is a parsing ambiguity in the following code:

type C[T [int]*string] struct{}
zigo101 commented 2 years ago

It looks the custom generics feature has sentenced this proposal to death.

const S = [2]int{1, 2}
const T = 2

// The following declaration is always thought as a generic type declaration.
type BoolArray [S[1] * T]bool // T (untyped int constant 2) is not a type

// If S is a constant array, then the above line may also viewed as array type declaration.

[update]: To avoid absolutely sentencing the constant array proposal to death, elements of constant array must not be treated as constants, just as elements of constant strings are not treated as constants (they are just immutable but not constants).

leaxoy commented 2 years ago

Any progress, this has been proposed for nearly ten years.

myaaaaaaaaa commented 11 months ago

I think a more interesting approach would be to try and adapt the untyped nature of Go's consts to other Kinds:

package main

func main() {
    const aConst = {
        1: "a",
        3: "b",
        5: "c",
    }
    var aMap1 map[int]string = aConst // OK
    var aMap2 map[any]any    = aConst // OK
    var aSlice []string      = aConst // OK, results in a slice of length 6
    var aArray [4]string     = aConst // Index out of bounds error

    const bConst = {
        "name": "bob",
        "age":  30,
    }
    var bMap1 map[string]any = bConst // OK
    var bMap2 map[string]int = bConst // Incompatible assign error due to "name": "bob"
    var bStruct1 struct {             // OK
        name  string
        email string
        age   int
    } = bConst
    var bStruct2 struct { // Incompatible assign error due to lack of an "age" field
        name string
    } = bConst
}