definitions
variables
In ghūl local variables are introduced with the let keyword. A bare let is immutable: it takes an initializer, and reassigning the variable afterwards is rejected. The compiler infers the type from the initializer:
An explicit type can be given alongside the initializer. The initializer must be assignment compatible with the type:
The explicit type can be wider than the initializer expression:
A trailing mut makes the variable reassignable, and then the initializer can be dropped for a deferred-init local that starts at its type's default value: let total mut = 0 reassigns later, and let result: int mut; defaults to 0. Either form can also take its value from default, which initializes to the default of the type the context expects - let x = default, or default[T] to pin the type.
Multiple variables can be defined in the same let statement, with each variable either taking its type from its initializer or given an explicit one:
The name _ is a discard placeholder. It can stand in for any variable name, but the value that would be assigned to it is discarded. _ is accepted in let definitions, tuple destructuring, anonymous function parameters, and for loop variables:
Variables can only be defined within functions, methods or property bodies. Variable names should be in snake_case
functions
In ghūl functions consist of a name and a parenthesized formal arguments list, followed by an optional return type after -> (omitting it makes the function void), and then either a return expression or a function body:
=> introduces a single-expression body, while the is and si keywords are used to delimit block bodies.
Functions can only be defined at global scope. Functions can be generic, which will be covered later. Function names should be in snake_case
arguments
Arguments consist of a name followed by a type. The type is mandatory as the compiler cannot infer types here.
types
classes
Classes consist of a name optionally followed by a superclass name and the types of any traits implemented, and then the class body. The class body is delimited by keywords is and si:
A class defines a new reference type, instances of which are assignment compatible with its superclass type and any traits it implements.
Instances of classes are created via a constructor expression, which consists of a type expression followed by a parenthesis delimited list of actual constructor arguments. For a class, the type expression is the class name, qualified with any namespaces if needed:
A class can also declare its constructor parameters directly in the header. Each parameter becomes a parameter of the synthesised constructor, and an auto-generated same-named field or property holds the supplied value:
The two forms are equivalent. The primary form is the shorter shape when every field is initialized from a constructor argument; the classic form is the better fit when the body owns extra fields or properties beyond what the constructor takes. See constructors for the rest of the primary-constructor surface area.
Two postfix modifiers shape the hierarchy. Without open, a class can be subclassed only within the assembly that declares it; open opts in to cross-assembly subclassing. abstract bars direct construction, so only subclasses exist at runtime, and a class is implicitly abstract when it declares a body-less instance method, since that method is a contract for subclasses to satisfy. The closure feeds type narrowing: on the else edge of an isa test the compiler can rule the tested subclass out, and an abstract root can leave a single remaining subclass.
Classes can only be defined at global scope. Classes can be generic, which will be covered later. Concrete class names should be in MACRO_CASE. Abstract class names should be in PascalCase.
structs
Structs consist of a name, then the types of any traits implemented, and then the struct body again enclosed in is / si. A struct can also use the primary-constructor header form:
Structs are constructed the same way as classes, with a constructor expression:
A struct defines a new value type, which means any values that the struct encapsulates are collected together as a new kind of value: copying the struct involves copying all the encapsulated values and the built in equality operator == performs a memberwise equality check:
Structs can only be defined at global scope. Structs can be generic, which will be covered later. Struct names should be in MACRO_CASE.
traits
A trait consists of a name, the types of any parent traits that must also be implemented, and then the trait body:
Traits are similar to interfaces in other languages. Trait methods and properties without a default implementation must be implemented by any class, struct, or union that declares the trait:
A trait method or property can provide a default body. Implementing classes inherit the default and only need to override it to change the behaviour:
[log] hello [log] HELLO
A class override can call the trait's default with super.method().
Traits can only be defined at global scope. Trait methods and properties can be abstract or have a default implementation. Trait names should be in PascalCase.
unions
A union consists of a name and then a union body, which contains one or more variants. Each variant has a name, and then an optional list of fields:
Unions are a reference type. A reference of union type can point to only one variant at a time. To discover which variant a union currently holds, test it with isa Variant(value):
have tree node
isa Variant(value) does two things at once: it tests the variant, and within the then-branch it narrows the value to that variant, so the variant's own fields are accessible directly:
Unions support structural equality through the =~ operator. Two union references compare equal when they hold the same variant with member-wise equal fields:
A variant with no fields is a unit variant: it is referenced by name without parentheses, and interned to one shared value per generic instantiation. A union with a single field-carrying variant, or one variant marked default, is option-shaped, so u? tests whether that variant is present and u! unwraps its value:
A union can declare a primary-constructor header for state shared across every variant. Each variant splices the shared parameters into its field list with .., and a variant with no extra fields drops the list entirely. A union can also implement traits after its header, with each trait member satisfied by a default or by a property the union supplies:
identifier [identifier]
Unions can only be defined at global scope. Union names should be in PascalCase and variant names should be in MACRO_CASE
enums
An enum consists of a name and then an enum body, which contains one or more elements. Each element has a name and an optional constant integer value
Enums can only be defined at global scope. An enum type name should be in PascalCase, and its members in MACRO_CASE
partial and impl blocks
Members can be added to a class, struct, or union already declared in the same assembly from a separate block, even in another file. The added members are ordinary members of the target, with the same private access and virtual dispatch as members written in its own body. A partial block names the type and adds to it; for a union, whose body holds only variants, it is the only way to give the type methods:
square s=4
An impl Trait for Type block additionally makes the target implement a trait, so a type can satisfy a trait without naming it in its header. The trait's type arguments are written on the target after for, and inside the body self has the concrete target type, so a union's variants can be matched directly:
1 2 nil
The target can be a qualified name, including a single union variant (impl Printer for List.NIL). The interface must be a trait, and the target a type declared in the same assembly; an imported type cannot be reopened.
properties
A property consists of the property name followed by the property's type and, optionally, bodies for getter and setter methods.
Public properties with no getter or setter are automatically backed by a hidden field. Private properties with no getter or setter are implemented as a plain field.
Properties can be defined globally and within classes, structs and traits. Property names should be in snake_case.
methods
Methods are syntactically the same as functions, except they are defined within classes, structs or traits.
A method or function can take a postfix pure modifier, which asserts that it only reads and never writes to the heap. The compiler already infers this for many functions; the modifier states it for one the compiler can't prove, and then callers keep type narrowing facts across a call to it. Every override of a pure member must itself be pure:
42
As with functions, methods should be named in snake_case
constructors
In ghūl methods named init are constructors. When an object is constructed using a constructor expression, the corresponding init method overload will be called based on the actual argument types:
Constructors can be defined in classes and structs.
primary constructors
When the constructor only assigns its arguments to same-named fields, the class or struct header can declare those parameters directly. The compiler synthesises the matching init and a same-named field or property for each parameter:
alice is 30 years old
A trailing modifier on a primary parameter overrides the default visibility:
x: int public- public read and write.x: int field- plain field rather than the default auto-property._x: int- private field, named_x.x: int init- no field is generated;xis in scope only insideinit.
A body field or property declaration with a name matching the parameter (under the same _x/x rule) overrides auto-generation and receives the auto-init copy. This is also how to rename the underlying storage without using the modifier suffix:
(10, 20)
A class with a primary header can also include a super(...) body declaration that forwards expressions to its superclass init, and secondary init(.., extras) overloads. The .. splice expands to the primary parameters; an implicit chain to the primary init runs before the secondary's body:
rex the labrador can sit
A primary-constructor class or struct also gets a synthesised deconstruct built from its public-readable parameters, so let (x, y) = POINT(3, 4) destructures without writing one out.
A class or struct with a primary header and no body declarations can end with a terminating ; instead of is ... si:
The classic form is the better fit when the body owns extra fields or properties beyond what the primary parameters cover.
namespaces
Namespaces are introduced with the namespace keyword followed by the namespace name and then the namespace body.
Namespaces can be nested inside other namespaces
did something
A dotted namespace name is shorthand for nesting namespaces
did something
namespace aggregation
A namespace definition is an instance of that namespace. Namespace instances are aggregated across all source files to form a single namespace scope. This means that all definitions within a namespace instance are visible unqualified within all other instances of that namespace in all source files:
source-file-1.ghul:
source-file-2.ghul:
definitions outside any namespace
If a source file contains no namespaces, then all definitions in the file are placed in a compiler generated namespace that is private to that source file. This is useful for examples and tests:
Hello, world!
For definitions to be visible from other files, they must be placed in an explicitly declared namespace.
namespace usage consistency
If a source file contains any explicitly declared namespaces, then all definitions in that file must be within a namespace. Bare definitions outside of namespaces are not allowed in files with namespace declarations:
importing symbols with use
Symbols can be brought into the current namespace instance's scope using the use keyword. Imported symbols can then be used without qualification:
use applied to a namespace imports all symbols from that namespace:
Note that use only applies within the current namespace definition. It does not import a symbol into all instances of the current namespace:
visibility of symbols
In ghūl, the visibility of symbols outside their defining scope is managed by a naming convention which is partially enforced by the compiler. The compiler also warns when a declaration's name doesn't match the convention for its kind - non-snake-case-name, non-pascal-case-name, or non-upper-snake-case-name - each suppressible per declaration, per file, or project-wide. A class with only static members is a utility container that is never constructed, and accepts either PascalCase or MACRO_CASE.
global symbols
Classes, structs, traits, unions, global functions and global properties are accessible from any namespace. Prefixing their names with _ indicates they are intended to be private, but this is not enforced by the compiler:
methods
Methods are public by default. To make a method protected, prefix its name with an underscore _:
Protected access to methods is enforced by the compiler
properties
Properties are public read, protected write, unless they start with _, in which case they are protected read and write:
planned changes
Protected access will become private in a future release: derived types should not rely on reading or writing members with _ prefixed names