Skip to content

[SUGGESTION] User-defined Language Constructs #382

Closed
@msadeqhe

Description

@msadeqhe

Preface

"Express language facilities as libraries" and "Eliminate need for preprocessor" are two parts of Cpp2's 2016 roadmap.

If we look at for construct in Cpp2:

for items do: (item) = { ... }

a question may arise: "Is it a lambda after do?", and the answer seems to be yes. What if for construct could be implemented as a library feature? How about if and while constructs?

A language construct has a feature that normal functions or lambdas cannot support. Language construct doesn't evaluate an expression at first evidence, and it can be evaluated multiple times. For example:

while count < 10 next count++ {
    std::cout << count;
}

Here, count++ will be not evaluated before statement block { ... }, but it will be evaluated in each loop. Consider if it was a function:

while_func(count < 10, count++, : () = {
    std::cout << count;
});

No, while_func doesn't work as you maybe expected. Currently, lambdas are so close to this feature but we have to specify arguments if we don't capture variables:

while_func(count, : (count) = count < 10, : (inout count) = count++, : () = {
    std::cout << count;
});

while_func(: () = count$ < 10, : () = count$++, : () = {
    std::cout << count;
});

In the second one, capture count$++ doesn't work, because it's a copy of count variable, therefore inside while_func implementation we have to be aware of that.

Suggestion Detail

User-defined Language Constructs are a way to pass expressions without evaluating their value at first evidence. Because of their different look (e.g. while condition next count++ { ... }) they are like functions (e.g. wwhile(condition, count++, : () { ... })) but their parameters are expressions instead of variables and lambdas, and they have keywords (e.g. next) instead of parenthesis and comma.

The syntax is not important, I just introduce a syntax (which I was thinking about it) but its syntax can be anything else:

name: <template-params> name opt-keys (expr-param) ... = { implementation }

Everything before = is the signature of an user-defined language construct:

  • name is the name of the user-defined language construct.
  • <template-params> is optional. It's the list of template parameters (e.g. <T: type, U: type>).
  • opt-keys is optional. It's a list of keywords seperated by white-space (e.g. do next).
  • (expr-param) is optional. It's the name and type of an expression parameter. I'll explain it in more detail.
  • ... is optional. It's a group of opt-keys (expr-param) which can be repeated.
  • implementation is optional. It's the actual code which implement the user-defined language construct.

I have to explain (expr-param) and how to declare an expression parameter. An expression parameter with name expr-name and with type expr-type is like this:

(expr-name => expr-type)

The type of an expression parameter can be either a value, a type, an expression, a statement block or an init statement. The syntax of them is described blow:

  • param => something: Here, param is an expression parameter which accepts only expressions with type something.
  • param => {} -> something: Here, param is an expression parameter which accepts only a statement block which returns a value of type something.
  • param => : (params) -> something: Here, param is an expression parameter which accepts only a function with (args) with return type something.
  • and etc.

I don't want to go into detail, becuase the syntax of my suggestion is not important. Now, I write an user-defined language construct to explain it (expression parameters are always between parenthesis):

check: <T: type> check (condition => T) do (run_always => void) (run => {} -> void) = {
    if (condition) {
        (run);
    }
    (run_always);
}

Here are the descriptions:

  • check is the name of the user-defined language construct, consider that this name has to be repeated before and after :.
  • <T: type> is the template parameter. This template is used to specify the type of expression parameter run_always.
  • (condition => T) is an expression with name condition and its type after evaluation must be T which is a template parameter.
  • do is a user-defined keyword.
  • (run_always => void) is an expression with name run_always and it doesn't return anything after evaluation.
  • (run => {} -> void) is a statement block which must not return anything.

We can use check in the following way:

check count < 0 do call_always() {
    call_it();
    call_another_one();
}

It generates the following code:

if count < 0 {
    call_it();
    call_another_one();
}
call_always();

Now, let's define something similar to for loop which is currently a built-in language construct in Cpp2:

for: <T: type> for (list: std::container<T>) do (lambda: (a: T)) = {
    item: = begin(list);
    while item != end(vector) next item++ {
        lambda(item);
    }
}

Cpp2 kewords like do, else, next, if and ... can be used for user-defined language constructs. Furtunately Cpp2 doesn't treat if/while/... constructs as expression, therefore it's possible to use keywords in User-defined Language Constructs without any ambiguity.

User-defined language constructs are like functions in which they can be overloaded by keywords in their signature, for example we can defined two check language construct:

check: check (condition => bool) (run => {} -> void) = { ... }
check: check (condition => bool) turn (something => sometype) (run => {} -> void) = { ... }

main: () = {
    x: = 1;
    y: sometype = 0;

    check x < 10 { ... }
    check x < 10 turn y { ... }
}

Also they can be used in generic programming with template parameters and requires:

check: <T: type> requires std::is_integral_v<T>
    check (condition => bool) turn (something => T) (run => {} -> void)
    = { ... }

This suggestion is not mature yet. I don't know if there is interest in user-defined language constructs... If you agree, I would improve the syntax and semantics and ...

Examples and Usages

Many language constructs can be user-defined in libraries:

for: <T: type> for (list: std::container<T>) do (lambda: (a: T)) = {
    item: = begin(list);
    while item != end(vector) next item++ {
        lambda(item);
    }
}

// => : _ is for variable declaration
for: for (initialize => : _) if (condition => bool) next (step => _) (run => {} -> void) = {
    (initialize);
    while (condition) {
        (run);
        (step);
    }
}

for: for every (duration => seconds) (run => {} -> void) = {
    ...
}

loop: loop (run => {} -> void) = {
    while true {
        (run);
    }
}

skip: skip (run => {} -> void) = {
    try {
        (run);
    }
    catch {
        //,ignore
    }
}

// It's simply a user-defined language construct that gets nothing and does nothing
// It's like pass keyword in Python.
pass: pass;

// => : T is for variable declaration
using: <T: type> using (initialize => variable : T = value) (run => {} -> void) = {
    (variable): T;
    try {
        (variable) = (value);
        (run);
    }
    finally {
        (variable).close();
    }
}

if: if is not (condition => bool) (run => {} -> void) else (run_else => {} -> void) = {
    if ! (condition) {
        (run);
    }
    else {
        (run_else);
    }
}

These are examples of how we use the above user-defined language constructs:

for items do: (item) = {
    //,statements...
}

for x: int = 0 if x < 10 next x++ {
    //,statements...
}

// min is a user-defined literal which means minutes
// This will repeat statements every 1 minutes
for every 1min {
    //,statements...
}

loop {
    //,statements...
}

// Ignore errors
skip {
    //,statements...
}

pass;

// It's like C# construct
using x: some_type = init_value {
    //,statements...
}

// Multi-keywords are allowed...
if is not condition {
    //,statements...
}
else do {
    //,statements...
}

And in the last example is not is not ordinary is and not, they are specialized in user-defined language construct.

Why do I suggest this change?

Becuase it's a general language feature. User-defined Language Constructs have the following benefits:

  1. This feature is not possible today with Cpp1 (without macros) or Cpp2. We want a way to pass an expression and evaluate it later. Lambdas are too much verbose and strange in comparison to user-defined language constructs, and captures are by value (see Preface section for explanation).
  2. They are integrated into the language:
    • They can be used in generic programming with template parameters, concepts and requires clause.
    • They are a better replacement for macroes, because their expression parameters are typed and integrated.
    • They can overload existing and built-in language constructs such as if/while/... by different keywords, e.g. if let and if has are overloads of built-in if construct. Another example is that user-defined language constructs check in, check at and check from can be used in the same code without any problem.
    • They can be inside namespaces, e.g. my::for
  3. They make some Cpp2 language constructs (e.g. for) to be implemented as library features.
  4. They help library writers to express the power of their library.
  5. They are more readable than using indirect solutions.
  6. They are not ambiguous with normal functions and lambdas:
    • Expression parameters are between keywords of language construct, therefore they won't be evaluated at first evidence. Statement blocks start with { ... therefore variables don't have to be captured inside it just like if/while/....
    • Function Parameters are inside parenthesis, therefore they will be evaluated at first evidence. Lambdas start with : (args) = { ... therefore variables have to be captured inside it.
  7. They add more power to Cpp2 in addition to reflections and expressing features like namespace, enum and ... as ordinary libraries.
  8. They make Cpp2 a pseudo language for langauge designers, or test new language construct features, or ...

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?

No.

Will your feature suggestion automate or eliminate X% of current C++ guidance literature?

Yes.

Describe alternatives you've considered.

At first I tried to make expression parameters to be look like captures (e.g. (expr-param$: expr-type or (expr-param: expr-type)$), but the relation between expression parameters and captures wasn't enough. Also currently I don't know if @hsutter wants to support a language feature similar to user-defined language constructs or not, therefore I didn't think enough for declaration syntax alternatives.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions