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

Quasiquoting based metaprogramming APIs #1989

Closed
mraleph opened this issue Nov 24, 2021 · 60 comments
Closed

Quasiquoting based metaprogramming APIs #1989

mraleph opened this issue Nov 24, 2021 · 60 comments
Labels
static-metaprogramming Issues related to static metaprogramming

Comments

@mraleph
Copy link
Member

mraleph commented Nov 24, 2021

Right now a lot of metaprogramming discussion is centered on generating the source and consequently string based metaprogramming API. I think there might be an alternative approach which is much more structured and instead based on quasiquoting, similar to existing metaprogramming systems (LISP dialects, Template Haskell, Scala, Metalua, Converge).

In a nutshell quasiquoting (QQ for short) mechanisms provide a way to move between layers (e.g. compilation and execution can be seen as two layers) in a structured way.

Example

Here is how it could look like:

-{
  Member toJson(Class currentClass) {
    return +{
      Map<String, dynamic> toJson() => {
        -{[ 
          for (var field in currentClass.fields)
            +{ -{field.name} : -{ field.ref } } 
        ]}
      }
    };
  }
}

class Foo {
  final String a;
  final String b;

  -{toJson(currentClass)}
}

I am using here -{...} and +{...} syntax for quasiquotes which originates from Metalua (one of the systems I had a chance to extensively work with in another life), but I am not fixed on any particular syntax as long at it allows to express at the very least the following concepts:

  • +{code} shifts up a metalevel (i.e. from compilation to execution), essentially it evaluates to an abstract representation of the given code fragment.
  • -{code} shifts down in the metalevel (i.e. from execution to compilation), essentially given an abstract representation of the code fragment it splices this fragment into another one.

It might be that we would need to introduce few different QQ-variants for different context (and potentially to simplify parsing rules - which otherwise might need to use types for figuring out how to parse things, which might not be desirable).

Note that in the example above I combine both compile-time and execution-time layers in a single file. This might not be desirable for performance reasons. In which case the following could also work:

// In package:json_serializable/macros.dart

Member toJson(Class currentClass) {
  return +{
    Map<String, dynamic> toJson() => {
      -{[ 
        for (var field in currentClass.fields)
          +{ -{field.name} : -{ field.ref } } 
      ]}
    }
  };
}
// In user code
-{ import 'package:json_serializable/macros.dart' }

class Foo {
  final String a;
  final String b;

  -{toJson(currentClass)}
}

Lets look at more examples:

-{  print("I am executed during compilation"); }

void main() {
}
-{ 
 Expression expr = +{ 1 * 2 };
 print(expr);  // AST representation for 1 * 2 
}

void main() {
}
-{
 String compileTimeString = "Hello!" 
 Expression expr = +{ -{compileTimeString} * 2 };
 print(expr);  // AST representation for "Hello!" * 2 
}

void main() {
}

Pros of quasiquotation

  • QQ is built around normal syntax so all tools (e.g. formatting, syntax highlighting) work naturally with it.
  • Once developer grasps QQ concepts - QQ based code is naturally more readable then string based code generation.
  • QQ can work together with static typing:
    • to surface macro-errors early (e.g. if you do Member x = +{1 * 2} compiler can tell you that 1 * 2 is not a valid AST for a member and instead is an Expression).
    • to provide some degree of autocompletion when writing expressions at different metalevels.
  • QQ is friendly to optimizations, e.g. given the toJson function from the very first example compiler has a natural way of splitting it into outlining phase (which generates just the member signatures) and body generation phase (which generates member bodies).

Cons of quasiquotation

QQ is built around abstract representation of the source, rather than the source itself, so it might be harder to debug than source based solution. On the other hand there is a natural mapping from QQ constructed ASTs to source code on the disk so for debugging purposes it should be simply possible to dump that and leave it to user's inspection.

Prior Art

/cc @leafpetersen @lrhn @jakemac53 @munificent

@mraleph mraleph added the static-metaprogramming Issues related to static metaprogramming label Nov 24, 2021
@mateusfccp
Copy link
Contributor

I think that, overall, this is the best approach. The syntax details may be revisited, but, IMO, quasiquoting is a much more natural and structured way of metaprogramming. In special, I think expression-level macros makes more sense with quasiquoting, although supporting them is not in the current plans.

@rrousselGit
Copy link

rrousselGit commented Nov 24, 2021

Could this be covered by string interpolation? Such as:

return '''
class Foo {
  ${someType} myMethod() {}
}
''';

This is how various Dart code generators work.

This proposal has more advanced features, like for/ifs inside code blocks. But maybe those could be added to string interpolation?

Like:

'''
class Foo {
  ${if (condition) 'void doSomething() {}'}
}
'''

@mateusfccp
Copy link
Contributor

Could this be covered by string interpolation?

Although you may achieve the same, the approach is different, and string-based generation is way worse in terms of tooling, as we are dealing with, well, strings.

@mraleph
Copy link
Member Author

mraleph commented Nov 24, 2021

QQ is an interpolation of sorts - it is just interpolation of structured program representation rather than strings.

Having QQ does not change expressivity of metaprogramming system, but rather makes it more structured and easier to analyse. Arguably it makes it also easier to read (imagine code highlighting working, imagine jump to definition working etc).

Strings are ultimately just sequences of characters, they are incomplete, untyped, unstructured.

QQ contents are actual structured pieces of Dart code.

@jakemac53
Copy link
Contributor

jakemac53 commented Nov 24, 2021

What would the execution model be in Dart for a feature like this? It seems like maybe an interpreter would make the most sense, or did you have something else in mind?

I am also curious about how scoping etc would work, do you get a totally new global (compile time) scope for each library? For each outer -{} block?

@rrousselGit
Copy link

I'm skeptical about tooling.

AngularDart uses string templates for html, yet it has autocompletion, errors and co.

We definitely can have syntax highlighting too. There are a few examples in the JS world where they use string interpolation for things like css or other, yet the strings are highlighted as if it was a ".css" file.

@bwilkerson
Copy link
Member

QQ is an interpolation of sorts - it is just interpolation of structured program representation rather than strings.

Would it be correct to say that what you're proposing is a literal syntax for an AST structure, where that syntax

  • supports interpolation, and
  • matches Dart as much as possible (basically other than the delimiters and interpolation)?

@jakemac53
Copy link
Contributor

#1988 is also relevant and could provide a nice way for tooling to treat "strings" as dart code.

@jakemac53
Copy link
Contributor

jakemac53 commented Nov 24, 2021

On the other hand there is a natural mapping from QQ constructed ASTs to source code on the disk so for debugging purposes it should be simply possible to dump that and leave it to user's inspection.

This ends up being a lot more complicated than it might seem on the surface, and its a major sticking point. For the current macro proposal we have ended up defining an entire additional language feature (augmentation libraries) just to solve this problem.

We have multiple tools that have to agree when talking about source locations (analyzer, ides, and the various backends and runtimes), so source locations in generated code have to be well defined, and we need to be able to show the user something that actually makes sense to them in a standalone context.

This is for instance why we aren't discussing expression or statement level macros seriously at this time. I think this proposal has pretty much the same debugging/tooling issues that those types of macros would.

@mraleph
Copy link
Member Author

mraleph commented Nov 25, 2021

@jakemac53

What would the execution model be in Dart for a feature like this? It seems like maybe an interpreter would make the most sense, or did you have something else in mind?

I think the execution model is the same as for source based macro-generation: given the source files compiler can extract code that needs to run in the compile time and load that code dynamically (either into the same or a different isolate).

I am also curious about how scoping etc would work, do you get a totally new global (compile time) scope for each library? For each outer -{} block?

I think all outer -{...} blocks exist within the same scope forming a compile time version of the library in which they reside.

There is another related question about scoping (which I kinda skimmed about): how do we define the scope in which +{code} content is going to be parsed, e.g. in the toJson example:

Member toJson(Class currentClass) {
  // What is the scope in which the below code exists?
  return +{
    Map<String, dynamic> toJson() => {
      -{[ 
        for (var field in currentClass.fields)
          +{ -{field.name} : -{ field.ref } } 
      ]}
    }
  };
}

One option is to produce parsed but unresolved ASTs from +{...} which would then be resolved when splicing then within actual scope, but that also means that you can't necessarily provide useful completions within +{...} - and can't pre-typecheck the AST before you split it into the actual context. (which might be okay - but I really would like to think about early errors, completion and potential optimizations).

@rrousselGit

I'm skeptical about tooling.

AngularDart uses string templates for html, yet it has autocompletion, errors and co.

We definitely can have syntax highlighting too. There are a few examples in the JS world where they use string interpolation for things like css or other, yet the strings are highlighted as if it was a ".css" file.

That's true to an extent. Highlighting is indeed a relatively simple case. Completions are somewhat harder - note that AngularDart uses constant templates not dynamically generated ones. To do proper analysis on a code template built through string interpolation analyzer would have to understand how to unfold it into the source code - that's much easier to do if you can provide some structural guarantees about how this code is constructed which is what QQ is about.

@bwilkerson

Would it be correct to say that what you're proposing is a literal syntax for an AST structure, where that syntax

I think that a possible way of looking at it. The only major difference from string interpolation based API is that the AST structure is constructed in a structured way, rather than as a raw string. Strictly speaking QQ can be to an extent backed by string interpolation (-{...} become ${...} and +{...} become "..."):

Member toJson(Class currentClass) {
  return Member.fromSource("""
	Map<String, dynamic> toJson() => {
      ${[ 
        for (var field in currentClass.fields)
          "${field.name} : ${field.ref}" 
      ].join(',\n')}
    }  
""");
}

Circling back to @rrousselGit point: I think with QQ and string based meta-programming (SB for short) can be viewed as roughly equivalent approaches. However to provide a certain developer experience on top of SB you would need to build understanding of code-inside-strings into the analysis tools - but if you are going to do that, you are just reimplementing a worse version of QQ: hidden inside strings rather than being explicit.

On the other hand there is a natural mapping from QQ constructed ASTs to source code on the disk so for debugging purposes it should be simply possible to dump that and leave it to user's inspection.

This ends up being a lot more complicated than it might seem on the surface, and its a major sticking point. For the current macro proposal we have ended up defining an entire additional language feature (augmentation libraries) just to solve this problem.

I am not sure it's actually complicated or related to augmentation libraries. Augmentation libraries solve a different problem: they are about generating Dart source into a separate file and having it being spliced into another existing Dart file. What if we don't actually need that property? If splicing is controlled on the AST level, the augmentation libraries are not really needed as a feature.

So how would we do debugging? Well, there are several different layers at which you want to debug.

  • Compile time layer: debugging compile time execution. This is not that different from debugging compile time execution of source based meta programming:
    • In both cases we would have support for print based debugging out of the box.
    • Supporting step-through debugging with IDE is also possible. You just need to provide a way to connect to the compiler while it is compiling the source and place breakpoints in the code that runs during compile time. I think with an intermediary like DDS - which we have now - it is even possible to build a very transparent debugging experience here. Intermediary will take care of maintaining two debug connections - one to the VM that runs compiler and one the VM that would run the result of the compilation and takes care of the "multiplexing". To the IDE it would look like we are debugging the same program, even though we will have two different layers of execution.
    • You can even support features like "jump back to macros code which generated this piece of code" (it can be done in both QQ and SB - but it kinda naturally follows from QQ and in SB case it would require some special magic).
    • Similarly when issuing a compilation error for a macrogenerated source in QQ case it is very easy to point back to which QQ actually generated a particular piece of code, in SB case it is harder to achieve that.
  • Execution layer: debugging generated code. In QQ case compiler can generate a file on the side which contains all generated code. Note that this file does not have to be a valid Dart file, (e.g. it can be a generated toJson() method dumped into a separate file) that does not really prevent IDE from showing this file during debugging session.

We have multiple tools that have to agree when talking about source locations (analyzer, ides, and the various backends and runtimes), so source locations in generated code have to be well defined, and we need to be able to show the user something that actually makes sense to them in a standalone context.

I am not entirely sure what you concern here is. Could you elaborate? Backends and runtimes don't actually care about source locations - they get those from CFE, and CFE can point these to the synthetic source it itself synthesises (so no concern about source location being in sync here).

There is also no need to agree between analyzer and CFE because each can synthesise their own source if needed (e.g. to display in error messages or to allow user to open one in the IDE to understand the context of an error message).

@rrousselGit
Copy link

I'm not particularly fond of the tokens USD for opening/closing code blocks.
-{} and +{} are too close to each other IMO, and {} are already used a lot in the language. Combined with syntax highlighting, I'm worried about readability.

If we ignore tooling, Strings are clearer on that aspect. ''' ... ''' clashes a lot more with the rest of the code, and we can't put advanced expressions within ${...} so within a code block we won't find other {} characters.

Completions are somewhat harder - note that AngularDart uses constant templates not dynamically generated ones. To do proper analysis on a code template built through string interpolation analyzer would have to understand how to unfold it into the source code - that's much easier to do if you can provide some structural guarantees about how this code is constructed which is what QQ is about.

String templates could be constant too, as we don't have to rely on string interpolation.

For example we could do:

@Template(r'''
class $name {
  void foo() {}
}
''')
class MyTemplate {}

voluntarily using r''' to disable interpolation.
Then MyTemplate would be used this way:

MyTemplate(
  // parameter generated based on the string content
  name: 'HelloWorld',
);

This should allow proper analysis

@mraleph
Copy link
Member Author

mraleph commented Nov 25, 2021

I'm not particularly fond of the tokens USD for opening/closing code blocks.

I am just using -{...} and +{...} because those are the ones I am familiar with. They originate from Metalua (which belongs to Algol syntax family) and thus are probably a better fit there. I am not really set on any particular syntax. What I am proposing is not a syntax, but rather an idea that code should be constructed structurally (with all associated benefits) as opposed to being built as raw strings.

We can even use string inspired QQ syntax:

Member toJson(Class currentClass) {
  return code """
    Map<String, dynamic> toJson() => {
      ${[ 
        for (var field in currentClass.fields)
          code "${field.name} : ${field.ref}" 
      ]}
    }
 """;
}

code "..." is +{...} and ${...} is -{...}.

With the difference that code "..." is interpreted directly by the compiler (similar to how r modifier changes how string is interpreted), rather than just being a variant of string interpolation.

@rrousselGit
Copy link

code '''...''' definitely looks better.

Similarly, are we sure this would allow us to cover all the use-cases?
In particular, how would this syntax deal with anything that involves a separator and dynamic content?

A common issue with the string interpolation approach is defining dynamic function prototypes/invocations, where the parameters depend on the input code. It rapidly becomes tricky to manage, especially when considering named vs positional and commas.
The same issue arises with extends/implements/with clauses or scenarios such as generating a ==/hashCode

@mraleph
Copy link
Member Author

mraleph commented Nov 25, 2021

In particular, how would this syntax deal with anything that involves a separator and dynamic content?

There is no reason to worry about separators because you are building it structurally, rather than textually: in other words if you want to build a call you will build a list of expressions which will serve as call arguments, rather than building a string with commas separating arguments. That's by the way another reason why QQ is simpler than building strings.

I am not sure I understand the concern about dynamic content: the example with toJson builds a map literal from a list of fields based on the class declaration - I assume that's the kind of content that could be considered dynamic.

@rrousselGit
Copy link

rrousselGit commented Nov 25, 2021

There is no reason to worry about separators because you are building it structurally, rather than textually: in other words if you want to build a call you will build a list of expressions which will serve as call arguments, rather than building a string with commas separating arguments. That's by the way another reason why QQ is simpler than building strings.

It isn't obvious to me how this would work.

Say I have the following as input:

@annotation
void fn<T>(int positional, {T? named}) {...}

@annotation
void fn2() {...}

how would you be able to, from those inputs, have code blocks that generate the following?

class FnClass<T> extends StatelessWidget {
  FnClass(int positional, {T? named, Key? key}): super(key: key);
}

class Fn2Class<T> extends StatelessWidget {
  FnClass({Key? key}): super(key: key);
}

Similarly I don't understand how your toJson example works.
How does it know whether to generate { "name": ...} vs { name: ...}? Is that name vs ref?

If so, what if I wanted toJson to generate:

class Settings {
  Color color;
}

class Document {
  Settings settings;
}

// generated:

class GeneratedClass {
  void doSomething(Document doc) {
    print('doc.settings.color is equal to ${doc.settings.color}');
  }
}

In this case, how would we get a ref for that doc.settings.color since doc is not part of the analysis?

@mraleph
Copy link
Member Author

mraleph commented Nov 25, 2021

how would you be able to, from those inputs, have code blocks that generate the following?

Something along the lines of:

Declaration fnClass(FunctionDeclaration fn) {
  final name = makeClassName(fn);
  return +{
    class -{name}<T> extends StatelessWidget {
      -{name}(-{fn.parameters.positional}, {Key? key, -{fn.parameters.named}}) : super(key: key) {
      }
    }
  };
}

The rule I'd make here is that if you grammatically in the place where you expect a list of something (e.g. a list of parameters) and you have a unquote/splice operation (-{...}) which evaluates to a subtype of Iterable<T> then you just expand the list in place. I think this would make common operation like adding parameters where easy to write.

How does it know whether to generate { "name": ...} vs { name: ...}? Is that name vs ref?

Yep: name returns a String and String when spliced into code naturally turns into a string literal. ref returns some object that represents access to a particular field.

There is actually an alternative to that is we could make -{str} (where str has static type String) mean different things depending on the context (remember that QQ gives us context), so +{return -{"something"}} would give AST for return "something", but +{return this.-{"something"}} would give AST for return this.something - but I think this is pretty confusing, so I'd opt maybe for something like +{return -{"something".asRef}} to build return something. In this case my original JSON example would be +{ -{field.name} : -{ field.name.asRef } }. (asRef might be a poor name though, maybe).

In this case, how would we get a ref for that doc.settings.color since doc is not part of the analysis?

If this is some fixed names you can just write:

Declaration makeDoSomething() => +{
	void doSomething(Document doc) {
   		print('doc.settings.color is equal to ${doc.settings.color}');
  	}
};

If you want to use some strings coming from somewhere then we need to add an operation which turns that into a name, rather than a string:

Declaration makeDoSomething(String settingsFieldName) => +{
	void doSomething(Document doc) {
   		print('doc.' -{settingsFieldName}  '.color is equal to ${doc. -{settingsFieldName.asRef} .color}');
  	}
};

// makeDoSomething("settings")

Declaration makeDoSomethingEx(List<String> names) => +{
	void doSomething(Document doc) {
   		print('doc.' -{names.join('.')} ' is equal to ${doc. -{names.asRef}}');
  	}
};

// makeDoSomethingEx(["settings", "color"])

(a Iterable<String>.asRef would give you an iterable of "names" and spliced after . is would create a chain of .f0.f1.f2.etc).

@TimWhiting
Copy link

I like QQ better than the other metaprogramming proposals for the following reasons:

  1. It is more clear how it would extend to statement / expression macros (which are likely to be a highly requested feature if the current proposal ships without them).
  2. There aren't 15+ classes that need to be implemented / used depending on what your use case is. Also there isn't multiple phases of execution that you have to keep track of. Instead it is more clear that there is a compile time vs runtime phase.
  3. The code can get generated in places that annotations are not allowed because the code"myMacro()" / -{} gets expanded at compile time, and whatever it generates just needs to be a valid expression / class / replacement for the macro invocation. However the downside is that this means that code"" / -{} needs to be allowed pretty much anywhere in the grammar (or at least in the place of statements, expressions and declarations).
  4. Building up the AST using QQ / tagged string based QQ is a lot more readable than building up Code objects. (Though tagged strings would help in the current proposal - the tagged strings might be the focus of tagged string based QQ and likely include syntax highlighting and some autocompletion, which would be lacking in the current macro + tagged strings proposal).

Additional language features are no longer needed

  1. Augmentation libraries (yet another library type) would not be needed. We already have parts, library files, libraries, and potentially macro libraries, adding augmentation libraries might be nice for private identifiers & adding imports & manually writing wrappers for functions, but why not just evolve parts so that they can add imports, and QQ can take care of function wrappers? Anyways, we have an explosion of library types.
  2. Augmentation declarations would not be needed
  3. Compile time imports not needed - This is essentially replacing that with code"" or -{} compile time feature
  4. Annotations would have to be allowed on statements / expressions eventually for statement / expression macros

The only pitfall I see is the lack of complete (resolved AST) introspection, code highlighting & autocompletion.
As @mraleph mentions code highlighting could be done by treating the 'code' tagged string QQ specially or having a special QQ syntax delimiter. Autocompletion might be more difficult, but I believe it is an addressable problem. Introspection I assume would be on an unresolved AST level, which would likely work for a lot of things, but has some pitfalls when dealing with type inference.

Personally I like the tagged string based QQ or something like code`${}` as the delimiter since braces are already used so much in dart.

The one question I have is about introspection. Currently it sounds like @mraleph is thinking of an AST based introspection, which had a lot of pushback from the dart team in the previous proposals because of language version and language changes potentially causing breakages in the AST that macros might use. Personally I feel like it is just like any package breaking on a change in version of a dependent package, so I'm not as concerned about it. Also are function declarations / classes really going to change that much? You have basic concepts like named parameters, positional parameters, class members, fields, getters, superclasses, etc. If you remove any of those from the language that would be a large breaking change to everyone, not just the macro authors. I believe some simplified form of the AST is relatively stable and is generally useful as an introspection API. However, I am concerned by lacking types for things that have inferred types, which is another advantage of the current proposal. Most getters, setters, fields, and function parameters have explicit types (at least how I write my code), however for many fields I have default initializers or late initializers, and use the inferred type. This I believe to be the biggest problem of QQ, which isn't a problem for languages that are meant to be dynamically typed or which don't use a lot of type inference.

I'm am interested in any form of metaprogramming, (current proposal or QQ) and will likely use it a lot in whatever form it ends up. But it seems like if you want to have something powerful and not just a quirk of the language you need AST based metaprogramming / compile time QQ not just annotation based metaprogramming, which is why I pushed a lot to get Code parameters for macro invocations in the current proposal. One of the things that I have heard from the dart team is that it would be ideal if metaprogramming could let library authors create small language features that are specific to their use cases rather than introducing for example 10 different types of value classes. Well I think that QQ might allow for creating more use-case specific language features. Especially because it can be used in expression / statement context in addition to the annotation context, and is not as limited by a class based API. However, if expression / statement macros are included in the current proposal, I see them as roughly feature equivalent, but view the current proposal as more work because of all of the classes and new language features that are required to enable it. However, it is also more powerful because of the introspection on types, and can have more fine grained invalidation and recompilation. In other words I see the pros and cons of both and am glad that there is continued discussion on whether there is a better way than the current proposal with the right balance of features and simplicity. Thanks for all of your hard work!

@rrousselGit
Copy link

  • Augmentation libraries (yet another library type) would not be needed. We already have parts, library files, libraries, and potentially macro libraries, adding augmentation libraries might be nice for private identifiers & adding imports & manually writing wrappers for functions, but why not just evolve parts so that they can add imports, and QQ can take care of function wrappers? Anyways, we have an explosion of library types.
  • Augmentation declarations would not be needed

There are use-cases that these solve which are not covered by quasiquoting, such as the ability to add initializers to constructors.

@mraleph
Copy link
Member Author

mraleph commented Nov 25, 2021

@tatumizer It would looks something like this with QQ as I imagine it:

class Foo {
  -{(){
     var list=<int>[2];
     for (int i=3; list.length <100; i+=2) {
        if (isProbablePrime(i)) list.add(i);
     }
     return +{ static const smallPrimes= -{ list }; }
    }()}
}

@TimWhiting

Currently it sounds like @mraleph is thinking of an AST based introspection, which had a lot of pushback from the dart team in the previous proposals because of language version and language changes potentially causing breakages in the AST that macros might use.

I was explicitly opaque on the introspection part. I would like to decouple metaprogramming into two parts: how program structure is understood and how it is manipulated.

My initial somewhat naive approaches to metaprogramming were based on Kernel AST manipulation (which gives you both an understanding and manipulation of the program) but over the years I came to appreciate that Kernel ASTs are hidden from Dart users.

I think the following should probably be true:

  • AST fragments constructed through +{...} should be mostly opaque to the user - though few different opaque types should exist to allow differentiating declarations from expressions, etc.
  • There should be an small API for accessing information about declarations in the program. It's not entirely clear to me how detailed that information should be and which parts of it should be opaque and which ones not. (e.g. function bodies should likely be opaque)

@Levi-Lesches
Copy link

@TimWhiting, your comparison of QQ to other solutions didn't mention basic string interpolation, which seems to be the closest analog to QQ. String interpolation hits all the same pros that you list.

@mraleph, I think your comparisons to string interpolation are too clunky. The following two examples:

Member toJson(Class currentClass) {
  return Member.fromSource("""
	Map<String, dynamic> toJson() => {
      ${[ 
        for (var field in currentClass.fields)
          "${field.name} : ${field.ref}" 
      ].join(',\n')}
    }  
""");
}

and

Member toJson(Class currentClass) {
  return code """
    Map<String, dynamic> toJson() => {
      ${[ 
        for (var field in currentClass.fields)
          code "${field.name} : ${field.ref}" 
      ]}
    }
 """;
}

can be written instead as:

Member toJson(Class currentClass) => [
  "Map<String, dynamic> toJson() => {",
  for (final field in currentClass.fields)
    "${field.name} : ${field.ref},",
  "};",
].join();  // and let Dart Format take care of everything

Even using code "string" wouldn't make it any less readable. I think in terms of easily looking at macros and understanding what they should be outputting, string interpolation seems to have the most basic syntax.

@mraleph
Copy link
Member Author

mraleph commented Nov 26, 2021

@tatumizer

@mraleph: do you agree that comptime/emit notation is more readable?

It's basically the same thing in my eyes. There is a bit of noise around immediate invoked anonymous closure (() { } ()) but the rest is basically isomorphic to your example.

@Levi-Lesches

can be written instead as:

That's fair, it's a bit more readable then other string interpolations. Not sure if more readable then QQ though because it squishes different syntactic layers together and has some punctuation ",.

Also note that the more imperative you make string-building code the harder it gets to analyse this statically, e.g. with QQ there is a natural (= well defined) way to split declaration building from body building. In your example both are squished together into basically linear code and splitting body building from outline (declaration building) requires some analysis.

@jakemac53
Copy link
Contributor

  • Execution layer: debugging generated code. In QQ case compiler can generate a file on the side which contains all generated code. Note that this file does not have to be a valid Dart file, (e.g. it can be a generated toJson() method dumped into a separate file) that does not really prevent IDE from showing this file during debugging session.

This was my original thought for metaprogramming, but it really doesn't work out in the experiments and discussions we had. You have to somehow provide enough context in the generated file for the user to understand what they are looking at, while also not confusing them. And provide the IDE with real line numbers etc for debugging, setting break points, etc, and also not have the same lines duplicated across multiple files.

That is what augmentation libraries are all about, and why they exist as a part of the proposal. It is a general purpose language feature that users can understand, and is also powerful enough to implement macros. It gives us a structured format for presenting the generated code to the user in a readable way.

@munificent
Copy link
Member

I'm really glad you filed this, because it's good to talk about it. I'm familiar with quotation and quasiquotations and it's something I've mulled over as part of the metaprogramming story for Dart.

I think you are proposing two roughly separable concepts here:

  1. Like @bwilkerson characterizes, you have quotations, which are essentially another kind of literal syntax that produce AST objects. You could imagine us adding them to Dart completely independent of metaprogramming:

    main() {
      var ast = -{ print('Hello, world!'); };
      print(ast); // Some kind of AST representation...
    }

    Obviously, it wouldn't be useful to add quotations without other metaprogramming features, but you could. Conceptually, it's just syntactic sugar for something like:

    main() {
      var ast = ExpressionStatement(
          FunctionInvocation('print',
              StringLiteral('Hello, world!')));
      print(ast); // Some kind of AST representation...
    }

    This syntax also allows nested unquotation to interpolated existing values into the created AST node. Just like expressions in string interpolation.

  2. Allowing you to use a quotation anywhere in a Dart program. When one is encountered, it is evaluated at compile time and the resulting AST node is inserted at that point in the program and then compilation proceeds using that code.

I think it's useful to separate these because they operate at different levels, and we could conceivably do one, the other, or both.

Quotation syntax

I don't know about Jake, but I do think quotation syntax is nice. I put some time into trying to come up with a nice design for Dart. One of the main challenges is that Dart's grammar is so complex and syntactically overloaded. Consider:

var ast = -{ {} };

Is that an expression node for an empty map/set, a statement node for an empty block, an empty class body, an empty function body, etc.?

If we only allow quotations to appear embedded in surrounding Dart code, then we can use that context to disambiguate, but I think that would be too much of a restriction for the kinds of complex metaprogramming that our known use cases require.

Instead, we'd probably need some kind of marker to indicate what where in the grammar your quotation is for:

var ast1 = expr -{ {} }; // Empty map/set.
var ast2 = stmt -{ {} }; // Empty block.

Figuring out the full set of these gets hard. Obviously, we'd want expression, statement, declaration, and type annotation. Probably class member, list element, and map element. Parameter? Parameter list? Function body? The more I thought about it, the more I felt like Dart's grammar is just too rich to directly support some kind of syntax literals for it. We are really far from s-exprs.

I proposed tagged strings instead because I think they give us most of the brevity of quotations while being a much simpler language feature. They reuse the string and interpolation syntax users already know, which is nice. They let us decide which set of grammar entrypoints to support at the library level instead of baking into the language. And it's a general purpose language feature that could be used outside of metaprogramming.

Tagged strings probably don't give as nice of an IDE experience as real quotation syntax would—syntax highlighting, parsing, etc.—but I have some optimism that that is doable even with tagged strings. A Dart IDE can statically resolve the tag name and if it points to our own metaprogramming library, then a smart IDE could possibly parse and syntax highlight the contents of the string.

Strings also give users the luxury of composing code out of fragments that are themselves not complete grammar productions. For example, a pair of arguments is a meaningful thing to want to interpolated into some generated code, but it's not a meaningful syntactic unit in the Dart grammar.

I realize semi-stringly-typed programming isn't a totally first-class developer experience, but I think the simplicity of it does have some value.

Directly embedded quotations

The metaprogramming part of your proposal is that if quotation appears where code is expected, the quotation is run at compile time and the result interpolated. Almost as if the entire library is itself a quotation that can contain unquoted expressions.

I really like how easy this would make it for users to insert a little one-off metaprogramming in their code. But my understanding is that those kind of uses are a minority of the use cases we have. The stuff users are asking for is mostly reusable metaprogramming. Things like JSON serialization, data classes, auto-disposable, etc.

My expectation is that most metaprogramming would be "framework-like" code in that a single proficient author will write a macro that gets consumed by many users. In that model, what I think matters most is that applying metaprogramming feels as easy, simple, and seamless as possible.

Given:

-{ import 'package:json_serializable/macros.dart' }

class Foo {
  final String a;
  final String b;

  -{toJson(currentClass)}
}

This requires users to understand the quotation syntax, and that some kind of special imports are required. Compare that to what we're aiming for with the macro proposal:

import 'package:json_serializable/macros.dart'

@toJson
class Foo {
  final String a;
  final String b;
}

This is already perfectly valid, familiar Dart code today. It's the code users already write when they are using a library that does code generation. Macros just provide a more integrated way to implement that.

I'm also worried that requiring all metaprogramming code to live inside quasiquotations would make it hard to do metaprogramming in the large. Many of the use cases we have are a lot more complex than just synthesizing a statement or two. Metaprogramming authors need to do fairly complex introspection and build up function bodies using a decent amount of imperative code. Because of that, I want it to be easy for authors to split their metaprogramming into multiple functions, reuse code, etc. That gets harder if everything has to live inside immediately invoked closures.

A big part of this is that our use cases for metaprogramming in Dart are quite different from metaprogramming in other languages. In languages like Lisp and Scheme (and maybe Rust?), you're often using metaprogramming to add new small-scale syntactic niceties. Macros are for extending the imperative syntax of the language and making.

Dart is already quite expressive at the statement and expression level (for better or worse), so we don't have many use cases for new syntax. There's only so many control flow structures one could want and we have a whole lot of them already.

Most of our use cases are really about procedurally generating entire declarations. It's about code reuse and programmability at the declaration level. That requires metaprogramming that has fairly deep introspection ability, access to typed and resolved elements, and the ability to write pretty complex macro code. That's why we're leaning towards an approach where macros are full-featured Dart classes.

But, having said that, I wouldn't rule out some kind of quotation syntax. I pitched tagged strings because it's simpler (and can be used for other purposes), but other options are definitely on the table.

@TimWhiting
Copy link

TimWhiting commented Nov 30, 2021

As much as I like the idea of Quasiquoting, I've been convinced that it is not the right fit for the metaprogramming side of things because of the introspection / type information that you can get from the current proposal. Also I appreciate that augmentation libraries and tagged strings solve more use cases than just metaprogramming. However, I still don't like the idea of generated files all over the place, but it is something I can deal with (since that is the way we have it currently & we can hide them from the IDEs etc).

@leafpetersen
Copy link
Member

This ends up being a lot more complicated than it might seem on the surface, and its a major sticking point. For the current macro proposal we have ended up defining an entire additional language feature (augmentation libraries) just to solve this problem.

I am not sure it's actually complicated or related to augmentation libraries. Augmentation libraries solve a different problem: they are about generating Dart source into a separate file and having it being spliced into another existing Dart file. What if we don't actually need that property? If splicing is controlled on the AST level, the augmentation libraries are not really needed as a feature.

Just to clarify a bit, the augmentation library approach was introduced specifically to solve this exact problem (the problem of how to surface macro generated code to the user). The original approach we proposed was to do exactly the style of splicing you are discussing here, and very strong concerns were raised about this from the teams who would be tasked with implementing this, around how to give a good UX to end users. Augmentation libraries were proposed as a way to provide a unified metaphor for how to surface macro generated code to the user across our tools.

@mraleph
Copy link
Member Author

mraleph commented Dec 3, 2021

I think I have managed to conflate a lot of topics into a single issue due to the complicated nature of the topic. Lets try to split it out a bit: my main concern is about generating structured data (Dart code) from other structured data (Dart code) through an unstructured medium - raw text. What I am trying to argue is that there are strong benefits in using structured medium instead - QQ syntax. One interesting one that I have brought up before is that you can statically split a single QQ based macro into "outline" only and "body" parts without programmer actually doing it manually.

Other things that we discuss are tangential, and are not related to the direct core of the proposal. QQ can be used in macroses that are triggered using annotation syntax and QQ can be used to generate augmentation libraries.

Instead, we'd probably need some kind of marker to indicate what where in the grammar your quotation is for:

I have considered this problem before and I think we can just rely on types, e.g. Expression v = -{ {} } (-{ {} } as Expression) is an empty collection literal (more precise disambiguation requires either types inside QQ or a full AST to run type inference on).

I'm also worried that requiring all metaprogramming code to live inside quasiquotations would make it hard to do metaprogramming in the large. Many of the use cases we have are a lot more complex than just synthesizing a statement or two. Metaprogramming authors need to do fairly complex introspection and build up function bodies using a decent amount of imperative code. Because of that, I want it to be easy for authors to split their metaprogramming into multiple functions, reuse code, etc. That gets harder if everything has to live inside immediately invoked closures.

I am not sure where my proposal implied that this has to be done this way. I have written some code inline in a QQ but I have also indicated that you can move it to a separate file if you want. You can split and structure your code in any way you want. QQ is just a way to move between the levels. It has no implications on how you would organise the macros code (instead that you would have to organise it in a structural way).

Member toJson(Class currentClass) {
  return +{
    Map<String, dynamic> toJson() => {
      -{[ 
        for (var field in currentClass.fields)
          +{ -{field.name} : -{ field.ref } } 
      ]}
    }
  };
}

// Can be written as 

Tree _serializeAField(Field field) => 
    +{ -{field.name} : -{ field.ref } };

List<Tree> _serializeFields(Class currentClass) => [
  for (var field in currentClass.fields) _serializeAField(field)
];

Member toJson(Class currentClass) {
  return +{
    Map<String, dynamic> toJson() => {
      -{_serializeFields(currentClass)}
    }
  };
}

A big part of this is that our use cases for metaprogramming in Dart are quite different from metaprogramming in other languages.

I don't think Dart is unique in how we plan to use macros. Generating boilerplate code and declarations is a common use case for compile-time metaprogramming features in languages that have complicated ASTs.

@bwilkerson
Copy link
Member

Many of the use cases we have are a lot more complex than just synthesizing a statement or two.

It might be worthwhile to write a short side-by-side comparison of the two approaches (not necessarily in this issue) using one of those complex use cases. That might make the conversation a bit more concrete.

@mraleph
Copy link
Member Author

mraleph commented Dec 3, 2021

If there are existing examples using string based code APIs - I would be happy to port some to QQ myself.

@yanok
Copy link

yanok commented Aug 21, 2023

(Have to admit I didn't read all the comments yet)

I understand that switching to QQ API completely might be hard. But could we at least keep the current API but add some special syntax for Code literals with QQ instead of building them using strings? Tagged strings will help a bit, but tagged strings are still strings.

So instead of writing

builder.declareInType(DeclarationCode.fromParts([
      'external ',
      // ignore: deprecated_member_use
      await builder.resolveIdentifier(Uri.parse('dart:core'), 'int'),
      ' get hashCode;',
  ]));

we could write

builder.declareInType(+{ external int get hashCode; });

or, as a more complicated example, instead of the long function body here

for (var param in superconstructor.positionalParameters) {
we could write

builder.declareInType(
  +{ -{ clazz }.gen( -{ superconstructor.parameters } )
    -{
      switch (superconstructor) {
        null => +{},
        _ => +{: -{ superconstructor } ( -{ args } )}
      }
     };
   });

(which of course also requires calculating args separately and context-sensitive splicing).

@jakemac53
Copy link
Contributor

Tagged strings will help a bit, but tagged strings are still strings.

Actually, they can produce any kind of object :). So this example (modified to assume int is an Identifier):

builder.declareInType(+{ external -{int} get hashCode; });

Could be something like the following:

builder.declareInType(declaration 'external $int get hashCode;');

I think that generally accomplishes what you are asking for, while being a generally useful feature for other purposes.

Essentially, you can think of it as just a shorthand for passing lists of strings mixed with other values into a function.

@yanok
Copy link

yanok commented Aug 21, 2023

Consider:

Statement stmt = -{ 1 + 2; }
//                       ^

But why this won't just be an error? Splice pastes one piece of AST into a bigger one, here not only you are pasting a statement into RHS of an assignment, but also the code can't be correct for any splice because of missing semi-colon.

@yanok
Copy link

yanok commented Aug 21, 2023

Actually, they can produce any kind of object :). So this example (modified to assume int is an Identifier):

Right, I looked at the proposal. But the point is produce here. What bothers me is they still take strings. So, not only there will be no syntax highlighting in IDEs, but it's also super easy to mess things up.

I actually didn't mean int to be an identifier and I see that as another pro of QQ: one can rely on normal resolution to resolve basic things like dart:core int, instead of doing it all by hand.

@jakemac53
Copy link
Contributor

Right, I looked at the proposal. But the point is produce here. What bothers me is they still take strings. So, not only it won't be support for syntax highlighting in IDEs, but it's also super easy to mess things up.

They can be nicely typed though, so in the case of the Code apis, they would only accept other Code objects for the "thunks". It will be easy to just assume things from dart:core exist (which will in practice be a prefixed import so they won't exist), but other than that it should be pretty reasonable.

See also the reflected imports proposal to make it easier to get something like "normal resolution" for identifiers that a macro knows they will want (and have the ability to import). I am not convinced this is actually a good proposal though because it implies macros need access to the API of all transitive reflected imports to compile them, and those could require platform specific code that we don't want to be available to macros. So some other mechanism is probably better, although it would work OK for basic cases.

@yanok
Copy link

yanok commented Aug 22, 2023

Thanks, I've missed the nicely typed part. And I guess if hand written parser fails at compile time, with some effort on the IDE side we could propagate the error back to the string literal.

I agree it's mostly achieves what I'd like to achieve. I also agree that it's a more general solution, since users could provide parsers for their own grammar. FTR, Template Haskell's quasi-quoters, mentioned in the issue description, are working exactly like tagged strings proposal (with the only difference that they inherit the host language lexer, so they work with lists of tokens instead of strings, but each quasi-quoter still has to do parsing on its own).

OTOH, having to use that for the host language grammar feels kinda like poor man's quasi-quoting. Personally I'd appreciate having true QQ for Dart syntax alongside with tagged strings.

@yanok
Copy link

yanok commented Aug 22, 2023

@munificent I think you generally mix usage of +{} and -{}, making your examples a bit hard to follow.

Top-level unquotes (splices, -{}) know what needs to be spliced during parse time, because of parsing context.

Some examples:

-{myMacro()} // myMacro must return a top-level declaration/definition or a directive.
class C {
  -{otherMacro()} // otherMacro must return a member declaration/definition
}
var x = -{exprMacro()}; // exprMacro must return an expression

Quotes (+{}) must either be tagged or rely on type inference. Of course, code that runs on the meta level must be analyzed to get syntax errors for the main level, that is intertwining the usual "parse then analyze" sequence indeed, but that's the essence of meta-programming.

@davidmorgan
Copy link
Contributor

There's a lot of great discussion here, thanks!

As far as I can tell tagged strings #1988 is the main actionable proposal, if there is anything else we might do please reopen / file as more specific issues.

@mraleph
Copy link
Member Author

mraleph commented Sep 4, 2024

I have tried to use the current incarnation of the macros and I think that current code generation API leaves a lot to be desired. I don't think they are easy to read and write:

    final commonTypes = await _resolveTypes(builder);
    final implName = '_${clazz.identifier.name}Impl';
    builder.declareType(
        implName,
        DeclarationCode.fromParts([
          'class $implName implements ',
          NamedTypeAnnotationCode(name: clazz.identifier),
          ' {\n'
              '   final ',
          commonTypes.arena,
          ' _arena;\n',
          '   final ',
          commonTypes.pointerUint8,
          ' _data;\n',
          '   $implName._(this._arena, this._data);\n}\n'
        ]));

And the best thing you can do to make this readable within the current paradigm is to split it into smaller chunks.

Text centrism also gives raise to lots of hard to figure corners in the APIs (e.g. how do I resolve a type? how do I substitute a type reference into the code, etc).

Needing to remember ; and \n in various places and indent things within generated code is an anticherry on top.

The quasi-quoting gives answer to all these and more. Dart syntax becomes an API for generating Dart syntax.

final implName = '_${clazz.identifier.name}Impl';

final type = declare {
  import 'dart:typed_data';
  import 'package:simdjson/src/rt.dart' as rt;

  class ${implName} implements ${clazz} {
    rt.Arena _arena;
    Pointer<Uint8> _data;
    
    ${implName}._(this._arena, this._data);
  }
};

I am using declare{} where I previous used +{} and ${} where I previously used -{} - but, again, this is not about concrete syntax, but rather the conceptual idea as whole.

I don't want to outright reopen this issue - but I strongly feel we should reconsider the current state of things.

@jakemac53
Copy link
Contributor

tagged strings would significantly improve the ergonomics of creating code objects.

@mateusfccp
Copy link
Contributor

mateusfccp commented Sep 4, 2024

@jakemac53

Would tagged strings allow for internal string highlighting and static analysis?

@jakemac53
Copy link
Contributor

Would tagged strings allow for internal strings highlighting and static analysis?

No they would not, but macros are not generally written as a single giant declaration in my experience. They are built from pieces of shared generated code with lots of imperative validation code mixed in. So, you aren't going to get much syntax highlighting from quasi quoting either I don't think in practice.

@davidmorgan
Copy link
Contributor

@mraleph I think the dart_model work helps here: the "macros API" will be the serialization boundary, not the macro client library. So the macro code can look like anything at all, we will not be stuck--including third party packages, new language features or even being written in a language other than Dart.

@rrousselGit
Copy link

IMO we should have some form of string template.
When working on Freezed, I built my own. I'm sure others did the same.

For comparison, the following:

    final commonTypes = await _resolveTypes(builder);
    final implName = '_${clazz.identifier.name}Impl';
    builder.declareType(
        implName,
        DeclarationCode.fromParts([
          'class $implName implements ',
          NamedTypeAnnotationCode(name: clazz.identifier),
          ' {\n'
              '   final ',
          commonTypes.arena,
          ' _arena;\n',
          '   final ',
          commonTypes.pointerUint8,
          ' _data;\n',
          '   $implName._(this._arena, this._data);\n}\n'
        ]));

Becomes:

final implName = '_${clazz.identifier.name}Impl';

builder.declareTypeString(
  implName,
  args: {'classId': clazz.identifier},
  '''
class $implName implements {{classId}} {
  $implName._(this._arena, this._data);

  final {{package:arena/arena.dart#Arena}} _arena;
  final {{package:typed_data/typed_data.dart#U8intBuffer}} _data; 
}'''); 

I'm sure we could have a package for this.

@lrhn
Copy link
Member

lrhn commented Sep 5, 2024

Tagged strings can support anything tools can figure out to support
I see no reason something like:

Declaration ids(int count) =>
  decl"var ids = [for (var i = 0; i < $count; i++) "id\$i"];";

could not be recognized and highlighted. (But maybe we want it to be highlighted slightly differently from non-template code, otherwise it could be harder to see what's going on.)

@Wdestroier
Copy link

I would like to show an example of the T4 syntax from C#.

<#@ template language="C#" #> // Useless in Dart, .NET has more languages
<#@ output extension=".cs" #> // T4 generates files on disk, like build_runner, which is a bad DX.

<# 
// Define a list of properties for the class
var properties = new List<Tuple<string, string>> {
    Tuple.Create("int", "Id"),
    Tuple.Create("string", "Name"),
    Tuple.Create("DateTime", "DateOfBirth")
};
#>
// Auto-generated class
public class GeneratedClass
{
<# foreach (var prop in properties) { #>
    public <#= prop.Item1 #> <#= prop.Item2 #> { get; set; }
<# } #>
}

Output:

// Auto-generated class
public class GeneratedClass
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
}

It's almost pure C# code. It's more readable than the first syntax with + and - around and provides the same benefits.

I feel like <#= adds too much clutter, but here's how it could look in Dart:

<# 
// Define a list of properties for the class
var properties = [
  ('int', 'id'),
  ('String', 'name'),
  ('DateTime', 'dateOfBirth')
];
#>
// Auto-generated class
class GeneratedClass {
<# for (var prop in properties) { #>
  <#= prop.$1 #> <#= prop.$2 #>;
<# } #>
}

@tatumizer
Copy link

tatumizer commented Sep 11, 2024

Another possibility is to introduce a unary operator > that writes to a string buffer with a standard name e.g. out:
The structure of a generated program becomes more visible.

final out = StringBuffer();
> 'class $implName implements ${NamedTypeAnnotationCode(name: clazz.identifier)};';
> '{\n';
> '   final ${commonTypes.arena} _arena;\n';
> '   final ${commonTypes.pointerUint8} _data;\n';
> '}\n';
builder.declareType(implName, out.toString());

@lrhn
Copy link
Member

lrhn commented Sep 12, 2024

Using plain strings does not get your syntax checking. It's not putting into a StringSink that is hard.
It's defining valid Dart code syntax as data.

Using a special operator with a hard-coded receiver name requires a language feature, and it's not wildly more readable than

out + ' ....';
out + '.....';

with extension SBAdd on StringBuffer { StringBuffer operator+(Object out) => this..write(out); }.

Or even:

extension on String {
  void operator~() { 
     ((Zone.current[#_out] ?? (throw StateError("No current output"))) as StringSink).write(this); 
  }
}
R withOutput<R>(StringSink output, R Function() action) =>
    runZoned<R>(action, zoneVariables: {#_out: output});

which you use as:

var buffer = StringBuffer();
withOutput(buffer, () {
  something;
  ~"text to output ${something} or else\n";
  ~"more text to output;
});

But none of those are as readable as var write = buffer.write; write("...."); write("...");, and none of them actually help with making it correct Dart syntax.

@rrousselGit
Copy link

rrousselGit commented Sep 12, 2024

I think readability is more important than guaranteeing the validity of the generated code here.

Badly generated code will lead to a well contained compile error and a bug report on the macro. It can get fixed quickly.
Unreadable macro implementation will lead to unmaintainable macro in the long run.


On a different note, one thing I was considering was writing a macro that's applied either on a String literal, or on a random Dart class/function/...
And have the macro generate various Dart functions to generate code that's looking similar.

For example, we could have:

@Template('myTemplate')
class $Name extends MyBaseClass<$BuildT> {
  $BuildT build() => throw UnimplementedError();
}

Which generates code that can be used as:

builder.writeTemplate(
  myTemplate(
    args: (
      name: 'Example',
      buildT: Uri.parse('dart:core#int'),
    ),
    /* some optional args, to maybe enable adding mixins or methods in here */,
  ),
);

// Will generate:

class Example extends MyBaseClass<int> {
  int build() => throw UnimplementedError();
}

Obviously the exact API needs refining. But I think that could be an alternative way to get readable generation and sane ouput.

@tatumizer
Copy link

tatumizer commented Sep 12, 2024

For readability, every piece of noise counts.
Here's a take 2, which eliminates more noise (e.g. \n is added automatically, like in print; the buffer gets created by code and passed as a parameter; code(...) returns a string, so there's no need to call toString explicitly).

builder.declareType(implName, code ((out) {
~ 'class $implName implements ${NamedTypeAnnotationCode(name: clazz.identifier)} {';
~ '   final ${commonTypes.arena} _arena;';
~ '   final ${commonTypes.pointerUint8} _data;';
~ '}';
});

@rrousselGit : Creating a template for a single use is too much hassle with no clear benefit. For every program with a template, you can write an equivalent cleaner program using the above notation (IMO).

I think readability is more important than guaranteeing the validity of the generated code here.

I agree with that wholeheartedly :-). Chasing too many rabbits is rarely an advisable strategy :-)

@rrousselGit
Copy link

rrousselGit commented Sep 12, 2024

@tatumizer I already use something similar: #1989 (comment)

What's being discussed is a way to be more safe with regards to what's generated. Like preventing missing/extra , or ;.
String are readable, but they are the equivalent of using dynamic.

Quasiquoting, or even Templates would be more type-safe. It can do things like only allowing to write a constructor in a class/enum.

@tatumizer
Copy link

Like preventing missing/extra , or ;.

This is impossible in a general case. The fragments of the program can be generated conditionally, or in a loop - they won't exist as a single piece until joined together. Anyway, the resulting overall output will be shown in UI, with a normal highlighting and errors. Frankly, I don't see a problem worth solving here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
static-metaprogramming Issues related to static metaprogramming
Projects
Development

No branches or pull requests