Goose  Meta programming

Named constants

The using statement can be used to create a named alias for any compilation time expression, including types. It serves both as an equivalent of typedef (or using in modern c++), and an equivalent of #define.

using u32 = uint(32)
using someConstant = 456

The content of using statement is parsed lazily. This allows using expressions to refer to one another in any order, but cyclic references are not allowed.

As a result of this, the using statement needs to know where the expression finished without parsing it. As a result, it is the only place in goose where the newline character is significant: using will stop at either the next newline or the next semicolon.

Note that it does skip blocks wholesale, so in some cases it is actually possible to make a using expression span multiple lines:

using something = (
    a + b
    +c
)

Note that all operators and keywords are just values that contain pratt parsing rules, so you can alias them as well:

using define = using
define a = 3545

This feature is not quite intentional but more of a side effect of the compiler's architecture. In the future, a way to disable it may be added. The fact that operators and keywords are value also means that we can restrict the usage of some features depending on modules/namespace by changing their visibility, even though no facility is implemented to actually do that yet.

Conditional compilation

#if/#else work similarly to the normal if/else, except that it requires a condition that evaluates to a constant at compilation time, and the blocks have to be enclosed in braces.

Depending on the condition, one of the blocks will be parsed (it doesn't create a scope despite the braces), and the other one skipped without parsing (braces, bracket and parenthese blocks contained insides are recursively skipped however)

#if is equivalent of both the C preprocessors's #if and C++'s if constexpr.

Compile time execution

Whenever the parser encounters a function call, if all of its arguments can be evaluated to constants at compilation time, it will attempt to execute the call at compilation time as well. If it succeeds, it will push the resulting value, otherwise it will push a computed value of the called function's return type, and a call instruction as the expression.

This is why it is possible for types to be any compile time expression that produces a type: after evaluation, the parser will know the last pushed value is a type and will act accordingly. For instance, if it encounters an unresolved identifier and the last pushed value is a type, it will create a declaration.

Of course, compile time execution is undecidable (ie it can go in an infinite loop or recursion), so there is a limit to the amount of jump instructions that can be executed by the interpreter before it gives up with an error.