Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

[SUGGESTION] Statement-expressions, result vs return #391

Closed
msadeqhe opened this issue Apr 23, 2023 · 3 comments
Closed

[SUGGESTION] Statement-expressions, result vs return #391

msadeqhe opened this issue Apr 23, 2023 · 3 comments

Comments

@msadeqhe
Copy link

msadeqhe commented Apr 23, 2023

Preface

Statement-expression is a solution to have a group of statements in place of an expression.

I suggest to have statement-expressions in Cpp2 with syntax : = { ... } and the result of them are returned with result keyword. In this way, the following code:

call(: = { n: = num(); result n + 0; },
     : = { n: = num(); result n + 1; });

... is somehow equal to the following code except that we don't have to specify the type of arguments:

arg0: int; { n: = num(); arg0 = n + 0; }
arg1: int; { n: = num(); arg1 = n + 1; }
call(arg0, arg1);

Statement-expressions can be used to declare variables too:

// The type of `variable` is `int`.
variable: = {
    value: int;
    //: statements...
    result value;
}

Suggestion Detail

Currently we declare functions in Cpp2 in the following syntax:

// function or lambda style
func0: (param) = {
    return param;
}

// expression style
func1: (param) = param;

In a similar syntax, we can declare variables in Cpp2 with statement-expressions:

// statement-expression style
var0: = {
    result 0;
}

// expression style
var1: = 0;

Statement-expressions are unnamed variables in a similar manner that lambdas are unnamed functions:

  • We have to use result instead of return within statement-expressions.
  • We can use a statement-expression everywhere that an expression is required.
  • A declaration within their { ... } has the lifetime of that scope.
  • Their { ... } will be executed one time in their place.
  • They don't create a new function scope, therefore variables from outer scope don't have to be captured within their { ... }.

Statement-expressions will be not allowed to be used in place of statements. Because it's an error to have an unused value (literal or identifier) in Cpp2:

main: () = {
    // ERROR! It has to be in place of an expression not an statement.
    : = {
        x: = 0;
        result x + 1;
    }
    // ERROR! Because `x + 1` is an unused value (literal or identifier).
}

Expressions, Functions and Blocks

Now { ... } has different meanings in the following categories:

  1. : Type = { ... } is a statement-expression in which Type can be omitted and deduced from { ... }.
    • The execution of statements in { ... } ends with result something;.
    • It can be a part of variable declaration, or in place of an expression (e.g. function argument):
    x0: = 0;
    x1: = { result 0; }
    call(: = { result 0; })
  2. : (params) -> Type = { ... } is a function or a lambda in which -> Type can be omitted if the return type is void.
    • The execution of statements in { ... } ends with return something; or the end of }.
    • It can be a part of function declaration, or in place of an expression (e.g. function argument):
    x0: (param) = param;
    x1: (param) = { return param; }
    call(: (param) = { return param; })
  3. (params) { ... } is a parameterized statement block. Whereas { ... } is a statement block without (params) in which () must be omitted.
    • The execution of statements in { ... } ends with:
      • ... the end of }.
      • ... break and continue in loop control structures.
      • ... result something; when their outer scope is a statement-expression.
      • ... return something; when their outer scope is a function or a lambda.
    • It can be a part of control structures:
    for items do (item) { call(item); }
    if condition { call(); }

In all of the above categories, they have this similarity:

  • A declaration within their { ... } has the lifetime of that scope.

Additinally they have different behaviours in statement execution and variable capturing:

  • Statement-expressions (case 1) have to execute their { ... } one time in their place.
    • But other cases can execute their { ... } multiple times in any place.
  • Functions and lambdas (case 2) create a new function scope therefore variables from outer scope have to be captured within their { ... }.
    • But other cases don't create a new function scope, therefore variables from outer scope don't have to be captured within their { ... }.

I should mention that statement-expressions, functions and lambdas, statement blocks and parameterized statement blocks can be nested inside each other.

Your Questions

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?

Yes. In a way that it helps Xto organize code and separate related statements in nested blocks to easily fix or prevent unwanted bugs which are a result of having mixed unrelated statements together.

Will your feature suggestion automate or eliminate X% of current C++ guidance literature?

Yes. In the following ways:

  • It will make the declaration syntax of variables and functions to be similar and consistent.
  • It helps programmers to separate related statements easily without defining extra variables.
  • It allows programmers to use if, while and other control structures as an expression that is more readable in variable assignment, because the intention is clearly written in code, see the example below.
  • It allows programmers to be less specific about types in generic programming, see the example below.

Consider how the following code:

variable: = {
    if condition {
        result hello();
    }
    else {
        result bye();
    }
}

... is more readable and generic than the following code which we have to also specify the type of the variable:

variable: int;

if condition {
    variable: = hello();
}
else {
    variable: = bye();
}

Considered Alternatives

Instead of result keyword, it's possible to use symbols such as => or nothing (like in Rust). For example:

variable: = {
    if condition {
        => hello();
    }
    else {
        => bye();
    }
}

... or this one (like in Rust):

variable: = {
    if condition {
        hello() // without ;
    }
    else {
        bye() // without ;
    }
}

Howerver I think that having a keyword like result or a notation like =>, is more readable than having nothing.

References

This suggestion is inspired from discussions in this issue and this paper mentioned by @JohelEGP and this informative comment from @hsutter which describes Unifying Functions and Blocks.

@msadeqhe
Copy link
Author

msadeqhe commented Apr 24, 2023

do { ... }

It's not a part of my main suggestion. Additionally I suggest open a discussion to consider to require do before { ... } and call it do { ... } language construct. It's useful because Cpp2 has statement scope parameters after this commit. In the following example, (x: int = 0) is different from (item: int = 0). So (x: int = 0) is for initialization and (item: int = 0) is for argument passing (with default value 0 if Cpp2 supports this someday):

(x: int = 0) for items do (item: int = 0) {
    //: statements...
}

But in the following example, (x: int = 0) is for initialization while it feels like its syntax is for argument passing because the parenthesis are immediately followed by braces { ... }:

(x: int = 0) {
    //: statements...
}

On the other hand if it was written with do { ... }, it would be like the following code which is also obvious that (x: int = 0) is for initialization because it's not immediately followed by braces { ... }:

(x: int = 0) do {
    //: statements...
}

So in a nutshell with do { ... }:

  • All statement blocks will be a part of control structures.
  • It creates a new scope for variables and declarations. It's important enough that it deserves a control structure. do { ... } will be complementary to do { ... } while condition;.

EDIT: Why Not?

I'm going to express the opposite view that why { ... } is better than do { ... }:

  • It's obvious enough that if (...) is for initialization or for argument passing in parameterized statement blocks, the syntax will be generally like this:
    • In the following list: construct may be if, while, do, for, next or anything else in the future. (inits) is for initialization and (params) is for argument passing in parameterized statement blocks.
    • (inits) { ... }
    • (inits) construct { ... }
    • (inits) construct (params) { ... }
    • construct (params) { ... }
    • In a nutshell, (params) are after construct, and (inits) are at the start of control structures. It is a simple rule.
  • Statement blocks can be used alone without any control structures.
  • It creates a new scope for variables and declarations. It's obvious enough from { ... } notation and do is somehow an unrelated name for it.

At first I didn't get the grammar of statement scope parameters, Now I find that { ... } is the right choice as it's now, and there isn't any reason to change the syntax to do { ... }.

@AbhinavK00
Copy link

If I understand correctly, his is just do expressions paper from Barry Revzin (without the do part ofcourse), no?
I like this suggestion, helps to get rid of immediately invoked lambda expressions (which are not bad, but this is simpler).

@msadeqhe
Copy link
Author

msadeqhe commented Apr 25, 2023

Yes, that's it. Only the syntax is changed to : = { ... result something; ... } for Cpp2.

I like this suggestion, helps to get rid of immediately invoked lambda expressions (which are not bad, but this is simpler).

Also variable capturing, is not needed.

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

2 participants