#Language design question: why conflate pointers and mutability for function arguments?

1 messages · Page 1 of 1 (latest)

paper turret
#

Apologies for the basic question, and I'm sure there is a good reason for this, but I'm wondering if its written up somewhere? I went looking for an issue or something with discussion but was wondering if anyone had anything to point me to.

Feels inconsistent to me that adding a * flips the assumption from const-by-default to mutable-by-default for arguments.

IE if I have:

fn some_func(
   // const, contents of some_type aren't mutable (ie, can't arg1.field += 1)
   arg1: some_type,
   // contents of pointed at some_type are mutable. (arg2.field += 1 is ok)
   arg2: *some_type,
   // const again (arg3.field += 1 is not ok)
   arg3: *const some_type,
   // but ? doesn't add mutability
   // (arg4.field += 1 is not ok)
   arg4: ?some_type,
) void
{
  ...
}
cedar lantern
#

arg2 isn't mutable

umbral ore
#

its consistent, the pointer is const, just not what it points to

cedar lantern
#
fn someFunc(arg: *u8) void {
   arg = &"a"[0]; // <- this is an error
}
paper turret
#

sure, but the stuff it points at is mutable

cedar lantern
#

yep

paper turret
#

thats the confusing part

#

I'll update the note

umbral ore
#
   // but ? doesn't add mutability
   // (arg4.field += 1 is not ok)

why would an optional make it mutable?

paper turret
#

why would a *?

umbral ore
#

because youre pointing to mutable memory

paper turret
#

I guess the question is why are pointers not const by default

#

or * const or whatever

cedar lantern
#

might want to see if there's an issue and if not open one

umbral ore
#

thats just what zig does, we specify const instead of mut

paper turret
#

except in arguments

umbral ore
#

not really

#

you cant make an argument mutable

paper turret
#

well, effectively you can, for structs

cedar lantern
#

how tho

#

the only way I know how to is to assign to a variable

#
fn amogus(non_mutable: u8) void {
    var mutable = non_mutable;
    mutable = 69; // <- this is now allowed
}
paper turret
#

sure, but in the example I posted, if this is a struct, and you have:

arg1.struct_field += 1 or whatever, if you switch the argument to have a *, you don't have to change the syntax - arg1.struct_field is now allowed

umbral ore
#

the argument is still const

dusk radish
#

the ptr is non changeable, so the pointer is the one that is const

#

is still consistent

honest junco
#

Without this all functions would have to be pure and tons of things would not work.

uncut vessel
#

does zig force you to clone args to variables if you want to change them? (in a pure function, just for inside calculations).

#

ie are args always treated as const

dusk radish
#

the pointer is inmutable but the pointed memory is not

uncut vessel
#

yeah I know how passing by reference works but I wondered if there was syntax for specifying that a function call should clone its arguments (by value)

dusk radish
#

Why? reasoning? need details

uncut vessel
#

okay so a primitive argument in C is copied, then pushed onto the call stack as part of the function stack frame

#

imagine my stack is

#

arg |

#

and I call a function

#

that stack will be arg | ret adress - arg - local variables etc |

#

the (primitive) argument is passed by value and thus copied and pushed along with the rest

#

which allows you to change that argument in the function scope (the original won't be affected)

#
#include <stdio.h>

void changenum(int i){
    i = 8; // i is const but can be rebound because it was cloned
    fprintf(stderr, "%i\n", i); // prints 8
}

int main() {
    const int i = 9;
    fprintf(stderr, "%i\n", i); // prints 9
    changenum(i); // clones i and pushes it along with the function 
frame
    fprintf(stderr, "%i\n", i); // still prints 9
    return 0;
}```
#

whereas in zig

#
const std = @import("std");

fn changenum(i: i32) void {
    i = 8; // error: cannot assign to a const
}

pub fn main() void {
    const i: i32 = 9;
    std.debug.print("{}\n", .{i}); // prints 9
    changenum(i); // arg is not cloned apparently 
}
#

get what I mean?

uncut vessel
#

I'm wondering if the only way to rebind an argument is by cloning it

#
.LC0:
        .string "%i\n"
changenum:
        sub     rsp, 8
        mov     edx, 8
        mov     esi, OFFSET FLAT:.LC0
        mov     rdi, QWORD PTR stderr[rip]
        mov     eax, 0
        call    fprintf
        add     rsp, 8
        ret
main:
        sub     rsp, 8
        mov     edx, 9
        mov     esi, OFFSET FLAT:.LC0
        mov     rdi, QWORD PTR stderr[rip]
        mov     eax, 0
        call    fprintf
        mov     edi, 9
        call    changenum
        mov     edx, 9
        mov     esi, OFFSET FLAT:.LC0
        mov     rdi, QWORD PTR stderr[rip]
        mov     eax, 0
        call    fprintf
        mov     eax, 0
        add     rsp, 8
        ret

this is the c asm (the 9 is pushed again)

dusk radish
#

if you want to asign to it, pass it a variable a thing that is not constant and as a pointer

uncut vessel
dusk radish
#

Then don't

#

😄

uncut vessel
#

well now I just reassign the arg to a var at the start of the function but that seems excessive

#

so I thought maybe there's a way to circumvent it

dusk radish
#

Nah

uncut vessel
#

wait lemme put it differently

dusk radish
#

As your name say: Git gud

uncut vessel
#

it's unique for a value passed primitive argument to not be mutable, because internally all function frames contain clones of the arguments anyways

dusk radish
#

The whole point of it being constant is that zig can elide copies at will

uncut vessel
#

there is no matter of promising right, this is how stack frames work period? unless you're inlining functions

dusk radish
#

This is not how function frames work

uncut vessel
#

so you're saying a function is called with an argument and that argument is not found in the function frame?

dusk radish
#

A function frame is a ficticious thing

uncut vessel
#

because as far as I'm aware the whole point of function frames is to contain everything a function owns in terms of variables

dusk radish
#

And this stack will be made however you want xD

#

Each arch and each codegen thing will do whatever it needs to make the high level construct work

uncut vessel
#

it's still a stack though

dusk radish
#

It can still elide the copy

uncut vessel
#

so the current function is on top

dusk radish
#

specially since args can and probably will be passed on registers or the stack

#

so no

#

Arguments won't always end up in the stack

uncut vessel
#

fair

#

so zig optimisation could place an argument directly into a register and not clone it?

dusk radish
#

If it fits in the register

#

sure

uncut vessel
#

most primitives fit in registers right

#

apart from maybe 128bit numbers

dusk radish
#

Even 128 bit numbers

#

tho not a lot of them in most arch

uncut vessel
#

that's just firmware magic though right

#

like instruction magic

#

because the ALU does not support 128 bit arithmatic usually

dusk radish
#

Implementation defined

uncut vessel
#

okok

#

the more you know

dusk radish
#

But yes, basically the inmutable thing let zig pass by reference (register) or by value (copy to stack) without you really caring

#

You then can force passing by reference via pointer