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

Macros and user-defined control flows #3507

Closed
msadeqhe opened this issue Dec 14, 2023 · 2 comments
Closed

Macros and user-defined control flows #3507

msadeqhe opened this issue Dec 14, 2023 · 2 comments
Labels
leads question A question for the leads team

Comments

@msadeqhe
Copy link

msadeqhe commented Dec 14, 2023

Summary of issue:

Carbon can provide a way for programmers to declare macros or user-defined control flows. They help programmers to write implementations easy and fast.

Details:

First of all, let's introduce :? declaration syntax:

var a: i32 = 0;
var b: i32 = 0;

condition:? bool = a < b;

if (condition) { Print("`condition` is `true`."); }
else { Print("`condition` is `false`."); }

b = 1;
Print("`b` value is changed.");

if (condition) { Print("`condition` is `true`."); }
else { Print("`condition` is `false`."); }

// :output
// `condition` is `false`.
// `b` value is changed.
// `condition` is `true`.

We can declare expression variables with :? notation. They can be either compile-time or run-time, it depends on the context. In the example, condition is compile-time. The compiler will replace every condition with a < b in if.

But :? will be at run-time if it's a function parameter. The compiler will create a mutable lambda for the expression, and the mutable lambda expression will be passed to the function.

fn Test(condition:? bool, step:? i32) {
    while (condition) {
        Print("Ding!");
        step;
    }
}

var i: i32 = 0;

Test(i < 3, ++i);

// :output
// Ding!
// Ding!
// Ding!

In the example, the compiler will declare mutable lambdas for i < 3 and ++i.

Now, considering forced-inlined functions, we may declare them with fn! keyword. The compiler will not create mutable lambdas for expression arguments in this case. The compiler will replace the parameters in the function with passed expression variable arguments at compile-time:

fn! Test(condition:? bool, step:? i32) {
    while (condition) {
        Print("Ding!");
        step;
    }
}

var i: i32 = 0;

Test(i < 3, ++i);

// :output
// Ding!
// Ding!
// Ding!

It's like the previous example except that it's at compile-time. That's because Test function is declared with fn! keyword as a forced-inlined function. The compiler will generate the following code for the above example:

fn! Test(condition:? bool, step:? i32) {
    while (condition) {
        Print("Ding!");
        step;
    }
}

var i: i32 = 0;

// This part is generated by the compiler.
// It's the function scope.
{
    while (i < 3) {
        Print("Ding!");
        ++i;
    }
}

// :output
// Ding!
// Ding!
// Ding!

With this feature, we can declare user-defined control flows too. Let's assume that we can apply fn! functions to block statements if they are declared with deducable statement parameter. In the following example, I'm using the syntax of proposed variadics #2240 in Carbon:

fn! Test[... each statement:? Statement](condition:? bool, step:? i32) {
    while (condition) {
        ... each statement;
        step;
    }
}

var i: i32 = 0;

Test(i < 3, ++i) {
    Print("Ding!");
}

// :output
// Ding!
// Ding!
// Ding!

The point is, the programmer can write its own statements instead of Print("Ding!"); and call Test function on it at compile-time.

Here, statement parameter will be deduced from block statement {...}. each statement is each statement that are written inside {...}. Maybe we can find better names for statement and Statement. The compiler will generate the following example from the above example:

fn! Test[... each statement:? Statement](condition:? bool, step:? i32) {
    while (condition) {
        ... each statement;
        step;
    }
}

var i: i32 = 0;

// This part is generated by the compiler.
// It's the function scope.
{
    while (i < 3) {
        Print("Ding!");
        ++i;
    }
}

// :output
// Ding!
// Ding!
// Ding!

Any other information that you want to share?

The rules and syntax that I've explained here are not a major part of my suggestion. If you are interested in adding such feature to Carbon programming language, I'll prepare a proposal for it. And I'll try to improve/enhance it as much as possible. Thanks.

@msadeqhe msadeqhe added the leads question A question for the leads team label Dec 14, 2023
@msadeqhe msadeqhe changed the title Macros are forced-inline functions. Macros and user-defined control flows Dec 14, 2023
@geoffromer
Copy link
Contributor

fn! Test[... each statement:? Statement](condition:? bool, step:? i32) {
    while (condition) {
        ... each statement;
        step;
    }
}

var i: i32 = 0;

Test(i < 3, ++i) {
    Print("Ding!");
}

This example illustrates a few potential concerns with this proposal:

  • The value of condition:? bool isn't a bool, it's a piece of code that can be evaluated to produce a bool -- in other words, it's a callback.
  • It's easy for a reader to miss the fact that while (condition) actually invokes an arbitrary amount of code, because it looks like it's just reading a variable, and the difference depends on a single ? character, and in a more complex example that character might be quite far away.
  • Conversely, it's very easy for a reader to miss the fact that ++i might be evaluated more than once, and might not be evaluated at all.
  • It doesn't seem like statement needs to be a pack, and it's hard to think of examples that would benefit from working with a pack of statements in this way. It seems like we could just as well represent the whole block as a single statement.

Pulling all those observations together, here's my pitch: I think this proposal is really about lambdas. For example, in C++ we can already write your example like this:

void Test(std::function<bool()> condition, std::function<i32()> step, std::function<void()> statement) {
    while (condition()) {
        statement();
        step();
    }
}

i32 i = 0;

Test([&]{return i < 3;}, [&]{return ++i;}, [&]{
    Print("Ding!");
  });

This version addresses the concerns I raised above:

  • The callback types make it clear that they're callbacks.
  • The function calls in the body of Test are obvious to the reader, because they use familiar function-call syntax.
  • It's clear to the reader of the Test call that expressions like i < 3 aren't evaluated immediately, and may be evaluated more than once.
  • There's no need for variadics, even if we wanted to execute multiple statements in the loop.

However, it has some problems of its own:

  • std::function's type erasure has a run-time cost. We could avoid that by making Test a function template, but then we lose definition checking.
  • The lambda syntax adds a lot of boilerplate to the callsite of Test, making it hard to even see the application logic.
  • It's visually awkward that the loop body is nested inside both parentheses and curly braces.

The good news is that those problems seem solvable. In fact, Carbon has solved the first one already: Test can be a generic function defined in terms of the Call interface. That just leaves the other two. For those, I think we need two things:

For example, if we use Rust's lambda syntax and Swift's approach to trailing closures, your example could be:

fn! Test[Cond:! Call(()) where .Result = bool, Step:! Call(()) where .Result = i32,
         Statement:! Call(()) where .Result = ()]
      (condition: Condition, step: Step, statement:Statement) {
    while (condition()) {
        statement();
        step();
    }
}

var i: i32 = 0;

Test(||i < 3, ||++i) {
    Print("Ding!");
}

@chandlerc
Copy link
Contributor

This issue is really proposing a full new feature. If a new feature makes sense, it should be proposed with Carbon's proposal process.

We also agree with @geoffromer 's observations, so it would seem good if and when exploring this to consider things like a lambda-based approach.

As to whether it makes sense to work on this type of metaprogramming (macro-like constructs that expand to control flow), the leads don't think expanding our metaprogramming facilities matches the current priorities of the Carbon project. If and when its a better fit for the priorities, we'd definitely be interested in proposals to advance this area, but right now we're focused on getting the critical path to 0.1 ironed out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
leads question A question for the leads team
Projects
None yet
Development

No branches or pull requests

3 participants