Spec chapter 13
Compile-Time Features
Compile-time evaluation
Whatever can be computed at compile time is computed at compile time.
- An expression made only of literals,
Consts, operators, and casts is evaluated by the compiler, never at run time. Constdeclares a read-only value. If its initializer can be computed at compile time, it is; otherwise it’s computed once at run time (top-levelConsts together with theGlobals).- Places that need a value at compile time (array sizes, const generic arguments,
Casevalues) need aConstwhose value is known at compile time. A Const type argument computed from Const type parameters, likeN - 1, is worked out in each instance, where dividing by 0 or a size below 1 is a compile error (07). - Overflow, division by zero, or an invalid cast during compile-time evaluation is a compile error, in debug and release builds alike.
- Calls are evaluated at compile time when the function is a
Const Function(below) and its arguments are known. - A top-level
Constknown at compile time can be used anywhere in its file, even above its line, and in a declaration’s type, like a Field’sInt[SIZE]. One computed at run time is initialized in source order with theGlobals. AConstthat needs its own value is an error.
Const SECONDS_PER_DAY = 24 * 60 * 60 // 86400, computed by the compiler
Const START = Clock.Now() // read-only, but computed at run time
Local timeout = 5 * 60 * 1000 // stored as 300000; nothing computed at run timeConst functions
Functions marked Const are evaluated by the compiler when called with constant
arguments.
Const Function Fib(n: Int): Int
Return n < 2 ? n : Fib(n - 1) + Fib(n - 2)
End Function
Const FIB_20 = Fib(20) // computed at compile time
Local table: Int[Fib(10)] // usable in types- A Const Function is a top-level
Functionthat can’t throw. It takes and gives numbers, Bools, Strings, Chars, and Enums (or aT?of one). A generic one, likeConst Function Largest<T: Comparable>(a: T, b: T): T, runs at compile time when its type arguments are those types, and a built-in Interface’s Operators on its type parameters are the built-in ones. Its body can use Locals of those, and Structs, tuples, and fixed arrays of them, and calls only Const Functions,Assert,Panic, and the New a Struct gets. It doesn’t use Globals, or Consts known only when the program runs, and makes no objects,T[]s, or function values. - A call whose arguments are all known runs in the compiler, and its value is the call’s. With any argument that isn’t known, it’s an ordinary call that runs with the program.
- A panic, an overflow, a division by zero, an index out of range, more than 10 million steps, calls more than 10,000 deep, or variables that hold more than 256 MiB of text and parts while it runs is a compile error at the call, pointing at where in the Const Function it happened: for a limit, the last call, operation, or Local’s declaration it got to, or else the function’s name.
- A Const Function runs as the build would run it, so a release build leaves its DebugAsserts out, as it does the program’s.
- A size in a declaration’s type, like a Field’s
Int[Cells()], can come from a Const Function anywhere in the file, since Const Functions are declared first. While types are still being declared, one whose code reaches a Struct or Class whose members aren’t declared yet can’t run there, which is an error.
Attributes
@Inline Function Lerp(a: Float, b: Float, t: Float): Float
@Deprecated("use Spawn2") Function Spawn(...)
@Flags Enum Layer ...
@Open Enum Kind ...
@CLayout Struct JPH_Vec3 ...
@MainThread Function RenderWorld()
@PanicsAtCaller Method Pop(): T
@Test Function ParsesALevel()| Attribute | Effect |
|---|---|
@Inline | force inlining |
@Deprecated(msg) | warning at use sites |
@Flags | enum values are powers of two |
@Open | an enum holds any value of its integer type |
@CLayout | C-compatible struct layout |
@MainThread | callable only from the main thread |
@PanicsAtCaller | its panics report where it’s called |
@Test | a test, which fb test runs, and builds leave out |
- An attribute goes before a declaration, on its line or alone on the line above.
@Inlinegoes on a Function, Method, Operator, or Property with a body. Every call to it is inlined, in debug builds too.@Deprecatedand@Deprecated("message")go on anything declared outside a body: a Function, Method, Property, Struct, Class, Interface, Enum, Field, Global, or Const. Each use outside it, and outside its type’s own code, gets a warning: “‘Spawn’ is deprecated: use Spawn2”.@MainThreadgoes before a Function or Method, which work on another thread can’t call (see 10-concurrency.md).@PanicsAtCallergoes before a Function, Method, Operator, New, or Property, whose panics then report where it’s called (see 09-errors.md), not Destroy or an Extern Function, and of an Interface’s members only a Final, generic, or conditional default, which a call runs directly.@Testgoes before a top-level Function that takes nothing and gives nothing back, though it can throw, whichfb testruns (see 09-errors.md).@CLayoutlays a Struct out as C does (see 12-ffi.md).@Serializeisn’t needed: Load and Save (below) convert every Struct and Class, so it’s an error that says so.
Declaring attributes
Attribute Skip
Attribute Rename(Name: String)
Attribute Range(Low: Int = 0, High: Int = 100)
Struct Player
@Rename("hp") @Range(High: 200)
Field Health: Int
@Skip
Field Cache: Int
End StructAttribute Name(parameters)declares an attribute, which reflection reads. It’s a Struct whose Fields are its parameters, sof.Attribute(Rename)gives aRename?.- Its parameters are numbers, Bools, Strings, Chars, and Enums. A default is known at compile time. It can’t take a built-in attribute’s name.
- A use takes an argument for each parameter without a default, positional ones first, then named ones, as in a call. Each argument is known at compile time, like a Const’s value.
- It goes on anything declared outside a body: a Function, Method, Property, Struct, Class,
Interface, Enum, Field, Global, or Const. A declaration has each attribute at most once.
Attributeis only a keyword before a name. - An attribute from a module imported with As goes after its qualifier, as other names do:
@C.Rename("hp"), andf.Has(C.Rename)in reflection. A qualified one is never a built-in, so@C.Inlineis C’s own: an Attribute can take a built-in’s name, which code reaches only through a qualifier, since@Inlinealone is always the built-in.
Reflection
Compile-time only. Code can inspect types (fields, methods, attributes) during compilation to generate code, e.g. serializers. There is no runtime reflection, which keeps binaries small and fast.
Function Describe<T>(value: T): String
Local text = ""
For Field f: F In T
Where F: Printable
If f.Has(Skip) Then Continue
Local name = f.Attribute(Rename)?.Name ?? f.Name
text = text + "{name}={f.Get(value)} "
End For
Return text
End Function
Function Clear<T>(ByRef value: T)
For Field f: F In T
Where F: Numeric
f.Set(ByRef value, F.Zero())
End For
End FunctionFor Field f: F In Tgoes over the fields of a Struct, a Class (the inherited ones first), or a tuple (“0”, “1”, and so on), in order, but not an Atomic or Mutex, which can’t be copied. The compiler makes a copy of the body for each field, in whichFis that field’s type.: Fcan be left out when nothing names it.- Over a
T?, through any number of?s, it goes over its T’s fields, so generic code reads the declarations of a Field likeBuild: BuildArgs?, as std.os’s Args does for subcommands: each has its Name, Index, Has, Attribute, and type, but aT?may hold no T, sof.Getandf.Setin a copy over one are an error that says so. A loop written over a type parameter’sT?puts its Where lines on the T’s fields, as one overTdoes. - The body is checked once, with
Fa type parameter that only its Where lines, at the top of the body, say anything about. Over a concrete type, each field’s type must meet them. Over the function’s own type parameterT, they’re constraints onT’s fields, which each call checks:Describe(p)needs each ofp’s fields to be Printable. A type parameter’s fields meet a constraint only when one of its own loops says so. - The field has
f.Name,f.Index,f.Get(value), which gives anF,f.Set(ByRef value, x),f.Has(A), andf.Attribute(A), which gives anA?, for an AttributeA.fisn’t a value of its own. A ReadOnly Field changes only in its Class’s New, sof.Seton one is an error where the loop’s copy for it is made, and so isf.Seton a Field that the loop’s code couldn’t assign by name: a Private one outside its file, a Package one outside its package, and a Protected one outside its file and the code of the Classes extending its Class. What a type keeps to itself, like the memory a std.ffi Buffer owns, stays as its own code has it, whilef.Getstill reads every Field. For Method m In Tgoes over the Methods of a Struct or Class that take nothing, give nothing, and throw nothing (the inherited ones first, each in the place of what it overrides), but not Destroy, Unsafe, or ByRef Methods, or Async ones, which give a Task.m.Call(value)calls it, through the Method table for a Class.mhasm.Name,m.Has(A), andm.Attribute(A)too.For Method m: M In TwithWhere M: Func(Int): Stringgoes over the Methods of that Func type instead, andm.Call(value, 5)passes their arguments. Its Where line picks the Methods; it isn’t a constraint to meet. A Func type has no ByRef, so a Method with a ByRef parameter is never among them.- A name in that Func type that isn’t a type is a type parameter of the loop’s own, which each
Method binds:
Where M: Func(A): Task<Response>goes over the Methods that take one argument, of any type, and give aTask<Response>, and in each copyAis what that Method takes. The body is checked once, generic overA, which the loop’s other Where lines constrain, likeWhere A: Defaultable, and which are in force in the Func type too, soFunc(Weak<A>): Stringgoes withWhere A: Class. What each Method binds meets them, every overload’s and what an Extend the loop sees adds, over a concrete type at the loop and over the code’s type parameter at each call, as a For Field’sFdoes. There, another loop over the same type parameter says they’re met only when it’s of the same Func type, with the same line on the same type parameter in it. A name that is a type stays one, an outer loop’s own type parameter too, but the Func type can’t name the loop’s ownM, and one Where line onMsays which Methods. - Async Methods are among a typed loop’s Methods when its Func type gives a Task: an Async
Method that gives a T has the Func type
Func(...): Task<T>, andm.Callstarts it and gives its Task, whichAwaitwaits for. - A typed loop takes Methods that throw when its Func type throws:
Func(A): R Throws E, withWhere E: Error, goes over the Methods that take an A, give an R, and throw one error, which E is in each copy, andm.Callcan throw E, so it needs Try, as a call does;Func(A) Throws Eis the Methods that give nothing. A Throws after the Func type’s result is the Func type’s, as a header’s is, soFunc(A): Func(): R Throws Egoes over Methods that throw and give a Func. An Async Method’s errors are in its Task, so it’sFunc(A): (Task<R> Throws E), whose Await re-throws them. Without a Throws, the loop goes over Methods that don’t throw, and an Override that throws what its base doesn’t takes the base’s place in neither. Only the loop’s own Func type throws, not a Func type in it: a Func value can’t, nor a Callback. - A Where line on the loop’s variable picks Methods by attribute:
Where m Has Get Or Postgoes over only the Methods with one of those Attributes, so no other Method gets a copy, or is checked against the loop’s other Where lines. Each such line picks again, and an Override goes by its own attributes. Over a type parameter, another loop says its Where lines are met only when it picks the same way.HasandOrare words only there. Only a For Method loop picks; the other loops’ bodies testf.Has(A). For Property p: P In Tgoes over a Struct’s or Class’s Properties (inherited ones first), withp.Name,p.Get(value),p.Has(A), andp.Attribute(A), andFor Static Field s: S In Tover its own Static Fields, withs.Get(). They read; a Property without a Set and a ReadOnly Static Field couldn’t be told apart, so neither has a Set. Where lines on P or S work as on F.- The loops go over Private members too, and over what an Extend of the type, or of that one
instance like
Box<Int>, adds, but only where the loop’s file sees the Extend’s module, as a call there would. A Method or Property that Where lines on the type’s parameters make conditional is among them only for the instances whose type arguments meet the lines. InFor Property, an Override takes the place of what it overrides, while another Property of its name, like one an Extend adds beside a Private one, is a Property of its own. - A loop can go over the code’s own type parameters: its function’s, or in a Method, its type’s,
whose Where lines then make the Method conditional, as a Where line on it would.
Selfis the type of the code it’s in, and in an Interface’s default, the type that takes it, which has to meet the loop’s Where lines. - A lambda in the loop’s body is one for each copy. A lambda in code with type parameters can loop over them too, since each instance of the code has its own copy of the lambda.
T.Has(A)andT.Attribute(A)read the attributes on a type’s own declaration, for a type or a type parameter, unless it has a Static member by that name. On aT?, through any number of?s, they read its T’s, as For Field does.TypeName<T>()is a built-in, like SizeOf: a String of T’s declared name, without its module, like"Map<String, Int32>"or"(Int32, String?)", the same wherever it’s asked for, known at compile time, and in generic code the instance’s, soTypeName<T[]>()in an instance for Int32 gives"Int32[]". A program’s own TypeName takes its place.Continuegoes on to the next copy, andBreakpast the last. Other types, like Int, have no fields or Methods, so the loop’s body is never run.
Load and Save
Class Settings
@Rename("player_name") Field Name: String
Field Volume: Double = 0.8
@Skip Field Cache: Int[] = []
Field Keys: Map<String, Key>
New()
Keys = {}
End New
Method OnLoaded() Throws ParseError
If Volume > 1.0 Then Throw ParseError(ParseErrorKind.OUT_OF_RANGE, "Volume is past 1")
End Method
End Class
Local saved = Save(settings) // a Value, which a format writes out
Local loaded = Try Load<Settings>(saved) // and makes a Settings from again- The built-ins
Load<T>(value)andSave(x), orSave<T>(x)for an x that converts to a T, convert between std.core’sValueand a T, which the compiler checks it can, Field by Field through what T is made of, by compile-time reflection. In generic code, each instance’s T is checked, by the same rules, where Loador Save is called. A module’s own Load or Save, or an imported one, takes the name’s place. - The numbers are integers and Doubles, a Bool a BOOL, a String a STRING, and an Enum its member’s
name, as its text has it, “WORLD | PLAYER” for a @Flags Enum’s flags, or an integer for a
@Flags value with bits no member has. Load also takes a name in another case, or an integer,
which for an Enum without @Flags is a member’s. A
T?is Null or a T, aT[]an Array, which Load also takes as one Value alone, as XML and a command line give one item, aUInt8[]bytes, which Load also takes as a String of their standard base64, with its ’=’ padding, as JSON writes them, or as an Array of integers, and aMap<String, V>an Object. A String that’s base64 is bytes before it’s one Value alone, so any bytes JSON writes read back, though “0012”, which is both, is 3 bytes and not [12]. A Value is copied, so the T and the Value don’t share one. - A Struct or Class is an Object of its Fields, the inherited ones first, by name, but not
Properties. Load makes one without running its New: each Field takes the Object’s entry for
its key, else its default, else its zero, else it’s missing, and keys the type has no Field for
are left alone. Then
its
OnLoaded()runs, which can check what was loaded, or work out what wasn’t, and throw a ParseError. A Class whose Load fails before every Field is filled is let go of without its Destroy, which could see a Field with no zero that’s still null. A Class is saved as its own type’s Fields, whatever Class extends it, and an Abstract one can’t be loaded. A Func, an Interface’s object, a tuple, a fixed array, a Weak, a Ptr, a Task, and the other types like them can’t be converted. - Load also reads a number or a Bool from a String that reads as one, like “8080” or “true”, as a command line’s and an environment’s values are; an integer from a whole Double, and a Double from an integer; and a Bool from the integer 0 or 1, as SQLite and others keep one.
- std.core’s attributes on a Field change it:
@Rename("key")gives it another key,@Skipleaves it out (Load leaves it at its default), and@Renamed("old")reads an older key when the Object doesn’t have its own, so what an earlier version of a program saved still loads.@Flattenputs a Struct’s or Class’s Field’s own Fields in the Object with the others, not in an Object of their own; a Map or a Value, which Load and Save convert whole, can’t be flattened. A Field that @Skip leaves at its default has one, a type with a zero or a default of its own; two Fields can’t have one key, a @Flatten Field’s own Fields’ keys among them, and a Field’s old key is no other’s. - A type that converts itself implements std.core’s
Loadable, withStatic Method FromValue(value: Value): Self Throws ParseError, which Load calls in its place, orSaveable, withMethod ToValue(): Value, which Save does. OnLoaded()is a Method of a Struct or Class that takes and gives nothing, a ByRef Method in a Struct, and throws only ParseErrors. It isn’t generic, Static, or Async, and an Extend can’t add one. An Interface’s, which a type gets as a default, follows the same rules, but a Struct’s copy of it isn’t ByRef.- The built-in
Defaults<T>()gives a new Value of what a T’s Fields are when nothing sets them, as Save writes them: an Object of each Field at its default, else its zero, by its key, but without one that has neither, which Load would find missing. It’s made without its New, its OnLoaded, or a Class’s Destroy, so no code sees a T with a Field that has neither; a @Flatten Field’s own Fields are at theirs. A T? is its T, and a type without Fields of its own, like an Int or a T[], gives its zero’s Value, or a Null without one, as an Abstract Class, of which there’s no object, does; so generic code can ask it of any T that Save writes. It’s what a program shows as the defaults of what it loads, as std.os’s Args does in its help. Like Load and Save, an instance’s T is checked where it’s called, and a module’s own Defaults takes the name’s place. - Load throws a ParseError, of
WRONG_TYPEfor a Value of another kind,OUT_OF_RANGEfor a number too big for its type, andMISSINGfor a Field that the Object has no entry for, and that has no default or zero. ItsPathsays where in the Value it is, like"options.sizes[2]", or""for the Value itself, and its Message starts with it:options.sizes[2]: expected Int32, found "big". A Value alone read as a T[] has no index in it. A key that a path’s steps can’t tell apart from the rest, like one with a dot in it, is quoted in brackets:["a.b"]. Its Place is the Loadcall’s, and an error that FromValue or OnLoaded throws keeps its own Place and text, with the path to where it was thrown before its own, "" for the Value itself. - Load goes 256 levels deep into a Value’s Arrays and Objects, and throws a ParseError of
OUT_OF_RANGEpast that, and Save goes as deep into Fields and elements, and panics past that where it’s called, as for a Class that holds itself, whose Save would never end.
Embedding files
Const Help = EmbedText("help.txt") // a String known at compile time
Local logo = Embed("art/logo.png") // a new UInt8[] of the file's bytes
Local levels = EmbedFolder("levels") // a new Map<String, UInt8[]>
Local first = levels["one.txt"]Embed(path),EmbedText(path), andEmbedFolder(path)are built-ins that read files when the program is built, and put them in it. Embed gives a file’s bytes, as they are, in a newUInt8[]each time. EmbedText gives its text as a String known at compile time, which a Const can hold. EmbedFolder gives a newMap<String, UInt8[]>of a folder’s files, each in a newUInt8[], by its path in the folder, like"maps/one.txt", in path order, without hidden files and folders, whose names start with.. A program’s own Embed takes its place.- The path is a String known at compile time, from the folder of the file the call is in, with
/(or\) between folders. It stays in that file’s package’s folder, as an Import’s path does: the main file’s folder without a project.toml, and std/ for std’s own files. It can’t go through a link or a junction that leads out of it, or into another package’s folder inside it, and without a project.toml, it can’t start with a folder called std. A path that spells a name in other letter cases than the disk has it gets a warning, as an Import’s does, since only a disk that ignores them finds it. - It’s an error at the path when there’s no such file or folder, when it names a folder for Embed or EmbedText or a file for EmbedFolder, when EmbedFolder’s folder holds the program’s own code, which isn’t data (its own folder would bring the last build’s executable along), where that code really is, whatever letter case or link names the folder, when EmbedText’s file isn’t UTF-8, or when one call would put more than 64 MiB in the program: a file that large is better read when the program runs.
- EmbedFolder’s folder can’t hold a link or a junction, which some systems follow and others don’t, and which can lead back to the folder, so the program would differ by where it’s built; nor another package’s folder, whose files only its own code embeds.
- Each build reads the files again, as it does the program’s source files, each file once
however many embeds name it, and the program holds its bytes once.
fb build -odoesn’t write over one.
Conditional compilation
#If DEBUG
DrawDebugOverlay()
#ElseIf PLATFORM_WINDOWS
...
#Else
...
#End If- A directive must be the first thing on its line.
- Conditions combine symbols with
!,&&,||, and parentheses. A symbol is true when it is defined. - Predefined symbols:
DEBUG,RELEASE,PLATFORM_WINDOWS,PLATFORM_LINUX,PLATFORM_MACOS. A build definesDEBUGorRELEASE, asfb build --releasesays, and the platform it’s built on;fb check --releasechecks the release build’s code. A symbol that isn’t one of these gets a warning, since it’s never defined. - Code in an inactive branch is skipped by the compiler and does not need to parse. Directives
in it still count, so its
#Ifs and#End Ifs pair up. As anywhere, a line inside a block comment or a multi-line string is no directive, a block comment after a directive can go on past its line, and invalid UTF-8 is an error. - Each
#Ifneeds an#End If,#ElseIfand#Elsecome after an#If, and nothing comes after#Elsebut#End If.
Assertions
Assert(hp >= 0, "negative hp") // panics on failure, in debug and release builds alikeAn Assert is checked in every build, since the code after it counts on its condition: after
Assert(e != Null), e is used as non-null. See 09.
Report an issue with this page or with FutureBlitz on GitHub.