Skip to content

Commit d8c1a50

Browse files
committed
First checkin of partial meta function support, with 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.
1 parent 65fcd0f commit d8c1a50

File tree

9 files changed

+945
-184
lines changed

9 files changed

+945
-184
lines changed

regression-tests/pure2-types-inheritance.cpp2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

2-
Human: type = {
3-
speak: (virtual this);
2+
Human: @interface type = {
3+
speak: (this);
44
}
55

66
N: namespace = {

regression-tests/test-results/pure2-types-inheritance.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#line 2 "pure2-types-inheritance.cpp2"
1111
class Human;
12+
1213

1314
#line 6 "pure2-types-inheritance.cpp2"
1415
namespace N {
@@ -27,10 +28,11 @@ class Cyborg;
2728
#line 2 "pure2-types-inheritance.cpp2"
2829
class Human {
2930
public: virtual auto speak() const -> void = 0;
30-
31+
public: virtual ~Human();
3132
public: Human() = default;
3233
public: Human(Human const&) = delete;
3334
public: auto operator=(Human const&) -> void = delete;
35+
3436
#line 4 "pure2-types-inheritance.cpp2"
3537
};
3638

@@ -86,6 +88,8 @@ auto main() -> int;
8688

8789
//=== Cpp2 function definitions =================================================
8890

91+
#line 0 "pure2-types-inheritance.cpp2"
92+
Human::~Human(){}
8993

9094
#line 6 "pure2-types-inheritance.cpp2"
9195
namespace N {

source/common.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ struct source_line
5454
bool all_tokens_are_densely_spaced = true; // to be overridden in lexing if they're not
5555

5656
source_line(
57-
std::string const& t = {},
58-
category c = category::empty
57+
std::string_view t = {},
58+
category c = category::empty
5959
)
6060
: text{t}
6161
, cat{c}
@@ -258,10 +258,10 @@ struct error_entry
258258
bool fallback = false; // only emit this message if there was nothing better
259259

260260
error_entry(
261-
source_position w,
262-
std::string const& m,
263-
bool i = false,
264-
bool f = false
261+
source_position w,
262+
std::string_view m,
263+
bool i = false,
264+
bool f = false
265265
)
266266
: where{w}
267267
, msg{m}

source/cppfront.cpp

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ class positional_printer
724724
if (auto newline_pos = s.find('\n');
725725
!leave_newlines_alone
726726
&& s.length() > 1
727-
&& newline_pos != std::string_view::npos
727+
&& newline_pos != s.npos
728728
)
729729
{
730730
while (newline_pos != std::string_view::npos)
@@ -1228,7 +1228,7 @@ class cppfront
12281228
assert (!section.second.empty());
12291229

12301230
// Get the parse tree for this section and emit each forward declaration
1231-
auto decls = parser.get_parse_tree(section.second);
1231+
auto decls = parser.get_parse_tree_declarations_in_range(section.second);
12321232
for (auto& decl : decls) {
12331233
assert(decl);
12341234
emit(*decl);
@@ -1329,7 +1329,7 @@ class cppfront
13291329
assert (!map_iter->second.empty());
13301330

13311331
// Get the parse tree for this section and emit each forward declaration
1332-
auto decls = parser.get_parse_tree(map_iter->second);
1332+
auto decls = parser.get_parse_tree_declarations_in_range(map_iter->second);
13331333
for (auto& decl : decls) {
13341334
assert(decl);
13351335
emit(*decl);
@@ -1398,7 +1398,7 @@ class cppfront
13981398
assert (!section.second.empty());
13991399

14001400
// Get the parse tree for this section and emit each forward declaration
1401-
auto decls = parser.get_parse_tree(section.second);
1401+
auto decls = parser.get_parse_tree_declarations_in_range(section.second);
14021402
for (auto& decl : decls) {
14031403
assert(decl);
14041404
emit(*decl);
@@ -1412,7 +1412,7 @@ class cppfront
14121412
// Finally, some debug checks
14131413
printer.finalize_phase();
14141414
assert(
1415-
tokens.num_unprinted_comments() == 0
1415+
(!errors.empty() || tokens.num_unprinted_comments() == 0)
14161416
&& "ICE: not all comments were printed"
14171417
);
14181418

@@ -3226,7 +3226,7 @@ class cppfront
32263226

32273227
emit(*n.expr);
32283228

3229-
// emit == and != as infix a @ b operators (since we don't have
3229+
// emit == and != as infix a ? b operators (since we don't have
32303230
// any checking/instrumentation we want to do for those)
32313231
if (flag_safe_comparisons) {
32323232
switch (op.type()) {
@@ -3322,7 +3322,7 @@ class cppfront
33223322

33233323
lambda_body += lhs_name;
33243324

3325-
// emit == and != as infix a @ b operators (since we don't have
3325+
// emit == and != as infix a ? b operators (since we don't have
33263326
// any checking/instrumentation we want to do for those)
33273327
if (flag_safe_comparisons) {
33283328
switch (term.op->type()) {
@@ -4660,6 +4660,69 @@ class cppfront
46604660
)
46614661
-> void
46624662
{
4663+
// First, do some deferred sema checks - deferred to here because
4664+
// they may be satisfied by metafunction application
4665+
4666+
// If this is a nonvirtual function, it must have an initializer
4667+
if (
4668+
n.is_function()
4669+
&& !n.is_virtual_function()
4670+
&& !n.has_initializer()
4671+
)
4672+
{
4673+
errors.emplace_back(
4674+
n.position(),
4675+
"a nonvirtual function must have a body ('=' initializer)"
4676+
);
4677+
return;
4678+
}
4679+
4680+
{
4681+
auto this_index = n.index_of_parameter_named("this");
4682+
auto that_index = n.index_of_parameter_named("that");
4683+
4684+
if (this_index >= 0) {
4685+
if (!n.parent_is_type()) {
4686+
errors.emplace_back(
4687+
n.position(),
4688+
"'this' must be the first parameter of a type-scope function"
4689+
);
4690+
return;
4691+
}
4692+
if (this_index != 0) {
4693+
errors.emplace_back(
4694+
n.position(),
4695+
"'this' must be the first parameter"
4696+
);
4697+
return;
4698+
}
4699+
}
4700+
4701+
if (that_index >= 0) {
4702+
if (!n.parent_is_type()) {
4703+
errors.emplace_back(
4704+
n.position(),
4705+
"'that' must be the second parameter of a type-scope function"
4706+
);
4707+
return;
4708+
}
4709+
if (that_index != 1) {
4710+
errors.emplace_back(
4711+
n.position(),
4712+
"'that' must be the second parameter"
4713+
);
4714+
return;
4715+
}
4716+
if (this_index != 0) {
4717+
errors.emplace_back(
4718+
n.position(),
4719+
"'that' must come after an initial 'this' parameter"
4720+
);
4721+
return;
4722+
}
4723+
}
4724+
}
4725+
46634726
// In phase 0, only need to consider namespaces and types
46644727

46654728
if (
@@ -4700,8 +4763,8 @@ class cppfront
47004763

47014764
// If we're in a type scope, handle the access specifier
47024765
if (n.parent_is_type()) {
4703-
if (n.access) {
4704-
printer.print_cpp2(n.access->to_string(true) + ": ", n.access->position());
4766+
if (!n.is_default_access()) {
4767+
printer.print_cpp2(to_string(n.access) + ": ", n.position());
47054768
}
47064769
else {
47074770
printer.print_cpp2("public: ", n.position());
@@ -4859,9 +4922,9 @@ class cppfront
48594922
// is one, or default to private for data and public for functions
48604923
if (printer.get_phase() == printer.phase1_type_defs_func_decls)
48614924
{
4862-
if (n.access) {
4925+
if (!n.is_default_access()) {
48634926
assert (is_in_type);
4864-
printer.print_cpp2(n.access->to_string(true) + ": ", n.access->position());
4927+
printer.print_cpp2(to_string(n.access) + ": ", n.position());
48654928
}
48664929
else if (is_in_type) {
48674930
if (n.is_object()) {
@@ -5319,10 +5382,9 @@ class cppfront
53195382
{
53205383
assert(
53215384
!is_main
5322-
&& prefix.empty()
5385+
// prefix can be "virtual"
53235386
// suffix1 will be " &&" though we'll ignore that
5324-
&& suffix2.empty()
5325-
&& "ICE: a destructor shouldn't have been able to generate a prefix or suffix (or be main)"
5387+
// suffix2 can be "= 0"
53265388
);
53275389

53285390
// Print the ~-prefixed type name instead of the operator= function name
@@ -5331,10 +5393,13 @@ class cppfront
53315393
&& n.parent_declaration->name()
53325394
);
53335395
printer.print_cpp2(
5334-
type_qualification_if_any_for(n)
5396+
prefix
5397+
+ type_qualification_if_any_for(n)
53355398
+ "~" + n.parent_declaration->name()->to_string(true),
5336-
n.position() );
5399+
n.position()
5400+
);
53375401
emit( *func, n.name(), false, true);
5402+
printer.print_cpp2( suffix2, n.position() );
53385403
}
53395404

53405405
// Ordinary functions are easier, do all their declarations except

source/io.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -846,14 +846,12 @@ class source
846846
)
847847
{
848848
cpp1_found = true;
849-
//lines.push_back({ &buf[0], source_line::category::preprocessor });
850849
add_preprocessor_line();
851850
while (
852851
pre.has_continuation
853852
&& in.getline(&buf[0], max_line_len)
854853
)
855854
{
856-
//lines.push_back({ &buf[0], source_line::category::preprocessor });
857855
add_preprocessor_line();
858856
pre = is_preprocessor(buf, false);
859857
}

source/lex.h

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ enum class lexeme : std::int8_t {
8686
Dot,
8787
Ellipsis,
8888
QuestionMark,
89+
At,
8990
Dollar,
9091
FloatLiteral,
9192
BinaryLiteral,
@@ -182,6 +183,7 @@ auto __as(lexeme l)
182183
break;case lexeme::Dot: return "Dot";
183184
break;case lexeme::Ellipsis: return "Ellipsis";
184185
break;case lexeme::QuestionMark: return "QuestionMark";
186+
break;case lexeme::At: return "At";
185187
break;case lexeme::Dollar: return "Dollar";
186188
break;case lexeme::FloatLiteral: return "FloatLiteral";
187189
break;case lexeme::BinaryLiteral: return "BinaryLiteral";
@@ -515,20 +517,23 @@ auto expand_raw_string_literal(
515517
//-----------------------------------------------------------------------
516518
// lex: Tokenize a single line while maintaining inter-line state
517519
//
518-
// line the line to be tokenized
520+
// mutable_line the line to be tokenized
519521
// lineno the current line number
520522
// in_comment are we currently in a comment
521523
// current_comment the current partial comment
522524
// current_comment_start the current comment's start position
523525
// tokens the token list to add to
524526
// comments the comment token list to add to
525527
// errors the error message list to use for reporting problems
528+
// raw_string_multiline the current optional raw_string state
526529
//
527530

528531
// A stable place to store additional text for source tokens that are merged
529532
// into a whitespace-containing token (to merge the Cpp1 multi-token keywords)
530533
// -- this isn't about tokens generated later, that's tokens::generated_tokens
531-
static auto generated_text = std::deque<std::string>{};
534+
static auto generated_text = std::deque<std::string>{};
535+
static auto generated_lines = std::deque<std::vector<source_line>>{};
536+
532537

533538
static auto multiline_raw_strings = std::deque<multiline_raw_string>{};
534539

@@ -677,7 +682,7 @@ auto lex_line(
677682
auto i = std::ssize(tokens)-1;
678683

679684
// If the third-to-last token is "operator", we may need to
680-
// merge an "operator@" name into a single identifier token
685+
// merge an "operator?" name into a single identifier token
681686

682687
if (
683688
i >= 2
@@ -1286,6 +1291,9 @@ auto lex_line(
12861291
break; case '?':
12871292
store(1, lexeme::QuestionMark);
12881293

1294+
break; case '@':
1295+
store(1, lexeme::At);
1296+
12891297
break;case '$':
12901298
if (peek1 == 'R' && peek2 == '"') {
12911299
// if peek(j-2) is 'R' it means that we deal with raw-string literal
@@ -1721,7 +1729,7 @@ class tokens
17211729
//-----------------------------------------------------------------------
17221730
// lex: Tokenize the Cpp2 lines
17231731
//
1724-
// lines tagged source lines
1732+
// lines tagged source lines
17251733
//
17261734
auto lex(
17271735
std::vector<source_line>& lines
@@ -1732,7 +1740,7 @@ class tokens
17321740
auto raw_string_multiline = std::optional<raw_string>();
17331741

17341742
assert (std::ssize(lines) > 0);
1735-
auto line = std::begin(lines)+1;
1743+
auto line = std::begin(lines);
17361744
while (line != std::end(lines)) {
17371745

17381746
// Skip over non-Cpp2 lines
@@ -1879,6 +1887,8 @@ class tokens
18791887

18801888
};
18811889

1890+
static auto generated_lexers = std::deque<tokens>{};
1891+
18821892
}
18831893

18841894
#endif

0 commit comments

Comments
 (0)