Skip to content

[BUG] Initializer as a statement: generated code does not compile #131

Closed

Description

@h0nzZik

Describe the bug
The fact that initializers of unnamed-declarations are statements makes it possible to write code whose C++ image does not compile.

unnamed-declaration:
  : id-expression-opt = statement

To Reproduce
Steps to reproduce the behavior:

  1. This Sample code is accepted by cppfront
// weird2.cpp
#include <functional>
main: () -> int = {
  x: std::function<int()> = return 0;
}

This works and generates weird2.cpp:

$ cppfront weird2.cpp2

but clang 12.0.1 reject the generated file

$ clang++ -std=c++2b -I include/ weird2.cpp
weird2.cpp2:4:28: error: expected expression
  std::function<int()> x { return 0;  }; 

I would expect the sample code to be rejected by cppfront.

Comment
Personally, I could imagine that the grammar would instead say:

unnamed-declaration:
  : id-expression-opt = expression ;

It feel weird that initializer is a statement, particularly when the generating code is treating it as if it were an expression: e.g., here, and here.

Edit: another problem I see with this approach is that it is not clear how should semantics of an initializer be described in a language specification.

Activity

  1. hsutter commented on Dec 28, 2022

    @hsutter
    Owner

    Thanks! Yes, I should reject that code... done.


    But there's a method to my madness here... it is deliberate that every initializer is a statement, regardless of whether the declaration is of an object, function, or (soon) type: The legal grammar for the initializer of all of them is identical, with semantic rules allowing a subset of the grammar for each.

    For example, the type declaration syntax I'm planning to add soon looks like this:

    shape: type = {
        // ordinary declarations, for example:
    
        // ordinary object declaration
        x: int = 0;  // nonstatic data member
        y: int = 0;
    
        // ordinary function declarations, with explicit "this" and "that" parameters
        // that default to ": mytype" so you don't have to write that
        draw: (this, on: canvas)    = { /*...*/ }  // nonstatic member function (has a 'this')
        move: (inout this, offset: position) = { /*...*/ }  // another, this time mutable 'this' (aka non-const)
        register: (obs: *observer)  = { /*...*/ }  // static member function (doesn't have a 'this')
    
        operator=: (out this, that) = { /* see d0708, this covers all of {copy,move}x{ctor,assign} */ }
    
        // ordinary type declaration (nested, and using a metaclass function example)
        colors: type is enum = { 
            red := 0; // ordinary object declaration syntax again
            green; // for convenience can allow simple _expression-statements_ that contain only an identifier, and let the metaclass transform it
            blue;
        }
    
        // etc.
    }
    

    Goal: "Yes, you already pretty much know how to write classes, once I implement those."

    Note this looks (and parses) just like a function body, except it has no computation/iteration statements (yet; this leaves the door open to adding top-level class statements someday if that's useful)... the type initializer must be a compound-statement (i.e., { } enclosed), that contains declaration-statements (i.e., ordinary declarations)

    Note this works because in Cpp2's design (disclaimer: implementation still in progress):

    • this is an explicit ordinary parameter; there is no special/weird member function declaration grammar
    • base classes will declared and initialized the same as data members (including they have names, and this has other advantages including layout and initialization control)

    The only grammar extensions I plan to add beyond what's already implemented are:

    • an access-specifier on any declaration-statement (allowed only in type initializers)
    • a way to specify an object declaration is a base, which might be spelled is (allowed only in type initializers).
    • a this-specifier, which can be implicit for implicit conversion functions, or virtual, override, final for virtual functions (allowed only on this parameters) ... you'll see this is already in the parse.h grammar comments for parameter-declaration

    So, yes, it's deliberate that everything in Cpp2 is grammatically defined/initialized with a statement, i.e., an applicable subset of the statement grammar. My hope is that it will be consistent and perhaps even elegant.

  2. SebastianTroy commented on Dec 28, 2022

    @SebastianTroy
  3. hsutter commented on Dec 28, 2022

    @hsutter
    Owner

    @SebastianTroy Great question.

    In a nutshell: Yes, and more... not just asserting the type meets requirements, but actually actively generating the intended kind of type. See the talks and papers at README.md > 2017: Reflection, generation, and metaclasses, particularly paper P0707, "Metaclass functions: Generative C++". If you like videos, I can recommend the beginning of the ACCU video which is the only time I've ever done a live pseudo-"usability study" in front of a live audience with genuinely unprepared volunteers. Thanks again to the volunteers and ACCU!

  4. SebastianTroy commented on Dec 30, 2022

    @SebastianTroy
  5. hsutter commented on Dec 30, 2022

    @hsutter
    Owner

    Does this mean that cpp2 aims to unify C++ typing even further by making enums, structs, and unions meta classes rather than distinct language features?

    Exactly.

    I did notice however in the enum meta classes section you didn't give an example of usage. func(MetaEnum val) wouldn't work because in the case of a meta class enum MetaEnum is a typename, and the enumeration values are all member variables of some type. func(MetaEnum::value_t val) where I assume value_t = reflexpr(underlying).as(new_type) to maintain strong typing. And while that works, func(MetaEnum::value_t val) feels like a step back when everything else is a step forward.

    It sounds like you're mainly concerned with having to utter the name of the enum :: the enumerator, i.e., the enumerators are scoped inside the enum? I view that as a feature rather than a bug, because C enums inject their enumerator names into the enclosing scope which litters it. This is it's one of the reasons I, Bjarne Stroustrup, and David Miller designed C++11 enum class. The proposal paper N2347 goes into the rationale. For convenience, C++20 added using enum. Does that help address your questions, even if you may still prefer the C enumerator non-qualification convenience?

  6. SebastianTroy commented on Dec 30, 2022

    @SebastianTroy
  7. JohelEGP commented on Dec 30, 2022

    @JohelEGP
    Contributor
    class(basic_enum) Shape { auto Circle, Square; };
    void foo(Shape s) { ... } // no longer makes sense, we want the user to specify one of Shape's members, not an instance of Shape!

    It seems to me like you're having issues with your own interpretation of basic_enum. I'd expect basic_enum to transform Shape into something like

    class Shape {
      static constexpr Shape Circle = {0};
      static constexpr Shape Square = {1};
      int underlying;
    };

    which would work more like today's enum class. Although you can't have data members of Shape, since it's an incomplete type.

  8. hsutter commented on Dec 30, 2022

    @hsutter
    Owner

    [Edited now that I realize @JohelEGP was replying to @SebastianTroy, not me]

    @JohelEGP Right.

    Referring to P0707r4 section 3.8, this example is named enum_class (to avoid the Cpp1 keyword(s)) and there's also a flag_enum (we really want both to be first-class and have different names). Injecting the value instance is done in the basic_enum metaclass function that is common to both enum_class and flag_enum, after computing the proper type (using the type the user specific, or a default type if they didn't specify one):

    // from inside P0707r4 basic_enum()'s code (note: speculative evolution of today's C++ syntax)
    ->(target) {
        U value; // the instance value
    }
    

    Pasting some of the examples from 3.8:

    [[Note: not Cpp2 syntax, this is from P0707]]

    Here is a state enumeration that starts at value 1 and counts up:

    class(enum_class) state {
        auto started = 1, waiting, stopped; // type is int
    };
    state s = state::started;
    while (s != state::waiting) {
        // ...
    }
    

    Here is a different enumeration using a different value type and setting some values while using incremented
    values where those are useful:

    class(enum_class) skat_games {
        char diamonds = 9, hearts /*10*/, spades /*11*/, clubs /*12*/, grand = 24;
    };
    

    In Cpp2, I aim to write those with roughly this syntax:

    state: type is enum = {
        started := 1; waiting; stopped;
    };
    s := state::started;
    while s != state::waiting {
        // ...
    }
    
    skat_games: type is enum = {
        diamonds: i8 = 9; hearts /*10*/; spades /*11*/; clubs /*12*/; grand = 24;
    };
    

    Does that address what you were wondering about?

  9. hsutter commented on Dec 30, 2022

    @hsutter
    Owner

    @SebastianTroy

    Anyway, thank you for taking the time to personally respond, but I think I've had enough attention, I'm more keen to see the fruits of your type implementation labour's, as I'm sure everyone else is!

    Thanks for the kind words, and no worries! I respond on purpose because it's worth the time to reply to to good questions == ones that I think others will have, so they can see the answers too and future readers can reference these answers. The more common ones should be promoted to a Design Note on this repo's wiki, but even having a reply here in the comments makes them referenceable. And it's nice to take a break from coding to write English, and vice versa. :)

    Also, writing a few of these answers helps preserve a repoint-in-time design snapshot, a self-documenting record of intentions and ideas that I can refer to if I want to look up the design archaeology if this part of the design evolves further in the future.

    Thanks!

  10. SebastianTroy commented on Dec 30, 2022

    @SebastianTroy
  11. hsutter commented on Dec 30, 2022

    @hsutter
    Owner

    The Cpp2-declared type is enum behaves the same as a Cpp1-declared enum class. Both have a number of compile-time-constant named static enumerator values in their scopes, and a nonstatic dat amember that stores this object's value. All are of the enumeration's underlying type.

    All the metaclass function is doing is generating the boilerplate, so you can write just this

    state: type is enum = {
        started := 1; waiting; stopped;
    };
    

    instead of writing out all of this (rough syntax, my current thought is to support only constexpr static data members and not support const nonstatic data members, so I don't think I need a static keyword)

    //  Writing it all by hand, instead of letting the 'enum' metaclass function do it for us
    state: type = {
        // "static constexpr" data members
        started: const int = 1;
        waiting: const int = 2;
        stopped: const int = 3;
    
        // nonstatic data member
        value: int;
    
        // construction
        operator=: (out this, v: int) = { value = v; }
        operator=: (out this, that) = { value = that.value; }
    
        // comparison
        operator<=>: (this, that) = { return value <=> that.value; }
    
        // and maybe one or two more like a to_string function
    };
    

    Does that help?

  12. SebastianTroy commented on Dec 30, 2022

    @SebastianTroy
  13. added a commit that references this issue on Feb 24, 2023
    6d4cada
  14. added a commit that references this issue on Feb 4, 2024
    4f7e8b2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions