Alternative declaration syntax

I’ve been thinking about the order of tokens from time to time.
In this example we should group right part as *(const T).
And sometimes I have some gut feeling that something is wrong here.

I will try to explain.

Let’s start with these simple declarations, in C and in Zig:

const u32 i = 7; // modifier, type, name, initializer
      u32 i = 0; // type, name, initializer
const i: u32 = 7; // modifier/qualifier(???), name, type, initializer
  var i: u32 = 0; // modifier/qualifier(???), name, type, initializer

Here I’d like to note that I definitely like Zig’s way more, it’s very good, that Zig have not embraced C’s token order. Variable name first, variable type last!

But it seems to me, that const/var here serve two different purposes:

  • denote a name for a memory cell
  • denote mutability of the content of that memory cell.

So it’s a kind of confusion of notions. Let’s split them!

val ro i: u32 = 7; // ro = read-only, i.e immutable, constant
val rw i: u32 = 0; // rw = readable-writable, i.e mutable, variable

You could say - “remove this val keyword and you will get same story”. But wait, here come pointers!

val rw ptr: *ro T; // ptr is a mutable address to constant data.

or may by

val ptr rw: *T ro = &something;

Which would mean “Hey, compiler, listen up! I want memory cell which I want to name ptr, I want this cell to be changable, treat this cell as a pointer to T, but do not let me change the pointee”.

What’s your opinion? :slight_smile:

BTW, Rust is (partly) doing exactly this (let <here is implicit immut>, let mut).

Look at it this way.

The modifier acts on the mapping between a name and a value.
name <> value
pointer <> cell

What it denotes is whether rhs-item can be modified through the lhs-item.
That is to say, it marks whether lhs allows reassignment. No confusion in sight.

Your val [ro|rw] V is exactly analogous to [const | var] V.
Your *[ro|rw] T is exactly analogous to *[const|ø] T.
You just changed the names.


On proposed syntax
Nothing should intervene between the modifier and the name, because this would break grep-ability.

No, I decomposed the semantic of const/var into two absolutely independent parts:

  • give a memory cell/cells a name/type
  • indicate whether cell content is mutable or not

In C:

  • there is no special keyword for a variable
  • but there is a const modifier

In Zig:

  • there are both var and const, used to introduce name and type for a memory cell
  • const is also used as a modifier/qualifier for pointers, on the right side of the : separator

In Rust:

  • both changeable and unchangeable memory cells are introduced with let
  • but in order to be mutable, memory cell must be declared as let mut

Add imm (for ex.) keyword to Rust and this will be what I am talking about:

let mut x = 0;
let imm y = 7;
val ptr rw: *T ro = &something;

I can not see anything between ptr (name) and rw (modifier), as well as between T (type name) and ro (modifier) here, can you?

val rw ptr: *ro T;

Same here.

If we were to value consistency over convenience (less typing), I think the syntax would be:

// Variable data in memory
var x: u32 = 42;
// Constant data in memory
const y: u32 = 42;

// Variable pointer to variable data in memory
var x_ptr: *var u32 = &x;
// Variable pointer to constant data in memory
var y_ptr: *const u32 = &y;

// Constant pointer to variable data in memory
const x_ptr: *var u32 = &x;
// Constant pointer to constant data in memory
const y_ptr: *const u32 = &y;

More verbose, but symmetric, consistent, and in sync with how it’s read in English; especially the common meaning of something constant that doesn’t change, and something variable that can change (vary).

4 Likes

Yep, that was my idea
But I am talking about something else. I’m talking about single keyword both for variables and constants (because they are both names for memory cells), like Rust’s let and some special keywords for modifiers (mut/imm, rw/ro…)

In that case mem is more accurate than val:

mem rw ptr: *ro T;
mem ro ptr: *rw T;

If we were to only put qualifiers on the type:

mem ptr: rw* ro T;
mem ptr: ro* rw T;
// or
mem ptr: var* const T;
mem ptr: const* var T;
// or
mem ptr: *var const T;
mem ptr: *const var T;

just remembered this from “The Zen of Python”:

Explicit is better than implicit

Look who’s talking! :rofl:

Oh, yeah, this was one of my two variants, I just decided to use val in this imaginary syntax.

Great!!!

mem ptr: rw* ro T;

mutable pointer to permanent data, did I guess it right?

1 Like

Also think about Harvard architecture. In avr-libc there is special macro. Where would you place such an instruction for Zig?

I can imagine something like this:

mem ro,progmem x: u8 = 0x33; // list of modifiers!
val x ro,progmem : u8 = 0x33;

Constants aren’t always in memory other than the program itself though, so I don’t see how mem is more accurate. Also const T = u32; exists. There the variable and the value in it doesn’t even exist after leaving the compiler.

2 Likes

I don’t feel you at all took to heart what I wrote.
I expressed the exact same thing that you say here with *[const|ø] T. Everything else is exactly symmetric. You just provided a name for what is ø.

I’ll reiterate:
Your val [ro|rw] v is exactly analogous to [const | var] v .
Your *[ro|rw] T is exactly analogous to *[const|ø] T.

You did not decompose it into absolutely independent parts, you remapped
const -> ro,
var -> rw, and
ø -> rw.

And the addition of val is simply redundant. You can look at Rust’s let and let mut as either let [ø|mut] or [let | let mut]. The outcome is exactly the same. As a matter of fact, & and &mut are considered different types in Rust, that is, the mut here is not a qualifier, but part of the type name.

You did not change the semantics – both variants flag the lvalue as either open to assignment or no. *[const|ø] T flags the assignment operation t.* = x; as legal or not, just as [const|var] v flags v = y; as legal or not.

I see zero benefit from this apart from granting a wish for symmetry.

1 Like

And your idea that [var|const] v is essentially a reference to a memory cell in Zig is misguided. They deal with symbol declarations and they can have different meanings, like const std = @import("std") or var x: u0 = undefined. Both are legal and do not refer to any item in memory at all.

1 Like

maybe just an unpopular opinion but i like C’s function decleration more may be not from an artistic point of view, but you can see the return type of the function right away and decide if you care about it, but again most of the popular C return type is just int to let the caller know if it succedded or not, again what am i talking bout then lol.

but i think even for variables you might want to see the type first to decide “is it the thing i care about” rather than depending on the variable name and going to the end and see the variable type… just feels like C’s way of variable/function decleration can help you a little to read the code fast?

Agreed. First one is using const for type declaration, and the second… hmm :slight_smile:
But, please, do not get me wrong - I am by no way criticizing Zig syntax nor making any proposals. I just was pondering over various ways of declaring variables/constants -
originally the head post of the topic was just a comment in this topic and I had no intention to make a full-blown discussion about it.

But then @Sze considered that comment to be off topic (and it really was there)
and suggested to make a separate topic in Brainstorming category, I agreed, why not? :slight_smile:

So, my ramblings was not about Zig at all, it was an attempt to brain around various ways of declaring entities in general.

1 Like

Yes, “explicit is better than implicit”, see “The Zen of Python” again :rofl:

In C (no var keyword) everything is mutable by default.
But there is special const (true!) modifier.

In Rust everything is immutable by default.
But there is special mut (true!) modifier.
Does not matter if that mut is a part of type or not - it’a separate token.
It 's a modifier for a type (in &mut), ok?

In Zig there is no defaults, you have to use var/const explicitly.
And that’s great, but why only for the left part for declaration?
Why var ptr: *T, why not var ptr: *var T?

Let’s look again at const ptr: *const T.

What does the first const mean? The meaning is twofold, “two-in-one”, so to say:

  • to declare a name
  • to indicate that a value named by that name is immutable

What does the second const mean? Here is only one:

  • it’s a modifier/qualifier for T, that’s all

You can omit the second (thus making the pointee mutable), but you can not omit the first.

Now there is const s32 = i32, yeah. Types are values at comptime, I know. :slight_smile:
When I saw Zig for the very first time is was very unusual, btw… but I got accustomed to this very quickly.

All in all - const in Zig can have three different meanings depending on context.

Another interesting opinion: On Removing Let and Let Mut

1 Like

This is it:

Specifying whether the variable can change

  • changeable/mutable variable is a tautology
  • unchangeable/immutable variable is an oxymoron.

When they say “the variable”, they actually mean “the content of a memory cell”, don’t they?