Author Topic: Macros again  (Read 16397 times)

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Macros again
« on: October 20, 2018, 07:14:06 PM »
I just wanted to discuss some possible ways of doing it that I was playing with.

First, we have to consider macros as always expanding where they are referenced to keep it simple.

Code: [Select]
macro @foo(int v) {
  v++;
  if (v > 10) return 10;
  return v;
}
int a = 10;
@foo(a);

This code would then be exactly equal to:

Code: [Select]
int a = 10;
a++;
if (a > 10) return 10;

Macros simply expand in place.

Secondly, we can have macros returning values:

Code: [Select]
macro int @foo2(int v, int w) {
  v++;
  if (v > 10) return 10;
  w += 3;
  return 0;
}
d = 0;
a = 10;
int b = @foo(a, d);
[/mode]

This expands to:
[code]
a = 10
a++
int b;
if (a > 10) {
  b = 10;
} else {
  b = 0;
  d += 3;
}

Note that I'm using a sigil to indicate the code expansion. I think this is a good idea, even if @ collides with current C2 use and we need to pick another sigil. It makes it very obvious that macro expansion occurs.

We can allow the macro to take a body (yeah, I'm calling the type of the body "{}" it should be something better obviously!):

Code: [Select]
macro int @foo3(int a, {} body) {
   while (a > 10) {
      a--;
      @body();
   }
}

b = 20;
@foo3(b) {
  print(b);
}

We expand this to:
Code: [Select]
b = 20;
while (b > 10) {
  b--;
  print(b);
}
« Last Edit: October 22, 2018, 07:03:44 PM by lerno »

bas

  • Full Member
  • ***
  • Posts: 220
    • View Profile
Re: Macros again
« Reply #1 on: October 22, 2018, 12:25:09 PM »
In the foo2 example, do you mean that variable d is used in the macro, but not passed as an argument?
I think specifying all variables that are used from the 'calling' scope is a good thing. But maybe they need to
be separate from arguments?

Rust uses an exclamation mark (!) to indicate a macro(-function). I think it's better to make the use explicit also.
Currently the @ sign is used for attributes (@ttributes :)). I'll have to see if this use confuses the parser. In the
specifying part, we dont need the @, since the macro keyword makes it unambigous.

I understand what you try to do with the return in the macro. It makes the macro more readable.  It might be hard(er)
to implement.

body: that would solve a lot of current issues in macros that wrap around another piece of code. They always need a
pre and post macro. This would be nice, since you only have to write loop-code once. Trying to do reduce the same loop
code to 1 piece in C requires using a function pointer construction. The way functional languages solve this is much nicer,
so using that way would be very nice. I don't think this can be done without having functions an first-class citizens..




lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Macros again
« Reply #2 on: October 22, 2018, 07:01:21 PM »
Sorry, that was just an error in my example  :-[ d should be passed in as well of course!

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Macros again
« Reply #3 on: October 22, 2018, 09:22:08 PM »
If you look at Ruby, that uses blocks extensively, you need to be rather ad hoc in your implementation if you want to make it useful with closures.

Consider the following Ruby code:

Code: [Select]
def x(foo)
  3.times do |x|
    return x if foo[x] == x
  end
  return -1
end

def y(foo)
  l = ->(x) { return x if foo[x] == x }
  3.times(&l)
  return -1
end

In this case x([2, 2, 2]) would return 2 and y([2, 2, 2]) returns -1.

So even though the "times" method is defined using a lambda, there is special handling (return exits the outer scope) if the block is defined inline.

The closure/first class function version in y cannot express the same semantics. And usually "escaping the outer scope" is exactly what's so useful in many cases! This is also separate from the added complexity of optimizing away the lambdas, which is necessary for good performance.

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Macros again
« Reply #4 on: October 27, 2018, 06:09:15 PM »
I'm going to backpedal a bit on this proposal. :D

Consider my proposed definitions:

macro int @foo2(int v, int w)
macro @foo(int v)
macro int @foo3(int a, {} body)

First the parameters. I think it's reasonable that we could mix both actual real values / variables, and placeholders. So for this, let's use the & character to indicate a variable that is taken from the parent scope.

This means macro @foo(int v) should be macro @foo(int &v).

Secondly, in order to make things easier I suggest dropping the type unless it is passed by value.  This further modifies it to macro @foo(&v) if type is required for readability (and perhaps strict matching), then we can use "auto": macro @foo(auto &v). That would allow strict typing, like macro @foo(i32 &v), but this type would be strict, so it would fail to compile if the variable is not i32 (i64, i16, u32 would all be disallowed) in this case.

With this, we can add pass-by-value: macro @foo(i32 v). Here the macro would work identical to an inlined function. Not very useful. The usefulness comes from combining it with body and variable references: macro @foo(i32 v, auto &b, {} body) in usage: @foo(32 * a, c) { print("."); }. Here 32 * a is guaranteed to be evaluated once.

We should also consider nested macros, that is, we can give a macro and expand it similar to the "body" here. In fact, we might be able to consider the {} as a macro that's passed in to the macro and expanded as the macro renders.

To summarize:

Code: [Select]
macro @foo1(i32 a, i32 &b) { ... }  // Works like you would expect from C++ if this was an inlined function. b must be i32.
macro @foo2(i32 a, auto &b) { ... }  // Like above but the b type can be anything (unlike for a function)
macro @foo3(auto a, auto &b) { ... }  // Both a and b have wildcard types
macro i32 @foo4(auto a, auto &b) { ... }  // Same as above, but is guaranteed to return an i32
macro auto @foo5(auto a, auto &b) { ... }  // Return type determined during macro expansion

(The advantage of the "@" in front of the name is that we don't need to declare @foo1 / @foo2 / @foo3 as void to make parsing simple)



Another thing is the amount of analysis. I wonder if it is a good idea to have much typing on the macros. The more statically typed, the more complex it is to express the type restrictions, and that complexity both makes it harder to use and harder to read. Let's use the strength of C as a weakly typed language and don't go too far. I'm thinking that typed macros (ones that does not use auto in the examples above) should be few rather than many. Semantic analysis on the macros can be done by assuming wildcard types and just check that "there could be a type that satisfies this", trying to hit the sweet spot between Cs text macros and more complex macro systems.

It should be noted that LISP, hailed for its powerful and useful macro system, is completely untyped when it expresses a macro. One of the things that really stand out when reading the examples on C2 is that the code is very clean and simple code compared to many other recent languages out there, and it would be nice to keep that for the macro system as well if possible.

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Macros again
« Reply #5 on: November 06, 2018, 08:29:14 PM »
The SMART C macro system has a bit more stuff to consider: https://repository.upenn.edu/cgi/viewcontent.cgi?referer=https://www.google.com/&httpsredir=1&article=1474&context=cis_papers

Worth a read. Note that they add macro FOR, IF-ELSE, SWITCH. I think this is a reasonable thing to do. "SWITCH" is present in the __Generic macro in C, IF-ELSE is obviously there already, then finally the FOR loop... I think that is an important additions since its lack is making a lot of macros much more complicated.
« Last Edit: November 06, 2018, 08:32:05 PM by lerno »

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Macros again
« Reply #6 on: November 19, 2018, 12:48:08 PM »

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Macros again
« Reply #7 on: November 19, 2018, 03:43:54 PM »
A tiny way to modify macros would be to simply extend "#define" with a { ... } syntax:

Code: [Select]
#define ADD_TO(x, y) {
   x += y;
}

ADD_TO(x, 1)

The { } introduces a multiline macro that does not need explicit linebreaks.

Secondly we could add the "$" symbol to introduce hygienic temporaries:

Code: [Select]
#define SWAP(x, y) {
   typeof(x) $tmp = x;
   x = y;
   y = $tmp;
}

Here $tmp will actually be replaced by __<macro>_<variable_name>_<instance> when translating to C, so __SWAP_tmp_1, __SWAP_tmp_2 etc.

We then introduce the syntax macros using:

Code: [Select]
macro swap(&a, &b) {
   typeof(a) $tmp = a;
   b = a;
   a = $tmp;
}

The use of &a follows C++ standard: it simply refers to a variable OR EXPRESSION that is imported into its scope. Using the unadorned variable name as evaluated expression allows us to write this code:

Code: [Select]
macro max(a, b) {
  return (a > b ? a : b)
}

The above code is equivalent to:

Code: [Select]
macro max(&a, &b) {
  typeof(a) $tmp_a = a;
  typeof(b) $tmp_b = b;
  $tmp_a > $tmp_b ? $tmp_a : $tmp_b
}

Or in (GNU) C:

[/code]

Code: [Select]
#define max(a,b) \
   ({ __typeof__ (a) _a = (a); \
       __typeof__ (b) _b = (b); \
     _a > _b ? _a : _b; })

To recap:

1. We add the { } format to #define for multiline defines.
2. We add the $<name> format as hygienic variable names.
3. We add the syntax "macro" type of definition.
4. The syntax macro makes a difference between "normal" parameters (with & as prefix) and "evaluated" parameters (unadorned variables)

In addition we need to make macros have a definite scope. I suggest the following:

#define is always defined local to a scope (unlike in C).

This means that

Code: [Select]
#define FOO printf("foo");
{
   #define BAR printf("bar");
}
FOO // adds printf("foo");
BAR; // Error, define not available in scope;

This also means that a define can be declared public to be accessed as if defined from the top of the file scope:

Code: [Select]
// file 1
module foo
public #define FOO { printf("FOO!\n"); }

// file 2
import foo

func void test() {
  foo.FOO
}

Only defines in the file scope that exists in the file scope may be public and used in other modules.

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Macros again
« Reply #8 on: December 10, 2018, 12:45:46 AM »
Another C type lang with semantic macros worth looking at: http://zl-lang.org/zl-dissertation-univf.pdf