-
Notifications
You must be signed in to change notification settings - Fork 251
Commit
interface
meta…
… type function This commit includes "just enough" to make this first meta function work, which can be used like this... ``` Human: @interface type = { speak: (this); } ``` ... where the implementation of `interface` is just about line-for-line from my paper P0707, and now (just barely!) compiles and runs in cppfront (and I did test the `.require` failure cases and it's quite lovely to see them merge with the compiler's own built-in diagnostics): ``` //----------------------------------------------------------------------- // interface: an abstract base class having only pure virtual functions auto interface( meta::type_declaration& t ) -> void { bool has_dtor = false; for (auto m : t.get_members()) { m.require( !m.is_object(), "interfaces may not contain data objects"); if (m.is_function()) { auto mf = m.as_function(); mf.require( !mf.is_copy_or_move(), "interfaces may not copy or move; consider a virtual clone() instead"); mf.require( !mf.has_initializer(), "interface functions must not have a function body; remove the '=' initializer"); mf.require( mf.make_public(), "interface functions must be public"); mf.make_function_virtual(); has_dtor |= mf.is_destructor(); } } if (!has_dtor) { t.require( t.add_member( "operator=: (virtual move this) = { }"), "could not add pure virtual destructor"); } } ``` That's the only example that works so far. To make this example work, so far I've added: - The beginnings of a reflection API. - The beginnings of generation from source code: The above `t.add_member` call now takes the source code fragment string, lexes it, parses it, and adds it to the `meta::type_declaration` object `t`. - The first compile-time meta function that participates in interpreting the meaning of a type definition immediately after the type grammar is initially parsed (we'll never modify a type after it's defined, that would be ODR-bad). I have NOT yet added the following, and won't get to them in the short term (thanks in advance for understanding): - There is not yet a general reflection operator/expression. - There is not yet a general Cpp2 interpreter that runs inside the cppfront compiler and lets users write meta functions like `interface` as external code outside the compiler. For now I've added `interface`, and I plan to add a few more from P0707, as meta functions provided within the compiler. But with this commit, `interface` is legitimately doing everything except being run through an interpreter -- it's using the `meta::` API and exercising it so I can learn how that API should expand and become richer, it's spinning up a new lexer and parser to handle code generation to add a member, it's stitching the generated result into the parse tree as if it had been written by the user explicitly... it's doing everything I envisioned for it in P0707 except for being run through an interpreter. This commit is just one step. That said, it is a pretty big step, and I'm quite pleased to finally have reached this point. --- This example is now part of the updated `pure2-types-inheritance.cpp2` test case: // Before this commit it was this Human: type = { speak: (virtual this); } // Now it's this... and this fixed a subtle bug (can you spot it?) Human: @interface type = { speak: (this); } That's a small change, but it actually also silently fixed a bug that I had written in the original code but hadn't noticed: Before this commit, the `Human` interface did not have a virtual destructor (oops). But now it does, because part of `interface`'s implementation is to generate a virtual destructor if the user didn't write one, and so by letting the user (today, that was me) express their intent, we get to do more on their behalf. I didn't even notice the omission until I saw the diff for the test case's generated `.cpp` had added a `virtual ~Human()`... sweet. Granted, if `Human` were a class I was writing for real use, I would have later discovered that I forgot to write a virtual destructor when I did more testing or tried to do a polymorphic destruction, or maybe a lint/checker tool might have told me. But by declaratively expressing my intent, I got to not only catch the problem earlier, but even prevent it. I think it's a promising data point that my own first attempt to use a metaclass in such a simple way already fixed a latent simple bug in my own code that I hadn't noticed. Cool beans. --- Re syntax: I considered several options to request a meta function `m` be applied to the type being defined, including variations of `is(m)` and `as(m)` and `type(m)` and `$m`. I'm going with `@m` for now, and not because of Python envy... there are two main reasons: - I think "generation of new code is happening here" is such a fundamental and important new concept that it should be very visible, and actually warrants taking a precious new symbol. The idea of "generation" is likely to be more widely used, so being able to have a symbol reserved for that meaning everywhere is useful. The list of unused symbols is quite short (Cpp2 already took `$` for capture), and the `@` swirl maybe even visually connotes generation (like the swirl in a stirred pot -- we're stirring/cooking something up here -- or maybe it's just me). - I want the syntax to not close the door on applying meta functions to declarations other than types. So putting the decoration up front right after `:` is important, because putting it at the end of the type would likely much harder to read for variables and especially functions.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
|
||
Human: type = { | ||
speak: (virtual this); | ||
Human: @interface type = { | ||
speak: (this); | ||
} | ||
|
||
N: namespace = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -724,7 +724,7 @@ class positional_printer | |
if (auto newline_pos = s.find('\n'); | ||
!leave_newlines_alone | ||
&& s.length() > 1 | ||
&& newline_pos != std::string_view::npos | ||
&& newline_pos != s.npos | ||
) | ||
{ | ||
while (newline_pos != std::string_view::npos) | ||
|
@@ -1228,7 +1228,7 @@ class cppfront | |
assert (!section.second.empty()); | ||
|
||
// Get the parse tree for this section and emit each forward declaration | ||
auto decls = parser.get_parse_tree(section.second); | ||
auto decls = parser.get_parse_tree_declarations_in_range(section.second); | ||
for (auto& decl : decls) { | ||
assert(decl); | ||
emit(*decl); | ||
|
@@ -1329,7 +1329,7 @@ class cppfront | |
assert (!map_iter->second.empty()); | ||
|
||
// Get the parse tree for this section and emit each forward declaration | ||
auto decls = parser.get_parse_tree(map_iter->second); | ||
auto decls = parser.get_parse_tree_declarations_in_range(map_iter->second); | ||
for (auto& decl : decls) { | ||
assert(decl); | ||
emit(*decl); | ||
|
@@ -1398,7 +1398,7 @@ class cppfront | |
assert (!section.second.empty()); | ||
|
||
// Get the parse tree for this section and emit each forward declaration | ||
auto decls = parser.get_parse_tree(section.second); | ||
auto decls = parser.get_parse_tree_declarations_in_range(section.second); | ||
for (auto& decl : decls) { | ||
assert(decl); | ||
emit(*decl); | ||
|
@@ -1412,7 +1412,7 @@ class cppfront | |
// Finally, some debug checks | ||
printer.finalize_phase(); | ||
assert( | ||
tokens.num_unprinted_comments() == 0 | ||
(!errors.empty() || tokens.num_unprinted_comments() == 0) | ||
&& "ICE: not all comments were printed" | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
hsutter
Author
Owner
|
||
); | ||
|
||
|
@@ -3226,7 +3226,7 @@ class cppfront | |
|
||
emit(*n.expr); | ||
|
||
// emit == and != as infix a @ b operators (since we don't have | ||
// emit == and != as infix a ? b operators (since we don't have | ||
// any checking/instrumentation we want to do for those) | ||
if (flag_safe_comparisons) { | ||
switch (op.type()) { | ||
|
@@ -3322,7 +3322,7 @@ class cppfront | |
|
||
lambda_body += lhs_name; | ||
|
||
// emit == and != as infix a @ b operators (since we don't have | ||
// emit == and != as infix a ? b operators (since we don't have | ||
// any checking/instrumentation we want to do for those) | ||
if (flag_safe_comparisons) { | ||
switch (term.op->type()) { | ||
|
@@ -4660,6 +4660,69 @@ class cppfront | |
) | ||
-> void | ||
{ | ||
// First, do some deferred sema checks - deferred to here because | ||
// they may be satisfied by metafunction application | ||
|
||
// If this is a nonvirtual function, it must have an initializer | ||
if ( | ||
n.is_function() | ||
&& !n.is_virtual_function() | ||
&& !n.has_initializer() | ||
) | ||
{ | ||
errors.emplace_back( | ||
n.position(), | ||
"a nonvirtual function must have a body ('=' initializer)" | ||
); | ||
return; | ||
} | ||
|
||
{ | ||
auto this_index = n.index_of_parameter_named("this"); | ||
auto that_index = n.index_of_parameter_named("that"); | ||
|
||
if (this_index >= 0) { | ||
if (!n.parent_is_type()) { | ||
errors.emplace_back( | ||
n.position(), | ||
"'this' must be the first parameter of a type-scope function" | ||
); | ||
return; | ||
} | ||
if (this_index != 0) { | ||
errors.emplace_back( | ||
n.position(), | ||
"'this' must be the first parameter" | ||
); | ||
return; | ||
} | ||
} | ||
|
||
if (that_index >= 0) { | ||
if (!n.parent_is_type()) { | ||
errors.emplace_back( | ||
n.position(), | ||
"'that' must be the second parameter of a type-scope function" | ||
); | ||
return; | ||
} | ||
if (that_index != 1) { | ||
errors.emplace_back( | ||
n.position(), | ||
"'that' must be the second parameter" | ||
); | ||
return; | ||
} | ||
if (this_index != 0) { | ||
errors.emplace_back( | ||
n.position(), | ||
"'that' must come after an initial 'this' parameter" | ||
); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
// In phase 0, only need to consider namespaces and types | ||
|
||
if ( | ||
|
@@ -4700,8 +4763,8 @@ class cppfront | |
|
||
// If we're in a type scope, handle the access specifier | ||
if (n.parent_is_type()) { | ||
if (n.access) { | ||
printer.print_cpp2(n.access->to_string(true) + ": ", n.access->position()); | ||
if (!n.is_default_access()) { | ||
printer.print_cpp2(to_string(n.access) + ": ", n.position()); | ||
} | ||
else { | ||
printer.print_cpp2("public: ", n.position()); | ||
|
@@ -4859,9 +4922,9 @@ class cppfront | |
// is one, or default to private for data and public for functions | ||
if (printer.get_phase() == printer.phase1_type_defs_func_decls) | ||
{ | ||
if (n.access) { | ||
if (!n.is_default_access()) { | ||
assert (is_in_type); | ||
printer.print_cpp2(n.access->to_string(true) + ": ", n.access->position()); | ||
printer.print_cpp2(to_string(n.access) + ": ", n.position()); | ||
} | ||
else if (is_in_type) { | ||
if (n.is_object()) { | ||
|
@@ -5319,10 +5382,9 @@ class cppfront | |
{ | ||
assert( | ||
!is_main | ||
&& prefix.empty() | ||
// prefix can be "virtual" | ||
// suffix1 will be " &&" though we'll ignore that | ||
&& suffix2.empty() | ||
&& "ICE: a destructor shouldn't have been able to generate a prefix or suffix (or be main)" | ||
// suffix2 can be "= 0" | ||
); | ||
|
||
// Print the ~-prefixed type name instead of the operator= function name | ||
|
@@ -5331,10 +5393,13 @@ class cppfront | |
&& n.parent_declaration->name() | ||
); | ||
printer.print_cpp2( | ||
type_qualification_if_any_for(n) | ||
prefix | ||
+ type_qualification_if_any_for(n) | ||
+ "~" + n.parent_declaration->name()->to_string(true), | ||
n.position() ); | ||
n.position() | ||
); | ||
emit( *func, n.name(), false, true); | ||
printer.print_cpp2( suffix2, n.position() ); | ||
} | ||
|
||
// Ordinary functions are easier, do all their declarations except | ||
|
4 comments
on commit d8c1a50
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this pretty big step which opens the door of code generation in Cpp2. And thanks for your smart decision to introduce @
in code generation. Also I think symbol @
fits well for user-defined language constructs too (if you have a plan to support it or something similar in the future):
if: <T: type> if let (@opt => std::optional<T>) do (@run => : (: T)) = {
if @opt.has_value() {
value: T = @opt.value();
@run(value);
}
}
if let optional_variable do: (value) {
...
value.call();
...
}
Obviously @identifier
is easily recognizable for code generation in comparison to my suggestion which I proposed parenthesis around it (identifier)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- There is not yet a general Cpp2 interpreter that runs inside the cppfront compiler and lets users write meta functions like
interface
as external code outside the compiler.
This seems to group up two big steps.
The interpreter, and name lookup.
You can write an interpreter with name lookup fixed on the built-in metafunctions.
Actual name lookup would be needed for the latter part:
and lets users write meta functions like
interface
as external code outside the compiler.
That would at least require also parsing the included .h2
headers.
Do you repeat this for every Cpp2 TU?
Or do you perform a more involved solution like Cpp1 modules.
The former would result in simpler cppfront
invocations, but increase compile times.
The latter would require more build system support, but decrease compile times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- There is not yet a general Cpp2 interpreter that runs inside the cppfront compiler and lets users write meta functions like
interface
as external code outside the compiler.
Is such a Cpp2 interpreter possible for Cppfront?
Even the built-in metafunctions require a Cpp1 compiler with reflect.h
and the C++ standard library.
I can't see how you could interpret those with a running cppfront
program.
Alternatives are possible with build system support.
Here I will present my current design based on a plug-in system.
I have no hands-on experience with those,
so the actual shape is more speculative,
although the idea itself is sound.
IIUC, libraries can be loaded at program startup.
So before cppfront
's main, metafunctions can be loaded.
That means replacing the fixed list of metafunctions with a global registry of metafunctions.
- When Cppfront compiles a TU, it can emit a library that just loads the declared metafunctions.
- When Cppfront compiles a TU, the libraries of dependencies are loaded at
cppfront
program startup.
You can't consume a metafunction on the same non-module TU or module that declares it.
Otherwise, you run into similar problems as when emitting concepts in Phase 1 "Cpp2 type declarations" (#578 (comment)).
This process should be fast with modules.
Ideally, the reflection API in reflect.h2
is refactored into a module
that doesn't depend on parse.h
(which depends on all sources but sema.h
and cppfront.cpp
).
Here's an example.
- Define the module
game_lib
in Cpp2, which declares the metafunctionentity
. - Define the module
game_lib
in CMake. cppfront
lowersgame_lib
and emits the module/librarycpp2.metafunctions_for.game_lib
.
cpp2.metafunctions_for.game_lib
populates the global registry of metafunctions
with the metafunctions declared ingame_lib
.- A CMake module takes care of the plug-in system part so that things just work.
- Define the TU
game
, which importsgame_lib
, and uses@entity
. - The
cppfront
program loaded by the CMake module to compilegame
will have the librarycpp2.metafunctions_for.game_lib
loaded.
This means that@entity
will be available tocppfront
as a metafunction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened #797 with a proof of concept using Boost.DDL.
In the case of
!errors.empty()
, theICE: not all comments were printed
message is misleading.