Skip to content

ambiguity with implicit returns and implicit semicolons #292

@thejoshwolfe

Description

@thejoshwolfe

The combination of implicit semicolons after certain statements and the implicit return at the end of a block (e.g. a function body) causes ambiguity.

Currently these are all equivalent:

fn abs(x: f32) -> f32 {
    if (x < 0.0) {
        return -x;
    } else {
        return x;
    }
}
fn abs(x: f32) -> f32 {
    return if (x < 0.0 {
        -x
    } else {
        x
    };
}
fn abs(x: f32) -> f32 {
    if (x < 0.0) {
        -x
    } else {
        x
    }
}
fn abs(x: f32) -> f32 {
    return if (x < 0.0) -x else x;
}
fn abs(x: f32) -> f32 {
    if (x < 0.0) -x else x
}

However, this is problematic because of the ambiguity that comes from an implicit semicolon after some statement-level constructs.

fn main() {
    if (a) {
        foo();
    } else {
        bar();
    } // no semicolon needed here
    if (b) {
        bar();
    } // or here
}

The implicit semicolon is valuable because C has so thoroughly conditioned us to expect compound statements that use {} to end at the }. But where does the implicit semicolon really go? What about these examples:

fn main() {
    if (a) { foo() } else { bar() }
    if (b) { bar() }
    if (a) foo() else bar()
    if (b) bar()

    if (a) foo else bar
    (); // does this call bar()?
    if (b) bar
    {} // does this struct-initialize bar{}?

    ( bar )();
    { bar }(); // what does this mean?
    if (b) { bar }(); // what does this mean?

    // this should probably be a syntax error, but it currently isn't:
    comptime 1 1;
}

Proposal:

When one of the following "compound statement"-like constructs appears at block scope, it can optionally qualify for an implicit semicolon if it uses {} around its "body"-like component (details below):

  • switch: already always uses {}, so always qualifies for implicit semicolon.
  • {...}: a nested block at block scope always qualifies for implicit semicolon.
  • while, for: putting {} around the body qualifies for implicit semicolon.
  • comptime: putting {} around the expression qualifies for implicit semicolon.
  • if, try: putting {} around the "else" body, if any, otherwise around the "then" body qualifies for implicit semicolon.

If a statement qualifies for an implicit semicolon, the statement ends at the } and cannot be its containing-block's return expression. In other words, the implicit semicolon is always effectively present, not optionally present if necessary.

Here are some examples of this proposal in action:

fn abs(x: f32) -> f32 {
    // no {}, so no implicit semicolon, so this is the return expression:
    if (x < 0.0) -x else x
}
fn abs(x: f32) -> f32 {
    // this `if` is not at statement level, so these {} don't earn any implicit semicolons:
    return if (x < 0.0) {
        -x
    } else {
        x
    };
}
fn abs(x: f32) -> f32 {
    // this statement gets a semicolon, and so does nothing
    if (x < 0.0) {
        -x
    } else {
        x
    }
    // error for not returning an f22
}
fn main() {
    { bar }(); // syntax error for empty `()`
    { bar };(); // equivalent to the above
    if (b) { bar }(); // syntax error for empty `()`
    if (b) { bar };(); // equivalent to the above

    comptime 1 1; // syntax error
    comptime {1} 1; // ok
}
fn getKindName(kind: Kind) -> []const u8 {
    // need the return here because switch uses {}
    return switch (kind) {
        KindA => "A",
        KindB => "B",
    };
}

A separate idea to consider is to require that the "then" and "else" bodies if if and try agree on use of {}. That would simplify this proposal slightly, but is really a separate idea.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugObserved behavior contradicts documented or intended behavior

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions