zax.io

A guide to the Zax programming language

View project on GitHub

Zax Programming Language

Compiler Directives

Official and extended directives

All officially supported directives must be understood to compile and never start with an x- prefix. Custom compiler directives and custom directive arguments are prefixed with x- in the directive name. Custom directives and custom directive arguments are ignored if they are not supported.

// example official directives
variable final [[inline]] : ()() = {
    // ...
}
[[source="example.zax"]]


// examples of unofficial custom directives...

func final [[x-bytecode=lacrosse]] : ()()

[[x-bogus="party on dudes!"]]

[[source="example2.zax", x-]]

source directive

A [[source="<path/file.zax>" |, required=<yes|no|warn>| |, generated=<yes|no>|]] directive instructs a compiler to pause compiling the current file and continue compiling tokens from a referenced file until that file is completely parsed then resume compiling the current file. A source directive always locates files relative to the current file. If a path is not found, then the parent of the current path is attempted (recursively to the root of a module) until a source is located or the file is not found (whereupon a compiler will issue an error). Paths are always separated with unix forward slashes (‘/’) regardless of the platform. File names are recommended to be always lowercase and words should be separated with a dash (-) sign.

An optional argument named required is available. If the value is yes then a file must be found (default behavior) or a source-not-found error will occur. If required is no then a compiler will ignore this file being absent. If required is warn then a compiler will issue the source-not-found warning. A file extension of zax is the recommended default extension.

An optional argument named generated is available. If the value is yes, then a source file needs to be generated by one of a compiler’s execute directives. Prior to attempting to load and and parse a file, the generated file date is checked and if the compile date is newer than the generated file date then a compiler will issue a generated-file-not-touched warning. Generated files must (at minimal) be “touched” by a generator. A default value of no is assumed (and redundant to specify).

A wildcard character pattern matching notation is allowed. Multiple matched files are imported in ascending ASCII sort order and a source will never import itself when using a wild card.

/*
file.zax
*/

[[source="options.zax", required=warn]]
[[source="graphics/*.zax"]]
[[source="sub-path/sub-file.zax"]]
[[source="generated/keyboardMapping.zax", generated=yes]]
/*
sub-path/sub-file.zax
*/

[[source="options.zax", required=no]]

asset directive

An [[asset="<path/file.ext>" |, required=<yes|no|warn>| |, rename="<new-path/new-name.ext>"| |, generated=<yes|no>|]] compiler directive instructs a compiler copy a file into an output target’s asset folder. An asset directive always locates files relative to the current file. If a path is not found, then a parent of the current path is attempted (recursively to the root of a module) until an asset is located or the file is not found (where a compiler will issue an error). Paths are always separated with unix forward slashes (‘/’).

An optional argument named required is available. If the value is yes then a file must be found (default behavior) or an asset-not-found error will be issued. If required is no then a compiler will ignore this file being absent. If required is warn then a compiler will issue an asset-not-found warning.

An optional argument named generated is available. If the value is yes, then the asset file needs to be generated by one of the compiler’s execute directives. Prior to attempting to copy a file, the generated file date is checked and if the compile date is newer than the generated file date then a compiler will issue a generated-file-not-touched warning. A default value of no is assumed (and redundant to specify).

The wildcard character pattern matching notation is allowed. Multiple matched files are imported in ascending ASCII sort order. If a wild card character is used with the rename option then the same wild card character must be present in the rename. All wild card characters in the rename must be present in the same order or the wild-character-mismatch error will be issued.

/*
file.zax
*/

[[asset="intro.wav", required=warn]]
[[asset="graphics/*.png"]]
[[asset="sub-path/example-*.pdf", rename="examples/*.pdf"]]
[[asset="palette.png", generated=yes]]
/*
sub-path/sub-file.zax
*/

[[source="options.zax", required=no]]

panic directive

panic function

When a panic occurs, the context’s panic function is called (___.panic(...)). Normally an error message is displayed for a programmer to understand a panic and then a program terminates due to an unexpected and unhandled condition. A default panic function can be replaced with an alternative function.

A panic function is called with an enumeration representing the current error code and a code location is passed containing the source location of where a panic condition was triggered.

pre-panic check function

When a panic occurs in code that is always compiled in (and cannot be removed by direct compiler directive), a context’s pre-panic check function is called (___.prePanicCheck(...)). This function accepts an enumeration representing an error where a lookup table can be searched to see if a panic is enabled for a panic code. If a panic is enabled then a context’s panic function is called. If a panic is suppressed then a panic function is never called.

A pointer to a panic lookup table is maintained within the context object. A programmer can swap in/out a lookup table pointer to other states if a code section has different runtime only panic criteria for a given context.

Enabling/disabling a compiler panics

Code generation for panic conditions can be enabled or disabled by using a [[panic=<option> |, <registered-panic-name>|]] directive. If a compiler compiles-in a panic directive, a compiler will enable or disable a compiler’s panic code generation. All compilers must register their panic options and meanings into a shared authority registry. Experimental non-standard panic names must include an x- prefix as part of a panic name. Naming a specific panic is optional. If a registered-panic-name is not specified that directive will apply to all panic conditions.

Caution: disabling panics does not prevent a panic scenario; disabling merely removes additional compiler generated protective code that would call a panic function. Without compiling-in panic detection, code may silently fail with undefined behaviors.

The options for panic conditions are:

  • yes - enables a panic for only to the current statement
  • no - disables a panic for only to the current statement
  • always - enables a panic for all statements that follow
  • never - disables a panic for all statements that follow
  • default - enables or disables a panic for all statements that follow according to a compiler’s defaults
  • lock - disallows any imported module from changing a panic state
  • unlock - allows any imported module from changing a panic state
randomValue final : (output : S32)() = {
    // ...
    return output
}

value := randomValue()

// code will generate a panic condition for this statement if an overflow occurs
// assuming the `intrinsic-type-cast-overflow` panic was not disabled at runtime
// or elsewhere in a compilation process
castedValue1 := value as U16

// code will generate a panic condition for this statement if an overflow occurs
[[panic=yes, intrinsic-type-cast-overflow]] \
castedValue1 := value as U16

// code will silently perform a casting without an overflow panic
[[panic=no, intrinsic-type-cast-overflow]] \
castedValue1 := value as U16

// code will not generate a panic as `unsafe as` was used to cast which does
// not cause a panic condition even though a panic is enabled
[[panic=yes, intrinsic-type-cast-overflow]] \
castedValue2 := value unsafe as U8

[[panic=always, intrinsic-type-cast-overflow]]

// ...

[[panic=never, intrinsic-type-cast-overflow]]

// ...

[[panic=default, intrinsic-type-cast-overflow]]

// ...

[[panic=never, x-strange-experimental-panic]]

Panic push and pop

The state of all panics can be pushed and popped into a compile stack using a [[panic=push]] and [[panic=pop]] compiler directives. A push operation will keep a copy of all compiler panic states and push those panic states on a compiler’s panic state stack. A pop operation will pop the last pushed compiler panic states and apply these panic states as the current compilation’s panic states.

Upon importing a module, all panic states are pushed and all panic states are popped at the end of an import statement. This ensures that imported modules cannot affect the panic states of an importing module.

// preserve a compiler's panic states
[[panic=push]]

[[panic=never, intrinsic-type-cast-overflow]]

// ... code with the `intrinsic-type-cast-overflow` panic check disabled ...

[[panic=pop]]

// ... code with restored panic state checks ...

Panic registry and meanings

The following are registered panic scenarios, default states, and their meaning:

  • out-of-memory (always)
    • memory was requested to be allocated but insufficient memory exists to fill the request (allocation failures normally panic rather than return a pointer to Nothing)
  • intrinsic-type-cast-overflow (always)
    • an intrinsic type may overflow during an as operator to a type with lower bit sizing if a value is beyond the capacity of a given intrinsic type
  • string-conversion-contains-illegal-sequence (always)
    • a string literal conversion was found to contain an illegal character sequence during a conversion process
  • reference-from-pointer-to-nothing (always)
    • a pointer was converted to a reference but a pointer points to Nothing
  • pointer-to-nothing-accessed (always)
    • a value (or function) was accessed but a pointer points to Nothing
  • not-all-pointers-destructed-during-allocator-cleanup
    • memory cleanup is being performed but not all allocated instances in memory from an allocator were destructed
  • impossible-switch-value (always)
    • a switch statement encounter a value which can never happen (because of a [[never]] directive or an [[always]] directive)
  • impossible-if-value (always)
    • an if statement encounter a value which can never happen (because of a [[never]] directive or an [[always]] directive)
  • impossible-code-flow (always)
    • a code path was followed what was marked as impossible to reach (because of a [[never]] directive or an [[always]] directive)
  • lazy-already-complete (always)
    • an attempt was made to call a lazy function that has already cause a final return from that lazy function
  • value-polymorphic-function-not-found (always)
    • a function supporting value polymorphism was called but none of the pre-condition checks succeeded

deprecate directive

A [[deprecate |=<option>| |, context=<context>| |, error| |, min="<x.x>"| |, max="<x.x>"|]] directive can be used to cause API usages to be considered deprecated. Any usages found to be deprecated will issue a deprecate-directive warning or error. This directive is useful for allow grace periods to exist when upgrading or obsoleting older APIs and offering an upgrade path to newer APIs (while still maintaining compatibility with an older API set).

The options for a deprecate directive are:

  • yes (default if not specified) - enables deprecation for only to the current statement
  • no - disables deprecation for only to the current statement
  • always - enables deprecation for all statements that follow
  • never - disables deprecation for all statements that follow (context, error, min, max, are disallowed with this option)

An error argument is optional and if specified then a warning is forced into an error without a possibility to ignore a deprecation as a mere warning.

Context for a deprecate warning:

  • import (default if not specified) - only warn on usages from a module performing an import
  • all - any usage cause a deprecation warning
  • local - any usage within the current module cause a deprecation warning but not usages from an importing module (min and max option are disallowed)

A min option requires an importing module must declare an import of at least this version to use an API. A max option requires an importing module must not declare an import version greater than this version to use an API. A compiler will treat versions as point release notations. A version can be specified using a version String declaration in an import statement. A min and max keyword are generally mutually exclusive but they can be used together should a need arise.

Only one always deprecation can be active at a time. A never directive will disable any active always deprecation. Usage of never cannot be declared with error, min, max or context as they have no applicability and a compiler will issue an incompatible-directive error. Usage of local cannot be declared with min or max as those values are only applicable to an importing module and thus a compiler will issue an incompatible-directive error. Individual yes or no temporarily override any always deprecations directives for a current statement. Any current deprecation state will not apply to an subsequently imported modules. No push or pop deprecation declarations exist unlike with panic, warning, or error directives as a deprecation relationship’s definition is limited between an importing module and an imported module exclusively.

[[deprecate]] \
MyOldBadlyDesignedType :: type {
    // ...
}

// usage of this type should only be allowed when an importer declares version
// `2.3` or higher
[[deprecate, min="2.3"]] \
MyShinyNewType :: type {
    // ...
}

ValidType :: type {

    // all functions and variables below are now obsoleted and cannot be
    // referenced by an importer declaring version `1.1` or higher
    [[deprecate=always, error, max="1.1"]]

    mrT80sFunc : ()() = {
        // ...
    }

    markyMarkFunc : ()() = {
        // ...
    }

    eightTrack : MyOldBadlyDesignedType

    // disable the last declared `always` deprecation directive
    [[deprecate=never]]


    // a deprecation warning is issued if this function is referenced within
    // the current module but a warning is not issued for an importing module
    [[deprecate, context=local]] \
    needsRedesignFunc : ()() = {
        // ...
    }

    // usage of this function always causes a deprecation warning
    [[deprecate, context=all]] \
    mightNeedThisSoNotReadyToRemoveFunc : ()() = {
        // ...
    }

    newerOkayFunc : ()() = {
        // ...
    }
}

inline function directives

An [[inline |=<option>|]] directive can be used to signal to a compiler when to inline a final function directly into a caller’s code block or when to call a function as an explicit function call. By default a compiler will decide if inlining a function is desirable based on a tradeoff of speed versus compilation size.

The following options are available for an inline directive:

  • maybe (default on functions if inline directive is not specified) - a compiler decides if it is best to inline a function or not
  • likely (default if no <option> is specified) - a compiler should prefer to inline a function but a compiler may ultimately decide
  • unlikely - a compiler should prefer calling a function rather than inlining but a compiler may ultimately decide
  • always - a compiler is forced to inline a function but all variables declared inside a called function are not visible to a caller’s scope
  • descope - a compiler is forced to inline a function and all variables within the immediate scope of an inlined function are declared as if they were declared inside a caller’s scope
  • never - a compiler may never inline this function
// prefer a function to be inlined
func1 final [[inline]] : ()() = {
    // ...
}

// let a compiler decide if inlining is preferred or not
func2 final [[inline=maybe]] : ()() = {
    // ...
}

// only allow the function to be called inline
func2 final [[inline=always]] : ()() = {
    // ...
    myValue : Integer
    // ...
}

// only allow the function to be called inline and expose all variables
// in the immediate scope of the function to be declared externally within the
// context of a calling function
func3 final [[inline=descope]] : ()() = {
    // ...
    myValue : Integer
    // ...
}

// never allow the function to be inlined
func4 final [[inline=never]] : ()() = {
    // ...
}

func1()
func2()

[[descope]] \
func3() // calling a function marked with an `[[inline=descope]]` requires a
        // declaration of `[[descope]]` prior to calling that function or a
        // warning `descope-directive-required` will be issued
func4()

// OKAY: a definition for `myValue` comes from the `[[inline=descope]]` `func3`
myValue *= 3

// ...

descope directive

A [[descope]] directive treats an inner scope as belonging to a scope of an outer scope. As such, no destructors will trigger at the end of scope and all variables declared as part of an immediate inner scope are treated with the same visibility as an outer scope. Variables declared within a descope are treated as having been declared as part of an outer scope thus non-polymorphic variables with the same symbolic name will cause a duplicate-symbol error rather than cause shadowing of an outer variable.

Example when used with a compile-time if statement:

coinFlip : (result : Boolean)() = {
    // ...
}

// cause a compile-time random decision if one definition or another should
// be used
[[descope]] if [[requires=compiles]] { return coinFlip() } {
    myValue : U32 = 5
    // ...
} else {
    myValue : U16 = 10
    // ...
}

// in some scenarios `myValue` will be a `U32` type and in others scenarios
// `myValue` will be a `U16` type
myValue *= 2

Example when used with a normal scope:

[[descope]] scope my_scope {
    myValue1 : Integer = 6
    myValue2 : Integer = 7
    // ...
    {
        // these values are not visible to the outer scope
        nonVisibleValue1 : String = "hello1"
        nonVisibleValue2 : String = "hello2"
    }
}

// OKAY: `myValue` is now visible to this scope
myValue1 *= 2 + myValue2

// ERROR: both `nonVisibleValue1` and `nonVisibleValue2` are not visible to
// this scope
nonVisibleValue2 = "goodbye 2 " + nonVisibleValue1

Example of possible flow control error condition when used with a normal scope:

coinFlip : (result : Boolean)() = {
    // ...
}

[[descope]] scope my_scope {
    myValue1 : Integer = 6

    if coinFlip()
        break   // ERROR: will issue `scope-flow-control-skips-declaration`
                // as `myValue2` declaration would be skipped

    myValue2 : Integer = 7
    // ...
}

myValue1 *= 2 + myValue2

compile directive

A compile directive can be used on input arguments to indicate a value must be pre-evaluated at compile-time or an error will be issued. Any input argument declared with a compile directive must also be declared as either constant or immutable. Any variadic types are treated as constant or immutable.

func print : ()([[compile]] ...) = {
    // ... all input arguments must resolve at compile-time ...
}

func final : ()(input [[compile]] : Integer constant) = {
    // ... `input` argument must resolve at compile-time ...
}

random1 final [[execute=dual]] : ()() = {
    // ... this random function can execute at compile-time or runtime ...
}

random2 final [[execute=target]] : ()() = {
    // ... this random function can only execute at runtime ...
}

// capture a runtime only value
value := random2()

func(5)            // OKAY: `5` value is constant
func(random1())    // OKAY: `random1` function is allowed to resolves as a
                   // compile-time constant

func(random2())    // ERROR: `random2` function cannot resolve at compile-time
func(value)        // ERROR: `value` is runtime only and it is not constant

print(5, "hello", random1())   // OKAY: all values are compile-time constants
print(random2(), value)        // ERROR: runtime only values cannot become
                               // compile-time constants

compilation directive

A [[compilation |=<option>|]] directive operates on a scope and causes all code within the remainder of a scope to evaluate as if it were in the context of a host or target scenario.

The options are as follows:

  • default (default if not specified) - a compilation context is whatever a default would have been for a given context (i.e. resolves to either target or host)
  • target - force all code to generate as if it were evaluating in the target’s context
  • host - force all code to generate as if it were evaluating in the host’s context

As the language supports both compile-time and runtime compilation, some intrinsic types sizing and endian encoding are entirely relative to a host system or a target system. As such, a compilation’s context will influence if these operators will result in sizing and encodings relative to a target or relative to a host. A compiler will automatically determine the appropriate context and evaluate these operators accordingly. However, a programmer can override a compiler’s defaults for a compiled scope.

Example scenario where a host is a 64-bit system and the target is a 32-bit system:

print final [[execute=dual]] : ()(...) = {
    // ...
}

func final [[execute=dual]] : ()() = {
    print("CHECKING...")

    [[compilation=host]]
    if size of Integer > size of U32 {
        print("HOST: YES")
    }

    [[compilation=target]]
    if size of Integer > size of U32 {
        print("TARGET: YES")
    }

    [[compilation=default]]
    if size of Integer > size of U32 {
        print("DEFAULT: YES")
    }
}

funcCompileTime final [[execute=host]] : ()() = {
    // ... this scope is definitely a compile-time context ...

    // `func` in this context will be compile-time 
    func()
}

funcRunTime final [[execute=target]] : ()() = {
    // ... this scope is definitely executing at runtime ...

    // `func` in this context will be runtime
    func()
}

funcCompileTime()
funcRunTime()

When compiling the output will be:

CHECKING...
HOST: YES
DEFAULT: YES

During runtime the output will be:

CHECKING...
HOST: YES

compiles directive

A [[compiles |=<option>|]] directive evaluates a code block and converts the code block into a compile-time constant of true or false. A code block that follows a compiles directive is never executed and declarations and any statements within a code block do not become visible symbols. A code block that follows a compiles directive will not resolve into code instructions.

The options for compiles are as follows:

  • default (default if not specified) - a failure to compile a code block resolves to a false literal constant
  • error - if error is specified then a failure to compile a code block will evaluate to a real compile-time error

Meta-functions can be selected as a candidate or unselected depending on a true or false statement being present at the end of a functions declaration. A compiles directive can be used to enable or disable a meta-function as a candidate based on a compiles code block compiling or not. A code block will evaluate per instantiation of a meta function. All inputs and outputs arguments are considered captured within a compile code block’s function context allowing for compile-time reflection of all argument types. However, any captured variables will never evaluate to any value given a code block is never executed either at runtime or at compile-time.

Intrinsic types sizing and endian encoding are entirely relative to a host system or a target system. Operators exist to specifically query a host system or a target system’s sizing and endian encoding. However, a compiles directive works for a dual mode. A compiles may be used to evaluate candidate suitability or compile-time evaluation for a target system or it may be used to evaluate code suitability for a host system (and not a target system). As such, system context dependant operators (such as size of but not host size of nor target size of) will evaluate as if they are operating on a target context by default.

if [[compiles]] { ++value } {
    doSomething()
} else {
    doSomethingElse()
}

metaFunction : ()(input :) [[compiles]] {

    // check if a type is defined
    testedFeature : TestedFeature

    // check if an `input` argument is compatible and convertible to a simple
    // `Integer` type
    check : Integer = input as Integer

} = {
    // ...
}

/*

// metaFunction's code block will evaluate to a `true` indicating the function
// is selectable as a candidate:
metaFunction : ()(input :) true = {
    // ...
}

// ... or to a `false` indicating the function cannot be selected as a
// candidate:
metaFunction : ()(input :) false = {
    // ...
}

*/

requires directive

A [[requires |=<option>|]] directive evaluates a code block that follows into a compile-time constant of true or false. A code block that follows a requires directive is executed if it compiles. Failure to compile a code block will result in a code block resolving to false. A code block must be capable of running at compile-time.

The options for requires are as follows:

  • default (default if not specified) - a failure to compile a code block resolves to a false literal constant
  • compiles - if compiles is specified then a failure to compile a code block will evaluate to a real compile-time error
  • error - if error is specified then a failure to compile a code block or a requires directive code block returning false will evaluate to a real compile-time error

Meta-functions can be selected as a candidate or unselected depending on a true or false statement being present at the end of a functions declaration. A [[requires]] directive can be used in-place of a function’s enable or disable boolean to enable or disable a meta-function as a candidate based on a function’s arguments. All inputs and outputs are considered captured in a function’s context allowing for compile-time reflection of argument types. Any values of input arguments that are not compile-time constants will not be constructed nor destructed and any access to input argument values may cause undefined behaviors. Output arguments are never constructed nor destructed and any access to an output argument may result in undefined behaviors. Any input argument can be checked if it contains a compile-time constant by using a is host constant operator.

Intrinsic types sizing and endian encoding are entirely relative to a host system or a target system. Operators exist to specifically query a host system or a target system’s sizing and endian encoding. However, a requires directive works in a dual mode. A requires may be used to evaluate candidate suitability or compile-time evaluation for a target system but a code block actually runs within the context of a host system (and not a target system). As such, system context dependant operators (such as size of but not host size of nor target size of) will evaluate as if they are operating on a target context by default.

if [[requires]] { return ++value > 2 } {
    doSomething()
} else {
    doSomethingElse()
}

metaFunction : ()(input :) [[requires]] {
    // if the size of `input`'s type is smaller than an `Integer` then do
    // not select this function as a candidate
    if (size of :input) < (size of Integer)
        return false

    // check if `input` is a compile-time constant
    if input is host constant {
        // safe to examine `input`'s value if it is a compile-time constant but
        // the `< 0` operation may fail to compile if `input`'s type does
        // not define an appropriate less-than operator
        if input < 0
            return false
        return true
    }
    return true
} = {
    // ...
}

/*

// if `metaFunction`'s `requires` block evaluates to `true` the function
// becomes selectable as a candidate as the `requires` code block becomes
// a `true` literal:
metaFunction : ()(input :) true = {
    // ...
}

// if `metaFunction`'s `requires` block evaluates to `false` the function
// is not selectable as a candidate as the `requires` code block becomes
// a `false` literal:
metaFunction : ()(input :) false = {
    // ...
}

*/

concept directive

A concept directive has two usages: [[concept]] and [[concept=<function>]]. A concept is similar to a requires directive but instead causes a final function to be treated as selection criteria for a meta-function or meta-type when used in meta-programming. A concept directive will evaluate a concept function whenever a concept function is declared in conjunction with a meta-function or meta-type. Inside a concept function, code is executed at compile-time to evaluate if a meta-type is compatible with an input or output argument. A concept function must return true or false to indicate if a meta-type is compatible with a concept. If a concept function fails to compile then a concept function will be treated as if it returned a false. A [[compiles=error]] or [[requires=error]] can be used inside a concept function to intentionally cause an error condition.

The form [[concept]] is declared on a final function indicating a function is to be treated as a concept. The second form [[concept=<function>]] is declared on a meta-type or a meta-function to indicate a type should use a concept function to determine if a type meets the selection criteria necessary (for a given meta-type or meta-function).

Functions using a concept have a single input argument representing a type to be evaluated and must return true (or a meta-type or meta-function will not be selected). A variable passed into a meta-function is not initialized, never constructed, and never destructed. An input argument is a placeholder to evaluate a type’s properties inside a concept function. A concept directive can use if clauses with a compiles directive to further evaluate compile-time checks without executing of any statements.

Intrinsic types sizing and endian encoding are entirely relative to a host system or a target system. Operators exist to specifically query a host system or a target system’s sizing and endian encoding. However, a concept directive works in a dual mode. A concept may be used to evaluate candidate suitability or compile-time evaluation for a target system but a code block actually runs within the context of a host system (and not a target system). As such, system context dependant operators (such as size of but not host size of nor target size of) will evaluate as if they are operating on a target context by default.

IsSelectable$(Type) final : (result : Boolean)(ignored : $Type) [[concept]] = {
    // the type must support these operators or the type will not be selected
    if ![[compiles]] {
        // code must not execute as accessing `ignored` as a value may
        // cause undefined behaviors
        ++ignored
        --ignored
        ignored += 5
        ignored -= 5
    } return false

    // the type must be larger than the size of a `U32` or it is not selectable
    if size of $Type > size of U32
        return false

    return true
}

myFunc$(UseSimpleType [[concept=IsSelectable]]) final : ()(bar : $UseSimpleType) = {
    // ...
}

myFunc final : ()(bar : ) = {
    // ...
}

myFunc(5 as U8)     // first `myFunc` is used as it meets the most specific
                    // selection criteria
myFunc(5 as U64)    // last `myFunc` used as the first `myFunc` is not a
                    // a selectable candidate

execute directive

An [[execute |=<option>|]] directive allows for a function’s code block to immediately evaluate at compile-time on a host system, or restrict a code block from evaluating at compile-time where a function can only be run on a target system.

The options are as follows:

  • generate - a code block will evaluate on a host system and resolve any input arguments at compile-time and generate new code as a substitute for the code block (i.e. the code block emits replacement tokens as a string)
  • delegate - a code block will evaluate on a host system but no input arguments will be resolved; instead all input arguments will be treated as a constant array of string literal values; the code block will generate new code as a substitute for the code block at compile-time (i.e. the code block emits replacement tokens as a string);
  • dual (default on function is an execute directive is not specified) - a code block will execute immediately on a host system if all values referenced are compile-time constants, or a code block will evaluate at runtime on a target system if any values referenced are not compile-time compatible on a host
  • host (default is <option> is not specified) - a code block will evaluate at compile-time on a host system and all referenced values must evaluate to compile-time values
  • target - a code block will evaluate on a target system at runtime only

An execute directive with target must be used to create a main entry point function for a standard application running on a target system otherwise a called main function would immediately execute at compile-time rather than generating a compile-time target function:

// without an `execute` directive being present with a `target` option, the
// code will run immediately on a host system
main final [[execute=target]] : ()() = {
    // ...
}

main()

Detailed control of execution is possible for indicating compile-time versus runtime code on a host or target system:

random final : ()() = {
    // ...
}

double final [[execute=dual]] : (result : Integer)(value : Integer) = {
    return value * 2
}

compileItDouble final [[execute=compile]] : (result : Integer)(value : Integer) = {
    return value * 2
}

generateItDouble final [[execute=generate]] : (result : String)(value : Integer) = {
    // code here generates a string result which becomes parsed into tokens
    // which replaces the function call ...
    return result
}

doubleNow final [[execute]] [[resolve=now]] : (result : Integer)(value : Integer) = {
    return value * 2
}

generateSomething1 final [[execute=generate]] : (result : String)(...) = {
    // code here generates a string result based on examining compile-time
    // evaluated inputs and the output string result is parsed into tokens which
    // replaces the function call ...
    return result
}

generateSomething2 final [[execute=delegate]] : (
    result : String
)(
    inputs : String[]
) = {
    // this specific form of `delegate` causes each value passed into the string
    // to not be resolved and the output string result is parsed into tokens
    // which replaces the function call ...
    return result
}

// invoking `double` in this context causes it to execute at compile-time
// as no runtime variables are passed in
four := double(2)

// invoking `compileItDouble` in this context causes it to execute at
// compile-time as a compiler can generate complete code for all the
// execution functions required
value1 := compileItDouble(random())

// invoking `generateItDouble` in this context causes it to generate replacement
// code tokens for each execution
value2 := generateItDouble(random())

// forward declare the `seven` symbol
seven :: forward variable

// ERROR: not all of the terms are able to evaluate at this time;
// `[[resolve=now]]` on the `doubleNow` function is forcing order to matter
// where normally order resolution of `seven` would be okay to resolve later;
sevenDouble := doubleNow(seven)

// the `seven` symbol declared later
seven := 7

value2 = generateSomething1(value2)     // `generateSomething1(value2)` will
                                        // be replaced with alternative tokens

// the entire line below will be replaced with generated tokens and the inputs
// will not be be evaluated; each input will be converted to a string in
// an array representing each argument as a single string value in the array
generateSomething2(value2 * 5, "hello")

resolve directive

A [[resolve=<option> |, retry=<true/false>|]] directive indicates to a compiler when a specific import, type statements and execute, compile, requires, and concept directives must be resolved.

Options are:

  • trial (default if a resolve directive is not specified in any context) - attempt to resolve a declaration now
  • lazy - only attempt to resolve a declaration if it is referenced
  • last - only attempt to resolve a declaration at the last moment possible (all last directives are done in sequence they are found unless they are found to not resolve and then they are pushed to the back of a resolve queue)
  • now - immediately resolve a declaration and assume all terms required to evaluate must already be defined at this point

Retry options are:

  • true - (default for all but now) if a resolution fails, retry a resolve at a later time
  • false - (default for only now) if a resolution fails, consider a declaration unresolvable

align directive

An [[align=<n> |, reorder=<option>| |, compilation=<compilation-option>|]] directive forces contained types within another type to be aligned at specific memory byte addresses where a memory address offset of a contained type modulus an alignment value must equal 0. By default, all alignments are decided by a compiler. Non-power of 2 alignment values are not supported and will cause a bad-alignment error.

A reorder option is available to indicate contained types that follow the directive can be any order chosen by a compiler using an order priority preference given as a reorder option.

The alignment accepts:

  • <n> - values of 2^N where N >= 0 (default is a compilation context’s CPU’s alignment)

Reorder options are:

  • none (default) - disable reordering for contained types that follow
  • compact - reorder types for compactness
  • speed - reorder types for maximum speed

The compilation-option is as follows:

  • default (default) - the directive applies to any compilation context
  • target - the directive applies only to a compilation target
  • host - the directive applies only a a compilation host
MyType :: type {
    [[align=1]]
    value1 : Integer
    value2 : Float

    [[align=16]]
    value3 : Uuid
}

MyOtherType :: type {
    [[align, reorder=compact]]
    value1 : Byte
    value2 : Integer
    value3 : Float
    condition : Boolean

    [[align, reorder=none]]
    value4 : String
    value5 : ()()
}

reserve directive

A [[reserve=<n> |, initialize=<n>| |, compilation=<compilation-option>|]] directive forces bytes to be reserved into a type which have no value associated with the space. An initialize option allows a specific byte value or literal string pattern to be pre-filled into the reserved memory and no-initialization is presumed by default.

The compilation-option is as follows:

  • default (default if not specified) - the directive applies to any compilation context
  • target - the directive applies only to a compilation target
  • host - the directive applies only a a compilation host
MyPacket :: type {
    size : Integer
    [[reserve=1022]]
}

MyPing :: type {
    size : Integer
    id : Uuid
    [[reserve=256, initialize=0]]
}

void directive

A [[void]] directive declares a type that has a memory offset into a type as if a contained value existed but the size of a contained type does not reserve any bytes for the contained type.

assert final : ()(condition : Boolean) = {
    // ...
}

MyPacket :: type {
    [[align=1]]
    size : Integer
    id : Uuid

    fingerprint [[void]] : Uuid
    bytes [[void]] : Byte[1024]
}

// this assert should be `true`
assert(size of MyType == size of Integer + size of Uuid)

likely / unlikely directive

A likely and unlikely directive indicates which if path or switch statements are likely or unlikely to occur as a hint to a compiler for optimizations and CPU branch prediction. Attempting to specify both if paths as likely or unlikely will result in an incompatible-directive error.

A switch statement can have one or more case statements marked as likely or unlikely but not all case statements may have the same marking. This will tell a compiler to arrange comparison checks in the most optimal way for these scenarios. If a switch is done with alternative operators, the order of the tests is fixed and but a likely or unlikely directive may offer hints to a compiler to optimize for some scenarios before others so long as a testing order would not result in a different outcome than the one a programmer specified.

On a switch, this directive applies to a case statement and not a case statement’s code block. This is done because a compiler optimizes each individual test case and not on a destination code path. Because of case fallthrough, one case may be likely and another case may be unlikely yet they both execute the same code path.

Using the directive with the if statement:

doSomething final : ()() = {
    // ...
}

doSomethingElse final : ()() = {
    // ...
}

func final : ()() = {
    condition final : (result : Boolean)() = { return true }
    failure final : (result : Boolean)() = { return false }

    // placing the `likely` directive prior to the execution block after the
    // `if` treats the `true` condition as `likely`
    if condition() [[likely]]
        doSomething()
    else
        doSomethingElse()

    // placing the `unlikely` after the `else` statement treats the
    // `false` condition as as `unlikely`
    if condition()
        doSomething()
    else [[unlikely]]
        doSomethingElse()

    if failure() [[unlikely]]
        return
    
    // ...
}

Using the directive with the switch statement:

doSomething : ()() = {
    // ...
}

func final : ()(value : Integer) = {
    switch value {
        case 1 [[likely]]
            doSomething()
        case 2
            doSomething()
        case 3 [[likely]]
        case 4 {
            doSomething()
        }
        case 5
        case 6
            doSomething()
        case 7
        case 8
        default [[unlikely]] {
            doSomething()
        }
    }
}

always and never directives

An always and never directive indicate to a compiler that a code path will always be followed or never be followed. This allows for references to code to exist but for those paths to be optimized to always follow a code path or to never follow a code path. The compiler may issue an impossible-if-value panic if an explicit or implicit [[never]] code path was followed.

When [[never]] is applied to a case statement in a switch, the case will be treated as an impossible value and the case will be eliminated. This feature can be useful to ensure all enumerator values are explicitly handled but to also indicate to a compiler that certain cases can never happen. A compiler may issue an impossible-switch-value panic if the value is executed.

When [[always]] is applied to a case statement in a switch, the case will be treated as the only possible value that can occur. A compiler will issue an impossible-switch-value panic if any other case is ever found.

Using an always and never directive with an if statement:

doSomething final : ()() = {
    // ...
}

doSomethingElse final : ()() = {
    // ...
}

func final : ()() = {
    condition final : (result : Boolean)() = { return true }
    failure final : (result : Boolean)() = { return false }

    // placing an `always` directive prior to an execution block after an
    // `if` treats a `true` condition as if a result would `always` be `true`
    if condition() [[always]]
        doSomething()
    else
        doSomethingElse()

    // placing an `always` directive after an `else` statement treats a
    // `false` condition as if a result would `always` be `false`
    if condition()
        doSomething()
    else [[always]]
        doSomethingElse()

    if failure() [[never]]
        return
    
    // ...
}

Using a never directive with a switch statement:

doSomething final : ()() = {
    // ...
}

func final : ()(value : Integer) = {
    switch value {
        case 1 [[never]]
            doSomething()
        case 2
            doSomething()
        case 3 [[never]]
        case 4 {
            doSomething()
        }
        case 5
        case 6
            doSomething()
        case 7
        case 8
        default {
            doSomething()
        }
    }
}

never directive in code flow

A never directive can be used to mark code flows which are impossible to reach. This will allow a compiler to not generate output for code paths that cannot happen. A panic of impossible-code-flow may be issues if an impossible code flow was followed (if that panic is not explicitly disabled).

Example of an impossible code path:

print final : ()(...) = {
    // ...
}

random final : (result : Integer)() = {
    // ...
}

forever {
    value := random()

    if value >= 0 {
        print("outcome is likely")
        continue
    }

    if value < 0 {
        print("outlook not so good")
        continue
    }

    // cannot reach here; an integer must be either be >= 0 or < 0
    [[never]]
}

export directive

An [[export=<option>]] directive instructs a compiler to export symbols, or disable exporting of symbols.

The options are:

  • always - all exportable symbols are automatically exported after this directive
  • never (default if export directive has not been declared) - disable exporting of all exportable symbols after this directive
  • yes (default if no export <option> specified) - cause the next symbol declared as exported
  • no - cause the next symbols to not be exported
  • push - push the current export state onto a compiler’s export stack
  • pop - pop the previous export state from a compiler’s export stack
[[export=always]]

foo : Integer

MyType :: type {
    // ....
}

[[export=never]]

myPrivateData : Integer

MyPrivateType :: type {
    // ...
}

[export] \
visibleToImports : Boolean = true

Literal directives

A [[compiler,<sub-value> |, <option>| |, <sub-option>|]], [[module, <sub-value> |, <option>|]], and [[file, <sub-value> |, <option>|]] represent string, numerical or boolean literals related to a compilation, module, or a compiled file. Short forms of common usages of literals exist, such as [[file]], [[line |, increment=<n>|]], [[function]] exist for [[file, default]], [[file, line |, increment=<n> |]], and [[file, function]] relatively.

If a function is inlined then the literal values become inlined to the context where the directives were inlined into and not the inlined values of the function being inlined (and possibly recursively if inlined functions were inturned inlined). If any of these values are not applicable, an empty string literal or default numerical value of 0 is used in its place.

Each file currently being compiled will have its own literals for properties relative to the current compiling file. Other literals are relative to a current module and do not change per compiled file (but do change per compiled module). Finally other literals are entirely global to the compilation process.

The following literals are relative to a compiler using a compiler directive:

  • time - the time of compilation (as a string or as an Integer, see time options)
  • date - the date of compilation (as a string)
  • compiler - the brand name of the compiler application compiling the source code (as a string)
  • version - a compiler’s version (as a string)
  • compilation - values relative to the current compilation context (i.e. either target or host depending which is compiling) with these options:
    • big-endian - if the compilation context is big endian (as a true or false literal)
    • little-endian - if the compilation context is little endian (as a true or false literal)
    • feature - if a specific feature is enabled for a the compilation context where the sub-option is a feature name (e.g. CPU extensions) (as a true or false literal)
    • env - contains an environment value specific to the compilation context (as a string)
  • target - values relative to the target compilation context with options:
    • big-endian - if the target compilation context is big endian (as a true or false literal)
    • little-endian - if the target compilation context is little endian (as a true or false literal)
    • active - if the current compilation context is set to the target (as a true or false literal)
    • feature - if a specific feature is enabled for the target compilation where the sub-option is a feature name (e.g. CPU extensions) (as a true or false literal)
    • env - contains an environment value specific to a target compilation (as a string)
  • host - values relative to the host compilation context with options:
    • big-endian - if the host compilation context is big endian (as a true or false literal)
    • little-endian - if the host compilation context is little endian (as a true or false literal)
    • active - if the current compilation context is set to the host (as a true or false literal)
    • feature - if a specific feature is enabled for the host system compilation where the sub-option is a feature name (e.g. CPU extensions) (as a true or false literal)
    • env - contains an environment value specific to a host compilation (as a string)

The following literals are relative to the current module using a module directive:

  • location - URL of the current module (as a string)
  • git - the following option are available:
    • default (default) - if the current module uses git (as a true or false literal)
    • tag - the current module git tag (as a string)
    • branch - the current module git branch (as a string)
    • commit - the current module last git commit hash (as a string)
  • path - the full path to the current module as located on the host file system (as a string)
  • import-version - the API version requested for the module being imported (as a string)
  • hash - the current hash calculated for the current importation definitions (as a string)
  • env - contains an environment value specific to a module (as a string)

The following literals are relative to the current compiling file using a file directive:

  • default (default) - the path and source file name being compiled (as a string)
  • path - the source path for the current file relative to the module root (as a string)
  • filename - the source filename of the file being compiled (without the path) (as a string)
  • line - the line number being compiled (as an Integer)
    • an additional increment=n option is available on the line literal
  • function - the function being compiled (as a string)
  • date - the file date of the file being compiled (as a string)
  • time - the file time of the file being compiled (as a string or as an Integer, see time options)
  • generator - the name of the generator of the current file, otherwise an empty string (as a string)
  • hash - the current hash of the source file (as a string)
  • env - contains an environment value specific to a file (as a string)

The options for compiler.version and module,import-version are:

  • default (default) - the full semantic version (as a string)
  • major - the major version number (as an Integer)
  • minor - the minor version number (as an Integer)
  • patch - the patch version number (as an Integer)
  • pre-release - the pre-release (as a string)
  • build - the build identifiers (as a string)

The options for [[compiler,time,<option>]] and [[file,time,<option>]] are:

  • default (default) - the full time (as a string)
  • unix - the compile-time number of seconds since the time since the unix epoch (as an I64)
  • nt - the compile-time expressed as the number of 100 nanoseconds since the time since the NT epoch (as an I64)

A line directive [[line=<n> |, increment=<n>|]] sets the current’s source’s line number being compiled. An increment argument indicates how much to increment a counted line number per line of the currently compiled source file (default for increment is 1).

print final : ()(...) = {
    // ...
}

trace final [[inline=always]] : ()() = {
    currentFile := [[file]]
    currentLine := [[line]]
    currentFunction := [[function]]

    print(currentFile, currentLine, currentFunction)
}

trace()

Override literal directives

All literal directives can be overridden for a given module, compilation file, or compilation. This allows literals to be changed for generated files, or for the purpose of tweaking some of the values related to the compilation process.

For example, generated source files can use a file/line directives [[file="<name>"]] indicates to a compiler if the current source was generated by some process using another file as input. The name indicates the path to that original file. A line directive [[line=<n>, increment=<n>]] can be used to track an original source’s line number so a generated output’s line number matches an original source file’s line numbering rather than a connecting with a generated output’s line number. The increment argument indicates how much to increment a counted line number of the original source file per output line found in the file being compiled (whose default is 1).

// override URL location for a generated module
[[module,location="https://exmplae.com/"]]

[[file,generator="Acme FooBar Generator"]]

// example file generated by Acme FooBar Generator
[[file="inputs.csv"]]

[[line=21]]
apple : Integer
banana : Float
carrot : String

[[line=400, increment=0]]
donut : Float
egglessChocolateMousse : String

[[line=401, increment=0]]
fudge : Integer
gulabJamun : String

isRelease := [[compiler,target,feature,release=true]]
usingSseInstruction := [[compiler,target,feature,sse=true]]

[[compiler,host,env,custom-value="free"]]

[[compiler,target,env,special="professional"]]

A compiler will expose a module with compile-time functions to read and write literals in a programmatic methodology rather than through compiler directives (including the ability to adjust compile-time flags). Directives are included for convenience for quick access to literal values.

lock-free directive

A lock-free directive disables the creation of this safety code around a singleton’s instance creation and assumes that construction of a type will happen entirely single threaded without any possibility of multiple threads competing for an singleton’s instance creation. Normally, a once keyword will automatically generate thread-safe barriers around a singleton’s instance construction to ensure only a single instance will ever be constructed of a variable (regardless of the number of threads accessing a variable). However, this adds additional safety overhead that might not always be necessary.

MyType :: type {
    value1 : String
    value2 : String 
}

giveMeMyType final : (myType : MyType &)() = {
    // no thread locking mechanism will surround this code
    singleton once [[lock-free]] : MyType
    return singleton
}

// force initialization globally on startup (which must be single threaded)
initializeMyType private := giveMeMyType()

synchronous directive

Function are assumed to be implicitly asynchronous for promise, task or channel functions. This default can be overridden by using a synchronous directive. When a synchronous directive is used, a function is declared to not operate asynchronously and all assumptions about any asynchronous intentions are no longer present. A [[synchronous]] directive effectively changes the expectations of a function from asynchronous to synchronous and indicates the code path does not need to be thread-aware.

An implicit assumption for asynchronous functions is that pass by-values arguments must be qualified as deep. A compiler issues an asynchronous-not-deep warning on asynchronous functions for any pass by-value arguments that are not explicitly qualified as deep (or pre-qualified as deep based on a type’s definition). This check is done to ensure that values potentially crossing a thread boundary are automatically deep copied in an effort to prevent concurrency issues. A function can be labelled explicitly as deep or shallow to suppress this warning by forcing semantics on specific arguments.

If a promise, task, or channel declared function will never be used from a different thread contexts then an [[synchronous]] directive can be used to acknowledge a declared function is exclusively synchronously accessed and thus a deep qualifier need not be applied. Further, any type declared as deep (which normally would cause a deep copy to occur) will perform instead a shallow copy of that type. Individual arguments for promises or tasks declared as deep explicitly will still perform deep copies of any arguments.

If a promise or task truly is asynchronous (as it would be implicitly) but a pass by-value should only be shallow copied, then a shallow qualifier can be specified. This changes a pass by-value from being implicitly a deep copy to explicitly being a shallow copy, and an asynchronous-not-deep warning will be suppressed for that argument.

A [[synchronous]] and [[asynchronous]] directive are mutually exclusive and they indicate opposite code intentions.

MyType :: type {
    value1 : Integer * @
}

func final : ()(myType : MyType) promise [[synchronous]] = {
    // ...
}

myType : MyType

// OKAY: a warning `asynchronous-not-deep` will not be issued
later := func(myType)

// ...

// must be called from the same thread or undefined behaviors may result
callable := later.then = { /* ... */ }
callable()

asynchronous directive

Unlike a promise, task, or channel, normal functions are assumed to operate in a synchronous fashion. Using an asynchronous directive tells a compiler that a function will perform asynchronous operations despite not being a promise, task, or channel (which are already default assumed to be [[asynchronous]] implicitly). An [[asynchronous]] directive effectively changes a function’s expectations from synchronous to asynchronous and indicates a normal function is designed to be thread-aware.

When a function is labelled as asynchronous, a function is excepting that all pass by-value arguments are qualified with a deep specifier. A compiler will issue an asynchronous-not-deep warning if a deep qualifier is missing (as normally values are implicitly shallow). Adding a deep qualifier will override a default shallow behavior. If a value should be shallow copied then a shallow qualifier can be used either on an individual pass by-value argument or on a function as a whole.

A [[synchronous]] and [[asynchronous]] directive are mutually exclusive and they indicate opposite code intentions.

MyType :: type {
    // ...
}

// functions labelled as `asynchronous` expects all pass by-value functions
// to use a `deep` qualifier rather than an implicit `shallow` thus a warning
// is issued to indicate the oversight
func1 final : ()(myType : MyType) [[asynchronous]] = {
    // ...
}

// the pass by-value is `shallow` copied explicitly thus no warning is issued
func2 final : ()(myType : MyType shallow) [[asynchronous]] = {
    // ...
}

// the pass by-value is `deep` copied thus no warning is issued
func3 final : ()(myType : MyType deep) [[asynchronous]] = {
    // ...
}

// all pass by-values are `shallow` copied thus no warning is issued
func4 final : ()(a : MyType, b : MyType) shallow [[asynchronous]] = {
    // ...
}

// all pass by-values are `deep` copied thus no warning is issued
func5 final : ()(a : MyType, b : MyType) deep [[asynchronous]] = {
    // ...
}

variables and types mutability defaults directive

variables default directive

A [[variables=<option>]] directive declares defaults when declaring variables. See the mutability section for more details. This directive only applies to all source code following a variables directive and does not change any defaults for an imported module.

The options are:

  • final - all declared variable are final
  • varies (default if variables directive has not been declared) - all declared variables are declared as varies
  • pliable - all declared variables are pliable (thus constant declarations will not be respected)
  • unpliable (default if variables directive has not been declared) - all declared variables are unpliable (thus constant declarations will be ignored)
  • push - push a current variables state onto a compiler’s variables stack
  • pop - pop a previous variables state from a compiler’s variables stack

Example of how varies / final default applied to variables:

MyType :: type {
    value1 : Integer = 5
    value2 : String = "hello"
}

[[variables=varies]] 

x1 := 5
x1 = 6          // OKAY

x2 final := 5
x2 = 6          // ERROR: variable value is `final`

x3 varies := 5
x3 = 6          // OKAY

mx1 : MyType
mx1.value1 = 6  // OKAY

mx2 final : MyType
mx2.value1 = 6  // OKAY

mx3 varies : MyType
mx3.value1 = 6  // OKAY


[[variables=final]]

y1 := 5
y1 = 6          // ERROR: variable value is `final`

y2 final := 5
y2 = 6          // ERROR: variable value is `final`

y3 varies := 5
y3 = 6          // OKAY

my1 : MyType
my1.value1 = 6  // OKAY

my2 final : MyType
my2.value1 = 6  // OKAY

my3 varies : MyType
my3.value1 = 6  // OKAY

Example of how pliable / unpliable default applied to variables:

MyType :: type {
    value1 : Integer = 5
    value2 : String = "hello"
}

[[variables=unpliable]] 

x1 := 5
x1 = 6          // OK (unpliable variables respect a type's mutability)

x2 : constant = 5
x2 = 6          // ERROR: type is `constant`

x3 : inconstant = 6
x3 = 6          // OK (unpliable variables respect a type's mutability)

mx1 : MyType
mx1.value1 = 6  // OKAY

mx2 final : MyType constant
mx2.value1 = 6  // ERROR: type is `constant`

mx3 varies : MyType inconstant
mx3.value1 = 6  // OKAY


[[variables=pliable]]

y1 := 5
y1 = 6          // OKAY

y2 : constant = 5
y2 = 6          // OKAY (type is constant but value is pliable)

y3 : inconstant = 6
y3 = 6          // OKAY

my1 : MyType
my1.value1 = 6  // OKAY

my2 final : MyType constant
my2.value1 = 6  // OKAY (type is constant but value is pliable)

my3 varies : MyType inconstant
my3.value1 = 6  // OKAY

my4 varies : MyType immutable
my4.value1 = 6  // ERROR: even though a value is `pliable` the underlying
                // `immutable` `type` qualifier is not affected by `pliable`

types default directives

The [[types=<options>]] directive declares defaults for the declaration of all types (and not a type’s definition). See the mutability section for more details. This directive only applies to all source code following the directive and does not change the defaults for any imported modules.

The options are:

  • mutable (default if types directive has not been declared) - if a default is not specified for a type, a declared type is assumed to be mutable
  • immutable - if a default is not specified for a type, a declared type is assumed to be immutable
  • constant - if a mutable type is declared, the type is assumed to be constant once constructed
  • inconstant (default if types directive has not been declared) - if a mutable type is declared, the type is assumed to remain mutable (unless constant is applied)
  • push - push the current types state onto the stack
  • pop - pop the previous types state from the stack

Example of how mutable / mutable default applied to types:

MyType :: type {
    value1 : Integer = 5
    value2 : String = "hello"
}

[[types=mutable]] 

x1 := 5
x1 = 6          // OKAY

x2 : mutable = 5
x2 = 6          // OKAY

x3 : immutable = 5
x3 = 6          // ERROR: type is immutable

mx1 : MyType
mx1.value1 = 6  // OKAY

mx2 : MyType mutable
mx2.value1 = 6  // OKAY

mx3 : MyType immutable
mx3.value1 = 6  // ERROR: type is immutable


[[types=immutable]]

y1 := 5
y1 = 6          // ERROR: type is immutable

y2 : mutable = 5
y2 = 6          // OKAY

y3 : immutable = 5
y3 = 6          // ERROR: type is immutable

my1 : MyType
my1.value1 = 6  // ERROR: type is immutable

my2 : MyType mutable
my2.value1 = 6  // OKAY

my3 : MyType immutable
my3.value1 = 6  // ERROR: type is immutable

Example of how constant / inconstant default applied to types:

MyType :: type {
    value1 : Integer = 5
    value2 : String = "hello"
}

[[types=inconstant]] 

x1 := 5
x1 = 6          // OKAY

x2 : inconstant = 5
x2 = 6          // OKAY

x3 : constant = 5
x3 = 6          // ERROR: type is constant

mx1 : MyType
mx1.value1 = 6  // OKAY

mx2 : MyType inconstant
mx2.value1 = 6  // OKAY

mx3 : MyType constant
mx3.value1 = 6  // ERROR: type is constant


[[types=constant]]

y1 := 5
y1 = 6          // ERROR: type is constant

y2 : inconstant = 5
y2 = 6          // OKAY

y3 : constant = 5
y3 = 6          // ERROR: type is constant

my1 : MyType
my1.value1 = 6  // ERROR: type is constant

my2 : MyType inconstant
my2.value1 = 6  // OKAY

my3 : MyType constant
my3.value1 = 6  // ERROR: type is constant

functions default directives

A [[functions=<options>]] directive declares default constant/inconstant qualifier for all functions with a type. See the mutability section for more details. This directive only applies to source code following a functions directive and does not change a default for an imported modules.

The options are:

  • constant - a function declared on a type is constant by default
  • inconstant (default if types directive has not been declared) - a function declared on a type is inconstant by default
  • push - push a current functions state onto a compiler’s functions stack
  • pop - pop a previous functions state from a compiler’s functions stack

Example of how constant / inconstant default applied to functions:


[[functions=inconstant]] 

MyType1 :: type {
    value1 : Integer = 5
    value2 : String = "hello"

    func1 : ()() = {
        value1 = 6              // OKAY
    }

    func2 : ()() inconstant = {
        value1 = 6              // OKAY
    }

    func3 : ()() constant = {
        value1 = 6              // ERROR: value1 is `constant`
                                // (as function is constant)
    }
}

[[functions=constant]] 

MyType2 :: type {
    value1 : Integer = 5
    value2 : String = "hello"

    func1 : ()() = {
        value1 = 6              // ERROR: value1 is `constant` (as function is
                                //  `constant`)
    }

    func2 : ()() inconstant = {
        value1 = 6              // OKAY
    }

    func3 : ()() constant = {
        value1 = 6              // ERROR: value1 is `constant` (as function is
                                // `constant`)
    }
}

abi directive

An [[abi=<options>]] directive can override a calling convention to force a particular ABI for a given function. For ABI compatibility purposes with C/C++, a function within a type can declare final function with an alternative ABI. Normally the Zax language makes no ABI commitments across compiled functions as source code is always compiled as a whole and compiled libraries are not considered compatible across compilers or compiler versions.

The ABI options are as follows:

  • virtual - this causes a function to assume C++ virtual calling conventions to be inserted into a virtual table for a type where a virtual table for a type will be auto-created and auto-maintained (warning: the context variable ___ will point to nothing upon entry to a function)
  • c - this causes a function to assume a c style calling convention (warning: the context variable ___ will point to nothing upon entry to a function and a _ will point to Nothing)
  • never - a function never returns and a compiler can generate jump assembly instructions without an instruction pointer stack frame pushed or return instructions
  • interrupt - a function will be called directly from an interrupt and a compiler should generate interrupt return instructions instead of standard return instructions
MyType :: type {
    func1 final [[abi=virtual]] : ()() * = {
    }

    func2 final [[abi=virtual]] : ()() * = {
    }
}

tab-stop directive

A [[tab-stop=<n>]] directive controls a source code’s tab stop for all tokens that follow. This control what alignment a tab ASCII character (\t) is assumed to have within all the contained source code. Tab stops are reset to a default value for each module imported. A default hard tab stop is 8 unless otherwise specified.

Typically tab-stop directive is declared in a module.zax to ensure all source files follow the same tab-stop directive. An Zax language aware editor may perform a shallow scan in a module’s modules.zax and assume a default tab stop for editing all files within a module.

[[tab-stop=4]]

func : ()() = {
    // ...
}