Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

[SUGGESTION] Parameterized Meta Functions; A replacement for Attributes, Contracts, Coroutines and User-defined Language Constructs #523

Closed
msadeqhe opened this issue Jun 22, 2023 · 3 comments

Comments

@msadeqhe
Copy link

msadeqhe commented Jun 22, 2023

1. Preface

I'm looking for a feature to pass an expression itself like macro parameters instead of immediately calculate the result of an expression to pass its result to functions. To have this feature in Cpp2, the best condidate is Metaclass Functions with @name, because it generates code. So @name expr will generate new code from expr, and it can be applied to declarations, null statements, block statements or parameterized block statements. It's all about Reflections and Generations.

2. Suggestion Detail

This suggestion heavely depends on @hsutter's P0707 paper about Metaclasses.

The idea is to allow Meta Functions to get a parameter, in addition to make its usage general. This suggestion requires chaining (args)class syntax for object construction as described in this suggestion (e.g. (args)type1(args)type2(args)type3(args)...). It would have the following use cases:

  • User-defined Language Constructs: It is similar to macros in Cpp1:
    @check (i < 10) every (1min) {
        //statements...
    }
  • Attributes: It can be used as attribute specifier sequence in Cpp1:
    func: () @noreturn = { exit(); }
  • Contracts: It can be used as contracts proposals in Cpp1 (see NOTE 1):
    func: (a: int, b: int) -> int @pre a < b = { /*statements*/ }
  • Coroutines: It can be used as coroutines in Cpp1 (via co_await, co_yield and co_return):
    x: = @co_await call();

NOTE 1:

Contracts are a way to have preconditions, postconditions and assertions on functions. Mainly two different syntax is proposed to support them in Cpp1:

  • Closure-based syntax:
func: (a: int, b: int) -> int   pre {a < b}  = { /*statements*/ }
  • Attribute-like syntax:
func: (a: int, b: int) -> int [[pre: a < b]] = { /*statements*/ }

This suggestion is just a feature to explore, and I know that it isn't going to be implemented anytime soon as Reflection isn't ready yet in Cpp2.

The rules of the suggestion are that:

  • Meta Functions can either have only one parameter or have not any parameter.
    x: int @unused = 2;
    y: int @deprecated "reason" = 2;
    z: int @what (x) less (y) = 2;
    In the last line, (x) less (y) is an expression which creates an object with (x) less and calls operator() on it with (y). It is the only one parameter.
  • Meta Functions can be applied to declarations, null statements, block statements or parameterized block statements.
    // declaration
    func: () @deprecated = { /*statements*/ }
    
    // null statement
    @assume something > 10;
    
    // block statement
    (copy i: int = 0) @whilee (i < 10) nextt (i++) { /*statements*/ }
    
    // parameterized block statement
    @forr list doo (items) { /*statements*/ }
  • Multiple Meta Functions can be applied. They must be written in a sequence.
    func: (a: int, b: int) -> (r: int) @pre a < b @post a < r < b @nodiscard "reason" @another = { /*statements*/ }
  • They can be inside namespaces. For example std::meta is applied to a null statement:
    @std::meta "text";

Multiple parameters are not needed if we use reflections to parse that one parameter. After that we would generate new code based on that one parameter. Literally any expression are allowed to be that one parameter. Types look like they are keywords (althouth they are not) within chained object construction with (args)class syntax (as described in this suggestion):

// function, class1 and class2 look like they are keywords but they are not.
@meta function (args) class1 (args) class2 (args)...

// class1 and class2 look like they are keywords but they are not.
@meta (args) class1 (args) class2 (args)...

The point is that a parameterized meta function would generate new code from its parameter.

For example:

(copy i: int = 0) @repeat iff (i < 10) nextt (i++) every (1min) {
    print("Hello");
}

The above code would generate this new code:

(copy i: int = 0, copy __t: timer = ()) while i < 10 next i++ {
    print("Hello");
    __t.delay(1min);
}

Use Cases

2.1. User-defined Language Constructs

They are similar to macros in C and Cpp1, except:

  • They are safe. They are integrated into Cpp2 syntax.
  • They are readable. They are distinguishable from other identifiers with @ prefix.
  • They cannot mutate the language. They cannot be redefined. They may be in namespaces.

User-defined Language Constructs are Meta Functions which would be applied to null statements, block statements or parameterized block statements. Cpp1 attributes in which they apply to null statements, are somehow language constructs. For example, [[assume(expr)]]; and [[fallthrough]]; attributes in Cpp1, would be like this with the suggestion:

//[[assume(something > 0)]];
@assume something > 0;

// If Cpp2 has switch-case control structure:
switch (n) {
case 1:
    // [[fallthrough]];
    @fallthrough;
case 2:
    // [[fallthrough]];
    @fallthrough;
case 3:
    call();
}

Also when the resource have to be released, we can have a user-defined language construct for it:

@using resource {
    //statements...
    // It would release the resource with `resource.close()`.
}

2.2. Attributes

Parameterized meta functions are similar to attributes in Cpp1, except they are user-defined as library features.

This is how to use them to change the compiler behaviour or to inform the compiler to how generate code. For example, this is equal to [[noreturn]] in Cpp1:

// This function never ends!
function: () @noreturn = {
    while true {
        std::cout << "Forever you see this message again!\n";
    }
}

Compilers can implement their own attributes in Cpp2 as library features instead of how they have implementation-defined attributes in Cpp1. For example, alias extension attribute in GCC:

#include <compiler>

__func: (params) -> int = {
    /// statements...
}

func: (params) -> int @weak @alias "__f";

In this case, they do not generate any statement, they only change the compiler behaviour.

The compiler could provide its options in compiler object:

compiler.set("func", "weak");
compiler.set("func", "alias", "__f");

Meta functions can change the compiler behaviour with compiler object (reflections API).

2.3 Contracts

Contracts pre and post are meta functions. They would add test conditions to the before and the after of function body. They generate new function body if that's necessary. For example:

func: (a: int, b: int) -> (r: int) @pre a < b @post a < r < b = {
    //statements...
}

The above declaration will generate this declaration:

func: (a: int, b: int) -> (r: int) = {
    assert(a < b);
    //statements...
    assert(a < r < b);
    // Here it would return `r` after the above line.
}

2.4 Coroutines

Coroutines could be a library feature in Cpp1 too (via co_await, co_yield and co_return). For example:

tcp_echo_server: () -> task<> = {
    //statements...
    while true {
        n: std::size_t = @co_await socket.async_read_some(buffer(data));
        @co_await async_write(socket, buffer(data, n));
    }
}

iota: (n: int = 0) -> generator<int> = {
    while true {
        @co_yield n++;
    }
}

func: () -> lazy<int> = {
    @co_return 7;
}

The above examples are modified Cpp2 version from cppreference.com examples.

3. Your Questions

This suggestion won't eliminate X% of security vulnerabilities of a given kind in current C++ code.

But this suggestion will automate or eliminate X% of current C++ guidance literature. It would unify Contracts, Attributes, Coroutines and User-defined Language Constructs to a single concept Parameterized Meta Functions.

Also this suggestion would help compiler and library writers:

  • Compilers need to provide their own extensions. Attributes in Cpp1 is a way for it.
    • This suggestion makes them to be library feature.
  • Libraries need to provide simpler use of their own interfaces. Macros in Cpp1 is a way for it.
    • This suggestion is a safe integrated aliternative way to macros.

4. Considered Alternatives

I've considered to use named arguments instead of (args)class1(args)class2(args)..., but the problem of named arguments is that they don't look like built-in language constructs, also they need comma:

@repeat list: = items, every: = 1min, do (arg) {
    print("Hello");
}
@msadeqhe msadeqhe changed the title [SUGGESTION] Parameterized *Meta Functions*; A replacement for Attributes, Contracts and etc [SUGGESTION] Parameterized Meta Functions; A replacement for Attributes, Contracts, Coroutines and User-defined Language Constructs Jun 22, 2023
@msadeqhe
Copy link
Author

msadeqhe commented Jun 22, 2023

This suggestion will bypass the following problems which are mentioned in @hsutter's P0707 paper.

  • Fragmented local dialects
    • Using meta functions, is like using a regular function or type. They are defined in a library without mutating the language.
  • Nonportable code
    • A library writer can extract a piece of code and reuse it in a different environment, because the language is not mutated.
  • Noncomposable code
    • They may be inside namespaces. They are like regular functions but they generate code instead of function call.
  • Unreviewable/unmaintainable “write-only” code
    • They are prefixed with @ for readability. It would mean a code generation is happening in place of @name ....

On the other hand:

  • No mutable language
    • They don't change any language feature.
  • No mutable meta functions
    • Similar to Metaclasses, they would only generate new code. They cannot change already defined types, variables, functions or other meta functions.
  • Their effects are local, not global
    • For contracts and attributes which are within declarations, they would satisfy this rule.
    • But for coroutines and user-defined language constructs, they would not satisfy this rule.

@msadeqhe
Copy link
Author

msadeqhe commented Jun 24, 2023

How to declare user-defined language constructs?

User-defined language constructs are meta functions which may be applied before a null statement, block statement or parameterized block statement:

// null statement
;

// block statement
{ /*statements...*/ }

// parameterized block statement
name (args) { /*statements...*/ }

This is an example of what the declaration syntax may be look like.

forr: (src: statement, arg: expr<every_op>) -> statement = {
    d: every_op = arg.data();
    // It's the new generated code.
    -> {
        (copy __t: timer = ()) while d.get_condition().to_string()$ {
            src.to_string()$
            __t.delay(d.get_time_interval().to_string()$);
        }
    }
}

every_op: type = {
    condition: expr<bool>;
    time_interval: expr<duration>;

    operator=: (out this, arg1: expr<bool>, arg2: expr<duration>) = {
        condition = arg1;
        time_interval = arg2;
    }
    get_condition: (this) -> expr<bool> = condition;
    get_time_interval: (this) -> expr<duration> = time_interval;
}

every: type = {
    condition: expr<bool>;

    operator=: (out this, arg: expr<bool>) = {
        condition = arg;
    }
    operator(): (inout this, time_interval: expr<duration>) -> every_op = {
        return (: every_op = (condition, time_interval));
    }
}

The above example, would be used like this:

main: () = {
    x: = random(1, 100);

    // Its parameter is `(x < 10) every (1min)`.
    @forr (x < 10) every (1min) {
        // Its block statement
        print(x);
        x = random(1, 100);
    }
}

The parameter (x < 10) every (1min) is a valid syntax without meta function, @forr would generate new code from its parameter and its block statement. It would generate this code:

main: () = {
    x: = random(1, 100);

    (copy __t: timer = ()) while x < 10 {
        print(x);
        x = random(1, 100);
        __t.delay(1min);
    }
}

When we call forr function with prefix @, the compiler would pass information about the statement to it. The code generation happens within forr meta function which has a parameter of type expr<every_op>. Because of @, also the compiler would create expr<...> and pass it to function calls for us. expr<...> is a type which contains information about the expression itself.

This example is a possible implementation, it may be like anything else. Its implementation syntax depends on Reflection which is not yet available in C++1.

@msadeqhe
Copy link
Author

msadeqhe commented Jun 24, 2023

Meta Function Overloading

It would be possible to overload meta functions. They are regular functions, therefore they can be overloaded:

// first signature
forr: (src: statement, arg: expr<every_op>) -> statement = { ... }
// Here is the declaration of `every_op` and `every` type, similar to the previous example.
every_op: type = { /*...*/ }
every: type = { /*...*/ }

// second signature
forr: (src: statement, arg: expr<on_released_op>) -> statement = { ... }
// Here is the declaration of `on_released_op` and `on_released` type, similar to the previous example.
on_released_op: type = { /*...*/ }
on_released: type = { /*...*/ }

// It would call the first signature.
@forr (x < 10) every (1min) {
    //statements...
}

// It would call the second signature.
@forr (x < 10) on_released (resource) {
    //statements...
}

Repository owner locked and limited conversation to collaborators Aug 30, 2023
@hsutter hsutter converted this issue into discussion #629 Aug 30, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

1 participant