Spec chapter 02
Types, Declarations, Conversions
Built-in types
| Type | Notes |
|---|---|
Bool | True / False. Never implicitly converted to or from integers; b As Int is 1 or 0 |
Int8 Int16 Int32 Int64 | signed integers |
UInt8 UInt16 UInt32 UInt64 | unsigned integers |
CLong CULong CWChar | C’s long, unsigned long, and wchar_t, the target’s sizes, for Extern Functions (see below) |
Float32 Float64 | IEEE floats |
String | immutable UTF-8, refcounted |
Char | a Unicode scalar value: a code point from U+0000 to U+10FFFF that isn’t a surrogate, 32 bits, which a literal like 'a' makes; it compares by its code, and its zero is U+0000 |
CString | C’s const char*, for Extern Functions; Null, C’s NULL, is its zero (see 12-ffi.md) |
Void | “no value”; only a Ptr’s type, Ptr<Void>, which points at anything |
The math value types, Vec2 to Vec4, Vec2i, Vec3i, Quat, Mat3, Mat4, Color, and
Color32, aren’t built in: they’re std.math’s Structs, which a program imports (see
stdlib.md).
Aliases:
| Alias | Type |
|---|---|
Byte | UInt8 |
Int | Int32 |
Long | Int64 |
Float | Float32 |
Double | Float64 |
A program declares aliases of its own the same way, each another name for a type:
Public Alias MetersPerSecond = Quantity<1, 0, -1>
Alias Scores = Int[]- An alias is the type it names, everywhere: a value of one is a value of the other, and
messages show the type itself, as they show
Int32forInt.MetersPerSecond(3)makes the Struct or Class it names, that instance, andMetersPerSecond.Zero()calls its Static. - It’s declared at the top level, takes
Public,Package, orPrivatelike any declaration, and nothing else, and it isn’t generic, so it takes no type arguments. A Public one can’t name a type that code outside its module can’t see. - It can name types and aliases declared anywhere in what its module sees, later ones too, but
not itself:
Alias A = B[]withAlias B = Ais an error, “‘A’ names itself through ‘B’, so it’s no type”.
C’s integers are as big as the target’s C has them: on Windows, a CLong is an Int32, a
CULong a UInt32, and a CWChar a UInt16; elsewhere they’re an Int64, a UInt64, and an
Int32, or a UInt32 on AArch64 outside Apple’s targets. Each is a type of its own, which
converts without As only where it would on every target, so code that compiles on one
compiles on all: an Int32 or a UInt16 widens to a CLong, a UInt32 to a CULong, a
UInt8 or UInt16 to a CWChar, and a CLong or CWChar to an Int64, a CULong to a
UInt64, and a CWChar to a CLong or a Float64; anything else, an Int64 to a CLong
or a CLong to an Int32, needs As. A literal of one fits every target’s, so a CLong’s is
an Int32’s and a CWChar’s from 0 to 65535, and a CWChar isn’t negated, since it’s
unsigned on Windows. Otherwise they’re integers like any: arithmetic, comparisons, As, Hash,
and strings work on them as on the target’s, and a Const of one is worked out as the target
programs are built for has it, as is SizeOf. An Enum’s integer type is one of the others.
Composite and special types:
| Form | Meaning | See |
|---|---|---|
T? | nullable T | below |
T[] | dynamic array | 14 |
T[N], T[W, H] | fixed-size array | 14 |
(A, B) | tuple | 05 |
Func(A, B): R | function type | 05 |
Ptr<T> | raw pointer (unsafe/FFI) | 08 |
Weak<T> | weak reference | 08 |
Declarations
| Keyword | Meaning |
|---|---|
Local | mutable local variable |
Global | module-level variable |
Const | read-only value, computed at compile time whenever possible (13) |
ReadOnly | field that only its Class’s New can change; a global that never changes is a Const |
Field | member of a struct or class |
Const MAX_HP = 100
Const GRAVITY: Float = -9.81
Global score: Int = 0
Local name = "grunt" // inferred String
Local hp: Int // typed, assigned laterAnnotation rules
- Any variable (
Local,Global,Const,Field,Static Field) may omit its type when it has an initializer; the type is inferred from the initializer. - A variable without an initializer must have a type annotation.
- Parameters and return types are always annotated.
Scopes
- Functions,
Globals, and top-levelConsts are visible everywhere in their file, even before their declaration. Locals in top-level code belong to the top-level code only; functions can’t see them. Use aGlobalto share state.- A name can’t be declared twice in the same scope.
- A local can’t hide a local or parameter of an enclosing block in the same function. It may hide a file-level name.
Global initialization
Globals and top-level Consts are initialized in source order, before the rest of the
top-level code runs. An initializer can only use Consts and Globals declared above it. A
lambda in one runs later, so it can use one further down whose type is written. A function
that an initializer calls can reach a Global whose initializer hasn’t run yet, through any
call, so a debug build panics where it uses it: “‘late’ is used before its initializer has
run”. A release build panics there too when the Global’s zero would hold a null object, as a
Class, an Interface, an array, a function value, or a Struct or tuple holding one would, since
using it would crash; any other Global reads as its zero, a value of its type, as overflow
wraps. A Global whose value is known at compile time has it from the start.
When the program ends, once its coroutines and jobs are done, the Globals let go of what they hold, the last first, so a Destroy that runs then can use the Globals above its object’s. Each String, object, or array a Global let go of reads as its zero after, and a use of a Global already let go of panics where a use before its initializer would: “‘late’ is used after it’s released”.
A Global without an initializer starts at 0, 0.0, False, or "", like a field.
ThreadLocal Globals
ThreadLocal Global rolls = Random(42)
ThreadLocal Global seen: IntThreadLocal Global gives each thread a Global of its own, which is as fast as a Global, in the
CPU’s own thread-local storage.
- A thread’s starts at its zero, or at its initializer’s value when that’s known at compile time. The first time the thread uses it, reading, assigning, or passing it ByRef, its initializer runs on that thread, before the use. An assignment first still runs it, and lets go of what it made.
- The initializer runs on whichever thread uses the Global first, so it’s checked as work on another thread (10): it can’t use what only the main thread can, a mutable Global’s value among them. One that reaches its own Global, through what it calls, panics there: “‘depth’ is used before its initializer has run, since its initializer reaches it”. An error it lets out ends the program, as top-level code’s does. While it runs on a pool worker, that worker’s waits run only the jobs it starts, not older ones queued there, which another worker runs, with its own.
- A thread lets go of its ThreadLocal Globals as it ends, the last made first: a thread of its own after its work, before Join returns; a pool worker’s when the pool stops, as the program ends; and the main thread’s once the top-level code and the jobs are done, before the Globals are released, so a ThreadLocal’s Destroy can use them. A use of one already let go of panics: “‘later’ is used after it’s released, since a thread releases its ThreadLocal Globals as it ends”. A thread C started lets go of its own as it exits. A job a Global’s Destroy starts runs on a pool worker, with that worker’s own, never on the main thread once it’s let go of its own.
- Code on any thread can use a ThreadLocal Global, which is its own, and its type needn’t be Sendable, since its value stays on its thread. It can’t be an Atomic or a Mutex, which are for what threads share. Only a top-level Global is ThreadLocal: not a Local, a Const, or a Static Field.
Definite assignment
Reading a Local before it has been assigned on every path is a compile error. (Globals
always have a value; see above.)
Local hp: Int // ✓ type given, no value yet
Local hp // ✗ error: needs a type or a value
Print(hp) // ✗ error: hp read before assignmentField defaults
- Primitive fields default to
0,0.0,False, or""when no initializer is given, and nullable ones toNull. - A field with no zero, a non-nullable Class or a Struct holding one, must be assigned by every
constructor (compile-checked): a Struct’s generated constructor requires it, and a Class with
one needs a
New, which assigns it on every path before reading it or usingSelf. - A field’s default must be known at compile time, like a parameter’s, or be
[], which gives each one a new empty array.
Nullability
- All types are non-null by default.
T?may holdNull. The?touches the type name (see 03).- After a null check, the compiler narrows
T?toT(flow typing). - A
T?of aT?is a type of its own: anInt??isNull, or holds anInt?, which can beNullitself. Generic code makes them, like aMap<String, Int?>’sGet, whoseNullis a missing key and whoseInt?is a key’s value,Nullor not, and they can be written too.
Local target: Enemy? = Null
Local hp = target?.hp ?? 0
If target != Null Then target.Hurt(5) // target is Enemy hereNulltakes its type from where it’s used:Local e: Enemy? = Nullworks, andLocal e = Nulldoesn’t. ATconverts to aT?withoutAs. AT?converts to aT??as a value, so itsNullis held, not theT??’s own, and aT??needs a check to be aT?.If,ElseIf,While,? :,&&,||,!, an earlyReturn, and assigning a value that can’t beNullall narrow. Narrowing applies to Locals, Consts, and parameters that aren’tByRef, and loop variables, and ends when the variable is assigned aT?or passedByRef. A loop that changes the variable can’t rely on a check from before it. Each check takes off oneT?: aT??narrows to itsT?, which a Local copy of it can narrow again.- A Global or a field can change during any call, so it isn’t narrowed: check a Local copy.
a?.bisNullwhenais, and aB?otherwise;a?.M()only callsMwhenaholds a value. Each?.is one check, so a chain isa?.b?.c.a ?? bgives the value ina, orbwhenaisNull;bonly runs then. For aT??it gives aT?, as?.takes off oneT?too. Comparing aT??withNullasks about the outer one.- Every
T?has two Methods of its own, which work on it whether it holds a value or not:x.Expect(message)gives the value, or panics with the message where it’s called, andx.OrElse(make)gives the value, or whatmake, aFunc(): T, gives, called only whenxisNull. - Two
T?values are equal when both areNullor both hold equal values. AT?and aTcompare the same way, theTas aT?holding it, withT’sOperator ==if it has one. - A
T?can’t go in a string; give a value forNullfirst:"{name ?? "nobody"}". ?.,??, or comparing withNullon something that can’t beNullis a warning.
Conversions
- Only lossless conversions happen implicitly (e.g.
Int32→Int64,Int32→Float64,Float32→Float64,T→T?). - A
Stringconverts to std.io’sPathwherever one is expected, and to aPath?: a call makes the Path with its New, as the program runs, soFile.ReadText("save.dat")reads naturally. It’s a conversion in overloads, so one that takes the String exactly wins. AString?doesn’t convert, since its Null isn’t a path, and neither does aString[]to aPath[], though an array literal’s elements do. A Path isn’t known at compile time, so a String isn’t a Path parameter’s default. No other type gets a conversion of its own. - Everything else requires an explicit cast:
x As Float. As?is a checked downcast that yieldsNullon failure (see 03).Boolnever converts implicitly.Asturns it into an integer:True As Intis1andFalse As Intis0. Numbers don’t convert toBool; compare instead (n != 0).- A
Charisn’t a number and never converts implicitly.Asturns it into an integer, its code, which keeps its low bits as any integer’s do:'a' As Intis97.Asturns an integer into aCharwhen it’s a Unicode scalar value,98 As Charis'b', and another, like0xD800or-1, is an error at compile time and a panic at run time.
Literals
- An integer literal converts to any numeric type that holds its value exactly:
Vec3(0, 1, 0)andLocal d: Double = 1work,Local b: Int8 = 300doesn’t. - A decimal literal converts to
FloatorDoubleonly. - A cast gives a literal its type:
5_000_000_000 As Long. - With no expected type, literals made only of integers are
Int, and literals with a decimal among them areFloat:1 + 0.5is aFloat. A literal too big forIntneeds an expected type or a cast. - Literals before a member, like
0.1.Secondsor5_000_000_000.ToString(), take the first type, from their default up, that holds them and has the member (see 01).
Casts between numbers
Asbetween integer types keeps the low bits (it wraps):300 As UInt8is44.Asfrom a float to an integer drops the fraction (toward zero). A value outside the integer’s range is an error at compile time and a panic at run time.Asto a float rounds to the nearest value it can hold.
Mixed numeric types
In arithmetic and comparisons between two numeric types, one side must convert to the other’s
type losslessly, and the result has that type: Int + Long is Long, Int + Double is
Double. Anything else is an error that needs a cast: Int + Float. A literal takes the other
side’s type when its value fits.
Integer overflow
- Debug builds: overflow traps (panic with location).
- Release builds: overflow wraps.
- A shift amount outside 0 to the width minus 1 panics in debug builds and is masked to that range in release builds.
- Integer division or remainder by zero, and a float
Asan integer that doesn’t fit, panic in every build. - Explicit intent is always available, as an integer’s Methods:
a.WrappingAdd(b),WrappingSubtract,WrappingMultiply, andWrappingNegate()wrap around in every build, anda.CheckedAdd(b),CheckedSubtract,CheckedMultiply,CheckedDivide, andCheckedNegate()give aT?that’s Null when the operation would overflow or divide by zero. With known values, they’re worked out at compile time, and a Const Function can use them.
Report an issue with this page or with FutureBlitz on GitHub.