In Which Code The Parsing Of Function Happen In Rustc?

Hello, I'm trying to experiment Rust with semicolon inference. For now, it can handle this, except the handling of call function that has return value at the last code of function that does not has return type isn't inserted semicolon yet

#![no_std]

fn return_i32() -> i32 {
    let a = 10
    let b = 20
    a + b
}

fn no_return() {
    let a = 
    return_i32
()
    let b = 30
}

fn expect_i32() -> i32 {
    let a = if 2 == 2 {
        let b = return_i32()
        b * 4
    } else {
        2 * 4
    }
    return_i32()
}

fn expect_no_return() {
    return_i32()
}

The result :

#![feature(prelude_import)]
#![no_std]

#![no_std]
extern crate core;
#[prelude_import]
use ::core::prelude::rust_2015::*;

fn return_i32() -> i32 { let a = 10; let b = 20; a + b }

fn no_return() { let a = return_i32(); let b = 30; }

fn expect_i32() -> i32 {
    let a = if 2 == 2 { let b = return_i32(); b * 4 } else { 2 * 4 };
    return_i32()
}

// this should be added semicolon, I'm trying to implement it
fn expect_no_return() { return_i32() }

I need to get boolean info whether current function has return type or not, can anyone pinpoint in which code the parsing of function happens? With direct link to the github file

The problem you're trying to solve is unsolvable because sometimes, both () and a non-() type are valid return types from the function. For example (playground):

trait Semicoloned {
    const SEMICOLONED: bool;
    
    fn detect_semicolon(&self) {
        if Self::SEMICOLONED {
            println!("Semicolon!");
        } else {
            println!("No semicolon!");
        }
    }
}
impl Semicoloned for () {
    const SEMICOLONED: bool = true;
}
impl Semicoloned for i32 {
    const SEMICOLONED: bool = false;
}

fn i32_with_side_effects() -> i32 {
    println!("Hi!");
    5
}

fn demo1() -> impl Semicoloned {
    i32_with_side_effects()
}
fn demo2() -> impl Semicoloned {
    i32_with_side_effects();
}

fn main() {
    demo1().detect_semicolon();
    demo2().detect_semicolon();
}

(The same problem can happen with blocks as well as functions; think about the difference between { i32_with_side_effects() } and { i32_with_side_effects(); }.)

Even outside cases like this, you are trying to feed back information from type checking into the parser, which doesn't really make sense because the parser runs so much earlier. Leaving the -> T off a function/method causes it to return (), but there are other ways to return () (e.g. writing -> () explicitly, or writing -> Self::AssociatedType where the associated type happens to be ()).

Automatic semicolon insertion would therefore probably need to be done in the type checker, if you did it at all (i.e. replace an optional ; with a coercion site that sometimes coerces anything into (). However, as my example above shows, sometimes both possibilities are reasonable.

There's also the chance that semicolon insertion entirely changes the meaning of the program, e.g. if m is a Mutex<()>, then let guard = { m.lock() }; will lock the mutex for the rest of the scope of guard, whereas let guard = { m.lock(); }; will lock the mutex for the duration of the statement m.lock(). The former is probably what you meant, but the latter is still both syntactically valid and meaningful.

2 Likes

Thank you for the feedback. In my current result, after I implement the one that I'm asking. It will become like this

fn i32_with_side_effects() -> i32 {
    println!("Hi!") // will have semicolon
    5 // no semicolon
}

fn demo1() -> impl Semicoloned {
    i32_with_side_effects() // no semicolom
}
fn demo2() -> impl Semicoloned {
    i32_with_side_effects() // no semicolon
}

// if wants to return (), the code is

fn demo2() -> impl Semicoloned {
    i32_with_side_effects() // semicolon
    () // no semicolon
}

Is there real valid end bug example, I mean by valid is the one that causes damage, because of that?

What I understand now is it can cause program that previously compile becomes must be fixed, kind like breaking change. But what is the valid end bug example, for example let's say it causes dangling reference, I want to know the real effect like that

What I'm trying to do is more like embedding proc macro inside the parser. So even knowing symbol -> is enough, it already tells the user write return value then no semicolon is inserted

What is the meaningfull effect that this causes?

let guard = { m.lock(); }; 

Because in my view, that is just lock and instantly unlock again. What is the real valid effect example?

Yeah my current result if writing

let guard = { m.lock() }

Become

let guard = { m.lock() };

My goal is also if there is already semicolon, no semicolon will be insterted. That means I can write semicolon for specific part, assuming doing it gives valid effect

I also has another idea by adding new keyword finish as replacement to the return without keyword. So finish will exit frok block, where return will exit from function. With that way I feel like now I can implement the semicolon inference without having that difference you mentioned, is it true?. But with downside can't compile old code that uses return value without keyword return. But that is ok because only my usage

If I move to implement this in type check section, what is the high level process that I should do? Because currently in the parser section, I do it by reading until it becomes valid expression, then adding semicolon

To be clear, the chances that a change like that will be accepted to Rust are astronomically slim. We're not changing the basic syntax of the language on a whim. You'd have to convincingly argue that this makes Rust programmers 10x more effective, or so, to have this outweigh the downsides in churn and in lost diagnostics (where bugs are currently easier to find because we require the ;). If you're just experimenting with this to learn and have fun with a fork of a compiler, that's entirely fine of course. I just don't want any false expectations to be raised here.

If you want to learn about the Rust parser implementation, the dev guide is a good starting point:

4 Likes

Relevant:

Well yes.

fn i32_with_side_effects() -> fn() -> i32 {
    || 1
}

Valid:

fn demo2() -> i32 {
    i32_with_side_effects()
    ()
}

Error:

fn demo2() -> i32 {
    i32_with_side_effects();
    ()
}

Personally I always #![deny(unused_results)] to avoid ; silently dropping things like that.

This is why I used the example of a function that has side effects but also returns a value. (MaybeUninit::write is a great example of this, where often the return value isn't useful, but sometimes you need it.) It's entirely reasonable to ignore the return value using ;, and also entirely reasonable to use it, so the semicolon is important in these cases because it expresses which option you intend.

I much prefer to be explicit because ; doesn't tell you that you're ignoring the return value -- it's usually used on () expressions. It's easy to accidentally ignore an important value if ; doesn't warn you.

Ah wrong reply :< @RalfJung @chrefr @tczajka Thank you for the feedback. But now I take difference approach to allow me to write clean Rust. I make custom language that transpiles to Rust, because with this approach I can add much more thing like lifetime inference, and in the next would be clean syntax for writing macro, and allow for more

The current result is already promising, like here the difference in the cleanliness

I think I can also add parameterized sync/async mode using this one :v