ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
35k stars 2.56k forks source link

Clarify compile time variables and calls with sigil or other prefix #1769

Closed lerno closed 5 years ago

lerno commented 5 years ago

One difficulty I have in reading compile time code is that variables are not easily distinguishable from normal variables.

Here is part of the code for the printf example:

pub fn printf(self: *OutStream, comptime format: []const u8, args: ...) anyerror!void {
    const State = enum {
        Start,
        OpenBrace,
        CloseBrace,
    };

    comptime var start_index: usize = 0;
    comptime var state = State.Start;
    comptime var next_arg: usize = 0;

    inline for (format) |c, i| {
        switch (state) {
            State.Start => switch (c) {
                '{' => {
                    if (start_index < i) try self.write(format[start_index..i]);
                    state = State.OpenBrace;
                },
                '}' => {
                    if (start_index < i) try self.write(format[start_index..i]);
                    state = State.CloseBrace;
                },
                else => {},
            },        

Here I would argue that it is difficult to know which ones are compile time parameters.

As a contrast, if we'd annotate them with a sigil it would become crystal clear what calculations involve compile time values, and since those values now become unambiguous we can eliminate the comptime qualifier even

pub fn printf(self: *OutStream, $format: []const u8, args: ...) anyerror!void {
    const State = enum {
        Start,
        OpenBrace,
        CloseBrace,
    };

    var $start_index: usize = 0;
    var $state = State.Start;
    var $next_arg: usize = 0;

    inline for ($format) |$c,$i| {
        switch ($state) {
            State.Start => switch ($c) {
                '{' => {
                    if ($start_index < i) try self.write($format[$start_index..$i]);
                    $state = State.OpenBrace;
                },
                '}' => {
                    if ($start_index < $i) try self.write(format[$start_index..$i]);
                    state = State.CloseBrace;
                },
                else => {},
            },        

The main advantage comes when compile time and runtime variables mix:

comptime var i = 0;
    var sum: usize = 0;
    inline while (i < 3) : (i += 1) {
        const T = switch (i) {
            0 => f32,
            1 => i8,
            2 => bool,
            else => unreachable,
        };
        sum += typeNameLength(T);
    }

Compared to:

    var $i = 0;
    var sum: usize = 0;
    $while ($i < 3) : ($i += 1) {
        const T = switch ($i) {
            0 => f32,
            1 => i8,
            2 => bool,
            else => unreachable,
        };
        sum += typeNameLength(T);
    }

Here I've taken the liberty of dropping "inline" for a sigil as well.

I've picked $ for sigil at random. It can be any sort of available symbol really. The main point here is that we can directly "know" – even if the declaration was further up – what variables will be available during runtime.

That is, we can quickly determine what "goes away" during runtime, reasoning like this:

    // var $i = 0;
    var sum: usize = 0;
    // $while ($i < 3) : ($i += 1) {
        const T =  // switch ($i) {
            //0 => f32,
            //1 => i8,
            //2 => bool,
            //else => unreachable,
        //};
        sum += typeNameLength(T);
    //}

And following that:

    var sum: usize = 0;
    const T1 =  // something
    sum += typeNameLength(T1);
    const T2 =  // something
    sum += typeNameLength(T2);
    const T3 =  // something
    sum += typeNameLength(T3);

This would be such a huge change to the language that I doubt this proposal will be accepted, but the problem is real and even though sigils might not be a solution for Zig, being able to distinguish between compile time and runtime variables is important, and need to be easy even for longer functions. If sigils are inappropriate, then maybe require some "inline" or similar prefix to indicate that here indeed we're mixing runtime with compile time values.

ghost commented 5 years ago

this could maybe be something that could be done via syntax highlighting, maybe using the language server in the self hosted compiler

andrewrk commented 5 years ago

This would be such a huge change to the language that I doubt this proposal will be accepted, but the problem is real and even though sigils might not be a solution for Zig, being able to distinguish between compile time and runtime variables is important, and need to be easy even for longer functions.

I agree with this.

One thing to consider is whether normal constants get the same treatment. Would the sigil only notate "mutable at compile time" variables and control flow? What about constants, which trigger the same kind of code deletion?

andrewrk commented 5 years ago

Another thing to consider is that if you execute a function at compile time, the sigils lose their meaning, and all variables become the same.

lerno commented 5 years ago

Syntax highlighting as proposed by @monouser7dig will definitely help. Some alternatives to sigils (that probably are worse):

Regarding the normal constants... I think not. $ would indicate that this is a variable that can change value during compilation. It think that's the tricky part and where you'd have to be careful. That's why my alternative was to annotate the conversion of compile-time to runtime variable assignment with a keyword so it's clear that the assignment is actually static once it reaches the runtime.