Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gotocase: a case for useful and not harmful goto #5950

Closed
y0shir opened this issue Jul 29, 2020 · 11 comments
Closed

gotocase: a case for useful and not harmful goto #5950

y0shir opened this issue Jul 29, 2020 · 11 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@y0shir
Copy link

y0shir commented Jul 29, 2020

While it's obvious that goto is considered harmful, sometimes it is still a usefull tool.
Because zig dosen't support switch falltrough, i propose an alternative: gotocase.
Let's consider this esample from https://wiki.c2.com/?GoTo in C

Mask *pMask(NULL); switch (type) {
case POINT:
	Mask tmpMask;
	vector<Location> &v=getVectorOfPoints();
	for (iterator it=v.begin();it!=v.end();++it)
	tmpMask.set(*it);
	pMask = &tmpMask;
	goto drawMask;
case MASK:
	pMask=getBitMask();

drawMask:
	// here's the code to draw the mask overlay
	break;

}

This obviously saves time and looks more elegant than combination of if statements and functions.
Perhaps with gotocase it could look like this:

switch (type) {
 POINT =>
	//dopointstuff
	gotocase else;
MASK =>
	pMask=getBitMask();
        gotocase else;
else =>
	// here's the code to draw the mask overlay
}

To enforce good code practices one could restrict gotocase to apear only at the end of the case and only cases that are further down.

@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jul 29, 2020
@andrewrk andrewrk added this to the 0.7.0 milestone Jul 29, 2020
@joachimschmidt557
Copy link
Member

I think the more idiomatic zig way for handling that example would be to define pmask with a switch:

const pmask = switch (xyz) {
    .Point => {
        // more complex code
    }
    .Mask => getBitMask(),
};

// code for drawing

@ghost
Copy link

ghost commented Jul 30, 2020

Why only cases that are further down? Seems like an unnecessary restriction, and along with only working inside switches makes this feature very edgecasey.

We could implement this from more flexible components with a restart keyword that jumps back to the beginning of a block or a loop iteration. I won't post an example for this use case as it hardly benefits, and is easily implemented idiomatically without repetition.

@joachimschmidt557
Copy link
Member

joachimschmidt557 commented Jul 30, 2020

gotocase, as far as I can see, is there for removing code duplication in situations where multiple cases of a switch contain very similar (almost exact) statements.

switch (abc) {
    .Car => {
        foo("car");
        bar(42);
        baz(1337);
    },
    .Train => {
        foo("train");
        bar(42);
        baz(1337);
    },
}

If they were exactly the same, we could just combine them.

switch (abc) {
    .Car, .Train => {
        foo("transportation thing idk kthxbai");
        bar(42);
        baz(1337);
    },
}

But we can solve this problem without needing gotocase.

fn doThing(name: []const u8) void {
    foo(name);
    bar(42);
    baz(1337);
}

switch (abc) {
    .Car => doThing("car"),
    .Train => doThing("train"),
}

Furthermore, a prominent example of the linked website on the first issue comment displays the usage of goto in FSM-based lexers. I actually don't think lexers using that are better. This makes understanding them a more tedious task. If there is not only the obvious way I can jump into a switch case, but also non-obvious ways (jumped from other cases), where did I come from? What does this case really do? The best counterexamples for the necessity of lexers with gotocase are the lexers for zig and C in the standard library.

@ghost
Copy link

ghost commented Jul 30, 2020

I agree with @joachimschmidt557, introducing even just forward gotos in switch statements would considerably reduce their transparency. At present, each switch case can be understood in isolation. With the proposed change this would no longer be true, because the environment could have been modified somewhere between entering the switch and getting to the case in question.

Giving up on this transparency is a pretty big deal and would require a commensurate benefit, which I don't see here. At the very least, a better example is needed, since the given one can be implemented in better ways even in C++, let alone Zig.

@pixelherodev
Copy link
Contributor

pixelherodev commented Jul 30, 2020 via email

@ekipan
Copy link

ekipan commented Aug 4, 2020

a restart keyword

So, continue! Switches already break, after all.

switch (val) {
  .case1 => continue .case3,
  .case2 => do_case2(),
  .case3 => do_case1_and_3(),
}

@joachimschmidt557
Copy link
Member

joachimschmidt557 commented Aug 4, 2020

@ekipan, as I mentioned in my comment earlier, what you wrote can be achieved without continue in the current zig syntax:

switch (val) {
  .case2 => do_case2(),
  .case1, .case3 => do_case1_and_3(),
}

@ekipan
Copy link

ekipan commented Aug 4, 2020

@joachimschmidt557 Sure, I was just talking syntax though. I've no opinion on the feature itself. Multicase prongs can't do this:

switch (val) {
  .case1 => do_case1(); continue .case3,
  .case2 => do_case2(),
  .case3 => do_case1_and_3(),
}

But again you could just use functions:

switch (val) {
  .case1 => do_case1(); do_case1_and_3(),
  .case2 => do_case2(),
  .case3 => do_case1_and_3(),
}

@raulgrell
Copy link
Contributor

The use case I thought this was going to address is implementing something like computed gotos, described here: https://eli.thegreenplace.net/2012/07/12/computed-goto-for-efficient-dispatch-tables

// In C

int interp_goto(unsigned char* code, int initval) {
    /* The indices of labels in the dispatch_table are the relevant opcodes*/
    static void* dispatch_table[] = { &&do_halt, &&do_inc, &&do_dec};
    
    #define DISPATCH() goto *dispatch_table[code[pc++]]

    int pc = 0;
    int val = initval;

    DISPATCH();
    while (1) {
        do_halt:
            return val;
        do_inc:
            val++;
            DISPATCH();
        do_dec:
            val--;
            DISPATCH();
    }
}

Where it would allow us to express the above with something like

// In Zig

fn dispatch(code: []const u8, pc: *u32) OpCode {
    defer pc.* += 1;
    return @intToEnum(OpCode, code[pc]);
}

fn interp_goto(code: []const u8, initval: u32) u32 {
    var pc: u32 = 0;
    var val: u32 = initval;

    switch (dispatch(code, &pc)) {
        .Halt => return val,
        .Inc => {
            val += 1;
            gotocase dispatch(code, &pc);
        },
        .Dec => {
            val -= 1;
            gotocase dispatch(code, &pc);
        }
    }
}

On one hand we lose the cue from "while" that there is effectively a loop.

@ghost
Copy link

ghost commented Aug 4, 2020

@raulgrell, the original proposal here is much more limited in scope than what you have in mind. By restricting jumps to be forward only, it allows avoiding certain kinds of duplication, but prevents most kinds of spaghetti code. If backward jumps were to be allowed, arbitrary control flow could be implemented, like the FSM in your example. At that point, we are dealing with a full-powered goto, and it would make little sense to stuff it into to switch statements with gotocase. I think this calls for a separate proposal 😄 .

@andrewrk
Copy link
Member

The use case in this proposal is sufficiently addressed by the comments in this issue as well as #8220.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

6 participants