~sircmpwn/hare-rfc

11 4

[RFC v1] Mutability overhaul

Details
Message ID
<CVIYDE2MYJYN.1VKHYOWIYQ2OK@notmylaptop>
DKIM signature
pass
Download raw message
The ideas surrounding this proposal have already been accepted:
https://lists.sr.ht/~sircmpwn/hare-dev/<constoverhaul20230121%40sebsite.pw>
I'm posting it here both for posterity's sake, and also because a lot of
the specific details still need to be worked out.

The motivation for all of this is that mutability should apply to
objects, not to types. So, a binding may refer to a mutable object, a
pointer may point to a mutable object, or the contents of a slice may be
mutable. But the general "const" type flag is removed, since mutability
is a property of the object rather than the type.

I'll first go over the proposal as it stands right now (and how it's
currently being prototyped), then go over the rationale of certain
decisions, then go over unsettled aspects of the design, and parts of
the proposal which I'm not a huge fan of. This is to say, please hold
off on judgement until you've read through everything, since lots of
context is provided in the latter two sections.

# Current proposal

Hopefully I don't forget anything here

Syntax changes:
- `def` is replaced with `const`.
- `const` (as in declarations) is replaced with `let`.
- `let` is replaced with `let mut`.
- Within the spec, "literal" is used instead of "constant" for things
  like integer literals, string literals, array literals, etc.
- iconst, fconst, and rconst are renamed ilit, flit, and rlit.
- `mut` can appear before `*` in a pointer type, before `[]` in a slice
  type, or before a type alias, to, respectively, make the object the
  pointer points to mutable, make the contents of the slice mutable, or
  do one of these things to the underlying type of the alias (which must
  be a pointer or slice type).

Semantic stuffs:
- str is always immutable.
- Mutability doesn't carry over between objects: `mut **foo` is a
  pointer to a mutable pointer to an immutable foo.
- You can only assign to mutable objects.
- Whether or not an object is mutable is irrelevant when passing by
  value or making a copy, since the type isn't affected.
- This also applies to slice assignment `x[..] = y`: as long as the
  contents of x are mutable, the mutability of y (and of y's contents)
  is irrelevant.
- Taking the address of an immutable object of type `foo` yields an
  object of type `*foo` (pointer to immutable foo).
- Taking the address of a mutable object of type `foo` yields an object
  of type `mut *foo` (pointer to mutable foo).
- (From here on, "immutable pointer" and "mutable pointer" are shorthand
  for pointer to immutable object and pointer to mutable object,
  respectively. Ditto for "immutable slice" and "mutable slice".)
- Mutable pointers can be assigned to immutable pointers, but not the
  other way around.
- This is true of any `mut` within the type: e.g. `mut *mut []T` is
  directly assignable to `*[]T`.
- Mutable pointers can be cast to immutable pointers, without any
  limitations.
- Immutable pointers can be cast to mutable pointers to the same type.
  This means that e.g. to cast an object x of type `*rune` to
  `mut *u32`, you'd need to do either `x: mut *rune: mut *u32` or
  `x: *u32: mut *u32`. This also means that `*[4]int` can't be cast to
  `mut []int` without an intermediary cast.
- Mutability does not convey any information about ownership. That is,
  you can free an immutable pointer or slice, and you can free strings.
- append, insert, and delete require that the object is itself mutable,
  *and* its contents are mutable. So, to declare a growable slice, you
  need to do `let mut x: mut []int = [];`. Both muts are required here.
  This is because append/insert/delete both mutate the contents of the
  slice *and* the data of the slice object itself: the length and
  capacity, and possibly the pointer to the data if it was reallocated.
- vaarg and vaend operate on mutable objects.
- Expressions which implicitly dereference their operand check the
  mutability of the object they're *actually* acting on: e.g. append can
  operate on a mutable pointer, or on an immutable pointer to a mutable
  pointer, so long as the final object is mutable.
- When selecting a valid tagged union subtype (tagged_select_subtype),
  additional logic needs to be used if the subtype is mutable: if the
  subtype is assignable to multiple of the tagged union's types, but
  only one of those types preserves mutability, then that type is
  chosen.
  e.g. `let mut a: [1]u8 = [0]; a: ([]u8 | mut []u8);`
  This compiles: even though a is assignable to both `[]u8` and
  `mut []u8`, only `mut []u8` will be selected here. This is necessary
  for things like memio::fixed: the function needs to take in
  `([]u8 | mut []u8)`, since it may or may not be writable. (The stream
  itself can store the slices in a (non-tagged) union; this is only for
  the initializer function.)
- `mut` can appear before a type alias, in which case the type alias
  must refer to a pointer, a slice, or a type alias referring to an
  allowed type. If the underlying type is a pointer, then the object
  being pointed to is made mutable; if the underlying type is a slice,
  then the contents of the slice are made mutable. The underlying type
  may already be mutable, in which case `mut` has no effect. When
  unwrapping a mutable alias, the `mut` carries over to whatever it
  should apply to. So, if the underlying type is a pointer, the
  unwrapped pointer is mutable. Ditto for slices. If the underlying type
  is another type alias, then the `mut` is carried over to that alias.
- `case let mut` can be used in match cases to make the binding mutable.
  Note that the binding is still a copy; the original data isn't
  mutated.
- Function parameter bindings are immutable. There isn't any special
  syntax to make them mutable, you can instead just shadow the binding:
  `let mut x = x;`

The central algorithm at play here (or, what does "mutable" even mean?):
- Put simply, a "mutable" object is: an object bound to a mutable
  variable (let mut), an object derived from a mutable pointer or a
  mutable slice, or an anonymous stack-allocated object (that is,
  literals are mutable by default, so `[]` can be assigned to `mut
  []u8`, and `&0` has type `mut *int`. You can see now why I wanted to
  stop calling them "constants").
- Specifically, mutability is determined by the "mutability algorithm",
  which takes in an expression and determines whether or not the object
  it refers to is mutable. The complete mutability algorithm is as
  follows:
  - If the expression is an identifier, check if the scope object is
    mutable.
  - If the expression is an indexing or slicing expression, check the
    storage of the object being indexed or sliced.
    - If it's an array, run the mutability algorithm on the expression
      designating the array object.
    - If it's a slice, check if the slice is mutable.
    - If it's a pointer, check the referent's storage.
      - If it's an array, check if the pointer is mutable.
      - If it's a slice, check if the slice is mutable.
      - If it's a pointer, run the same check on the referent's storage.
      - If it's an alias, run the same check on the underlying type.
    - If it's an alias, run the same check on the underlying type.
  - If the expression is a struct/tuple access expression, check the
    storage of the object the field is being selected from.
    - If it's a pointer, check if the inner-most pointer is mutable
      (i.e., `**mut *x` passes, but `mut ***x` doesn't).
    - If it's an alias, run the same check on the underlying type.
    - Otherwise, run the mutability algorithm on the expression
      designating the struct/tuple object.
  - If it's a pointer dereferencing expression, check if the pointer is
    mutable (not the inner-most pointer, just the topmost pointer).
  - If none of the above, the object is either stack-allocated and
    anonymous, or has no storage. The distinction doesn't really matter
    here, so in either case return true.

# Rationale

- As of now, "const"/"constant" in Hare has five separate mostly
  unrelated meanings: a constant declaration (as in `def`), an immutable
  global declaration (as in `const`), the "const" type flag for types,
  flexible constant types (iconst/fconst/rconst), and literal
  expressions ("constants"). With the proposed changes, "constant" is
  now *only* used to refer to constant declarations (which are now
  actually declared with `const`).
- One concern that people may have: constant defines within compound
  expressions are now supported, so when `def` is changed to `const`,
  some of these will silently change meaning, but still compile. In
  practice, I don't forsee this being an issue, since there will be a
  tool which automatically (at least partially) converts
  pre-mutability-overhaul code to post-mutability-overhaul code, and
  local constants are very easy to handle.
- The syntax `mut *foo`, where `mut` appears before the `*` or `[]` as
  opposed to after, is unfortunate. `*mut foo` makes more sense, since
  that makes it visually obvious that the foo being pointed to is what's
  mutable, not the pointer itself (as opposed to something like
  `nullable`, which is a property of the pointer). However, this is
  ambiguous when we factor in allowing `mut` to be used on type aliases:
  `*mut foo` could either be a pointer to a mutable foo, or a pointer to
  an immutable foo which contains a mutable object.
- One example use-case for using `mut` on type aliases is
  hare::ast::ident. The type is an alias for `[]str`, but by using
  `mut ast::ident` you end up with an alias for `mut []str`.
- The restriction that immutable pointers can only be cast to mutable
  pointers of the same type is similar to what's described in
  https://todo.sr.ht/~sircmpwn/hare/372. The idea is that making an
  object mutable is a dangerous operation, so it needs to be done
  explicitly on its own; it can't be combined with another dangerous
  operation (changing the secondary type of a pointer).
- Something to note is that type castability and assignability also
  depend on object mutability (e.g. `[3]int` -> `mut []int` only
  succeeds if the array object is mutable). There's no real way around
  this, and I think it's a worthwhile trade-off for mutable objects.
- In C, pointers to `const` types can't be freed, so effectively the
  `const` qualifier conveys semantic information about whether an object
  is freeable. This proposal does *not* do this, since ownership is a
  separate problem and thus requires a separate solution (most likely
  linear types).
- Anonymous stack-allocated objects being mutable may sound strange.
  However, I think it's the best approach. The example already used was
  using an array literal in place of a mutable slice. This is the most
  compelling example, but it's also nice to be able to create a pointer
  to a mutable object without needing to first create a binding for said
  object.
- The "const" type flag is entirely removed, so the only type flag left
  is "error". For now, the language and semantics surrounding type flags
  remains the same, especially since we may add type flags in the
  future, like for linear types. Maybe the best course of action here is
  actually to eliminate type flags entirely, but that's out of scope for
  this proposal. One really nice thing about the prototype for this
  proposal however is that we should be able to rid of strip_flags
  almost everywhere it's currently used, after making some changes to
  how harec handles type flags on aliases and how it handles error
  flags. (A side effect of this is that *int is no longer assignable to
  *!int, or vice versa. This is already the intended behavior within the
  spec).
- Having function parameters be immutable is consistent with having
  bindings be immutable by default. Adding a syntax to declare that a
  function parameter is mutable is problematic, since it would appear
  within the function prototype itself, so it either becomes exposed as
  part of the function's interface (which we don't want), or it doesn't,
  but in the declaration it's intermixed with the stuff that actually
  *is* exposed as part of the interface. Requiring that a new binding be
  created is a simple solution which doesn't require introducing any new
  language constructs.

# Open questions, and generally just things that are kinda meh

- I really really dislike putting `mut` before `*` and `[]`, and if it's
  at all possible to make it work after then it's becoming part of the
  proposal, in a heartbeat.
- We could solve the above problem by just, not allowing `mut` to be
  used on aliases. `mut` has limited utility here anyway: e.g.
  `mut types::slice` is an error, since the underlying type is a struct.
  I also think it's very strange to allow `mut` on aliases but not
  `nullable`. However, the use-cases for `mut` on aliases are very
  clear, so I'm conflicted. Any thoughts?
- Given the basis of this proposal (mutability applies to objects), the
  fact that append/insert/delete require that both the slice and the
  slice contents are mutable makes perfect sense. However, it's still
  annoyingly verbose.
- Generally, `mut` is used a lot. We suspected that using a symbol
  rather than a keyword within types may help here. However, the
  opposite seems to be true: it's much easier to parse a type (as a
  human) when the full keyword `mut` is written out then when using any
  symbol. The primary suggestion was to use `=` as a symbol here. So, a
  pointer to mutable foo would be `=*foo`, or possibly `*=foo`, or maybe
  `* =foo`? The idea is clever, but it looks really strange and
  confusing within declarations/bindings (or really anywhere tbh). I
  suggested `+`, not for any particular reason, just because it's a
  symbol that could work here. I like the way it looks better than `=`,
  but again, it's not as clear as `mut`. This is all to say that I don't
  think we're gonna be able to improve things much here (but see below
  for comments on `mut` within declarations/bindings).
- Function parameters being immutable by default poses a problem: the
  way they're made mutable is by shadowing them. However, doing so
  copies the object. QBE should be smart enough to recognize that the
  original parameter isn't used, so a copy isn't necessary. However, it
  often isn't. This is also partially harec's fault, since calls to
  rt.memcpy can't be optimized out. My instinct is to say that, if we
  keep function parameters immutable (but see below), this is probably
  fine. You shouldn't be passing large values as parameters anyway, so
  in practice I don't expect this to actually affect things.
- There's an even bigger problem with function parameter mutability: if
  function parameters are immutable, it's not possible to mutate
  variadic arguments without either allocating a copy of the slice or
  casting to a mutable slice (and I think that we should design things
  such that immutable->mutable pointer/slice casts should almost never
  be necessary, outside of very low-level code or code which delibrately
  wishes to break the rules for a specific purpose).
- There have been discussions about whether we even need to care about
  stack-allocated locals. That is, maybe `let` within functions should
  create bindings that refer to mutable objects, with no way to make
  them immutable. This would be more consistent with anonymous
  stack-allocated objects, which are mutable by default. It would also
  solve both the above append/insert/delete problem (by getting rid of
  one of the `mut`s), as well as the above function parameter problem
  (we could just continue to have function parameters be mutable, albeit
  variadic parameters would need to (also?) be a mutable slice). The
  rationale here is that there isn't actually much benefit to immutable
  locals: it's very infrequent that they're actually the source of bugs,
  and it's almost never an issue of safety. But for the few benefits
  immutability by default actually offers, a lot of friction is added to
  the language. I tentatively like this. To be clear, this doesn't
  influence the semantics of pointers or slices: those are both still
  immutable by default, and I think they should remain that way.
- If we follow through with having all stack-allocated objects be
  mutable, we need to decide on a syntax. Ideally, the syntax for local
  bindings and for globals is the same, so if `let` declares mutable
  locals, it should declare mutable globals as well. But we still need a
  way to declare immutable globals (there was some conversation about
  just, not supporting immutable globals, but I'm pretty strongly
  opposed to that). We shouldn't use `const`, since then the `const`
  keyword declares things that aren't constants, which is confusing (see
  https://todo.sr.ht/~sircmpwn/hare/603). And I generally like using
  `let` as "the default" way to create locals. We also should decide
  whether we should support immutable locals, even if locals created
  with `let` remain mutable. This would make sense, if we're already
  supporting a syntax for immutable globals, but I also don't think it
  makes much sense to support immutable locals if the expectation is
  that you probably won't use them (kinda similar to how `const` in C is
  pretty much never used for locals outside of a pointer). Can you tell
  I have no idea where I stand here?
- The proposal replaces `def` with `const`. This isn't strictly
  necessary, and, especially given that the `const` keyword isn't used
  anywhere else, we could keep the name `def` here. I personally prefer
  `const`, but I also could go either way here.

thank you for coming to my ted talk
Details
Message ID
<CVJ11RE8UQJF.1KC203XYTNROQ@monch>
In-Reply-To
<CVIYDE2MYJYN.1VKHYOWIYQ2OK@notmylaptop> (view parent)
DKIM signature
pass
Download raw message
overall i like this proposal, it's a big improvement over what we
currently have, and a very strong first step towards cleaning up this
area of hare's semantics

On Thu Sep 14, 2023 at 9:24 PM UTC, Sebastian wrote:
> - Mutable pointers can be cast to immutable pointers, without any
>   limitations.

should this be s/cast/assigned/? i think it would make sense for a
mutable pointer/slice to be able to silently (and transitively) become
immutable

>   e.g. `let mut a: [1]u8 = [0]; a: ([]u8 | mut []u8);`
>   This compiles: even though a is assignable to both `[]u8` and
>   `mut []u8`, only `mut []u8` will be selected here. This is necessary
>   for things like memio::fixed: the function needs to take in
>   `([]u8 | mut []u8)`, since it may or may not be writable. (The stream
>   itself can store the slices in a (non-tagged) union; this is only for
>   the initializer function.)

and the memio::fixed would only be readable in the []u8 case?

> - I really really dislike putting `mut` before `*` and `[]`, and if it's
>   at all possible to make it work after then it's becoming part of the
>   proposal, in a heartbeat.

agreed

> - We could solve the above problem by just, not allowing `mut` to be
>   used on aliases. `mut` has limited utility here anyway: e.g.
>   `mut types::slice` is an error, since the underlying type is a struct.
>   I also think it's very strange to allow `mut` on aliases but not
>   `nullable`. However, the use-cases for `mut` on aliases are very
>   clear, so I'm conflicted. Any thoughts?

i'm open to not allowing mut on aliases, but if we do allow it, i think
it makes sense to also allow nullable

> - Generally, `mut` is used a lot. We suspected that using a symbol
>   rather than a keyword within types may help here. However, the
>   opposite seems to be true: it's much easier to parse a type (as a
>   human) when the full keyword `mut` is written out then when using any
>   symbol. The primary suggestion was to use `=` as a symbol here. So, a
>   pointer to mutable foo would be `=*foo`, or possibly `*=foo`, or maybe
>   `* =foo`? The idea is clever, but it looks really strange and
>   confusing within declarations/bindings (or really anywhere tbh). I
>   suggested `+`, not for any particular reason, just because it's a
>   symbol that could work here. I like the way it looks better than `=`,
>   but again, it's not as clear as `mut`. This is all to say that I don't
>   think we're gonna be able to improve things much here (but see below
>   for comments on `mut` within declarations/bindings).

yeah i think sticking with mut is the right move

> - Function parameters being immutable by default poses a problem: the
>   way they're made mutable is by shadowing them. However, doing so
>   copies the object. QBE should be smart enough to recognize that the
>   original parameter isn't used, so a copy isn't necessary. However, it
>   often isn't. This is also partially harec's fault, since calls to
>   rt.memcpy can't be optimized out. My instinct is to say that, if we
>   keep function parameters immutable (but see below), this is probably
>   fine. You shouldn't be passing large values as parameters anyway, so
>   in practice I don't expect this to actually affect things.

i think function parameters should match the default for locals, and i
think there shouldn't be a way to change this

> - There's an even bigger problem with function parameter mutability: if
>   function parameters are immutable, it's not possible to mutate
>   variadic arguments without either allocating a copy of the slice or
>   casting to a mutable slice (and I think that we should design things
>   such that immutable->mutable pointer/slice casts should almost never
>   be necessary, outside of very low-level code or code which delibrately
>   wishes to break the rules for a specific purpose).

we could have this

fn a(args: t...) void;

desugar to:

fn a(args: mut []t) void;

which keeps them mirroring locals but should solve this

> - There have been discussions about whether we even need to care about
>   stack-allocated locals. That is, maybe `let` within functions should
>   create bindings that refer to mutable objects, with no way to make
>   them immutable. This would be more consistent with anonymous
>   stack-allocated objects, which are mutable by default. It would also
>   solve both the above append/insert/delete problem (by getting rid of
>   one of the `mut`s), as well as the above function parameter problem
>   (we could just continue to have function parameters be mutable, albeit
>   variadic parameters would need to (also?) be a mutable slice). The
>   rationale here is that there isn't actually much benefit to immutable
>   locals: it's very infrequent that they're actually the source of bugs,
>   and it's almost never an issue of safety. But for the few benefits
>   immutability by default actually offers, a lot of friction is added to
>   the language. I tentatively like this. To be clear, this doesn't
>   influence the semantics of pointers or slices: those are both still
>   immutable by default, and I think they should remain that way.

+1 to making locals always be mutable pending figuring out the syntax

> - If we follow through with having all stack-allocated objects be
>   mutable, we need to decide on a syntax. Ideally, the syntax for local
>   bindings and for globals is the same, so if `let` declares mutable
>   locals, it should declare mutable globals as well. But we still need a
>   way to declare immutable globals (there was some conversation about
>   just, not supporting immutable globals, but I'm pretty strongly
>   opposed to that). We shouldn't use `const`, since then the `const`
>   keyword declares things that aren't constants, which is confusing (see
>   https://todo.sr.ht/~sircmpwn/hare/603). And I generally like using
>   `let` as "the default" way to create locals. We also should decide
>   whether we should support immutable locals, even if locals created
>   with `let` remain mutable. This would make sense, if we're already
>   supporting a syntax for immutable globals, but I also don't think it
>   makes much sense to support immutable locals if the expectation is
>   that you probably won't use them (kinda similar to how `const` in C is
>   pretty much never used for locals outside of a pointer). Can you tell
>   I have no idea where I stand here?

yeah if let x = 0; is mutable i'd rather not have a way to define
immutable locals. maybe we just accept that let has different semantics
in globals vs locals? the idea being that `let x = y;` means "use the
default mutability semantics", with let mut being an override just for
globals

> - The proposal replaces `def` with `const`. This isn't strictly
>   necessary, and, especially given that the `const` keyword isn't used
>   anywhere else, we could keep the name `def` here. I personally prefer
>   `const`, but I also could go either way here.

i kinda prefer def here, mostly since it's what we already do, but i
don't have a very strong preference

> thank you for coming to my ted talk

thank you for giving this excellent ted talk
Details
Message ID
<CVJ1BU0K4KU6.10Z4YEE639TW1@notmylaptop>
In-Reply-To
<CVJ11RE8UQJF.1KC203XYTNROQ@monch> (view parent)
DKIM signature
pass
Download raw message
On Thu Sep 14, 2023 at 7:25 PM EDT, Ember Sawady wrote:
> > - Mutable pointers can be cast to immutable pointers, without any
> >   limitations.
>
> should this be s/cast/assigned/? i think it would make sense for a
> mutable pointer/slice to be able to silently (and transitively) become
> immutable

This specific point is about casting; assignability is already mentioned
beforehand:
> - Mutable pointers can be assigned to immutable pointers, but not the
>   other way around.
> - This is true of any `mut` within the type: e.g. `mut *mut []T` is
>   directly assignable to `*[]T`.

> >   e.g. `let mut a: [1]u8 = [0]; a: ([]u8 | mut []u8);`
> >   This compiles: even though a is assignable to both `[]u8` and
> >   `mut []u8`, only `mut []u8` will be selected here. This is necessary
> >   for things like memio::fixed: the function needs to take in
> >   `([]u8 | mut []u8)`, since it may or may not be writable. (The stream
> >   itself can store the slices in a (non-tagged) union; this is only for
> >   the initializer function.)
>
> and the memio::fixed would only be readable in the []u8 case?

That's the idea, yeah. If []u8 is passed in, the vtable used has writer
set to null, otherwise it has a write function.

> we could have this
>
> fn a(args: t...) void;
>
> desugar to:
>
> fn a(args: mut []t) void;
>
> which keeps them mirroring locals but should solve this

Assuming immutable locals, that still feels pretty weird and
inconsistent. Though I'm leaning towards having locals be mutable, at
least by default.

> yeah if let x = 0; is mutable i'd rather not have a way to define
> immutable locals. maybe we just accept that let has different semantics
> in globals vs locals? the idea being that `let x = y;` means "use the
> default mutability semantics", with let mut being an override just for
> globals

I, hm, I have no idea how I feel about this. I think I dislike it??
Details
Message ID
<CVJ44X8DFYI4.2RJQGZ5RR45FS@attila>
In-Reply-To
<CVIYDE2MYJYN.1VKHYOWIYQ2OK@notmylaptop> (view parent)
DKIM signature
pass
Download raw message
This is exciting! Some comments:

> Semantic stuffs:

> - Taking the address of an immutable object of type `foo` yields an
>   object of type `*foo` (pointer to immutable foo).

Calling *T a 'pointer to immutable T' imo does not convey the actual
meaning of the type expression. It's just this particular reference to the
underlying object that is immutable we don't know anything about the actual
object. Unfortunately, it's really easy to mess this up mentally when thinking
about immutability and I think it would be great if we came up with a better
way to talk about this.


> - Taking the address of a mutable object of type `foo` yields an object
>   of type `mut *foo` (pointer to mutable foo).

Not a huge fan of that. See my comment about mutability of anonymous locals.


> - Mutability does not convey any information about ownership. That is,
>   you can free an immutable pointer or slice, and you can free strings.

+100


> - Expressions which implicitly dereference their operand check the
>   mutability of the object they're *actually* acting on: e.g. append can
>   operate on a mutable pointer, or on an immutable pointer to a mutable
>   pointer, so long as the final object is mutable.

I assume you meant "or an immutable pointer to a mutable slice"?


> The central algorithm at play here (or, what does "mutable" even mean?):
> [...]

Very nice. May need some more thought with auto-dereferencing bits.


> # Rationale

> - One concern that people may have: constant defines within compound
>   expressions are now supported, so when `def` is changed to `const`,
>   some of these will silently change meaning, but still compile. In
>   practice, I don't forsee this being an issue, since there will be a
>   tool which automatically (at least partially) converts
>   pre-mutability-overhaul code to post-mutability-overhaul code, and
>   local constants are very easy to handle.

I don't think this is particularly problematic. Worst case, we do a transition
in two steps with the first step removing old const and second step adding new
const with sufficient time in between to force people to act on it.


> - The syntax `mut *foo`, where `mut` appears before the `*` or `[]` as
>   opposed to after, is unfortunate. `*mut foo` makes more sense, since
>   that makes it visually obvious that the foo being pointed to is what's
>   mutable, not the pointer itself (as opposed to something like
>   `nullable`, which is a property of the pointer). However, this is
>   ambiguous when we factor in allowing `mut` to be used on type aliases:
>   `*mut foo` could either be a pointer to a mutable foo, or a pointer to
>   an immutable foo which contains a mutable object.

Disagree. As noted earlier, interpreting `mut *T` as "pointer to mutable T" is
slightly but importantly incorrect. So is interpreting `mut *T` as "mutable
pointer to T". I think `mut` should be before the relevant * because that still
makes it more clear that we're talking about a property of the pointer/slice.


> - The restriction that immutable pointers can only be cast to mutable
>   pointers of the same type is similar to what's described in
>   https://todo.sr.ht/~sircmpwn/hare/372. The idea is that making an
>   object mutable is a dangerous operation, so it needs to be done
>   explicitly on its own; it can't be combined with another dangerous
>   operation (changing the secondary type of a pointer).

+1


> - Something to note is that type castability and assignability also
>   depend on object mutability (e.g. `[3]int` -> `mut []int` only
>   succeeds if the array object is mutable). There's no real way around
>   this, and I think it's a worthwhile trade-off for mutable objects.

This is a bit unfortunate, but I agree it's worthwhile. Perhaps an elegant way
to resolve this special case is to "redefine" array to slice assignments as
slice to slice assignments where the array gets an implicit `[..]` appended? I
thought about this before and it looks like it could help here (at least in the
mentioned case, maybe there are places where doing so wouldn't help in avoiding
this issue?).


> - Anonymous stack-allocated objects being mutable may sound strange.
>   However, I think it's the best approach. The example already used was
>   using an array literal in place of a mutable slice. This is the most
>   compelling example, but it's also nice to be able to create a pointer
>   to a mutable object without needing to first create a binding for said
>   object.

Hm, not sure what to think of this. Perhaps it would be nice to have a way to
tell the unary & operator what kind of pointer you want? Of course that doesn't
help with array literals, but other than that it would solve this,
and also a problem which I don't see mentioned anywhere: in `let mut a = 5; let b = &a;`,
what's the type of b? I don't think either answer is a clear default, so it
would be nice to express both as concisely as possible (meaning without using a
potentially lenghty cast).

No idea what to do about arrays though.


> - Having function parameters be immutable is consistent with having
>   bindings be immutable by default. Adding a syntax to declare that a
>   function parameter is mutable is problematic, since it would appear
>   within the function prototype itself, so it either becomes exposed as
>   part of the function's interface (which we don't want), or it doesn't,
>   but in the declaration it's intermixed with the stuff that actually
>   *is* exposed as part of the interface. Requiring that a new binding be
>   created is a simple solution which doesn't require introducing any new
>   language constructs.

The biggest problem here is that this makes tiny functions not
so tiny anymore because of all the rebinding.

While I agree that in present day Hare mutability information should not be a
part of the function interface, including it also opens up some interesting
opportunities if we decide to use our own ABI instead of C's.

Even without that, I don't see a huge problem in denoting mutability somewhere
in function declaration - we also specify parameter names there and they are
not part of the interface.


> # Open questions, and generally just things that are kinda meh
>
> - I really really dislike putting `mut` before `*` and `[]`, and if it's
>   at all possible to make it work after then it's becoming part of the
>   proposal, in a heartbeat.
> - We could solve the above problem by just, not allowing `mut` to be
>   used on aliases. `mut` has limited utility here anyway: e.g.
>   `mut types::slice` is an error, since the underlying type is a struct.
>   I also think it's very strange to allow `mut` on aliases but not
>   `nullable`. However, the use-cases for `mut` on aliases are very
>   clear, so I'm conflicted. Any thoughts?

I'm strongly against moving mut after the */[], but this alias thing is an
important issue nonetheless. I think a large part of it just boils down to the
question of what Hare aliases really are. Maybe this is one part of the
proposal where we should just experiment a bit and see what happens.


> - Function parameters being immutable by default poses a problem: the
>   way they're made mutable is by shadowing them. However, doing so
>   copies the object. QBE should be smart enough to recognize that the
>   original parameter isn't used, so a copy isn't necessary. However, it
>   often isn't. This is also partially harec's fault, since calls to
>   rt.memcpy can't be optimized out. My instinct is to say that, if we
>   keep function parameters immutable (but see below), this is probably
>   fine. You shouldn't be passing large values as parameters anyway, so
>   in practice I don't expect this to actually affect things.

As seen in a recent thread on bytes::index performance, a slice is already a
"large" value and we often pass those around by value, so this won't be without
consequences. Fortunately, stack consumption from variable shadowing can be
optimized in harec's gen if we decide we truly need it and think carefully
about corner cases.


> - There have been discussions about whether we even need to care about
>   stack-allocated locals. That is, maybe `let` within functions should
>   create bindings that refer to mutable objects, with no way to make
>   them immutable. This would be more consistent with anonymous
>   stack-allocated objects, which are mutable by default. It would also
>   solve both the above append/insert/delete problem (by getting rid of
>   one of the `mut`s), as well as the above function parameter problem
>   (we could just continue to have function parameters be mutable, albeit
>   variadic parameters would need to (also?) be a mutable slice). The
>   rationale here is that there isn't actually much benefit to immutable
>   locals: it's very infrequent that they're actually the source of bugs,
>   and it's almost never an issue of safety. But for the few benefits
>   immutability by default actually offers, a lot of friction is added to
>   the language. I tentatively like this. To be clear, this doesn't
>   influence the semantics of pointers or slices: those are both still
>   immutable by default, and I think they should remain that way.
> - If we follow through with having all stack-allocated objects be
>   mutable, we need to decide on a syntax. Ideally, the syntax for local
>   bindings and for globals is the same, so if `let` declares mutable
>   locals, it should declare mutable globals as well. But we still need a
>   way to declare immutable globals (there was some conversation about
>   just, not supporting immutable globals, but I'm pretty strongly
>   opposed to that). We shouldn't use `const`, since then the `const`
>   keyword declares things that aren't constants, which is confusing (see
>   https://todo.sr.ht/~sircmpwn/hare/603). And I generally like using
>   `let` as "the default" way to create locals. We also should decide
>   whether we should support immutable locals, even if locals created
>   with `let` remain mutable. This would make sense, if we're already
>   supporting a syntax for immutable globals, but I also don't think it
>   makes much sense to support immutable locals if the expectation is
>   that you probably won't use them (kinda similar to how `const` in C is
>   pretty much never used for locals outside of a pointer). Can you tell
>   I have no idea where I stand here?

From the amount of bugs point of view, having local immutability by
default is a clear win and for that reason I think we should have them immutable.
However, there are a bunch of downsides as well in addition to the ones you
pointed out, for example making locals immutable by default will likely make
the language even more expression-heavy.

I also don't think not having them would solve the append/delete/insert problem
entirely - you'd still need to handle multiple muts when operating with
auto-dereferenced pointers to slices.

---

Overall, this is great, thank you for working on this.
Details
Message ID
<CVJA0MAH86QY.3MWIX9QP3X64S@notmylaptop>
In-Reply-To
<CVJ44X8DFYI4.2RJQGZ5RR45FS@attila> (view parent)
DKIM signature
pass
Download raw message
On Thu Sep 14, 2023 at 9:51 PM EDT, Bor Grošelj Simić wrote:
> > - Taking the address of an immutable object of type `foo` yields an
> >   object of type `*foo` (pointer to immutable foo).
>
> Calling *T a 'pointer to immutable T' imo does not convey the actual
> meaning of the type expression. It's just this particular reference to the
> underlying object that is immutable we don't know anything about the actual
> object. Unfortunately, it's really easy to mess this up mentally when thinking
> about immutability and I think it would be great if we came up with a better
> way to talk about this.

Hm, I kinda see what you mean. I don't have any better ideas though,
since I think "(im)mutable pointer/slice" is even more misleading.

> > - Taking the address of a mutable object of type `foo` yields an object
> >   of type `mut *foo` (pointer to mutable foo).
>
> Not a huge fan of that. See my comment about mutability of anonymous locals.

Honestly, this was not a part of the proposal that I expected to be
controversial. I'll go into more detail later below.

> > - Expressions which implicitly dereference their operand check the
> >   mutability of the object they're *actually* acting on: e.g. append can
> >   operate on a mutable pointer, or on an immutable pointer to a mutable
> >   pointer, so long as the final object is mutable.
>
> I assume you meant "or an immutable pointer to a mutable slice"?

I meant mutable pointer here, since both the slice object and contents
have to be mutable, so the pointer to the slice must also be mutable
here.

> > The central algorithm at play here (or, what does "mutable" even mean?):
> > [...]
>
> Very nice. May need some more thought with auto-dereferencing bits.

Care to elaborate here?

> > - The syntax `mut *foo`, where `mut` appears before the `*` or `[]` as
> >   opposed to after, is unfortunate. `*mut foo` makes more sense, since
> >   that makes it visually obvious that the foo being pointed to is what's
> >   mutable, not the pointer itself (as opposed to something like
> >   `nullable`, which is a property of the pointer). However, this is
> >   ambiguous when we factor in allowing `mut` to be used on type aliases:
> >   `*mut foo` could either be a pointer to a mutable foo, or a pointer to
> >   an immutable foo which contains a mutable object.
>
> Disagree. As noted earlier, interpreting `mut *T` as "pointer to mutable T" is
> slightly but importantly incorrect. So is interpreting `mut *T` as "mutable
> pointer to T". I think `mut` should be before the relevant * because that still
> makes it more clear that we're talking about a property of the pointer/slice.

I guess that's where the disagreement lies: I don't think mutability is
a property of the pointer or slice. Saying that it's a pointer to
mutable T may be slightly incorrect, but, at least to me, `*mut T` is
much more intuitive than `mut *T`, since it indicates that the
mutability applies to the object with type T. I think "mutable pointer"
is much more misleading, since the pointer itself isn't mutable. At
least with "pointer to mutable T", it's implied that the object with
type T is mutable (hence, it may be subtly incorrect, but I can't
imagine this would confuse anyone, at least any more than the rest of
the mutability semantics).

Another (unrelated) argument in favor of `*mut T` (if possible): this is
the syntax used by other languages I know of which have pointers and a
concept of mutability (C, Rust, Zig). I'd prefer to not deviate from
this, unless there's a reason that we have to (as there currently is).

> > - Something to note is that type castability and assignability also
> >   depend on object mutability (e.g. `[3]int` -> `mut []int` only
> >   succeeds if the array object is mutable). There's no real way around
> >   this, and I think it's a worthwhile trade-off for mutable objects.
>
> This is a bit unfortunate, but I agree it's worthwhile. Perhaps an elegant way
> to resolve this special case is to "redefine" array to slice assignments as
> slice to slice assignments where the array gets an implicit `[..]` appended? I
> thought about this before and it looks like it could help here (at least in the
> mentioned case, maybe there are places where doing so wouldn't help in avoiding
> this issue?).

Hm, interesting idea. Not sure how much it'll actually improve the
situation in practice, but I'll definitely look into it.

> > - Anonymous stack-allocated objects being mutable may sound strange.
> >   However, I think it's the best approach. The example already used was
> >   using an array literal in place of a mutable slice. This is the most
> >   compelling example, but it's also nice to be able to create a pointer
> >   to a mutable object without needing to first create a binding for said
> >   object.
>
> Hm, not sure what to think of this. Perhaps it would be nice to have a way to
> tell the unary & operator what kind of pointer you want? Of course that doesn't
> help with array literals, but other than that it would solve this,
> and also a problem which I don't see mentioned anywhere: in `let mut a = 5; let b = &a;`,
> what's the type of b? I don't think either answer is a clear default, so it
> would be nice to express both as concisely as possible (meaning without using a
> potentially lenghty cast).
>
> No idea what to do about arrays though.

I think `let mut a = 5; let b = &a;` resulting in b having type `mut *a`
is the correct behavior here. Mutable pointers are assignable to
immutable pointers, and when taking the address of a mutable object,
then the resulting pointer points to, well, a mutable object.

> > - Having function parameters be immutable is consistent with having
> >   bindings be immutable by default. Adding a syntax to declare that a
> >   function parameter is mutable is problematic, since it would appear
> >   within the function prototype itself, so it either becomes exposed as
> >   part of the function's interface (which we don't want), or it doesn't,
> >   but in the declaration it's intermixed with the stuff that actually
> >   *is* exposed as part of the interface. Requiring that a new binding be
> >   created is a simple solution which doesn't require introducing any new
> >   language constructs.
>
> The biggest problem here is that this makes tiny functions not
> so tiny anymore because of all the rebinding.
>
> While I agree that in present day Hare mutability information should not be a
> part of the function interface, including it also opens up some interesting
> opportunities if we decide to use our own ABI instead of C's.

...does it? I feel like the mutability of a function parameter is
entirely an implementation detail, since the parameter is a copy.

> Even without that, I don't see a huge problem in denoting mutability somewhere
> in function declaration - we also specify parameter names there and they are
> not part of the interface.

That's a fair point, though parameter names convey useful information to
the user, whereas mutability doesn't convey anything.

> > - There have been discussions about whether we even need to care about
> >   stack-allocated locals. That is, maybe `let` within functions should
> >   create bindings that refer to mutable objects, with no way to make
> >   them immutable. This would be more consistent with anonymous
> >   stack-allocated objects, which are mutable by default. It would also
> >   solve both the above append/insert/delete problem (by getting rid of
> >   one of the `mut`s), as well as the above function parameter problem
> >   (we could just continue to have function parameters be mutable, albeit
> >   variadic parameters would need to (also?) be a mutable slice). The
> >   rationale here is that there isn't actually much benefit to immutable
> >   locals: it's very infrequent that they're actually the source of bugs,
> >   and it's almost never an issue of safety. But for the few benefits
> >   immutability by default actually offers, a lot of friction is added to
> >   the language. I tentatively like this. To be clear, this doesn't
> >   influence the semantics of pointers or slices: those are both still
> >   immutable by default, and I think they should remain that way.
> > - If we follow through with having all stack-allocated objects be
> >   mutable, we need to decide on a syntax. Ideally, the syntax for local
> >   bindings and for globals is the same, so if `let` declares mutable
> >   locals, it should declare mutable globals as well. But we still need a
> >   way to declare immutable globals (there was some conversation about
> >   just, not supporting immutable globals, but I'm pretty strongly
> >   opposed to that). We shouldn't use `const`, since then the `const`
> >   keyword declares things that aren't constants, which is confusing (see
> >   https://todo.sr.ht/~sircmpwn/hare/603). And I generally like using
> >   `let` as "the default" way to create locals. We also should decide
> >   whether we should support immutable locals, even if locals created
> >   with `let` remain mutable. This would make sense, if we're already
> >   supporting a syntax for immutable globals, but I also don't think it
> >   makes much sense to support immutable locals if the expectation is
> >   that you probably won't use them (kinda similar to how `const` in C is
> >   pretty much never used for locals outside of a pointer). Can you tell
> >   I have no idea where I stand here?
>
> From the amount of bugs point of view, having local immutability by
> default is a clear win and for that reason I think we should have them immutable.
> However, there are a bunch of downsides as well in addition to the ones you
> pointed out, for example making locals immutable by default will likely make
> the language even more expression-heavy.

I'm not entirely convinced, especially since shadowing is so common. I
can't remember the last time I've actually run into a bug related to
mutability of a binding, so I'm not convinced that local immutability by
default is a "clear win". Any examples of this?

I agree that placing a greater emphasis on expressions is, not a good
thing. Hare being expression-based is nice, but the boring imperative
style works really well for Hare, and it would suck if we had to worsen
the experience here. But I don't actually think immutability by default
really makes things more expression-heavy. If we normalize trying to get
rid of mutable objects wherever possible, sure. But we can instead
normalize just, using `let mut` when it's useful to do so.

But that's also why the idea of mutability by default for local bindings
has been growing on me: it's nice to not need to think much here, or
fight with the compiler if you mutate an immutable binding (which, at
least in my experience, is almost always correctly fixed by making the
binding mutable, so the error doesn't really uncover any bugs).

> I also don't think not having them would solve the append/delete/insert problem
> entirely - you'd still need to handle multiple muts when operating with
> auto-dereferenced pointers to slices.

Yeah, I suppose. But I think it's a step in the right direction
nonetheless, and I'm not sure if anything else can be done with
append/insert/delete.

> Overall, this is great, thank you for working on this.

Thanks for your feedback! :)
Details
Message ID
<CVNDE99PULA9.2T8BJVU85XRSN@attila>
In-Reply-To
<CVJA0MAH86QY.3MWIX9QP3X64S@notmylaptop> (view parent)
DKIM signature
pass
Download raw message
On Wed Sep 20, 2023 at 12:07 AM CEST, Sebastian wrote:
> On Thu Sep 14, 2023 at 9:51 PM EDT, Bor Grošelj Simić wrote:
> > > - Taking the address of an immutable object of type `foo` yields an
> > >   object of type `*foo` (pointer to immutable foo).
> >
> > Calling *T a 'pointer to immutable T' imo does not convey the actual
> > meaning of the type expression. It's just this particular reference to the
> > underlying object that is immutable we don't know anything about the actual
> > object. Unfortunately, it's really easy to mess this up mentally when thinking
> > about immutability and I think it would be great if we came up with a better
> > way to talk about this.
>
> Hm, I kinda see what you mean. I don't have any better ideas though,
> since I think "(im)mutable pointer/slice" is even more misleading.

Agreed, that's worse.


> > > - Taking the address of a mutable object of type `foo` yields an object
> > >   of type `mut *foo` (pointer to mutable foo).
> >
> > Not a huge fan of that. See my comment about mutability of anonymous locals.
>
> Honestly, this was not a part of the proposal that I expected to be
> controversial. I'll go into more detail later below.
>
> > > - Expressions which implicitly dereference their operand check the
> > >   mutability of the object they're *actually* acting on: e.g. append can
> > >   operate on a mutable pointer, or on an immutable pointer to a mutable
> > >   pointer, so long as the final object is mutable.
> >
> > I assume you meant "or an immutable pointer to a mutable slice"?
>
> I meant mutable pointer here, since both the slice object and contents
> have to be mutable, so the pointer to the slice must also be mutable
> here.
>
> > > The central algorithm at play here (or, what does "mutable" even mean?):
> > > [...]
> >
> > Very nice. May need some more thought with auto-dereferencing bits.
>
> Care to elaborate here?

Err, actually I can't remember what the problem was when I wrote that (it was
*very* late here). My guess is that I had an issue with the auto-dereferenced
pointers that are part of the struct/tuple access case not being mentioned
explicitly, though now it looks straightforward to deduce how to handle them.


> > > - The syntax `mut *foo`, where `mut` appears before the `*` or `[]` as
> > >   opposed to after, is unfortunate. `*mut foo` makes more sense, since
> > >   that makes it visually obvious that the foo being pointed to is what's
> > >   mutable, not the pointer itself (as opposed to something like
> > >   `nullable`, which is a property of the pointer). However, this is
> > >   ambiguous when we factor in allowing `mut` to be used on type aliases:
> > >   `*mut foo` could either be a pointer to a mutable foo, or a pointer to
> > >   an immutable foo which contains a mutable object.
> >
> > Disagree. As noted earlier, interpreting `mut *T` as "pointer to mutable T" is
> > slightly but importantly incorrect. So is interpreting `mut *T` as "mutable
> > pointer to T". I think `mut` should be before the relevant * because that still
> > makes it more clear that we're talking about a property of the pointer/slice.
>
> I guess that's where the disagreement lies: I don't think mutability is
> a property of the pointer or slice. Saying that it's a pointer to
> mutable T may be slightly incorrect, but, at least to me, `*mut T` is
> much more intuitive than `mut *T`, since it indicates that the
> mutability applies to the object with type T. I think "mutable pointer"
> is much more misleading, since the pointer itself isn't mutable. At
> least with "pointer to mutable T", it's implied that the object with
> type T is mutable (hence, it may be subtly incorrect, but I can't
> imagine this would confuse anyone, at least any more than the rest of
> the mutability semantics).

My concern are things like these:

fn f(a: *mut int, b: *int) void = {
	*a = *b + 2;
	assert(*a != *b);
};

fn caller() void = {
	let p = &0: *mut int;
	f(p, p);
};

The assertion in f will, counterintuitively, fail.
Thinking about the pointee of b in function f as immutable is wrong.

Of course we hope to eliminate this case by enforcing sensible aliasing rules
through the type system like Rust does, but at the moment it's not clear if
we'll actually manage to do that and we almost certainly won't be able to do so
for arbitratily complicated instances of this problem.

This can also be solved by just saying the kind of aliasing done in
f is not defined behavior or something along those lines. Yes, UB bad, but this
kind of aliasing is going to be a bug 99.9% of the time so making it UB is not
much worse (in fact, it may allow the implementation to catch some simple cases
and abort safely). Doing so allows for some very nice optimizations.

I don't know what's the best way to handle this issue, but the resolution
should probably be separate from this proposal, so at least for now we have to
treat this case as valid code.


> Another (unrelated) argument in favor of `*mut T` (if possible): this is
> the syntax used by other languages I know of which have pointers and a
> concept of mutability (C, Rust, Zig). I'd prefer to not deviate from
> this, unless there's a reason that we have to (as there currently is).

I don't consider C a relevant example here (C's const is generally broken and
C's type syntax is just beyond words) and as mentioned, for Rust this
syntax sort of makes more sense. I don't know what Zig does in edge cases like
the one above, but it'd be great if someone with the relevant knowledge and/or
a Zig toolchain at hand could tell us.


> > > - Something to note is that type castability and assignability also
> > >   depend on object mutability (e.g. `[3]int` -> `mut []int` only
> > >   succeeds if the array object is mutable). There's no real way around
> > >   this, and I think it's a worthwhile trade-off for mutable objects.
> >
> > This is a bit unfortunate, but I agree it's worthwhile. Perhaps an elegant way
> > to resolve this special case is to "redefine" array to slice assignments as
> > slice to slice assignments where the array gets an implicit `[..]` appended? I
> > thought about this before and it looks like it could help here (at least in the
> > mentioned case, maybe there are places where doing so wouldn't help in avoiding
> > this issue?).
>
> Hm, interesting idea. Not sure how much it'll actually improve the
> situation in practice, but I'll definitely look into it.

The suggestion was mostly for how to resolve the thing with least complication
in the spec, though I'd be happy to hear it works in practice as well.


> > > - Anonymous stack-allocated objects being mutable may sound strange.
> > >   However, I think it's the best approach. The example already used was
> > >   using an array literal in place of a mutable slice. This is the most
> > >   compelling example, but it's also nice to be able to create a pointer
> > >   to a mutable object without needing to first create a binding for said
> > >   object.
> >
> > Hm, not sure what to think of this. Perhaps it would be nice to have a way to
> > tell the unary & operator what kind of pointer you want? Of course that doesn't
> > help with array literals, but other than that it would solve this,
> > and also a problem which I don't see mentioned anywhere: in `let mut a = 5; let b = &a;`,
> > what's the type of b? I don't think either answer is a clear default, so it
> > would be nice to express both as concisely as possible (meaning without using a
> > potentially lenghty cast).
> >
> > No idea what to do about arrays though.
>
> I think `let mut a = 5; let b = &a;` resulting in b having type `mut *a`
> is the correct behavior here.
> Mutable pointers are assignable to
> immutable pointers, and when taking the address of a mutable object,
> then the resulting pointer points to, well, a mutable object.

I really can't think of anything better than making it mutable in this example,
but your reasoning for why it should be so is not good, as shown in the
aliasing example above. That's why making it mutable feels a bit arbitrary to
me.


> > > - Having function parameters be immutable is consistent with having
> > >   bindings be immutable by default. Adding a syntax to declare that a
> > >   function parameter is mutable is problematic, since it would appear
> > >   within the function prototype itself, so it either becomes exposed as
> > >   part of the function's interface (which we don't want), or it doesn't,
> > >   but in the declaration it's intermixed with the stuff that actually
> > >   *is* exposed as part of the interface. Requiring that a new binding be
> > >   created is a simple solution which doesn't require introducing any new
> > >   language constructs.
> >
> > The biggest problem here is that this makes tiny functions not
> > so tiny anymore because of all the rebinding.
> >
> > While I agree that in present day Hare mutability information should not be a
> > part of the function interface, including it also opens up some interesting
> > opportunities if we decide to use our own ABI instead of C's.
>
> ...does it? I feel like the mutability of a function parameter is
> entirely an implementation detail, since the parameter is a copy.

You don't actually need to perform the copy if the parameter is immutable.
You'd think that's the same as passing through an immutable pointer, but it
is not, for aliasing reasons, and it allows for some optimisations that
wouldn't be possible otherwise. There's a recent new language that does
something like that. Unfortunately, I forgot which one it is (there are so many!).


> > Even without that, I don't see a huge problem in denoting mutability somewhere
> > in function declaration - we also specify parameter names there and they are
> > not part of the interface.
>
> That's a fair point, though parameter names convey useful information to
> the user, whereas mutability doesn't convey anything.
>
> > > - There have been discussions about whether we even need to care about
> > >   stack-allocated locals. That is, maybe `let` within functions should
> > >   create bindings that refer to mutable objects, with no way to make
> > >   them immutable. This would be more consistent with anonymous
> > >   stack-allocated objects, which are mutable by default. It would also
> > >   solve both the above append/insert/delete problem (by getting rid of
> > >   one of the `mut`s), as well as the above function parameter problem
> > >   (we could just continue to have function parameters be mutable, albeit
> > >   variadic parameters would need to (also?) be a mutable slice). The
> > >   rationale here is that there isn't actually much benefit to immutable
> > >   locals: it's very infrequent that they're actually the source of bugs,
> > >   and it's almost never an issue of safety. But for the few benefits
> > >   immutability by default actually offers, a lot of friction is added to
> > >   the language. I tentatively like this. To be clear, this doesn't
> > >   influence the semantics of pointers or slices: those are both still
> > >   immutable by default, and I think they should remain that way.
> > > - If we follow through with having all stack-allocated objects be
> > >   mutable, we need to decide on a syntax. Ideally, the syntax for local
> > >   bindings and for globals is the same, so if `let` declares mutable
> > >   locals, it should declare mutable globals as well. But we still need a
> > >   way to declare immutable globals (there was some conversation about
> > >   just, not supporting immutable globals, but I'm pretty strongly
> > >   opposed to that). We shouldn't use `const`, since then the `const`
> > >   keyword declares things that aren't constants, which is confusing (see
> > >   https://todo.sr.ht/~sircmpwn/hare/603). And I generally like using
> > >   `let` as "the default" way to create locals. We also should decide
> > >   whether we should support immutable locals, even if locals created
> > >   with `let` remain mutable. This would make sense, if we're already
> > >   supporting a syntax for immutable globals, but I also don't think it
> > >   makes much sense to support immutable locals if the expectation is
> > >   that you probably won't use them (kinda similar to how `const` in C is
> > >   pretty much never used for locals outside of a pointer). Can you tell
> > >   I have no idea where I stand here?
> >
> > From the amount of bugs point of view, having local immutability by
> > default is a clear win and for that reason I think we should have them immutable.
> > However, there are a bunch of downsides as well in addition to the ones you
> > pointed out, for example making locals immutable by default will likely make
> > the language even more expression-heavy.
>
> I'm not entirely convinced, especially since shadowing is so common. I
> can't remember the last time I've actually run into a bug related to
> mutability of a binding, so I'm not convinced that local immutability by
> default is a "clear win". Any examples of this?

What I'm worried about is that in a language that makes you think about
(im)mutability, accidentially mutable things may wreak greater havoc than in a
language where you're used to living in a soup of mutable things, especially if
we're going to make pointers inherit (im)mutability from their pointees as
discussed above. But that's just a hunch, I can't really know that, and I
suspect we won't be able to tell without trying it out for an extended period
of time. So perhaps "clear win" was too strong. I'm still in favor of having
local immutability by default though.

>
> I agree that placing a greater emphasis on expressions is, not a good
> thing. Hare being expression-based is nice, but the boring imperative
> style works really well for Hare, and it would suck if we had to worsen
> the experience here. But I don't actually think immutability by default
> really makes things more expression-heavy. If we normalize trying to get
> rid of mutable objects wherever possible, sure. But we can instead
> normalize just, using `let mut` when it's useful to do so.

The problem here as I see it is that Hare users are mostly used to imperative
style and most of the problems Hare is designed to solve were historically (and
for good reasons!) solved in imperative languages, but safety, program
analysis and certain optimizations don't work well with imperativity. So we
need to find some kind of balance.


> But that's also why the idea of mutability by default for local bindings
> has been growing on me: it's nice to not need to think much here, or
> fight with the compiler if you mutate an immutable binding (which, at
> least in my experience, is almost always correctly fixed by making the
> binding mutable, so the error doesn't really uncover any bugs).
>
> > I also don't think not having them would solve the append/delete/insert problem
> > entirely - you'd still need to handle multiple muts when operating with
> > auto-dereferenced pointers to slices.
>
> Yeah, I suppose. But I think it's a step in the right direction
> nonetheless, and I'm not sure if anything else can be done with
> append/insert/delete.
>
> > Overall, this is great, thank you for working on this.
>
> Thanks for your feedback! :)
Details
Message ID
<CVNEDNO1UGGL.2SZ8BILAJEUPK@notmylaptop>
In-Reply-To
<CVNDE99PULA9.2T8BJVU85XRSN@attila> (view parent)
DKIM signature
pass
Download raw message
On Tue Sep 19, 2023 at 9:57 PM EDT, Bor Grošelj Simić wrote:
> My concern are things like these:
>
> fn f(a: *mut int, b: *int) void = {
> 	*a = *b + 2;
> 	assert(*a != *b);
> };
>
> fn caller() void = {
> 	let p = &0: *mut int;
> 	f(p, p);
> };
>
> The assertion in f will, counterintuitively, fail.
> Thinking about the pointee of b in function f as immutable is wrong.

This is a really good point actually.

I still mostly think that the semantics surrounding mutability with
pointers and slices is correct in this RFC, and I can't imagine this
would be a very common bug in practice, but I guess the terminology (and
thus the way we think of mutability with pointers/slices) could use some
work. I'm still moreso in favor of `mut` after `*`, but I'm much more
willing to put it before `*` now.

> This can also be solved by just saying the kind of aliasing done in
> f is not defined behavior or something along those lines. Yes, UB bad, but this
> kind of aliasing is going to be a bug 99.9% of the time so making it UB is not
> much worse (in fact, it may allow the implementation to catch some simple cases
> and abort safely). Doing so allows for some very nice optimizations.

-1. I wouldn't be strongly opposed to adding a @noalias attribute to
function parameters (though I'm still not entirely convinced it's worth
it), but we shouldn't introduce UB to allow for hypothetical
optimizations in other implementations. Even C doesn't impose such
strict aliasing rules; it requires explicitly using `restrict`.

I also disagree that it's often a bug. memmove is a good example of a
function which needs to be able to handle such aliasing.

> > Another (unrelated) argument in favor of `*mut T` (if possible): this is
> > the syntax used by other languages I know of which have pointers and a
> > concept of mutability (C, Rust, Zig). I'd prefer to not deviate from
> > this, unless there's a reason that we have to (as there currently is).
>
> I don't consider C a relevant example here (C's const is generally broken and
> C's type syntax is just beyond words) and as mentioned, for Rust this
> syntax sort of makes more sense. I don't know what Zig does in edge cases like
> the one above, but it'd be great if someone with the relevant knowledge and/or
> a Zig toolchain at hand could tell us.

Yeah, on second thought, I retract my usage of C as an example here.

Here's the equivalent Zig code:

	const assert = @import("std").debug.assert;

	fn f(a: *u8, b: *const u8) void {
	    a.* = b.* + 2;
	    assert(a.* != b.*);
	}

	pub fn main() void {
	    var n: u8 = 0;
	    var p = &n;
	    f(p, p);
	}

The assertion fails for Debug builds, just like in Hare. However, Zig
has a `noalias` keyword, so if the code is modified a little:

	fn f(noalias a: *u8, noalias b: *const u8) void {
	    a.* = b.* + 2;
	    if (a == b) {
	        // need if-statement here since assertions aren't
	        // compiled in release builds
	        @panic("ruh roh raggy");
	    }
	}

	pub fn main() void {
	    var n: u8 = 0;
	    var p = &n;
	    f(p, p);
	}

This panics in Debug builds, but in all Release builds the `a == b`
branch is optimized out. Note that this only happens when `noalias` is
present.

The main takeaway from this is that in Zig, `const` goes after `*`, but
the `const` doesn't make any guarantees about the mutability of the
object; it just disallows mutating it from the pointer (so, exactly how
immutable pointers work in this RFC).

> > > While I agree that in present day Hare mutability information should not be a
> > > part of the function interface, including it also opens up some interesting
> > > opportunities if we decide to use our own ABI instead of C's.
> >
> > ...does it? I feel like the mutability of a function parameter is
> > entirely an implementation detail, since the parameter is a copy.
>
> You don't actually need to perform the copy if the parameter is immutable.
> You'd think that's the same as passing through an immutable pointer, but it
> is not, for aliasing reasons, and it allows for some optimisations that
> wouldn't be possible otherwise. There's a recent new language that does
> something like that. Unfortunately, I forgot which one it is (there are so many!).

This is an interesting idea, though I don't think it's a good fit for
Hare. It's simpler to just treat all assignments (including function
arguments) as copies. Not to say we should disallow optimizations or
anything, but I think it's reasonable to say that if you don't want a
copy, then you should use a pointer.

[RFC v2] Mutability overhaul

Details
Message ID
<CVZAFP0FK7O9.3P1XCT5RKN6JG@notmylaptop>
In-Reply-To
<CVIYDE2MYJYN.1VKHYOWIYQ2OK@notmylaptop> (view parent)
DKIM signature
pass
Download raw message
# Terminology

None of this is part of the proposal per se, but this describes the
terminology the proposal will use. Bear with me through all of this; I'm
gonna be describing a new model for objects within the Hare abstract
machine and I swear this is relevant and I'll explain why in the
rationale, but yeah just trust me on this for now.

- An "object" within the Hare abstract machine is an instance of a type,
  which has a value. All objects are distinct, or, put another way,
  every object has a different address.
- Every expression yields a "reference" to an object. The object may
  have just been created, or it may be an object which was already
  referenced previously. References are the means by which objects are
  accessed and manipulated. There may be more than one reference to an
  object.
- A reference may have an "offset", which essentially means the
  reference points to somewhere in an object that isn't the beginning.
  Or, put another way, the address is that of the object plus the
  offset. Offsets are used for array indexing and struct/tuple field
  accessing.
- Every expression yields a reference, however, the following
  expressions will create a reference to an already existing object
  (either by yielding it directly, or by other means): binding,
  identifier access, array indexing, struct access, tuple access,
  slicing, alloc, unary &, and unary *.
- This means that persistent references only exist in the form of
  bindings, pointers, and slices. In all other instances, the reference
  stops existing after the expression of which it is an operand finishes
  evaluating, leaving the object inaccessible.
- "Mutability" is a property of a reference. A mutable reference can
  change the value of the object it refers to; an immutable reference
  cannot.
- Whether a reference is mutable is irrelevant when creating a new
  object (a copy), since the mutability applies to the reference, not
  the object itself.
- Throughout this proposal, I'll use the phrase "mutable binding" to
  refer to a mutable reference which was created by a binding
  expression, and which is used with an identifier access expression.
  "Mutable pointer" will refer to a pointer type containing a mutable
  reference, and "mutable slice" will refer to a slice type whose data
  is a mutable pointer (to an unbounded array).

It's also worth reiterating that the behavior described above is that of
the abstract machine, not the implementation of harec.

Here's some more terminology:

- "Literal" is used instead of "constant" for things like integer
  literals, string literals, array literals, etc.
- iconst, fconst, and rconst are renamed to ilit, flit, and rlit.

# Motivation

The motivation for all of this is that mutability should apply to
references, not to types. So, a binding may create a mutable reference,
and pointers and slices may contain mutable references. But the "const"
type flag is removed, since mutability doesn't apply to types.

I'll first go over the proposal as it stands right now (and how it's
currently being prototyped), then go over the rationale of certain
decisions, then go over unsettled aspects of the design, and parts of
the proposal which I'm not a huge fan of. This is to say, please hold
off on judgement until you've read through everything, since lots of
context is provided in the latter two sections.

# Current proposal

Syntax changes:
- In local bindings, `const` (for immutable bindings) is removed. That
  is, references created by local binding expressions are always
  mutable.
- In global bindings, `let` declares an immutable reference (replacing
  `const`), and `let mut` declares a mutable reference (replacing `let).
- `def` is replaced with `const`.
- `mut` can appear before `*` in a pointer type, before `[]` in a slice
  type, or before a type alias, to, respectively, represent a mutable
  pointer, a mutable slice, or a type alias whose underlying type is one
  of these.

Semantic stuffs:
- You can only assign through mutable references.
- Taking the address of an immutable reference of type `foo` yields a
  reference of type `*foo`. Taking the address of a mutable reference of
  type `foo` yields a reference of type `mut *foo`, indicating that the
  reference contained within the new pointer object is also mutable.
- Mutable pointers can be assigned to immutable pointers, but not the
  other way around.
- This is true of any `mut` within the type: e.g. `mut *mut []t` is
  directly assignable to `*[]t`.
- Mutable pointers can be cast to immutable pointers, without any
  limitations.
- Immutable pointers can be cast to mutable pointers to the same type.
  This means that e.g. to cast a `*rune` named x to `mut *u32`, you'd
  need to do either `x: mut *rune: mut *u32` or `x: *u32: mut *u32`.
  This also means that `*[4]int` can't be cast to `mut []int` without an
  intermediary cast.
- Mutability does not convey any information about ownership. That is,
  you can free an immutable pointer or slice, and you can free strings.
- append, insert, and delete require that the object reference is itself
  mutable, *and* has a mutable slice type. So, to pass a growable slice
  into a function, the type would need to be `mut *mut []t`. Both muts
  are required here.
- vaarg and vaend operate on mutable references.
- Expressions which implicitly dereference their operand check the
  mutability of the reference they're *actually* acting on: e.g. append
  can operate on a mutable pointer, or on an immutable pointer to a
  mutable pointer, so long as the final reference is mutable.
- When selecting a valid tagged union subtype (tagged_select_subtype),
  additional logic needs to be used if the subtype is mutable: if the
  subtype is assignable to multiple of the tagged union's types, but
  only one of those types preserves mutability, then that type is
  chosen.
  e.g. `let mut a: [1]u8 = [0]; a: ([]u8 | mut []u8);`
  This compiles: even though a is assignable to both `[]u8` and
  `mut []u8`, only `mut []u8` will be selected here. This is necessary
  for things like memio::fixed: the function needs to take in
  `([]u8 | mut []u8)`, since it may or may not be writable. (The stream
  itself can store the slices in a (non-tagged) union; this is only for
  the initializer function.)
- `mut` can appear before a type alias, in which case the type alias
  must refer to a pointer, a slice, or a type alias referring to an
  allowed type. This makes the reference contained in the underlying pointer or
  slice is made mutable. The reference may already be mutable, in which
  case `mut` has no effect. When unwrapping an alias which uses `mut`,
  mutability is preserved. So if the underlying type is a pointer, the
  unwrapped pointer is mutable. Ditto for slices. If the underlying type
  is another type alias, then the `mut` is carried over to that alias.
- Function parameter bindings remain mutable, except for the variadic
  paremeter of a function with Hare-style variadism, which is an
  immutable reference to a mutable slice.

The mutability algorithm:
- This is the central algorithm at play here. Given an expression, it
  determines whether or not the reference it yields is mutable.
- Put simply, a "mutable" reference is: a reference obtained by
  accessing an identifier created by a local binding, a
  global declaration using `let mut`, or a non-variadic function
  parameter; or a reference derived from a mutable pointer or a mutable
  slice, or an anonymous reference to a newly stack-allocated object
  (that is, the references created by literal expressions are
  mutable, so `[]` can be assigned to `mut []u8`, and `&0`
  has type `mut *int`. You can see now why I wanted to stop calling them
  "constants").
- The complete mutability algorithm is as follows:
  - If the expression is an identifier, check the expression or
    declaration that created the reference and put the identifier in the
    scope.
    - If it's a binding expression or a match expression, return true.
    - If it's a global declaration, check whether the declaration is
      mutable (that is, whether the declaration uses `let mut`).
    - If it's a function parameter, return true, unless it's a variadic
      parameter, in which case return false.
  - If the expression is an indexing or slicing expression, check the
    storage of the object being indexed or sliced.
    - If it's an array, run the mutability algorithm on the expression
      designating the array object.
    - If it's a slice, check if the slice is mutable.
    - If it's a pointer, check the referent's storage.
      - If it's an array, check if the pointer is mutable.
      - If it's a slice, check if the slice is mutable.
      - If it's a pointer, run the same check on the referent's storage.
      - If it's an alias, run the same check on the underlying type.
    - If it's an alias, run the same check on the underlying type.
  - If the expression is a struct/tuple access expression, check the
    storage of the object the field is being selected from.
    - If it's a pointer, check if the inner-most pointer is mutable
      (i.e., `**mut *x` passes, but `mut ***x` doesn't).
    - If it's an alias, run the same check on the underlying type.
    - Otherwise, run the mutability algorithm on the expression
      designating the struct/tuple object.
  - If it's a pointer dereferencing expression, check if the pointer is
    mutable (not the inner-most pointer, just the topmost pointer).
  - If none of the above, the reference refers to either a
    stack-allocated object, or an object which has no storage. The
    distinction doesn't really matter here, so in either case return
    true.

# Rationale

- As of now, "const"/"constant" in Hare has five separate mostly
  unrelated meanings: a constant declaration (as in `def`), an immutable
  global declaration (as in `const`), the "const" type flag for types,
  flexible constant types (iconst/fconst/rconst), and literal
  expressions ("constants"). With the proposed changes, "constant" is
  now *only* used to refer to constant declarations (which are now
  actually declared with `const`).
- So, uh, yeah, I guess I started this RFC by detailing a new model for
  the Hare abstract machine or something. That wasn't part of the
  original plan but it just kinda happened lol. The reason for this is
  that v1 of this RFC only referred to "objects", which caused issues
  since "pointer to immutable object" is subtly incorrect: the object
  may be mutable, it just can't be mutated indirectly through this
  pointer. Thanks bgs for pointing this out. I solved this by decoupling
  objects from the references to said objects, which actually solves a
  lot of problems: it means immutable pointers (and slices) can be
  correctly described ("pointer to immutable reference"), mutability can
  strictly apply to references (so it doesn't need to be a property of
  the pointer or slice, though it may be implemented as such), the
  object model is more versatile and allows for specifying complex
  pointer casts and arithmetic (#859), it fixes the internal
  inconsistency with unary & without changing its semantics, and I
  imagine it'll make specifying other parts of the mutability overhaul
  easier as well (e.g. casting an immutable pointer to a mutable pointer
  is only UB if no other mutable reference exists). I didn't separate it
  into its own RFC since it doesn't really have any implications for
  harec; it just rewords things in the spec, basically.
- Not *everything* described in the terminology section is inseparably
  relevant to the mutability overhaul, but I wrote all of it so it's
  clear how objects/references work with this model, since that part
  *is* relevant for this proposal.
- The terminology "mutable pointer" and "mutable slice" is not great,
  but the alternative is "pointer containing mutable reference" and
  "slice containing mutable reference", which are even less great,
  despite being more accurate.
- I anticipate that the removal of local immutable bindings in this
  proposal is going to be controversial. I was also initially skeptical,
  since I figured that immutability by default is always a good thing.
  But I don't think that's really the case, as I'll describe in the next
  few points.
- First of all, I've never encountered a bug in a Hare program that was
  caused by mutating a binding which I assumed wouldn't change, and I
  suspect that this is a very uncommon bug for others as well. This is
  especially true in Hare since shadowing is so common, so even if a
  *binding* is immutable, the identifier may refer to a completely
  different reference later on in the scope. So for something which
  isn't actually a problem in practice, having a distinction between
  mutable and immutable locals adds a lot of friction to the language,
  forcing the programmer to spend mental energy thinking about binding
  mutability when they could be thinking about more important things.
- In addition, I've found that the previous revision of this proposal
  (which distinguished between `let` and `let mut` for locals) put me
  into a state of believing that mutable=bad, and that I should try to
  avoid mutable bindings wherever possible. Needing to use `let mut`
  almost feels punishing, in a way. This is not a philosophy I want Hare
  programmers to follow: the most idiomatic Hare code is often
  imperative code which sets a variable by mutating it after it's
  declared. Avoiding mutability will result in more heavily
  expression-oriented code, which I think will be harder to follow and
  less idiomatic. And again, despite the syntax in v1 making it feel
  worse to create mutable locals, I don't think mutable locals are
  actually a real problem in practice.
- append/insert/delete must operate on both a mutable reference and a
  mutable slice. In the previous revision, code like
  `let mut x: mut []t = [];` was very common. Notice that `mut` needs to
  be used twice here, which is confusing and annoyingly verbose.
  Likewise, every for-loop initializer needed `mut` added to it, which
  is just more boilerplate. This isn't entirely fixed by this proposal
  (the double-mut is still present in growable slice parameters), but
  it's definitely an improvement.
- Finally, I'd like the mutability of function parameters to be the same
  as the "default" mutability for locals. Immutable function parameters
  actually cause some problems: the only way to mutate them, outside of
  introducing some new syntax for making the binding mutable (which I
  really don't want to do), is to shadow them. Within harec as it stands
  right now, this creates a copy, which can be pretty expensive. This
  flaw isn't intrinsic to immutable function parameters, since really we
  should be optimizing the copy out anyway. The bigger issue is that
  it's not possible to shadow a Hare-style variadic slice without
  allocating. And since it's not uncommon for code to want to mutate
  variadic parameters, we'd need to either make the variadic slice
  mutable, or, well, I'm not sure what the alternative would be. Having
  function parameters just be mutable side-steps this issue entirely.
- I also considered allowing immutable locals, but having mutable locals
  be the default. I decided against this as well, since I imagine that
  lots of Hare code simply wouldn't ever use immutable locals (as is the
  case in a lot of code now). This is also true in C, for instance (I
  know C is completely different with regard to const semantics, but my
  point still stands that you pretty much never see `const int x = 0;`
  in C). At that point, it almost becomes kinda more idiomatic to just
  have local bindings always be mutable. It sounds strange, but if you
  have to go through extra effort to make the binding immutable, and
  doing so isn't even that useful (as stated above), why spend mental
  energy thinking about it? It's also worth noting that this behavior is
  pretty much what Go does: `var` is to `let`, and `const` is to `const`
  (formerly `def`).
- Note that this revision uses a different syntax for locals than it
  does for globals (`let` creates mutable locals but immutable globals).
  This is temporary, and will be changed in a later revision, once we
  figure out the best syntax to use here. Unlike immutable locals,
  immutable globals are quite useful, so I have no plans to remove them.
- One concern that people may have: constant defines within compound
  expressions are now supported, so when `def` is changed to `const`,
  some of these will silently change meaning, but still compile. In
  practice, I don't forsee this being an issue, since there will be a
  tool which automatically (at least partially) converts
  pre-mutability-overhaul code to post-mutability-overhaul code, and
  local constants are very easy to handle.
- The syntax `mut *foo`, where `mut` appears before the `*` or `[]` as
  opposed to after, is unfortunate. `*mut foo` makes more sense, since
  that makes it visually obvious that the foo reference is what's
  mutable, not the pointer itself (as opposed to something like
  `nullable`, which is a property of the pointer). However, this is
  ambiguous when we factor in allowing `mut` to be used on type aliases:
  `*mut foo` could either be a pointer to a mutable foo, or a pointer to
  an immutable foo which contains a mutable reference.
- One example use-case for using `mut` on type aliases is
  hare::ast::ident. The type is an alias for `[]str`, but by using
  `mut ast::ident` you end up with an alias for `mut []str`.
- I think that the behavior of unary & within this proposal is correct.
  One suggestion was to make it so `&x` always yields an immutable
  pointer, and you'd need to do something like `&mut x` for a mutable
  pointer. I am pretty strongly against this, since 1. the semantics as
  they stand in my proposal check out: the reference contained within
  the pointer is identical to the reference yielded by the operand of
  the unary & expression, and 2. similar to my argument against `let
  mut` for locals, it adds a lot more verbosity and boilerplate to the
  language, which distracts from the code itself. Take `mut *io::stream`
  for instance. As of now, it's pretty common to have an io::stream
  implementation allocated on the stack, and pass it into functions with
  `&s`. Needing to use `&mut s` instead is, very bad. You could get
  around this by just making a binding initialized as `&mut s`, but this
  really shouldn't be necessary IMO.
- The restriction that immutable pointers can only be cast to mutable
  pointers of the same type is similar to what's described in
  https://todo.sr.ht/~sircmpwn/hare/372. The idea is that making a
  reference mutable is a dangerous operation, so it needs to be done
  explicitly on its own; it can't be combined with another dangerous
  operation (changing the secondary type of a pointer).
- Something to note is that type castability and assignability also
  depend on reference mutability (e.g. `[3]int` -> `mut []int` only
  succeeds if the array reference is mutable). There's no real way
  around this, and I think it's a worthwhile trade-off for mutable
  references.
- In C, pointers to `const` types can't be freed, so effectively the
  `const` qualifier conveys semantic information about whether an object
  is freeable. This proposal does *not* do this, since ownership is a
  separate problem and thus requires a separate solution (most likely
  linear types).
- Having literals yield mutable references may sound strange. However, I
  think it's the best approach. The example already given was using an
  array literal in place of a mutable slice. This is the most compelling
  example, but it's also nice to be able to create a mutable pointer
  without first needing to create a binding.
- The "const" type flag is entirely removed, so the only type flag left
  is "error". For now, the language and semantics surrounding type flags
  remains the same, especially since we may add type flags in the
  future, like for linear types. Maybe the best course of action here is
  actually to eliminate type flags entirely, but that's out of scope for
  this proposal. One really nice thing about this proposal (as I'm
  prototyping it) however is that we should be able to rid of
  strip_flags almost everywhere it's currently used, after making some
  changes to how harec handles type flags on aliases and how it handles
  error flags. (A side effect of this is that *int is no longer
  assignable to *!int, or vice versa. This is already the intended
  behavior within the spec).

# Open questions, and generally just things that are kinda meh

- I'm open to changing the terminology, like if we want "reference" to
  mean something different, in the context of linear types or something.
- I really dislike putting `mut` before `*` and `[]`, and if it's at all
  possible to make it work after then I'd really like to do it.
- We could solve the above problem by just, not allowing `mut` to be
  used on aliases. `mut` has limited utility here anyway: e.g.
  `mut types::slice` is an error, since the underlying type is a struct.
  I also think it's very strange to allow `mut` on aliases but not
  `nullable`. However, the use-cases for `mut` on aliases are very
  clear, so I'm conflicted. Any thoughts?
- Given the basis of this proposal (mutability applies to references),
  the fact that append/insert/delete require that both the reference and
  the slice are mutable makes perfect sense. However, it can still be
  kinda annoying, since growable slice parameters need to use `mut`
  twice. I don't think this is that big of a deal though.
- I kinda like having all local bindings be mutable. I'd like to hear
  other peoples' opinions though.
- The proposal replaces `def` with `const`. This isn't strictly
  necessary, and, especially given that the `const` keyword isn't used
  anywhere else, we could keep the name `def` here. I personally prefer
  `const`, but I also could go either way here.
- I don't really know where to go with the syntax for globals, assuming
  that we get rid of immutable locals. Ideally I'd like `let` to declare
  mutable globals, so it's consistent with locals, but then what about
  immutable globals? We can't use `const` since that's used by, well,
  constants, and even if it wasn't, I'm strongly opposed to using
  `const` to mean anything besides constants. How about `let immut`?
  Kinda ugly but, fine I guess? Not really a huge fan though. Maybe
  `var`? Nah, it's unclear whether `var` is for mutable or immutable
  globals, and there's no good way to remember it other than remembering
  that `let` is for mutable bindings. Also, `var` is mostly used in
  other languages for mutable bindings. Any ideas here?

thank you for coming to my ted talk (again)

Re: [RFC v2] Mutability overhaul

Details
Message ID
<CVZCOP8FCPTE.9Z5H0BGX67EH@d2evs.net>
In-Reply-To
<CVZAFP0FK7O9.3P1XCT5RKN6JG@notmylaptop> (view parent)
DKIM signature
pass
Download raw message
i'd like to see a prototype that doesn't allow mut on aliases, since if
that's workable it'd allow us to use the `*mut t` syntax, which i
prefer. not sure there's much more we can discuss in this context until
there's a harec implementation and the stdlib's been modified to work
under it

i'd kinda prefer to keep def for constants, mostly since that's what my
brain is used to, but i'm fine with const as well. i'm also fine with
global let being immutable and let mut being mutable, even though it's
inconsistent with local let: the idea is that let means "give me a
binding whose immutability is whatever usually makes sense for this
context", and let mut overrides this for globals. other than that,
let immut also seems reasonable, and if we keep constants as def then
tbh i wouldn't be opposed to const for immutable globals

other than that, +1 to everything here

Re: [RFC v2] Mutability overhaul

Details
Message ID
<CVZG9YD17NK2.2B0KXI54GUZO9@notmylaptop>
In-Reply-To
<CVZCOP8FCPTE.9Z5H0BGX67EH@d2evs.net> (view parent)
DKIM signature
pass
Download raw message
On Tue Oct 3, 2023 at 11:55 PM EDT, Ember Sawady wrote:
> i'd like to see a prototype that doesn't allow mut on aliases, since if
> that's workable it'd allow us to use the `*mut t` syntax, which i
> prefer. not sure there's much more we can discuss in this context until
> there's a harec implementation and the stdlib's been modified to work
> under it

Yeah, I'll experiment with this a bit.

> i'd kinda prefer to keep def for constants, mostly since that's what my
> brain is used to, but i'm fine with const as well.

So, I'm fine with def if people want to keep it like that. But that
being said, it kinda feels to me like "def" only exists since "const"
was already taken. It's more common to use "const" for constants in
other languages; the only example of "define" (or similar) being used
for constants that I know of is C, and even then it actually defines
macros, not "constants" per se. I've also seen "def" and "define" used
for various things that *aren't* constants, like functions in Python for
example. All of this is to say, I'm still fine with def, and tbh I don't
really care that much, but there should probably be some better
reasoning for it other than that being what you're used to.

> i'm also fine with
> global let being immutable and let mut being mutable, even though it's
> inconsistent with local let: the idea is that let means "give me a
> binding whose immutability is whatever usually makes sense for this
> context", and let mut overrides this for globals. other than that,
> let immut also seems reasonable, and if we keep constants as def then
> tbh i wouldn't be opposed to const for immutable globals

NACK to const for immutable globals. One of the main objectives of this
RFC is to make it so "const"/"constant" refers to only one thing:
constants. Having the "const" keyword be used to create things that
aren't constants is, very bad.

-1 to let being immutable for globals (I'm not staunchly opposed or
anything, I just don't think it's a very good idea as of now). The thing
is, there's no such thing as "mutability that usually makes sense for
this context", since only the programmer actually knows what makes sense
for any given declaration.

I wonder if we should always require something after the `let` for
globals, so that way you're always being explicit about the mutability
you want:

	let mut x = 0;
	let immut y = 0; // or something
	fn f() void = {
		x = 1; // works
		y = 0; // compile error
	};

This also avoids having it be unclear whether "let" is mutable or not,
and it means we don't need to make mutability/immutability the "default"
over the other.

Idk, `let immut` is reasonable I guess? But it feels weird, and I'm not
a huge fan of it. I think it's mainly the "immut" keyword rubbing me the
wrong way. Not gonna say that I'm opposed just yet, though.

Re: [RFC v2] Mutability overhaul

Details
Message ID
<CVZPB0RW03QT.3GLW7SBHBEI9N@taiga>
In-Reply-To
<CVZAFP0FK7O9.3P1XCT5RKN6JG@notmylaptop> (view parent)
DKIM signature
pass
Download raw message
Just because I am dumb and lazy can you annotate this with some code
samples demonstrating the before/after difference or special features of
this approach?

Re: [RFC v2] Mutability overhaul

Details
Message ID
<CW13M1B8ZC06.32BV661G3NMPR@d2evs.net>
In-Reply-To
<CVZG9YD17NK2.2B0KXI54GUZO9@notmylaptop> (view parent)
DKIM signature
pass
Download raw message
On Wed Oct 4, 2023 at 6:44 AM UTC, Sebastian wrote:
> > i'd kinda prefer to keep def for constants, mostly since that's what my
> > brain is used to, but i'm fine with const as well.
>
> [big pile of reasoning]

aight yeah +1, i'm fine with def but i'd also prefer const now

> Idk, `let immut` is reasonable I guess? But it feels weird, and I'm not
> a huge fan of it. I think it's mainly the "immut" keyword rubbing me the
> wrong way. Not gonna say that I'm opposed just yet, though.

i... guess we could use const here instead?

let mut x = 0;
let const x = 0;

but yeah let immut is probably the thing to do. i'd still be ok with
immutable `let` + mutable `let mut`, though
Reply to thread Export thread (mbox)