c3c icon indicating copy to clipboard operation
c3c copied to clipboard

Error when returning values within macro trailing bodies and block expressions

Open vssukharev opened this issue 11 months ago • 9 comments

Compiler throws an error when compiling such code:

// --- test.c3
fault SomeF
{
  A, B, C
}

macro usz! @insertBody(;@body())
{
  return {| @body(); |};
}

fn void main()
{
  usz! res1 = @insertBody() { return SomeF.A?; };
}

Here is an error:

$ c3c compile-run test.c3
11: 
12: fn void main()
13: {
14:   usz! res1 = @insertBody() { return SomeF.A?; };
                                         ^^^^^^^^
(/home/starleks/coding/probe/c3-sandbox/main.c3:14:38) Error: It is not possible to cast
 from 'void!' to 'void'.

vssukharev avatar Jan 24 '25 17:01 vssukharev

Seems like trailing is supposed to return void only. By changing an optional to a regular value is not solving a problem:

// ...
usz! res1 = @insertBody() { return 23; };
// ...
$ c3c compile-run test.c3
11: 
12: fn void main()
13: {
14:   usz! res1 = @insertBody() { return 23; };
                                         ^^
(/home/starleks/coding/probe/c3-sandbox/main.c3:14:38) Error: 'int' cannot implicitly be
 converted to 'void', but you may use a cast.

vssukharev avatar Jan 24 '25 17:01 vssukharev

This is somewhat surprising perhaps, but correct. The body always executes in the scope in which is is created. So returning anything here is returning from main.

So here:

fn void main()
{
  usz! res1 = @insertBody() { return SomeF.A?; };
}

This trailing body always evaluates in the defining scope, which is the main function in this case. This is very weird with the {| |} since one might assume that this one should transform the "return" here. However, the semantics for the trailing body macros is always in the external scope.

I think probably the problem here is rather that the {| |} doesn't issue an error.

I think it also highlights a missing feature, namely to pass in something that works as a macro into a macro in a normal way.

So for example, we might imagine this:

macro usz! @insertBody(#m)
{
  return {| #m(3); |};
}
fn void main()
{
  int a;
  @insertBody(macro (x) { 
    if (a > x) return SomeF.A?; 
    return a;
  });
}

Where we pass a macro that works like a closure. This is not yet a feature in C3 though.

lerno avatar Jan 24 '25 19:01 lerno

Yeah, such feature would be cool.

vssukharev avatar Jan 24 '25 20:01 vssukharev

Can I have some alternative for this, though? For example, to return from @body with some value?

vssukharev avatar Jan 24 '25 21:01 vssukharev

How do you mean?

lerno avatar Jan 25 '25 00:01 lerno

I mean something what you've mentioned above. Some kind of 'lambda macros' is the solution.

vssukharev avatar Jan 25 '25 17:01 vssukharev

Regarding the semantic reason for the behavior I've written about originally, I understand the @body as something to be inserted inside macro. So it's not guaranteed to be in the context of calling function, for instance, when using @body inside lambdas and (the issue I was stuck) expression blocks:

macro usz @someMacro(; @body())
{
  var inc = fn usz (usz i) { 
    @body();
    i + 1; 
  };
  return inc(2);
}

vssukharev avatar Jan 25 '25 17:01 vssukharev

It's actually like the macro code floats around the code than the other way around.

So in the case of

fn void main()
{
  int a;
  @someMacro() { a++; };
}

For your case then the code (if it had been permitted) should expand to:

fn void main()
{
  int a;
  {|
    var inc = fn usz(usz i) { 
      a++;
      return i + 1;
    };
    return inc(2);
  |};
}

But this could only work if C3 had closures in some way, but it doesn't, so the above would be a compile time error since a is not visible from the lambda anyway.

So this is a dead end, but passing macros into macros, that could work.

lerno avatar Jan 25 '25 23:01 lerno

Fee free to file a feature request for placing macros in macros.

lerno avatar Jan 25 '25 23:01 lerno

Closing this and instead track #2067

lerno avatar Oct 10 '25 19:10 lerno