ghūl (pronounced “ghoul” and always styled with a lower case 'g') is a statically-typed, general-purpose programming language that runs on the .NET platform. Created as an experimental project by a single developer, ghūl’s design philosophy is to explore language design while remaining fairly conventional in capability. It strives to be expressive enough for real-world development – in fact, the ghūl compiler itself is written in ghūl. Key goals include strong compile-time type safety, integration of functional and object-oriented programming paradigms, and seamless interop with existing .NET libraries.
ghūl’s syntax is distinctive yet familiar. It is influenced by Algol 68 and Pascal, using keywords to delimit code blocks instead of braces or significant indentation. Many keywords come in pairs with a closing keyword that mirrors the opening keyword. For example, a function or class body begins with is and ends with si (the reverse of “is”). Similarly, conditional blocks use if ... fi instead of { }. This leads to a clear structure without curly braces. Naming conventions also differ from C# – ghūl uses snake_case for variable, function, and property names; PascalCase for namespace and trait names; and MACRO_CASE for concrete type names like classes, structs, and enums.
Despite some quirky syntax, experienced .NET developers will find many familiar concepts. ghūl supports anonymous functions with closures, classes with inheritance and polymorphism, interfaces (called traits), generics, structured error handling with exceptions, and a rich type system. The language targets .NET 8, producing normal .NET assemblies and NuGet packages, so you can use .NET’s framework types and libraries. .NET classes, interfaces, and collections are directly usable in ghūl (with slight naming adjustments to fit ghūl conventions). For instance, System.Console appears as IO.Std in ghūl, so you can call IO.Std.write_line(...) to print to the console. Overall, ghūl’s goal is to blend functional and OO features in a concise syntax, while leveraging the .NET ecosystem.
ghūl organizes code into namespaces (analogous to C# namespaces or modules). Use the namespace keyword to declare a namespace, followed by the namespace name and an is ... si block containing its definitions. For example:
namespace MyApp.Utilities is
// definitions (types, functions, etc.) go here
class HELPER is
...
si
function_x() => ...
si
Namespaces may be nested (you can define a namespace Inner is ... si inside another) or declared in a dotted form (e.g. namespace Outer.Inner is ... si achieves the same nesting). All definitions inside the namespace belong to that namespace, and you refer to them with qualified names (e.g. Outer.Inner.function_x()).
Multiple source files can contribute to the same namespace. ghūl treats each namespace Name is ... si block as an instance of that namespace; all such instances across your project are aggregated into one combined namespace scope. This means that if two files declare namespace DataModels is ... si, types defined in one file’s DataModels are visible in the other without qualification. (If a file has no namespace declarations at all, the compiler wraps its contents in an implicit, file-private namespace. However, in multi-file projects you’ll typically use explicit namespaces so code can be shared.)
To import names from a namespace, ghūl provides a use statement. use brings a symbol or an entire namespace into scope so you can refer to it without qualification. For example:
use MyApp.Utilities.HELPER; // import a specific class
use IO.Std; // import only the Std type (System.Console) as 'Std'
use IO.Std.write_line; // import the static 'write_line' method for unqualified use
use IO; // import everything in the IO namespace (including Std)
use LetsCallItConsole = IO.Std; // import Std but rename it to LetsCallItConsole
use IO.Std;imports only theStdtype, so you can useStd.write_line(...).use IO.Std.write_line;imports the staticwrite_linemethod, so you can callwrite_line(...)directly.use IO;imports all public symbols from theIOnamespace, includingStd.use LetsCallItConsole = IO.Std;importsStdasLetsCallItConsole.
After use IO.Std.write_line;, you can call write_line("Hello") directly instead of IO.Std.write_line or Std.write_line. Using use NamespaceName; imports all public symbols from that namespace. Using use Namespace.Symbol; imports a specific item. This mechanism is analogous to C#’s using directive or F#’s open. Importantly, a use applies only within the current namespace block/file – it does not globally modify other files or other namespace sections in the same file. If you split a namespace across multiple blocks, each block needs its own use statements for any external symbols it relies on.
ghūl is strongly typed and offers several ways to define your own types: classes, structs, traits, unions, and enums. All these types are declared at the global scope (inside a namespace, but not inside other types). Here’s an overview of each:
-
Classes: A class defines a reference type with support for single inheritance and trait implementation. The syntax is
class Name [ : Superclass, Trait1, Trait2, ... ] is ... si. The class body can declare properties, methods, and constructors. For example:class PERSON is name: string; age: int; init(name: string, age: int) is // constructor self.name = name; self.age = age; si describe() -> string => "{name} is {age} years old"; siIn this example,
PERSONhas two fields and a constructorinitthat initializes them. Within methods,selfrefers to the instance (likethisin C#). A class can optionally extend a single superclass and implement any number of traits (interfaces). Instances of a class are created by calling a constructor expression: writing the class name like a function call. For instance,let p = PERSON("Alice", 30);calls the matchinginitconstructor to produce aPERSONobject. By default, class instances are compared by reference (identity) unless you override equality. Classes are named inMACRO_CASEif concrete, orPascalCaseif abstract. -
Structs: A struct defines a value type (like .NET struct) which holds its data directly. Struct syntax is similar to classes:
struct NAME [ : Trait1, Trait2, ... ] is ... si. Structs cannot inherit from a class (they have no superclass), but they can implement traits. For example:struct POINT is x: double; y: double; init(x: double, y: double) is // struct constructor self.x = x; self.y = y; si siEach
POINTvalue contains its ownxandy. Struct instances are constructed the same way as classes (e.g.let origin = POINT(0.0D, 0.0D);). Unlike classes, copying a struct copies all its fields (no pointers to a single heap object) and the==operator on structs performs memberwise equality by default. In the example above, twoPOINT(0.0, 0.0)instances would be==because their contents match. Structs are useful for lightweight data records. Like classes, they can define methods and multiple constructors (initoverloads), and are written inMACRO_CASE. -
Traits: Traits in ghūl are analogous to interfaces in other languages. A trait defines a set of methods and/or properties that can be implemented by classes or structs. Trait syntax:
trait Name [ : ParentTrait1, ... ] is ... si. Inside a trait, you declare method signatures or properties without bodies (these are implicitly abstract). For example:trait Printable is print(); // abstract method (no body) siThis trait requires a
print()method. Any class or struct that inherits from (implements)Printablemust provide a concreteprintmethod. Traits support trait inheritance as well (one trait can extend another, requiring all parent trait members to be implemented too). Trait names usePascalCase. In ghūl, a class can extend at most one class but implement multiple traits, enabling a form of multiple inheritance of interfaces. Traits are purely abstract; their methods have no implementations (current ghūl requires you to leave trait method bodies empty). Example implementation:class BOOK: Printable is title: string; author: string; init(title: string, author: string) is self.title = title; self.author = author; si print() is // implementing Printable.print() IO.Std.write_line("Title: {title}, Author: {author}"); si siHere
BOOKimplementsPrintableby providing aprint()method. AnyBOOKcan be used where aPrintableis expected. (Traits from .NET assemblies are mapped to ghūl traits as well. For example, a .NET interfaceIDisposablewould appear as traitDisposablein ghūl.) -
Unions: A union type (discriminated union or sum type) represents a choice between multiple variants, each of which may carry different data. The syntax is
union Name is Variant1(Type1, ...); Variant2(Type2, ...); ... si. Each variant has a name (by convention in MACRO_CASE) and an optional payload. For example:union Result is SUCCESS(value: string); ERROR(code: int, message: string); siThis defines a
Resulttype that can be eitherSUCCESSwith astringvalue, orERRORwith anintcode andstringmessage. At any given time, aResultinstance is in one of these variants. To construct a union, you qualify the variant with the union name, e.g.Result.SUCCESS("ok")orResult.ERROR(404, "Not found"). Under the hood, a union is a reference type (like a class) but with a tag indicating the active variant. ghūl automatically provides tag check properties – boolean properties named likeis_variantname– to test which variant is active. For instance, givenlet r = Result.ERROR(404, "Not found"),r.is_errorwould be true andr.is_successfalse. After checking a variant, you can access its payload via a similarly named property: for example,r.error.codeandr.error.messagegive the fields of theERRORvariant. (If a variant has exactly one field, accessingr.variantreturns that field directly. Variants with no fields are just constants – you only check the tag.) To illustrate usage:let outcome = Result.SUCCESS("All good"); if outcome.is_success then IO.Std.write_line("Yay: {outcome.success}"); // prints the success value elif outcome.is_error then IO.Std.write_line("Error {outcome.error.code}: {outcome.error.message}"); fiUnions enable algebraic data types similar to F# discriminated unions or Rust enums with data. ghūl doesn’t yet support pattern-matching syntax on unions (see Pattern Matching below), so code uses
if/eliforcasechecks on the tag properties as shown. Union type names usePascalCase, and variant names useMACRO_CASE.Common use case: A generic
Option[T]union is provided (or can be user-defined) to represent optional values. For example, one could define:union Option[T] is SOME(value: T); NONE; siThen
Option.SOME[int](42)represents anintpresent, andOption.NONErepresents no value. You would checkopt.is_someoropt.is_nonebefore usingopt.some. This pattern is analogous toSystem.Nullable<T>orOptionin F#, but works for any type and avoids nulls in APIs. -
Enums: An enum is a simpler kind of tagged type for constants. Enums in ghūl are similar to C-style enums: they define a fixed set of named constants of an integral type. Syntax:
enum NAME is MEMBER1, MEMBER2, ... si. Optionally you can assign specific integer values to each member. For example:enum SUIT is HEARTS, DIAMONDS, CLUBS, SPADES sidefines an enum
SUITwith four values. By default, the first member is 0, next is 1, etc., unless you explicitly assign values. Enum members are accessed asSUIT.HEARTS, etc., and can be cast to or from their underlying int value. Enums and their members should be named inMACRO_CASE. They are primarily useful for simple state flags or options. (Under the hood, .NET enums appear similarly in ghūl, just following the naming convention.)
Properties and Methods: Classes, structs, and traits can contain properties and methods in their bodies. A property is declared like name: Type and can optionally include a getter and/or setter implementation. For example, a property with custom setter:
class COUNTER is
_count: int; // backing field
count: int => _count, // getter returns _count
= new_value is // setter (runs on assignment)
assert new_value >= 0 else "Count must be non-negative";
_count = new_value;
si
si
Here count is a property of type int. The syntax count: int => _count defines a getter that returns _count, and the = new_value is ... si part defines a setter that performs an assertion and then sets the backing field. If no getter/setter is provided, ghūl will auto-generate one: a public property with no explicit getter/setter gets a hidden backing field and default getter (and if not marked protected, no public setter). By default, properties in ghūl are readable publicly but writable only within the defining type (protected set) – unless marked with a leading underscore _ to indicate intended privacy. Methods are declared just like free functions but inside a class/struct/trait. They should be in snake_case and can use self to access the instance. For example, print() in the BOOK class above is a method implementation. Constructors are simply methods named init – when you call TypeName(...), the appropriate init overload is invoked to construct the object.
Function declarations in ghūl start with an optional return type, the function name, and a parenthesized parameter list. After that, you provide either a single-expression body introduced by =>, or a block body enclosed by is ... si. For example:
// A function with a single-expression body:
add(a: int, b: int) -> int => a + b;
// A function with a block body:
multiply(a: int, b: int) -> int is
let result = a * b;
return result;
si
In the first case, the expression a + b is returned as the result of add. In the second, we use an explicit return. ghūl requires an explicit return statement in block bodies to yield a value (unless the function returns void). If a non-void function exits without hitting any return, it will return the default value of its return type (e.g. 0 for int, null for reference types). You can also omit the return type if the function returns void (unit type) – for instance, log_message(msg: string) is ... defaults to void return. All function parameters must have an explicitly declared type (the compiler does not infer types for parameters). Function names use snake_case.
ghūl functions are first-class citizens: you can create function values on the fly and pass them around. The function literal syntax uses => as well. For example, (x: int) => x * 2 evaluates to a function that doubles an integer. You can assign it to a variable (let f = (i: int) => i * 2;) and call f(10) which would yield 20. Functions can be stored in data structures or passed as arguments to other functions just like any other value. In a function type, the notation A -> B is used to denote a function from type A to type B. For instance, int -> int is the type of a function taking an int and returning an int. Below is a quick example of higher-order functions:
let twice = (x: int) => x * 2;
let apply_twice(f: int -> int, i: int) => f(f(i));
apply_twice(twice, 5); // returns 20
Here, twice is a function literal capturing no external state, and apply_twice accepts a function f and applies it twice to an integer. ghūl supports closures (function literals can capture variables from their surrounding scope). Recursive and mutually recursive functions are also supported – for named functions, recursion works as usual; for anonymous functions, ghūl provides a special rec keyword to refer to the function from inside itself (see the factorial example below).
Example – recursion in an anonymous function:
// Factorial using an anonymous recursive function:
let factorial = (n: int) rec => if n == 0 then 1 else n * rec(n - 1) fi;
IO.Std.write_line("5! = {factorial(5)}"); // outputs "5! = 120"
In this example, (n: int) rec => ... creates a function that can call itself by using rec(...) as though it were its own name.
Unlike F# or Rust, ghūl does not yet have a full pattern-matching expression for deconstructing data (this feature is planned but marked TODO). In current ghūl, you typically use explicit conditionals or a case statement to distinguish union variants or other conditions. We saw an example above using if outcome.is_success / is_error to handle a Result union. ghūl also provides a case construct (similar to a switch) for matching constant values:
case status_code
when 200:
IO.Std.write_line("OK");
when 404:
IO.Std.write_line("Not Found");
when 500, 501, 502:
IO.Std.write_line("Server error");
default:
IO.Std.write_line("Unknown status");
esac
Each when label can list one or multiple literal values (comma-separated) to compare against the case expression. The default clause handles any value not matched by earlier cases. You terminate the construct with esac (“case” reversed). The case statement is useful for matching primitive values like numbers or enum constants. However, it cannot deconstruct union types – you cannot directly pattern-match a Result.SUCCESS(...) vs Result.ERROR(...) in a case. Instead, you would either use the if/elif style shown earlier or switch on an auxiliary tag (for example, you might switch on an enum inside your union). In summary, until pattern matching is added to the language, handling variant types involves manual tag checks. This is one area where ghūl’s syntax is still evolving.
ghūl supports standard control flow constructs, often using keyword pairs to delimit blocks. All the familiar loops and conditionals are available, with a few syntactic twists:
-
Conditional (
if) Statements: Theif...then...fistatement works much like in other languages, but uses keywords instead of braces. You can optionally includeelif(else-if) clauses and a finalelse. For example:if x > 0 then IO.Std.write_line("x is positive"); elif x < 0 then IO.Std.write_line("x is negative"); else IO.Std.write_line("x is zero"); fiEach
if/elif/elseblock is a new scope for local variables. You can also use anifas an expression that yields a value – for instance:let sign = if x >= 0 then "non-negative" else "negative" fi;will setsignbased on the condition. (When used as an expression, all branches must produce a value of a compatible type.) -
Loops: ghūl provides a few looping constructs:
-
whileloop: Syntax:while condition do ... od. This loops as long as the boolean condition remains true (checked before each iteration). -
forloop: ghūl’sforis used to iterate over collections or ranges. Syntax:for item in collection do ... od. If the expression afterinis a range or an iterable, the loop will iterate over it. For example:for i in 1::5 do // i takes values 1,2,3,4,5 IO.Std.write_line("Number {i}"); odThe range operator
::produces an inclusive range (here 1 through 5). ghūl also has a..range operator which is inclusive of the start and exclusive of the end. So0..3would generate 0,1,2. You can iterate over any object that implements the enumerable pattern; for example, a list (IReadOnlyList) or array can be used infor. If iterating a key-value map, you’d get key/value tuples, etc., similar to C#’s foreach. -
doloop: Syntax:do ... od. This is a loop with no explicit condition – essentially an infinite loop, intended to be controlled viabreak/continueinside. You usedo/odwhen you want a manual loop that you break out of under certain conditions (similar towhile(true)in other languages). For example:do update_state(); if state.is_finished then break fi; odEach of these loops (
while,for,do) forms its own block scope, so variables defined inside the loop are local to the loop.
All loops support the
breakstatement to exit the loop immediately, andcontinueto skip to the next iteration. ghūl’sbreak/continuework analogously to those in C# or Java. -
-
try/catchException Handling: ghūl handles exceptions in a familiar way, with some syntactic differences. A try-block begins withtryand ends withyrt(“try” reversed). Between them, you can have one or morecatchclauses and optionally afinally. For example:try // code that might throw risky_operation(); catch ex: SomeExceptionType IO.Std.write_line("Failed: " + ex.message); finally cleanup(); yrtEach
catchclause names an exception variable (exabove) and a type to catch. Catches are checked in order, and a catch will handle any exception of the specified type or its subtypes (just like in .NET). Thefinallyblock (if present) always executes after the try and any catches, typically for resource cleanup. You can omitfinallyif not needed, or have multiplecatchblocks for different exceptions. The main difference from C# is just the delimiter keywords: end the whole construct withyrtinstead of a closing brace. -
assertstatements: ghūl includes a built-inassertfor sanity checks. An assert statement looks likeassert condition else expression;. If the condition is true, nothing happens; if it's false, the program throws an exception. If theelseexpression is a string, it will be wrapped in anAssertionFailedExceptionautomatically. If it’s already an exception object, that is thrown as-is. Example:assert index < array.count else "Index out of range";This will throw an
AssertionFailedExceptionwith the given message if the condition fails. Assertions are mainly for internal invariants (they are always checked at runtime, since ghūl doesn’t have a static analyzer for them). -
returnstatements: As shown earlier, usereturn value;to return a value from a function with a block body, or justreturn;to return from a void function. If a function has a non-void return type and you fall off the end without a return, ghūl implicitly returns the default value (e.g. null, zero). It’s good practice to return explicitly for clarity. Also note, ghūl does not have a separate yield statement – generators are typically implemented via lazy sequences (more on that in Data Structures).
ghūl leverages .NET’s collection types and also has built-in literals for common data structures:
-
Lists and Arrays: In ghūl, square brackets
[...]represent an array/list literal. For example,[1, 2, 3]produces a collection of those three integers. The element type is inferred to the most specific type that accommodates all elements. If you mix types, the common base type (oftenobject) is used. The literal[1, 2, 3, 4, 5]would be of typeList[int](read “list of int”). Internally this is a .NET arrayInt32[], but ghūl treats it as an immutable list by default. In fact, the type of a list literal is the read-only list interfaceCollections.List[T](which corresponds toIReadOnlyList<T>in .NET). This means you cannot add or remove elements from the literal list – you can only read or transform it. For example:let numbers = [1, 2, 3, 4, 5]; let first = numbers[0]; // indexing (0-based) works // numbers.add(6); // ERROR: no add method on IReadOnlyListIf you need a mutable list, you can explicitly use the
.NET Listtype which is exposed asCollections.LIST[T]in ghūl. For instance,let nums = LIST[int](); nums.add(42);would create a growable list of int. But most of the time, you'll work with immutable sequences and use functional transformations.ghūl’s pipeline operator
|provides a fluent way to work with list/sequence data. By writing an expression followed by| .method, you can call sequence extension methods in a chain (similar to LINQ in C# or pipelines in F#). ghūl supports typical sequence operations like map, filter, reduce, etc. For example, using the list defined above:let evens = numbers | .filter(x => x % 2 == 0); let doubled = numbers | .map(x => x * 2); let sum = numbers | .reduce(0, (acc, x) => acc + x); IO.Std.write_line("evens: {evens}"); // evens: [2, 4] IO.Std.write_line("doubled: {doubled}"); // doubled: [2, 4, 6, 8, 10] IO.Std.write_line("sum: {sum}"); // sum: 15In the above,
.filtertakes a predicate function and produces a new filtered list,.mapapplies a function to each element, and.reducefolds the list into a single value with a starting accumulator. These operations do not mutate the original list – they return new sequences (the originalnumbersremains[1,2,3,4,5]). This approach encourages an immutable, functional style. Under the hood, these are extension methods on theCollections.Iterable/Listinterfaces.You can also iterate through a list with a
forloop (as shown in Control Flow) or access elements by index with the[index]syntax. Lists have properties like.countfor length. -
Tuples: ghūl supports tuple literals using parentheses. For example,
(10, "hello")is a tuple of int and string, type(int, string). Tuples can also have named elements for clarity:(x: 10, y: 20)has type(x: int, y: int)and you can accesstup.xandtup.yif you bind it to a variable. Tuples are immutable structured data useful for returning multiple values from a function or grouping values ad-hoc. They implement structural equality and can be nested. Tuple element types are inferred if not explicitly annotated. -
Dictionaries (Maps): The language doesn’t have a literal syntax for dictionaries, but you can use .NET dictionaries via
Collections.MAP[K,V](the mutableDictionary<K,V>) or the read-only interfaceCollections.Map[K,V](which corresponds toIReadOnlyDictionary). For example:let dict = MAP[string, int](); dict.add("one", 1); dict.add("two", 2); let readOnly: Collections.Map[string,int] = dict;Here
dictis a mutable dictionary, andreadOnlyis typed as the interface. You can iterate a map withfor (key, value) in dict do ... od, and there are methods like.contains_keyor indexer accessdict["one"]. Like lists, the naming convention is that uppercaseMAPis the concrete type and capitalizedMapis the interface. Similarly,SET/Setfor hash sets, etc., following .NET’s generic collections. -
Option Type:
Note on Union Types: Union types in ghūl are a newly implemented feature and are reference types (not value types). The implementation is still evolving and may contain bugs or limitations. If you encounter existing code using unions, be aware that it may include workarounds for known or unknown issues in the union type system. When writing new code, use unions cautiously: if they are clearly the best fit for your use case, consider them, but be prepared to encounter and possibly work around implementation problems. Also, while an
Option[T]union type is available for representing optional values, there is currently no real library support for unions—most of the underlying library is just .NET 8, which does not natively support these constructs.
As discussed in the Types section, an Option[T] union can represent optional values. Rather than using null references, you can use Option.NONE or Option.SOME(x) to indicate missing or present values. For example:
function find_user(id: int) -> Option[User] is
let user = lookup_db(id);
if user? then // check if not null
return Option.SOME[User](user);
else
return Option.NONE;
fi
si
let result = find_user(42);
if result.is_some then
IO.Std.write_line("Found: {result.some.name}");
else
IO.Std.write_line("No user with that ID");
fi
In this snippet, user? is a quick null-check (true if user is non-null). We wrap the found user in Option.SOME, otherwise return NONE. Later, by checking result.is_some, we safely access result.some. This pattern avoids null reference errors. (ghūl does allow null for reference types since it’s on .NET, but using Option is safer and more idiomatic for optional data.)
Note on Null Checking: The obj? syntax used above is a convenient way to test if an object reference is not null. It returns a boolean – essentially sugar for obj != null. You’ll often see it before calling methods on possibly-null objects.
ghūl has a modern type system with generics (parametric polymorphism) and type inference to reduce verbosity:
-
Generics: You can parameterize classes, structs, traits, functions, and even union variants with type parameters. Generic type parameters are written in brackets after the name. For example, a generic function and struct:
print_something[T](value: T) => IO.Std.write_line("something is {value}"); struct Box[T] is item: T; init(item: T) is self.item = item; si siHere
[T]declares a type parameterTthat can be any type.print_something[T]simply prints whatever value it’s given. We can callprint_something[int](1234)orprint_something[string]("hello")to specifyTexplicitly. However, ghūl often infers generic type arguments from context. If we callprint_something(1234), the compiler sees an int and infersTas int. So you usually don’t need to write the type argument for generic functions – just callprint_something("hi")and it deducesT= string. Similarly, methods and static methods can be generic and perform type argument inference on calls.Generic classes/structs require type arguments when you construct them, unless the constructor itself provides enough clues for inference. For example, with
Box[T]above, you typically create an instance withBox[int](5)orBox[string]("text"). But if theinitarguments unambiguously determineT, ghūl can infer it. SupposeinitforBox[T]took aT– then writingBox(5)would let the compiler infer thatTis int. In general, ghūl can infer generic types for function calls and even constructor calls if all generic parameters appear in the parameter types and there's no ambiguity.All the core type constructs can be generic. For instance, you could have
class Pair[T,U],trait Comparable[T],union Either[L,R], etc. One constraint: currently ghūl does not support specifying explicit interface/trait constraints on generics (there’s nowhere T: SomeTraitsyntax yet), so generic code is somewhat limited to using methods available onobjectunless you cast or otherwise know the type (this is noted as a limitation). Still, generics are powerful for building reusable data structures and algorithms. -
Type Inference: ghūl is statically typed but tries to infer types to reduce noise. The compiler can infer:
- Local variable types: If you initialize a
letvariable without specifying a type, the type is inferred from the initializer. e.g.let count = 123;inferscount: int. If no initializer is given, you must specify a type (let count: int;). - List literal element types: As mentioned, the element type of list/array literals is inferred from the elements. If no common subtype exists, it defaults to
object. - Conditional expressions: The type of an
if ... elseexpression is inferred by finding a common type for the results of each branch. For example, inif cond then "yes" else StringBuilder() fi, the result type would be inferred asobject(since one branch is string and the other is aStringBuilderobject, their closest common type isobject). - Generic type arguments: As discussed, you can often omit explicit
[T]parameters on function and method calls – the compiler deduces them from the argument types. It can also deduce type arguments for constructors in some cases. - Anonymous function return types & parameters: If you write a lambda literal and don’t specify its return type, the compiler infers it from the expression or return statements inside. If you pass a lambda to a function where the expected delegate type is known, ghūl infers the parameter types of that lambda. For example, in
numbers | .filter(i => i > 3), the compiler knowsfilterexpects a function of typeint -> bool(becausenumbersis aPipe[int]orIterable[int]), so it infersiis anint. You don’t need to annotate lambda parameter types in such cases.
In summary, ghūl’s type inference is local (it won’t infer a function’s return type without a hint, except for
=>single-expression functions where it obviously comes from the expression). It’s powerful enough to avoid a lot of verbosity (especially with generics and collections) while still making types explicit where it matters (function signatures, public APIs). - Local variable types: If you initialize a
Below is a concise summary of ghūl syntax, keywords, and type system features for easy reference:
-
File Structure: Code is organized into
namespaceblocks. Syntax:namespace Name.Of.Namespace is ... si. If any namespace is declared in a file, all code must reside in some namespace (no free-floating definitions). If no namespace is given, the file has an implicit private namespace. -
Imports: Use
use NamespaceName;to import all symbols from a namespace, oruse Namespace.Symbol;to import one symbol. This allows using names without qualification within the current file’s namespace scope. -
Entry Point: The program entry point is a parameterless function named
entry. For example,entry() is IO.Std.write_line("Hello world"); siis analogous to a
Mainmethod. Theentryfunction should return void (you can omit the return type). -
Variables: Declared with
let. Example:let x = 42;. You can optionally specify the type (let x: int = 42;). Without an initializer, a type is required (let flag: bool;). ghūl uses type inference forletinitializers. All variables are block-scoped (there are no global mutable variables outside of a function). Reassignment of aletvariable is allowed (they are not immutable by default), but cannot change type. -
Naming Conventions: Use
snake_casefor variables, functions, and methods;PascalCasefor traits and namespaces;MACRO_CASEfor class, struct, enum, and union names. A leading underscore_nameon any identifier indicates intended privacy/protected status (non-public), enforced in some cases by the compiler (e.g., you cannot access another class’s_private_field). -
Primitive Types: ghūl uses .NET primitive types with familiar names (all lowercase). e.g.
int(System.Int32),long(Int64),double(Double),bool(Boolean),string(String), etc. Value literals look like in C#:1234,3.14Dfor double (or no suffix for single-precision float),'c'for char,true/falsefor bool,nullfor null reference. Numeric literals can use_as a digit separator and can have type suffixes (e.g.1_000Lfor a long,0xFFULfor unsigned long). -
Operators: Arithmetic
+ - * / %and comparison< <= > >= == !=work as in C-family languages. Boolean logic uses a slightly different notation: logical AND is written as/\and OR as\/(these correspond to&&and||). Logical NOT is!as usual. Bitwise operators use& | ^ ~similar to C#. The string concatenation operator is+(since strings are objects). ghūl supports string interpolation: any string literal can contain{expr}placeholders which will be replaced by the formatted value ofexprat runtime (no special prefix like$is needed). Example:"Hello, {name}". The interpolation calls the.ToString()or equivalent on the expression. -
Assignment: The
=operator is used for assignment to variables and fields. You must have declared a variable withletbefore assigning to it. In contrast to some functional languages, ghūl variables are not implicitly immutable – you can do:let count = 0; count = count + 1;to increment a variable. However, if
countwere a property with no public setter,count = ...would fail. Use ofassert(as shown above) is encouraged to enforce invariants on assignments where appropriate. -
Functions: Declare with optional return type, name, params, and body. Syntax recap:
- Single expression:
name(param1: Type1, param2: Type2) -> ReturnType => expression; - Block body:
name(params...) -> ReturnType is ... si
If ReturnType is omitted, it defaults to
void. In a block body, usereturn expr;to return a value. A function with no explicit return in a non-void function returns default value. Functions are declared at namespace scope (no nested function definitions inside other functions). They can be overloaded by parameter signature. No support for default parameter values (overload or optionalOptionparameters can be used instead). - Single expression:
-
Methods: Similar to functions but defined inside a class/struct/trait. They have an implicit
selffor instance methods. Declared just like a function in the class body. If a method name in a class starts with_, it is considered protected (only accessible to that class or subclasses). Otherwise methods are public by default. There are no explicit visibility keywords likepublic/private; naming convention controls it. -
Properties: Declared as
name: Typewith optional getter/setter bodies as described earlier. By default,name: Type;in a class creates a public get, protected set property. Prefix with_to make it protected get as well. You can also declare global properties at namespace scope (essentially global variables, though these should be used sparingly). -
Control Flow Keywords:
if ... then ... [elif ... then ...] [else ...] fi– conditional blocks. Can be used as statement or expression.while condition do ... od– pre-condition loop (while-loop).for x in iterable do ... od– foreach loop over a range or collection. Use..or::to create numeric ranges. You can destructure tuples in the loop variable, e.g.for (key, value) in my_map do ... od.do ... od– indefinite loop (execute repeatedly until broken out).break– exit the nearest loop immediately.continue– skip to next iteration of nearest loop.case expr ... when A: ... [when B: ...] ... [default: ...] esac– multi-way branch on constant values. Use for matching integers, enums, or other comparable constants. No fall-through (eachwhenis like its own block).try ... catch e: ExceptionType ... [finally ...] yrt– exception handling block. You can catch multiple exception types with multiplecatchblocks. Thefinallypart is optional.assert condition else expr;– runtime assertion. Throws an exception (AssertionFailedException or the given exception) if condition is false.return [expr];– return from function. Omittingexprmeans return void. In afinallyblock, you can useyrtas a closing keyword (you generally wouldn’treturnfrom a finally though).
-
Type Definitions:
class NAME [ : Superclass, Trait, ... ] is ... si– define class. Supports single inheritance and multiple traits. Useinitmethods for constructors. A class without a specified superclass implicitly inherits fromobject(System.Object).struct NAME [ : Trait, ... ] is ... si– define struct (value type). Cannot have a superclass. Copying struct copies data,==is memberwise compare.trait Name [ : ParentTrait, ... ] is ... si– define trait (interface). Members have no bodies (except possibly default implementations in future). Classes/structs implement traits by listing them after a colon.union Name is VARIANT1(...); VARIANT2(...); ... si– define union (discriminated union). Automatically getsis_varianttags and.variantaccessors for payload. Use qualified name to construct, e.g.Name.VARIANT1(data).enum NAME is MEMBER1[,=value1], MEMBER2[,=value2], ... si– define enum. Members are constants of that enum type. Backed by int (32-bit) by default.
-
Generics Syntax:
- For generic declarations, use
[T, U, ...]after the name. Examples:class Box[T] is ...,trait Comparable[T] is ...,union Option[T] is ...,pickFirst[T,U](a: T, b: U) -> T => .... - For usage, specify type args in brackets if needed:
let b = Box[int](5);orpickFirst[int,string](42, "hi"). Often you can omit the brackets and let the compiler infer them. - Inside a generic, the type parameter can be used in property and method definitions. There’s no direct way to restrict a type parameter (no
whereclauses yet), but you can require it implements a trait by using that trait in a function signature or as a superclass (not as a generic constraint, but e.g.doSomething[T: SomeTrait](x: T)is not yet supported in current ghūl).
- For generic declarations, use
-
Type Inference:
- Compiler infers
letvariable types from initializers. - It infers the element type of array/list literals and the result type of
if ... elseexpressions by finding a common type. - It infers generic type parameters for function calls and constructors when possible (contextual type or argument types guide it). You can always specify types explicitly if inference fails.
- It infers anonymous function return types from their body, and infers anonymous function parameter types from the expected function type if known.
- It does not infer types for function parameters or the top-level function return type – those you must write out (aside from the single-expression
=>shorthand where the return type can be omitted and is effectively inferred).
- Compiler infers
-
Null and Option: ghūl follows .NET’s model where reference types can be
nullby default (there’s no non-nullable reference type feature yet). Use theobj?syntax to check for null easily. For a safer alternative, wrap optional values inOption[T]and checkis_some/is_noneas described. -
Interoperability: Because ghūl compiles to .NET IL, you can call into any .NET library. ghūl will map .NET names to its naming conventions: e.g.
System.Collections.Generic.List<T>isCollections.LIST[T],IEnumerable<T>isCollections.Iterable[T],System.Stringisstring, etc. When in doubt, refer to ghūl documentation for naming, or use your IDE (the ghūl VSCode extension) to find the correct name. You can also escape identifiers that are reserved keywords by enclosing in backticks (e.g.let `class` = "test";if you needed such a name).
ghūl is still evolving, but this reference should give you a solid grasp of its syntax and semantics. With its mix of familiar .NET paradigms and new twists (like the keyword-based blocks and union types), ghūl enables a variety of programming styles. As you experiment, keep the official ghūl documentation handy – since “whatever the compiler accepts is the definitive reference” in this work-in-progress language. Enjoy exploring ghūl, and happy coding!
Sources: The ghūl language reference and examples are drawn from the official ghūl website and the ghūl compiler repository documentation, which provide further details and up-to-date information.