Skip to content

definitions

variables

In ghūl variables are introduced with the let keyword. Every local has an initializer, and the compiler infers the type from it:

ghul
let x = 10;

An explicit type can be given alongside the initializer. The initializer must be assignment compatible with the type:

ghul
let x: int = 42;

The explicit type can be wider than the initializer expression:

ghul
let o: object = "a string";

Multiple variables can be defined in the same let statement, with each variable either taking its type from its initializer or carrying an explicit one:

ghul
let
an_inferred_int = 123,
an_explicit_int: int = 456,
a_string = "hello";

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, lambda parameters, and for loop variables:

ghul
let _ = side_effect();
let (_, _, third) = (1, 2, 3);
let only_first = (x: int, _: int) => x;
for _ in 1..10 do
counter = counter + 1;
od

Variables may only be defined within functions, methods or property bodies. Variables names should be in snake_case

functions

In ghūl functions consist of a name and a parenthesized formal arguments list, followed by a return type, and then either a return expression or a function body:

ghul
sum_two_ints(i: int, j: int) -> int => i + j;
sum_three_ints(i: int, j: int, k: int) -> int is
return i + j + k;
si

=> 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.

ghul
do_something(what: string, why: string, to: int);

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:

ghul
class THING is
// class body
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:

ghul
let a_thing = THING();

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:

ghul
class POINT(x: int, y: int) is
si

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.

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:

ghul
struct POINT(x: double, y: double) is
si

Structs are constructed the same way as classes, with a constructor expression:

ghul
let origin = POINT(0.0D, 0.0D);
// or up, or down, or even left, depending on
// your co-ordinate system!
let right = POINT(1.0D, 0.0D);

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:

ghul
let zero_zero = POINT(0.0D, 0.0D);
assert origin == zero_zero;
assert origin != right;

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:

ghul
trait Printable is
print();
si

Traits are similar to interfaces in other languages. Trait methods and properties without a default implementation must be implemented by any class that inherits from the trait:

ghul
class BOOK(title: string, author: string): Printable is
print() is
write_line("Title: {title}, Author: {author}");
si
si

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:

ghul
trait Logged is
log(message: string) is
// the default body writes the message with a [log] prefix
write_line("[log] {message}");
si
si
class PLAIN(): Logged is
// no override - uses the trait default
si
class LOUD(): Logged is
// override the default, while still calling through to it with super
log(message: string) is
super.log(message.to_upper());
si
si
entry() is
PLAIN().log("hello");
LOUD().log("hello");
[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:

ghul
union Tree is
NODE(left: Tree, right: Tree);
LEAF(value: int);
si

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):

ghul
let tree: Tree = Tree.NODE(Tree.LEAF(123), Tree.LEAF(456));
let leaf = Tree.LEAF(123);
if isa Tree.NODE(tree) then
write_line("have tree node");
elif isa Tree.LEAF(tree) then
write_line("have tree leaf");
fi
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:

ghul
if isa Tree.NODE(tree) then
write_line(
"left {tree.left}, right {tree.right}"
);
elif isa Tree.LEAF(tree) then
write_line("leaf value {tree.value}");
fi

Unions support structural equality through the =~ operator. Two union references compare equal when they hold the same variant with member-wise equal fields:

ghul
let leaf1 = Tree.LEAF(123);
let leaf2 = Tree.LEAF(123);
let leaf3 = Tree.LEAF(456);
assert leaf1 =~ leaf2;
assert !(leaf1 =~ leaf3);

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

ghul
enum SUITS is
SPADES,
HEARTS,
DIAMONDS,
CLUBS
si

Enums can only be defined at global scope. Enums and their members should be named in MACRO_CASE

properties

A property consists of the property name followed by the property's type and, optionally, bodies for getter and setter methods.

ghul
class COUNTER is
count: int;
si
class Sized is
_size: int;
size: int => _size,
= new_size is
assert new_size > 0;
_size = new_size;
si
si

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 within 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.

ghul
class SCALER is
_scale: double;
scale(value: double) -> double => value * _scale;
si

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:

ghul
class COUNTER is
count: int;
init() is
count = 0;
si
init(initial_count: int) is
count = initial_count;
si
si
// calls the parameterless overload of init()
let c = COUNTER();
// calls init(initial_count: int)
let d = COUNTER(50);

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:

ghul
class PERSON(name: string, age: int) is
describe() is
write_line("{name} is {age} years old");
si
si
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; x is in scope only inside init.

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:

ghul
class POINT(x: int, y: int) is
// capture the primary parameters as renamed private fields
_x;
_y;
show() is
write_line("({_x}, {_y})");
si
si
class BOX(width: int public, height: int field, _depth: int) is
// width is a public read-write property
// height is a plain field
// _depth is a private field
si
(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:

ghul
class DOG(name: string, breed: string): ANIMAL is
// forward name to the superclass; the local DOG keeps breed as a field
super(name);
init(.., trick: string) is
// .. expands to (name, breed); the primary init has already run
write_line("{name} the {breed} can {trick}");
si
si
rex the labrador can sit

A class or struct with a primary header and no body declarations can end with a terminating ; instead of is ... si:

ghul
// a primary header with no body declarations:
class POINT(x: int, y: int);
// is equivalent to an explicit empty 'is' / 'si' body:
class VECTOR(dx: int, dy: int) 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.

ghul
namespace Example is
...
si

Namespaces may be nested inside other namespaces

ghul
namespace Outer is
namespace Inner is
do_something() is
IO.Std.write_line("did something");
si
si
si
Outer.Inner.do_something();
did something

A dotted namespace name is shorthand for nesting namespaces

ghul
namespace Outer.Inner is
do_something() is
IO.Std.write_line("did something");
si
si
Outer.Inner.do_something();
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:

ghul
namespace Example is
// this definition of Test is visible unqualified
// throughout the Example namespace:
trait Test is
run();
si
si

source-file-2.ghul:

ghul
// class TEST can implement the Test trait without having
// to qualify the name Test:
class TEST: Test is
run() is si
si

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:

ghul
// the compiler places this in an auto-generated
// namespace private to this source file
entry() is
IO.Std.write_line("Hello, world!");
si
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:

ghul
namespace Example is
entry() is
IO.Std.write_line("hello from a namespace");
si
si
greet() is
IO.Std.write_line("not in a namespace");
si
cannot mix global definitions and namespaces in the same file

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:

ghul
use Example.TEST;
...
let t = TEST();

use applied to a namespace imports all symbols from that namespace:

ghul
use Example; // imports Example.TEST and Example.Test
...
let t: Test;

Note that use only applies within the current namespace definition. It does not import a symbol into all instances of the current namespace:

ghul
namespace UseExample is
use Example;
class ANOTHER_TEST: Test is
run() is si
si
si
namespace UseExample is
// Test still needs qualification here
class YET_ANOTHER_TEST: Example.Test is
run() is si
si
si

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

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:

ghul
class PUBLIC is
si
public_function() -> int => 0;
public_property: int;
class _PRIVATE is
si
_private_function() -> int => 0;
_private_property: int;

methods

Methods are public by default. To make a method protected, prefix its name with an underscore _:

ghul
class THING is
do_something_public() is
si
_do_something_protected() is
si
si

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:

ghul
struct VALUE is
public_property: int;
_protected_property: string;
init(value: int) is
public_property = value;
_protected_property = "value is {value}";
si
si
let v = VALUE(1234);
// OK: public_property is publicly readable
write_line(v.public_property);
write_line(v._protected_property);
v.public_property = 5678;
_protected_property: string is not publicly readable
VALUE.public_property: Ghul.int is not publicly assignable

planned changes

Protected access will become private in a future release: derived types should not rely on reading or writing members with _ prefixed names