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 statementno
- disables a panic for only to the current statementalways
- enables a panic for all statements that follownever
- disables a panic for all statements that followdefault
- enables or disables a panic for all statements that follow according to a compiler’s defaultslock
- disallows any imported module from changing a panic stateunlock
- 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
)
- memory was requested to be allocated but insufficient memory exists to fill the request (allocation failures normally panic rather than return a pointer to
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
- an intrinsic type may overflow during an
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
- a pointer was converted to a reference but a pointer points to
pointer-to-nothing-accessed
(always)- a value (or function) was accessed but a pointer points to
Nothing
- a value (or function) was accessed but a pointer points to
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)
- a
impossible-if-value
(always)- an
if
statement encounter a value which can never happen (because of a[[never]]
directive or an[[always]]
directive)
- an
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)
- a code path was followed what was marked as impossible to reach (because of a
lazy-already-complete
(always)- an attempt was made to call a
lazy
function that has already cause a finalreturn
from thatlazy
function
- an attempt was made to call a
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 statementno
- disables deprecation for only to the current statementalways
- enables deprecation for all statements that follownever
- 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 importall
- any usage cause a deprecation warninglocal
- any usage within the current module cause a deprecation warning but not usages from an importing module (min
andmax
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 ifinline
directive is not specified) - a compiler decides if it is best toinline
a function or notlikely
(default if no<option>
is specified) - a compiler should prefer toinline
a function but a compiler may ultimately decideunlikely
- a compiler should prefer calling a function rather than inlining but a compiler may ultimately decidealways
- a compiler is forced toinline
a function but all variables declared inside a called function are not visible to a caller’s scopedescope
- a compiler is forced toinline
a function and all variables within the immediate scope of an inlined function are declared as if they were declared inside a caller’s scopenever
- a compiler may neverinline
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 eithertarget
orhost
)target
- force all code to generate as if it were evaluating in the target’s contexthost
- 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 afalse
literal constanterror
- iferror
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 afalse
literal constantcompiles
- ifcompiles
is specified then a failure to compile a code block will evaluate to a real compile-time errorerror
- iferror
is specified then a failure to compile a code block or arequires
directive code block returningfalse
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 anexecute
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 hosthost
(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 valuestarget
- 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 aresolve
directive is not specified in any context) - attempt to resolve a declaration nowlazy
- only attempt to resolve a declaration if it is referencedlast
- only attempt to resolve a declaration at the last moment possible (alllast
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 butnow
) if a resolution fails, retry a resolve at a later timefalse
- (default for onlynow
) 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 followcompact
- reorder types for compactnessspeed
- reorder types for maximum speed
The compilation-option
is as follows:
default
(default) - the directive applies to any compilation contexttarget
- the directive applies only to a compilation targethost
- 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 contexttarget
- the directive applies only to a compilation targethost
- 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 directivenever
(default ifexport
directive has not been declared) - disable exporting of all exportable symbols after this directiveyes
(default if noexport
<option>
specified) - cause the next symbol declared as exportedno
- cause the next symbols to not be exportedpush
- push the currentexport
state onto a compiler’s export stackpop
- pop the previousexport
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 anInteger
, seetime
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. eithertarget
orhost
depending which is compiling) with these options:big-endian
- if the compilation context is big endian (as atrue
orfalse
literal)little-endian
- if the compilation context is little endian (as atrue
orfalse
literal)feature
- if a specific feature is enabled for a the compilation context where thesub-option
is a feature name (e.g. CPU extensions) (as atrue
orfalse
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 atrue
orfalse
literal)little-endian
- if the target compilation context is little endian (as atrue
orfalse
literal)active
- if the current compilation context is set to the target (as atrue
orfalse
literal)feature
- if a specific feature is enabled for the target compilation where thesub-option
is a feature name (e.g. CPU extensions) (as atrue
orfalse
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 atrue
orfalse
literal)little-endian
- if the host compilation context is little endian (as atrue
orfalse
literal)active
- if the current compilation context is set to the host (as atrue
orfalse
literal)feature
- if a specific feature is enabled for the host system compilation where thesub-option
is a feature name (e.g. CPU extensions) (as atrue
orfalse
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 atrue
orfalse
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 anInteger
)- an additional
increment=n
option is available on the line literal
- an additional
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 anInteger
, seetime
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 anInteger
)minor
- the minor version number (as anInteger
)patch
- the patch version number (as anInteger
)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 anI64
)nt
- the compile-time expressed as the number of 100 nanoseconds since the time since the NT epoch (as anI64
)
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 arefinal
varies
(default ifvariables
directive has not been declared) - all declared variables are declared asvaries
pliable
- all declared variables arepliable
(thusconstant
declarations will not be respected)unpliable
(default ifvariables
directive has not been declared) - all declared variables areunpliable
(thusconstant
declarations will be ignored)push
- push a currentvariables
state onto a compiler’svariables
stackpop
- pop a previousvariables
state from a compiler’svariables
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 iftypes
directive has not been declared) - if a default is not specified for a type, a declared type is assumed to bemutable
immutable
- if a default is not specified for a type, a declared type is assumed to beimmutable
constant
- if amutable
type is declared, the type is assumed to beconstant
once constructedinconstant
(default iftypes
directive has not been declared) - if amutable
type is declared, the type is assumed to remainmutable
(unlessconstant
is applied)push
- push the currenttypes
state onto the stackpop
- pop the previoustypes
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 isconstant
by defaultinconstant
(default iftypes
directive has not been declared) - a function declared on a type isinconstant
by defaultpush
- push a currentfunctions
state onto a compiler’s functions stackpop
- pop a previousfunctions
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 assumeC++
virtual calling conventions to be inserted into a virtual table for atype
where avirtual
table for atype
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 ac
style calling convention (warning: the context variable___
will point to nothing upon entry to a function and a_
will point toNothing
)never
- a function never returns and a compiler can generate jump assembly instructions without an instruction pointer stack frame pushed or return instructionsinterrupt
- 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 : ()() = {
// ...
}