Fēng Programming Language
Statements are the basic instruction units of a program sequence. Except for block statements and control statements,
they must end with a ;:
For example, an assignment statement, where the equal sign separates the left and right parts—the left is the operand to
be assigned, and the right is the expression that computes the value:
var s = a + b;.
Another commonly used statement is the call statement, which contains only a call expression:
start();.
The expression on the right side of a statement is the main body of computation, composed of multiple operations
combined according to precedence: a + (b + c) * d - sin(e).
Functions and methods are reusable, independently executable blocks of code that take input ( parameters), perform a series of operations, and optionally return a set of output values. They are used to decompose complex tasks into smaller, more manageable, and maintainable modules, improving code reusability.
Examples below illustrate function formats, which also apply to methods.
Define a sum function with two int parameters, returning an int value:
func sum(a, b int) int {
return a + b;
}
Assume printf is a function provided by module fmt for printing to the terminal:
import fmt *;
func test() {
printf("%d + %d = %d \n", 1, 3, sum(1, 3));
}
A function with no return value:
import fmt *;
func test(a, b int) {
printf("%d\n", a + b);
}
A function with a return value:
import fmt *;
func div(a, b int) int {
return a / b;
}
func test() {
var a, b = 1, 3;
var s = div(a, b);
printf("%d / %d = %d\n", a, b, s);
}
Developers can define the following custom types: Classes and interfaces, structs, enumerations.
For example, define a custom derived class Complex and use it to define variables c1 and c2:
class Complex {
var real, imag float64;
}
func sample() {
var c1 Complex = {real=1.5,imag=2.4};
var c2 *Complex = new(Complex, {real=3.3,imag=0.6});
}
As a code organization unit, all files in the same directory belong to the same module, and the module name is the same as the directory name, so no declaration is needed in the file. Circular dependencies are not supported; the dependency graph must be a directed acyclic graph (DAG).
The main function is the entry point of an executable program, consistent with other languages.
The entry function has no return value and only one parameter, whose type must be [&!#][*!#]byte:
import std$os;
func main(args [&!#][*!#]byte) {
os$printf("Welcome to programming with Feng language");
}
A module containing a main function will be compiled into an executable file and cannot be imported as a library by
other modules. Modules without a main function are used as libraries, meaning they can be imported for use.
The following sections describe the definitions and usage of each syntactic element in detail.
A module is the basic unit of code management.
- Global symbols within a module cannot be reused; it's the same as within a single file.
- Everything inside a module is visible internally. Cross-module access is limited to exported symbols.
- Module names correspond one-to-one with paths; module names are not declared in files. Directory names must follow
the same rules as variable names.
For example, on Linux, module
com$jjj$base$utilcorresponds to the relative pathcom/jjj/base/util.
Any global symbol can be exported. Special cases for members:
- For an exported class, its members are not exported by default and must be exported individually.
- Methods of an exported interface are exported by default.
- Fields of an exported struct type are exported by default.
- All values of an exported enumeration type are exported by default.
For example, the following exports global variable gFoo, function aFoo, class Foo along with its field bar and
method go:
export var gFoo Foo;
export
func aFoo() Foo {
return gFoo;
}
export
class Foo {
export
var bar int;
export
func go() {
//
}
}
Declare importing the com$cossbow$fmt module:
import com$cossbow$fmt;
func test() {
fmt$println(string("Hello Fēng!"));
}
You can set a module alias:
import com$cossbow$fmt ccfmt;
func test() {
var m Sring = ccfmt$sprintf("Hello Fēng!");
ccfmt$println(m);
}
Primitive Types are built into the language and, from a memory perspective, can be placed directly in registers. Primitive Types include integers, floating-point numbers, and booleans.
Although string literals exist, there is no built-in string type: strings cannot be stored directly in registers, and strings are only meaningful when processing characters.
All built-in integer types are as follows:
- Signed:
int8/int16/int32/int64/int - Unsigned:
uint8/uint16/uint32/uint64/uint
The suffix number indicates bit width; types without a suffix are determined by the compilation target platform.
The highest bit of signed numbers is the sign bit: 0 for positive, 1 for negative. Thus, signed numbers have one
fewer bit for the numeric value.
Supports arithmetic operations, bitwise operations, and relational operations.
Explicit conversion is required between different integer types:
func test() {
var a uint16 = 123;
var b int32 = int32(a); // Convert uint16 to int32
}
Explicit conversion of integer types is equivalent to bitwise copying from low to high bits, which may cause integer overflow:
- Converting from a larger bit width to a smaller one truncates, causing overflow.
- When converting between signed and unsigned, the sign bit is copied to the corresponding bit position, causing the integer value to change.
The language itself does not check for overflow; programmers must handle it themselves.
Floating-point types are defined by the IEEE 754 standard, including
single-precision float32 and double-precision float64.
Floating-point types support arithmetic operations and relational operations.
The type symbol is bool, and it has only two values: true/false:
- Supports logical operations, relational operations (equality and inequality only), and bitwise operations (AND, OR, XOR).
- The result of a relational operation is always a boolean.
- No conversion to/from integers or floating-point numbers.
- Conditional expressions in
ifandforstatements must evaluate tobool.
The boolean type occupies 1 byte, using only the lowest bit; the values of other bits do not affect boolean operation results. The mapping between the lowest bit and boolean value:
| Boolean | Integer Value |
|---|---|
| false | 0 |
| true | 1 |
Boolean type supports logical operations, and the results of relational operations are of boolean type.
The following table lists the precedence of major operators (decreasing from top to bottom):
| Level | Operator Set | Notes |
|---|---|---|
| 1 | new(), parentheses, literals | |
| 2 | is, index, field access, function call, block | |
| 3 | +, -, ! | Unary operators |
| 4 | ^ | Exponentiation |
| 5 | *, /, % | |
| 6 | +, - | |
| 7 | <<, >> | |
| 8 | & | |
| 9 | ~ | |
| 10 | | | |
| 11 | <, <=, ==, !=, >, >= | |
| 12 | && | |
| 13 | || |
Levels 1 and 2 are special operators and are left-associative. Level 3 unary operators are right-associative, while binary operators (except exponentiation) are left-associative. Exponentiation is special: it is right-associative, with precedence higher than unary operators on the left but lower than those on the right:
func test(a,b int) {
var x int;
x = -a^b; // Equivalent to: -(a^b)
x = a^-b; // Equivalent to: a^(-b)
}
| Operator | Description |
|---|---|
| ^ | Exponentiation |
| * | Multiplication |
| / | Division |
| % | Modulo |
| + | Addition |
| - | Subtraction |
| Operator | Description |
|---|---|
| ! | Bitwise NOT |
| << | Left shift |
| >> | Right shift |
| & | Bitwise AND |
| ~ | Bitwise XOR |
| | | Bitwise OR |
| Operator | Description (left * right) |
|---|---|
| < | Less than |
| <= | Less than or equal to |
| == | Equal to |
| != | Not equal to |
| > | Greater than |
| >= | Greater than or equal to |
| Operator | Description |
|---|---|
| ! | Logical NOT |
| & | Bitwise AND |
| ~ | Bitwise XOR |
| | | Bitwise OR |
| && | Logical AND |
| || | Logical OR |
Since the boolean type only uses the lowest bit (others ignored), bitwise operations &, ~, | on
boolean values still produce valid boolean results. The results of & and && are consistent, as are | and ||. The
difference is that && and || have "short-circuit" behavior: when the left operand's result determines the final
outcome, the right expression is not executed.
Example illustrating the short-circuit behavior of &&:
func contains(v int) bool {
// TODO: check v is in the collection
}
func isEmpty() bool { return true; }
func test(v int) bool {
// isEmpty returns true, so the right side need not be evaluated; contains will not be called
return !isEmpty() && contains(v);
}
Only arrays support this operator by default. The index operator consists of square brackets containing an expression that evaluates to the index value. There are two usage patterns:
- Index expression on the right side is a read operation, retrieving the value of the element at the index as the
result.
When using a single variable on the left or using it in an expression, only the element value is returned. If the
element does not exist, execution terminates and an exception is thrown.
func test(arr [16]int) int { return arr[16]; // Index out of bounds, terminates and throws an exception } - On the left side as a write operation, modifying the value of the element at the index.
The capacity of an array cannot change after creation, so out-of-bounds access also terminates execution and throws an exception.
func test() { var arr [16]int; arr[15] = 0; // Modify element at index 15 to value 0 }
Used to dynamically create instances. Format: new(type, initialization parameters). Example:
// Create an instance of class Device
var a *Device = new(Device);
// The following parameter is for initialization
var b *Device = new(Device, {});
Reference type variables are separate from
instances; strong references can only reference instances created via new.
The parameter is optional; without it, the instance is initialized to default values. For arrays, you can pass an array expression; for classes and structs, you can pass a field expression.
A special literal expression specifically for array initialization, listing all elements of the array directly. Elements can be arbitrary expressions.
Example: initialize an int array variable by listing all elements:
var a [4]int = [1,2,3,4];
When initializing an array, the number of elements listed can be less than the array size; subsequent elements are set to default values:
var a [4]int = [1,2,3,4];
var a [4]int = [1,2,3];
It can also be empty:
var a [4]int = [];
You can specify the array type before the expression. The element types must match the variable, and the length cannot exceed the variable's length:
var a [4]int = [4]int[1,2,3,4];
var a [4]int = [3]int[1,2,3];
// var a [4]int = [5]int[1,2,3]; // Error: specified length exceeds variable length
The specified array type length cannot be less than the actual number of elements:
var a [4]int = [3]int[1,2,3];
// var a [4]int = [3]int[1,2,3,4]; // Error: number of elements exceeds specified length 3
var a [4]int = [4]int[1,2,3,4]; // Correct
The specified type length can be omitted, in which case the length equals the actual number of elements:
var a [4]int = []int[1,2,3];
If the variable type is omitted, the variable's type and length are automatically inferred from the number of elements:
var a = []int[1,2,3]; // a's type is: [3]int
The element type must match the array type, following the assignment rules. Refer to variable types and their definitions. Two simple error examples:
// var a = []int[1,false]; // Second element is bool
// var a = []int[1,3.1]; // Second element is float
If the type is not specified, the element type is inferred from the first element:
var a = [1,2,3]; // a's type inferred as: [3]int
// var a = [1,3.1]; // First element infers type int, but second is float
A more complex example:
class Animal {}
class Cat : Animal {}
var a = [new(Animal), new(Cat)]; // Inferred array type: [2]*Animal
// var a = [new(Cat), new(Animal)]; // Error: inferred type [2]*Cat, cannot store *Animal element
Can be nested for multi-dimensional array initialization:
var a = [2][3]int[[1,2],[3,4]];
// var a = [2][3]int[[],[],[]]; // Error: first dimension exceeds
// var a = [2][3]int[[1,2,3,4]]; // Error: second dimension exceeds
Nesting with field expressions:
class Car {
var id int;
}
var ca [2]Car = [{id=1},{id=2}];
Like array expressions, this is a special literal expression specifically for initializing value types of derived types with definable fields, such as struct types and classes.
For example:
class Car {
var id int;
var speed float;
}
var car Car = {id=10,speed=80.5};
Initialized fields need not be in the order defined, and fields cannot be initialized repeatedly.
// var car Car = {id=10,id=100}; // Error: duplicate field id
Like array expressions, types cannot be automatically inferred; either the type must be known from context, or a type prefix must be added to the expression:
var car = Car{id=10,speed=80.5};
Refer to Classes and Struct Types for other initialization details.
Used to initialize tuple values. A tuple expression arranges values within parentheses, corresponding to the tuple definition, and requires at least 2 values.
For example:
var empty (bool,int) = (false,0);
Unlike array expressions and field expressions, the type annotation for tuples is written after each value:
var empty = (false:bool,0:int);
If the values in a tuple are literals, the type annotation can be omitted for true/false because they can be unambiguously inferred as bool, but not for other literals. For example, an integer literal defaults to int, but could also correspond to uint, int32, and so on.
func far() {
var a = (false,1:int); // ✓: type annotation after false is omitted
// var b = (false,1); // ✗: type annotation after 1 cannot be omitted
var i int = 0;
var c = (false,i); // ✓: i's type is clear, can be omitted
}
Used to check whether a class reference can be converted. For example, a reference to a class instance can be passed to an interface or parent class pointer; conversion back requires type checking.
Returns a reference of the corresponding type. If the type does not match, it returns nil, which may cause a null
pointer:
func test(o *Object) {
var f *File = o?(*File); // Convert to File class reference
var w Writer = o?(*Writer); // Convert to Writer interface
o?(*Writer).write("Hello!"); // Used in an expression, risk of null pointer
if (var w Writer = o?(*Writer); w != nil) {
// This avoids null pointer
}
}
Used at compile time to calculate the memory size (in bytes) of a type. The type must be known to the compiler. Supported types:
- Integer and floating-point types.
- Struct types.
- Fixed-length arrays of the above (including multi-dimensional arrays)
For example, array reference [*]int cannot be used because its size is only known at runtime.
Classes do not support sizeof because field reordering is allowed.
Enumerations must remain simple and do not support sizeof.
bool is a special primitive type that the compiler may store efficiently, so sizeof is not supported.
Assignment operators are shorthand for operations: the left operand participates in the operation with the right operand, and the result is assigned back to the left operand. This requires that both operands be of the same type. If a type supports custom operators, the corresponding assignment operator is also supported. For example:
func test() {
var i = 0;
i += 2;
}
If an operator + is implemented to concatenate strings in order, then the += operator appends the right string to
the left string variable and assigns the result back to the left variable.
Assignment operators have no return value and cannot be used in expressions; they can only be used in specific statements.
A block expression resembles a block statement but must end with an expression as the return value:
class Foo {var id int;}
func test() {
var r *Foo = {
var f = new(Foo);
f.id = 0;
f // expression value
};
}
Entering the block creates a new scope; variables are automatically cleaned up upon exit.
Classes do not support operators by default, but some operations can be custom-implemented.
Custom operation code segments differ from methods and functions; they are implemented via
operator macros. Each operator has a fixed name, prototype, and operand list:
- Each operator has a fixed name.
- Operands are defined per operator (as macro parameters).
- Names can be the same as other method names.
Only certain operators support customization:
| Operator | Macro Name | Right Operand Type | Result Type |
|---|---|---|---|
| * | mul | Same as left | Same as left |
| / | div | Same as left | Same as left |
| % | mod | Same as left | Same as left |
| + | add | Same as left | Same as left |
| - | sub | Same as left | Same as left |
| < | lt | Same as left | bool |
| <= | le | Same as left | bool |
| == | eq | Same as left | bool |
| != | ne | Same as left | bool |
| > | gt | Same as left | bool |
| >= | ge | Same as left | bool |
Example with complex numbers:
class Complex {
var real,imag float64;
// Implement + operation
// result is returned after computation
macro operator add(rhs) {
{
real = real + rhs.real,
imag = imag + rhs.imag
}
}
}
func testAdd(a,b Complex) Complex {
return a + b;
}
The index expression, which is only supported by arrays by default, can also be customized.
Index operations are divided into read and write operations, implemented via two procedural macros: indexGet and
indexSet.
For example, a custom dictionary class Map providing indexed access with derived key and value types. Usage example:
class Map {
// Index read
macro operator indexGet(key, operand, exists) {
var n = getNode(key);
operand, exists = if (n != nil) n.value, true else nil, nil;
}
// Index write
macro operator indexSet(key int, value String) {
set(key, value);
}
}
func test() {
var m Map;
m[100] = 159;
// Checked read: if key does not exist at runtime, exists is false
var v, exists = m[100];
// Direct read: if key does not exist (exists false), execution terminates and an exception is thrown
var v int64 = m[100];
printf("m[100] = %s\n", v);
}
Whether a write operation terminates execution when an index does not exist depends on the implementation: For a typical Map, new keys can be added; arrays cannot be automatically resized.
Classes are a core concept of object-oriented programming, describing the common characteristics and methods of the instances they create. Classes correspond to categories in human knowledge and, in programs, classify instances (objects) and define their common properties (fields) and behaviors (methods).
A typical class definition (containing one field and one method):
class Car {
var engine *Engine;
func start() {
engine.start();
}
}
After defining a class, it must be instantiated for use: declare a value type variable of the class, or use new to
dynamically create an instance.
func sample(engine *Engine) {
var c1 Car = {engine=engine};
// Or
var c2 *Car = new(Car);
c2.engine = engine;
}
Most variable type definitions apply to class fields, but they cannot be defined as a phantom reference type.
class Cat {
const id int;
var name [*#]byte;
var mothr,father *Cat;
var children []*Cat;
// var who Cat; // ✖
}
The order of field definitions in a class does not need to correspond to actual memory layout. This differs from struct types.
When instantiating, const fields must be initialized; var fields are optional.
In the Cat class above, id must be initialized, while name is optional:
func test() {
var c1 Cat = {id=1001};
var c2 Cat = {id=1001, name="Tom"};
// Incorrect usage below
// var c3 Cat = {name="Tom"};
// var c4 Cat;
// var c5 Cat = {};
}
The same applies to dynamic instantiation via new:
func test() {
var c1 *Cat = new(Cat, {id=1001, name="Tom"});
// Incorrect usage below
// var c2 *Cat = new(Cat);
// var c3 *Cat = new(Cat, {name="Tom"});
}
If a class has no const fields, initialization is not required.
class Mouse {
var id int;
var name [*#]byte;
}
func test() {
var m1 Mouse;
var m2 *Mouse = new(Cat);
}
If not initialized, or if a field is not specified in initialization, it is set to the default state: all memory set to
0, and reference types set to nil.
Side effect: If an exported class has unexported const fields, it cannot be instantiated in other modules.
For example, the Dog class below can only be instantiated in the current module:
export
class Dog {
const id int;
var name [*#]byte;
}
export
func newDog(id int) *Dog {
return new(Dog, {id=id});
}
export
func dog(id int, name [*#]byte) Dog {
return {id=id, name=name};
}
Methods are largely the same as functions, with differences:
- They must be called via a class instance.
- Inside a method, the current instance's class members are accessible.
- Method names are unique IDs within the current class's method set, including inherited methods, but subclasses override parent methods with the same name.
For example, define a Task class for managing tasks (enum TaskState represents task status):
enum TaskState {WAIT, RUN, DONE,}
class Task {
var state TaskState;
func isRunning() bool {
return state == RUN;
}
func start() {
if (isRunning()) return; // Call another method
state = RUN; // Modify state
}
}
Call methods via value type variables:
func sample1() {
var task Task;
task.start();
printf("task state '%s'\n", task.state.name); // Prints: task state: 'RUN'
}
Or via references:
func sample2() {
var task *Task = new(Task);
task.start();
}
this is a special keyword inside a class that refers to the current instance itself.
Inside a member method, when a local variable has the same name as a field, this is used to access the field:
class Cat {
var name String;
func setName(name String) {
log(); // No log function above, so this refers to the log member method
this.name = name; // The name variable/parameter above conflicts with field name; must use this
}
func log() {
printf("%s: miao~~\n", name); // No name variable above; can omit this
}
}
During method invocation, this references the current instance, ensuring it is not deallocated:
func sample() {
new(Cat).log(); // The created instance can only be deallocated after the log method exits
}
this can be passed to phantom reference type variables. If called via a strong reference,
it can be passed to a strong reference; if called via a value type, it can be assigned to a value variable. Example:
class Foo {
var name String;
func aaa() {
var x &Cat = this;
}
func bbb() {
var x *Cat = this;
}
func ccc() {
var x Cat = this;
}
}
func use1() {
var f Foo;
f.aaa();
// f.bbb(); // ✖
f.ccc();
}
func use2(f *Foo) {
f.aaa();
f.bbb();
// f.ccc(); // ✖
}
func use3(f &Foo) {
f.aaa();
// f.bbb(); // ✖
// f.ccc(); // ✖
}
this can only be passed to strong references within the escaping method.
The super keyword refers to the immediate parent class. Its primary purpose is to call parent class methods, especially when you need to call a method that has been overridden.
Inheritance (also called extension) extends an existing class by adding new fields and methods. A subclass inherits the fields and methods of its parent class and may optionally add its own.
When inheriting, subclass fields cannot have the same name as parent class fields:
class Device {
var id int;
}
class Disk : Device {
// var id int; // ✖: Name conflict
var diskId int;
}
Methods can have the same name but must have identical prototypes, enabling polymorphism.
Polymorphism means that the same behavior can have multiple different forms or manifestations. Strictly speaking, abstraction (see Interfaces) is also a form of polymorphism.
Example of class polymorphism: first define a parent class Animal with a field name and a method eat:
class Animal {
var name [*#]byte;
func eat(food [*#]byte) {
printf("Animal %s eating %s\n", name, food);
}
}
Then define a subclass Cat that inherits the name field and implements a method eat with the same name and
prototype:
class Cat : Animal {
func eat(food [*#]byte) {
printf("Cat %s eating %s.\n", age, name, food);
}
}
An Animal reference can point to a subclass instance. When calling the eat method via the parent class reference,
the subclass's eat method is actually invoked:
func test() {
var animal *Animal = new(Cat, {name="Tom"});
animal.eat("fish-meat"); // Prints: Cat Tom eating fish-meat.
}
This example shows that the eat method can have multiple implementations after inheritance, and parent class
references pointing to different subclasses will call the subclass's implementation.
is expressions are supported for subtype checking:
func test(animal *Animal) {
var cat, ok = animal?(*Cat);
if (ok) cat.eat("mouse");
}
func test() {
test(new(Cat));
}
Parent and child classes only support reference passing; value type variables cannot be passed between them. Passing rules:
- When reference types are the same, a subclass can be passed to a parent class.
- A constant strong reference of a subclass can be passed to a phantom reference of the parent class.
Example with parent class Animal and subclass Cat:
func sample1(lc *Animal) {
var c1 *Cat = lc;
}
func sample2(lc &Animal) {
var c2 &Cat = lc;
}
func sample2(lc *Animal) {
var c2 &Cat = lc;
}
Polymorphic parent classes have their own method implementations, but interface methods have no implementation; the "subclasses" of the interface provide implementations. This is called abstraction. Abstraction serves as a better contract and specification:
- Managers focus only on interface implementations, hiding concrete classes, and provide instances that implement the interface.
- Users need not care about the specific class as long as the interface is implemented.
For example, define an interface Task containing only a simple method run:
interface Task {
run();
}
Define two implementation classes MyTask and YourTask:
class MyTask (Task) {
func run() {
println("Run my task!");
}
}
class YourTask {
func run() {
println("Run your task!");
}
}
Usage is similar to polymorphism:
func asyncRun(t *Task) {
t.run(); // Pretend this is asynchronous execution
}
func test() {
asyncRun(new(MyTask)); // Prints: Run my task!
asyncRun(new(YourTask)); // Prints: Run your task!
}
is expressions are also supported for type checking.
When a parent class reference points to a subclass instance, or an interface reference points to an implementing class instance, this operation is known in object-oriented programming as covariance.
Polymorphic method return values support covariance, meaning the return type of a subclass method can be a subclass or implementing class of the parent class method's return type. However, its parameters must remain identical, and the escaping and unmodifiable markers on the method must also match.
Here are examples of return type covariance only:
class A {}
class B : A {}
class ABox {
func get() *A {
return new(A);
}
}
class BBox : ABox {
func get() *B { // Override get() method, but the return type is a subclass of the parent's get() return type
return new(B);
}
}
Interface implementing classes also support covariance:
class A {}
class B : A {}
interface I {
func get() *A;
}
class Box (I) {
func get() *B { // Implement get() method, but the return type is a subclass of the interface's get() return type
return new(B);
}
}
Since classes support single inheritance, all classes form a tree based on inheritance relationships, with the root
class being Object.
Object is a built-in class. Any class without an explicit parent class inherits from Object by default.
Object has no members; an Object instance can be created.
func test() {
var o *Object = new(Object);
o = new(Device);
}
If a class is intended only for storing data or simple logic, and not for complex inheritance or interface design, it
can be marked as final.
Such a class cannot be inherited, has no base class (is not a subclass of Object), and cannot abstract interfaces (
should be possible, but not implemented).
class User final {
var id int;
}
Class members can be individually exported and are not exported by default.
Code in module util:
export List`T`{
var elements []T; // elements is hidden from external access
export func get(i int) { // get is exposed
// TODO: check index bounds
return elements[i];
}
}
When a class is annotated with the resource free macro method, it is marked as a resource class. The macro's code is
called when an instance of this class is deallocated.
This feature can be used to automatically release other resources, such as buffers allocated from C libraries:
class CBuffer {
const buf uint64; // Assume this field holds a buffer pointer value
macro resource free() {
cFree(buf); // Assume this calls C's free function
}
}
Resource classes can only be instantiated via new. This restriction prevents duplicate calls.
For example, in the CBuffer class above, if a value type variable copies the buf value, multiple instances would
call cFree(buf); repeatedly.
Some external resource releases, like file closures, are often time-consuming and may involve I/O errors or exceptions.
Such operations should be handled with exception statements rather than in free.
Inside a method marked as unmodifiable, this becomes an unmodifiable reference, meaning the instance cannot be
modified. If a constant value type variable
or unmodifiable reference calls a method that would modify the instance,
the rules of constness and immutability would be broken. Therefore, only unmodifiable methods can be called under such
circumstances.
The unmodifiable marker is #, placed before the parameter parentheses. For example, the get method:
class User {
var id int;
func get#() int {
// this.id = 0; // ✖: Cannot modify field
// id = 0; // ✖: Cannot modify field
// *this = {}; // ✖: Cannot assign via dereference
// const r &User = this; // ✖: Cannot pass to mutable reference
// this.set(0); // ✖: Cannot call mutable method
// set(0); // ✖: Cannot call mutable method
return id;
}
func set(id int) {
this.id = id;
}
}
In ordinary methods, this can only be used as a phantom reference. Inside an escaping method, this is used as a
const strong reference.
Once a method is marked with the escaping marker, it can only be called via a strong reference:
class User {
func foo*() {
const r &User = this; // ✔: Can be used as phantom reference by default
const r *User = this; // ✔: Can be used as strong reference
gar(); // ✔: Can call non-escaping method
}
func gar() {
const r &User = this; // ✔: Can be used as phantom reference by default
// const r *User = this; // ✖: Cannot be used as strong reference
// foo(); // ✖: Cannot call escaping method
}
}
Interfaces are a feature separated from polymorphism—they are parent classes without concrete implementations and
without fields.
Thus, an interface appears as a collection of methods, omitting the func keyword before method definitions.
Interfaces are only contracts and specifications; they cannot be instantiated, so interface type variables can only be references.
Interfaces can be composed:
- A composed interface includes all method prototypes from its components.
- A composed interface can be passed to a component interface because implementing the composed interface naturally implements the component interface.
- Method name conflicts are checked; methods with the same name from different components are considered the same method. Compilation fails if prototypes do not match.
For example, a file can be read and written; interfaces can be designed as follows:
interface Input {
read(b [&!]byte) int;
}
interface Output {
write(b [&!#]byte) int;
}
// Composed interface DataStorage includes both read and write methods
interface DataStorage {
Input;
Output;
query() [*!#]byte;
}
// An instance implementing the DataStorage interface naturally implements the Output interface
func use(ds *DataStorage) Output {
return ds;
}
Interface type variables are reference type variables and can only reference instances of implementing classes. Interface variable declarations require a reference identifier to indicate the reference type.
Allowed passing:
- When reference types are the same, an implementing class can be passed to the interface.
- Under allowed type conditions, a constant strong reference of an interface can be passed to a phantom reference of the interface.
- Under allowed type conditions, a constant strong reference of an implementing class can be passed to a phantom reference of the interface.
Example with interface Cache and implementation class LocalCache:
func sample1(lc *LocalCache) {
var c1 *Cache = lc;
}
func sample2(lc &LocalCache) {
var c2 &Cache = lc;
}
func sample3(lc *Cache) {
var c2 &Cache = lc;
}
func sample4(lc *LocalCache) {
var c2 &Cache = lc;
}
An enum is defined such that its domain is strictly limited to a finite set of values. As such, it cannot be null and will default to the first value in the sequence. Because the number of possible values is limited, all values must be enumerated exhaustively at the time of definition:
enum TaskState {WAIT, RUN, DONE,} // Note the required trailing comma ","
Enumeration variables must be one of the enumeration values; they cannot be nil.
Enumeration types have built-in special attributes determined at compile time:
id: An integer value automatically incremented in definition order. Changing the order changes the IDs.name: The literal name as defined. For example,WAIThas name"WAIT".value: A custom integer attribute. If not defined, the first enumeration value'svalueis0, and subsequent values increment by 1.
Enumeration values typically require the enum type as a prefix. If the variable type is clear, the prefix can be omitted. Example:
enum TaskState {WAIT, RUN, DONE,} // value not set, so equals id
enum BillState {WAIT, PAID=4, SEND, DONE,} // WAIT=0, SEND=5, DONE=6, ...
func test() {
var s1 = TaskState.WAIT; // s1 initialized to enum value WAIT
s1 = RUN; // s1 type known, so prefix omitted
var s2 TaskState = DONE; // s2 known, prefix can be omitted
var i int = TaskState.RUN.id; // i initialized to integer 1
i = s2.id; // i assigned 3
var n [*#]byte = s2.name; // n initialized to byte array reference containing string "DONE"
var v int = BillState.SEND.value; // v initialized to integer 5
}
When the type is clear (e.g., in a switch statement), if the case statements do not cover all values, a default
branch is required; otherwise, default is not allowed:
func sample(s BillState) {
switch(s) {
case WAIT {
}
case PAID, SEND {
}
default {}
}
}
Supports iteration loops over all enumeration values:
func test() {
for ( s : TaskState )
printf("name: %s, id: %d \n", s.name, s.id);
}
Supports direct indexing:
func sample() {
var s1 TaskState = TaskState[0];
var s2, ok = TaskState[4];
}
The default value for enumeration variables or array elements is the first enumeration value (with id equal to 0).
Structs and unions are collectively called struct types. Their definition and memory layout are consistent with C:
- Struct: All fields are allocated sequentially in order.
- Union: All fields share overlapping storage.
Field types can only be integers, floating-point numbers, struct types, and fixed-length arrays of these types.
Struct and union definitions have the same format, differing only in the keyword:
- Struct definition format:
structname{field list}.struct Message { type int; success byte; value float32; ext [12]int; } - Union definition format:
unionname{field list}.union DataType { type int; success byte; uv float32; }
Adjacent fields of the same type can be combined; non-adjacent fields cannot. Example with struct:
struct Request {
type, code int;
data [56]uint8;
}
You can specify the actual bit width used by a field. This can only be set for primitive type fields.
The configured bit width value ranges from 1 to the type's bit width; for example, for int32, the field bit width ranges from 1 to 32.
The configured bit width is placed after the field name in parentheses: field_name ( bit_width ) type.
The bit width must be a compile-time constant.
For example, setting the code field's bit-field to 6 (the type field is not set):
struct Request {
type, code(6) int;
}
A union can only specify one field during initialization:
union Foo {
tag int;
fly uint8;
}
var foo Foo = {tag=1};
// var foo Foo = {tag=1,fly=2}; // Error
Struct types can be instantiated in two ways:
- Defined as value types, supported as fields of variables, classes, or struct types.
- Dynamically allocated via
new.
Arrays are types that store a contiguous sequence of repeated elements. Elements can be of any type. Each element is equivalent to a variable and can be either a value type or a reference type:
var a [4]int; // primitive-type array
var b [4]Host; // Class array
var c [16]*Bus; // Class reference array
var d [12][4]int; // Array of fixed-length arrays: multi-dimensional array
var e [10][]int; // Array of variable-length arrays; elements are references (different from multi-dimensional)
Value type array elements' memory is allocated together with the array and can be used directly:
func test() {
// primitive type array
var a [4]int = [1,2,3,4];
a[0] += a[1];
// Class array
var b [4]Host = [{id=1}];
b[3].id = 111;
// Array of fixed-length arrays: multi-dimensional array
var c [4][8]int = [[1],[2]];
c[3][4] = 222;
}
Reference array elements require additional references to other instances; the default value is nil.
func test() {
var a [4]Device;
// a[2].name = "dev-2"; // Error: throws null pointer exception
a[0] = new(Device);
a[0].name = "dev-0"; // Only a[0] usable; other elements remain nil
}
The above uses fixed-length arrays as an example; variable-length arrays differ only in initialization; usage is the same.
Array length refers to the total number of elements the array can hold. Specifying or not specifying the size when declaring the variable type indicates two different types.
If the size is specified at declaration, it is a fixed-length array—the value in square brackets must be an integer literal or integer constant expression.
var a [4]int;
This type of array is a value type variable.
Initialized with array literal; the number of initial values cannot exceed the array length. If fewer, elements are initialized sequentially from the first position; remaining elements are zeroed:
// var a [4]int = [1,2,3,4,5];
var b [4]int = [1,2]; // b initialized to: [1,2,0,0]
When initialized with an expression, as a value type, assignment requires the same array type, including element type and length:
func foo() [4]int {
return [1,2,3,4]
}
func foobar() {
var a [4]int = foo(); // Same array type
// var b [2]int = foo(); // ✖: different length, not the same type
// var c [2]bool = foo(); // ✖: different element type, not the same type
}
Type inference from an untyped array literal is not supported.
When the left operand's type is already known:
func far() {
var a [4]int = [1,2,3]; // Explicitly declared type
a = [11,22]; // a's type is already known
}
When the left operand's type is unknown, such as when the type is omitted in a variable declaration, the type is inferred from the right side. In this case, the type prefix before the literal must not be used:
func far() {
var a = [4]int[1,2,3]; // Inferred type: [4]int
var b = []int[1,2,3]; // Inferred type: [3]int
// var c = [1,2,3]; // ✖: cannot infer type
}
Omitting the length creates a reference type variable, i.e., an array reference that can point to array instances of any length.
Array instances are allocated via new, and the length must be specified at allocation time. Format:
new([length]type)
Example creating an int array:
func test(size uint) {
var a []int = new([4]int);
var b []int = new([size]int);
}
Class fields can be arrays or array references, same as variable usage:
class Foobar {
var foo [4]int32;
var bar [*]int64;
}
Note: Struct type fields cannot be references; the length must be specified.
A tuple is a simple aggregate type that groups different types together. Notation: ( Type1 , Type2 , Type3 ).
The types in a tuple can be any type except phantom references, and there must be at least 2 elements.
For example:
var ga (bool,int);
var gb (bool,*int);
var gc (int,[*]uint64,[16]byte);
class Car {
var int speed;
}
var gd (int,Car);
A tuple is a value type and can be assigned directly, for instance using a tuple expression. For example:
var gr (bool,int) = (true,-1);
Individual elements of a tuple can be accessed using the syntax: tuple . index.
Note: the . operator is used to access elements, but what follows is not a field name — it is an index.
For example:
func far(a (bool,int,[*]byte)) {
var ok bool = a.0;
var code int = a.1;
var name = a.2; // The tuple has a known type, so name can be inferred as [*]byte
}
In the example above, the parameters are immutable and therefore cannot be modified. Below is an example with modification:
func bar(ok bool, code int) {
var a (bool,int);
a.0 = ok;
a.1 = code;
}
mappable defines types whose references can be freely converted to each other. Unlike class covariance/contravariance,
no type checking is required—only bounds checking.
Supported mappable types: struct types, integers, floating-point numbers, and fixed-length arrays of these types. These types occupy contiguous memory and contain no references (pointers), so their references can be freely converted like in C, with the only constraint being bounds checking.
Example: converting an int reference to an int16 reference:
func f1(a *int) *int16 {
return a;
}
Since the size is known at compile time, it can be checked. The above conversion does not cause out-of-bounds, but the following fails to compile:
func f1(a *int8) *int16 {
return a;
}
Struct conversion is similar; f1 is allowed, while f2 is out-of-bounds:
struct Foo {
v int32;
}
func f1(a *Foo) *int16 {
return a;
}
func f2(a *Foo) *int64 {
return a;
}
Array references can point to arrays of any length, so their length is computed at runtime. If the element size exceeds
the target, the size is 0:
struct Foo {
v int32;
}
func f1(a *Foo) [*]int16 { // length = 2
return a;
}
func f2(a *Foo) [*]int64 { // length = 0
return a;
}
Mappable types have well-defined memory layout consistent with C and contain no references (pointers). Struct types explicitly prohibit reference fields, and array elements cannot contain references, including multi-dimensional arrays:
func test() {
var a1 [*]int; // mappable
var a2 [*][2]int; // mappable
var a3 [*][3][4]int; // mappable
var a4 [*]*int; // not mappable
var a4 [*][*]int; // not mappable
var a4 [*][5]*int; // not mappable
var a4 [*][6][*]int; // not mappable
}
Definition format: func function-name ( parameter-list ) return-list { function-body }
The function name is required; parameter list, return list, and function body can all be empty. Three examples:
func run() {}
func start() { run(); }
func exec(a []Sting) *Error {
return nil;
}
The function name is the function's unique ID within the module's function set; functions are called by name.
func add(a,b int) int { return a + b; }
func test() {
var s = add(1, 2);
}
Parameters consist of a parameter name and type, are constants (implicit const), and their scope is within the
function.
Example defining parameters l of type Queue and a of type int:
func send(l Queue, a int) {
l.push(a);
}
Adjacent parameters of the same type can be combined. Example defining two int parameters a and b:
func add(a, b int) int {
return a + b;
}
The return type is declared between the parameter list and the function body. Example: function foo returns a float:
func foo() float {
return 0.1;
}
The function body consists of a sequence of statements:
func run(s int) {
var i = s+1;
do(i);
...
}
The context accessible inside the function body includes global variables, parameter list, and local variables.
const PI = 3.14;
func circlyArea(diameter float) float {
var radius = diameter * 0.5;
return radius * radius * PI;
}
A function prototype is a type of variable. A function definition without its body is a prototype: func function-name
( parameter-list ) return-list.
A variable of this type is either null or points to a function compatible with the
prototype. Example:
func add(a, b int) int { return a + b; }
func sub(a, b int) int { return a - b; }
func mul(a, b int) int { return a * b; }
func div(a, b int) int { return a / b; }
func Calc(a, b int) int;
func test(c Calc) {
printf("%d\n", c(rand(), rand()));
}
func test() {
test(add);
test(sub);
test(mul);
test(div);
}
Function prototypes support anonymous definition:
func test(c func(a, b int) int) {}
func supply(c int) func(a, b int) int {
switch(c) {
case 0 { return add; }
case 1 { return sub; }
case 2 { return mul; }
case 3 { return div; }
default { return nil; }
}
}
func test() {
var c1 func(a, b int) int = add;
var c2 = sub; // Type can be omitted; auto-inferred
}
Prototype variables are not reference types but can be nil. They can also be marked with a non-null prefix (!) to
indicate they cannot be null, following the same non-null rules as references:
func use1(a !func()) {
var c1 func() = a;
var c2 !func() = a;
// var c3 !func() = c1; // Error: cannot pass in reverse direction
if (c1 != nil) {
var c3 !func() = c1; // After explicit null check, can pass
}
}
Function prototypes support covariance, similar to method covariance: the return type of the right-hand value can be a subclass or implementing class of the left-hand operand's return type.
Variadic functions are special functions where the number and types of trailing parameters are not fixed and are automatically expanded at compile time. Their current use is limited to formatting and wrapping formatting.
The built-in string formatting function is a variadic function:
- The first parameter is an
&!Writer, i.e., an object implementing the built-inWriterinterface. - Format string literal:
"This is for {}!", where{}is a placeholder for formatting subsequent parameters. - Parameter instances to output; their count must match the number of placeholders. Types can be basic types, classes, and interfaces.
Usage:
import std$bytes;
func test() {
var buf bytes$BufferWriter;
format(buf, "This is first line.");
}
Currently, custom variadic parameters can be defined, but they can only be passed to another variadic parameter. In other words, this feature is currently only used to wrap the format function.
Variadic parameters are indicated by ..., which can only be placed at the end of the parameter list, so only one set of variadic parameters can be defined.
For example, define a function that prints to standard output:
// Global object stdout defined above
func printf(fmt [&!#]byte, ...) { // Define variadic parameters
format(stdout, fmt, ...); // Forward variadic parameters
}
A block statement is a sequence of statements enclosed by { and }. The inner context is
nested; local variables declared inside cannot be used outside:
func test() {
println("block 1");
{
println("block 2");
{
println("block 3");
// Nesting has no limit
}
}
}
Select one branch to execute based on a control condition. Two types.
if is followed by a parenthesized conditional expression, then the statement executed when the condition matches; the
else branch is executed when it does not match and is optional.
The expression result, used as the condition, must be of type bool.
Simple conditional statement:
func abs(m int) int {
if (m < 0)
return -m;
else
return m;
}
The else branch can be omitted:
func printIfError(err uint) {
if (err == 0) return;
printf("Error: %u\n", err);
}
An initialization statement can be added before the conditional expression:
func test(m Map`int,*Node`, k int) {
if (var n,ok = m[k]; ok) { // n and ok are only within this block
printf("value of %d is: %s\n", k, n.value());
}
// printf("value of %d is: %s\n", k, n.value()); // Error: cannot use outside
}
Nesting else with if creates multi-branch structures:
func compare(a, b int) int {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
}
The switch statement has a conditional expression as the value to be matched, and supports multiple matching rules
(case). Each rule supports multiple constants, and when a rule is matched, the following block statement will be
executed.
func numberName(k int) {
switch(k) {
case 0 {
println("zero");
}
case 1 {
println("one");
}
case 2,3,4 {
println("more");
}
default {
println("Error");
}
}
}
The parentheses after for contain a control body, which must include a control condition expression and can also
include initialization and update substatements. This is followed by the statement(s) to be executed, called the loop
body.
The loop body repeats as long as the control condition is satisfied:
- The control condition is a
boolexpression; the loop body executes only when the result istrue. - The loop body is a single statement; if multiple statements are needed, they must be enclosed in a block statement.
A simple loop with only a condition expression:
func test() {
var i = 0;
for ( i < 100 ) {
println(i);
i += 1;
}
}
The complete control body format: [Initialization]; [Expression]; [Update]
- [Initialization] executes once before the loop begins.
- Each iteration: evaluate [Expression]; if
false, exit the loop; iftrue, execute the loop body, then execute [Update]. - Loop control operations within the body:
continuejumps directly to the next iteration.breakexits the current loop or a specified loop.
Example: loop 100 times, printing the value of i each time:
func test() {
for (var i = 0; i < 100; i += 1) {
println(i);
}
}
For arrays, a simpler way to iterate over all elements:
func test() {
var src []int = [0,1,2,3,4,5,6,7,8,9];
for ( v : src ) // value only
handle(j);
for ( i,v : src) // both index and value
println(i, v);
}
continue and break are also effective in iteration loops.
By default, iteration loops work only for arrays. For custom classes, a custom iterator can be implemented to enable
iteration loops.
Iteration is implemented via a helper macro named Iterator. Since loops are very common syntax, the macro is directly
expanded by the compiler.
The macro's fields are unrestricted and include four methods: initializer, condition, updater, get
| Method | Purpose | Parameters |
|---|---|---|
| initializer | Initialize iterator | None |
| condition | Loop condition | None |
| updater | Update iterator | None |
| get | Get value(s) | Unrestricted |
Multiple get methods can be defined, but they must have different numbers of parameters.
Example:
class Node`T` {
var next *Node`T`;
var value T;
}
export
class List`T` {
var head *Node`T`;
macro helper Iterator {
cursor *Node`T`,
index int;
initializer() {
cursor = head;
index = 0;
}
condition() {
cursor != nil
}
updater() {
cursor = cursor.next;
index += 1;
}
get(v) {
v = cursor.value;
}
get(i, v) {
i = index;
v = cursor.value;
}
}
}
func test(src List`*Team`) {
for ( t : src) { // matches the first get
// TODO
}
for (i, t : src) { // matches the second get
// TODO
}
}
Assignment operations can only be used in statements:
func test() {
var i = 0;
i += 2;
}
The left side of an assignment statement is an operand (the object whose value will be modified), and the right side is expressions:
func test(x,y int, u *User, a []int) {
x = 2;
u.id = 1;
a[0] = 8;
x, y = 2, 4;
u.id, x, y, a[0] = 1, 2, 4, 8;
}
Declaring one or a group of variables starts with var or const, followed by the variable name(s), then
the variable type.
vardeclares an ordinary variable, optionally with an initial value.constdefines immutable values; they cannot be reassigned and must be initialized at declaration.
func test() {
var r int = 5;
var g float64;
var a float64 = 0;
const pi float64 = 3.1415926;
const pi = 3.1415926; // When initialized, type can be omitted
g = 2 * r * pi;
a = r * r * pi;
}
Since the type can be omitted, two cases arise:
- When omitted, the right side can contain expressions of different types; the corresponding variable types are inferred independently.
- If types are explicitly specified, all left variables share the same type, and the right expressions must be compatible.
func test() {
var a,b int = 1,2;
// var a,b int = 1, "ggyy"; // Error: must be split into two statements
var x,y = 1,false; // x is int, y is bool
}
The return statement unconditionally terminates execution of the current procedure (function or method). When a function needs to return a value, the return statement carries a value to the caller.
For a function with no return value, its return statement must not carry a return value:
func foo(n int) {
if (n == 0) {
return;
// return 0; // Error: cannot return a value
}
// TODO something with n
}
If the procedure defines a return type:
- All return statements must carry a return value.
- The return value must be compatible with the function's return type, following the same rules as assignment.
- Every reachable branch must have a terminating statement.
For example:
func test(n int) bool {
return n > 0;
// return; // Error: missing return value
// return n; // Error: return value type mismatch
}
The return statement is a type of terminating statement.
Exception statements are divided into two types: throwing and handling exceptions.
Throwing an exception handles errors not addressed by return values. After throwing an exception:
- Execution of the current procedure terminates; instead of executing the return statement, an instance containing error information is thrown.
- If a called procedure throws an exception A, execution of the current procedure terminates at the call site, and exception A continues to be thrown.
func example1() {
throw new(Exception);
}
func example2() {
example1();
println("example1() always throws an exception, so this line will not execute!");
}
func example3() {
example2();
println("example2() also throws an exception, so this line will not execute!");
}
Once an exception is thrown, it propagates up the call chain until caught by a catch block.
The type of the thrown exception must be defined by the user, as detailed in Exceptions.
Exception handling statements consist of three parts:
tryblock: mandatory, wraps the code to be monitored.catchblocks: multiple allowed, each matching a different exception type. When matched, the corresponding code block executes; otherwise, matching continues. If no block matches, the exception continues to propagate outward. Since the caught instance must be escaped, the catch type must be a strong reference, and must be declared as required and unmodifiable.finallyblock: executes regardless of whether an exception occurred or was caught. If areturnstatement exists in thetryblock, the expression afterreturnis evaluated first, then thefinallyblock executes, then the return is completed. If nocatchblock or no match, thefinallyblock executes before re-throwing.
At least one of parts 2 and 3 must be present.
Complete example:
func calc() {
try {
step1();
step2();
} catch(e *!#NilException) {
println("Caught null pointer");
} catch(e *!#IllegalStateException | *!#IllegalArgumentException) {
println("Caught state error or argument error");
} finally {
println("Finally, execution continues after here");
}
return getResult();
}
With catch but no finally:
func calc() {
try {
step1();
} catch(e *!#IllegalStateException) {
println("Caught state error or argument error");
}
return getResult();
}
With finally but no catch:
func calc() {
try {
step1();
step2();
return getResult();
} finally {
println("Finally, execution continues after here");
}
}
finally can be used to release external resources, avoiding resource leaks. Example: closing a file:
func readTxt() String {
var f, er = open("tmp.txt");
if er != nil {
return string("");
}
try {
step1(f);
step2(f);
return getTxt(f);
} finally {
f.close();
}
}
Note: The parameter e in catch matching parentheses is a constant parameter.
This statement only takes effect when Debug mode is enabled; otherwise, it is ignored at compile time.
The assert statement is used to assert that an expression evaluates to true, and throws AssertException (a built-in exception) if it is false.
Usage example:
func query(id int) {
assert(id >= 0);
// Do querying ...
}
func test1() {
query(-1); // Passing a negative value fails the condition; assert will throw AssertException
}
Note: When Debug mode is not enabled, assert statements are ignored. If an assert statement calls an operation that produces side effects, it may affect code logic.
For example:
var counter int = 0;
func getAndInc() int {
counter += 1;
return counter;
}
func test() {
assert(getAndInc() > 0); // Calls getAndInc() which increments the counter
var c = getAndInc(); // The value of c differs between Debug and non-Debug builds
}
A terminating statement is not a distinct kind of statement, but a collective term for return statements and exception statements.
Every function that returns a value must end its statement list with a terminating statement.
Example of valid code:
func abs(n int) int {
if (n > 0) {
return n;
} else {
return -n;
}
}
A counterexample:
func abs(n int) int {
if (n > 0) {
// missing return statement here
} else {
return -n;
}
// The n > 0 branch falls through to here, clearly missing a return statement
}
Therefore, the following example is valid:
func abs(n int) int {
if (n > 0) {
// nothing done
} else {
return -n;
}
return n;
}
There is one special case that also counts as having a terminating statement: an infinite loop that the compiler can prove will never exit. Never exiting is equivalent to termination, because the only way to exit is for a statement inside the loop to throw an exception or forcibly terminate the program.
For example, where the loop condition is the constant true and the loop body contains no statement that would cause the loop to exit:
func test() {
while(true) {
run();
}
}
Note: if run() throws an exception, the entire test function terminates, rather than exiting the loop, so this is considered an infinite loop.
Terminating statements require the compiler to perform control flow analysis for more complex cases.
Refer to Variable Declaration Statement for declaration syntax.
Variables can be declared in two ways: mutable var and immutable const. The difference is that const cannot be
modified after its first assignment.
Variables have three categories: value types, reference types, enumerations, and function prototypes.
The variable and the instance are one; the variable's value is the instance itself. Assignment copies the instance's data:
- Basic type variables are just register values; modification usually requires a single machine instruction:
var a int = 1; // Variable a assigned literal 1, so a's value is 1 var b int = a; // Variable b assigned variable a, copying a's value to b b = 2; // a and b are different variables; modifying one does not affect the other - Derived types typically occupy more space than a register, so implementation often requires a set of instructions to
copy all field data:
class Vector { var x,y,z float64; } var a Vector = { x=1.0, y=0, z=-1.0 }; var b Vector = a; // Like primitive types, copy all field data from a to b b.x += 2.0; // Modifying b does not affect a; a.x remains '1.0' - Fixed-length array assignment is equivalent to iterating over all elements and assigning each:
For arrays of derived types, if the elements are value types, they are copied as well:
var a [4]int = [1,2]; // Initialize each element; unspecified ones get default (0 for int) var b [4]int; b = a; // Copy data from a to b // Equivalent to loop assignment for (var i = 0; i < a.size; i++) b[i] = a[i]; b[0] += 10; // Modifying b[0] does not affect a; a[0] remains '1'var a [4]Vector = [{x=1.0}, {x=2.0}]; // Initialize each element; unspecified get defaults (all fields 0) var b [4]Vector; b = a; // Copy data from a to b // Also equivalent to loop assignment for (var i = 0; i < a.size; i++) b[i] = a[i]; // Assignment refers to point 2 above b[0].x += 5.0; // Modifying b[0].x does not affect a; a[0].x remains '1.0'
Reference type variables are separate from instances; assignment changes the reference's target.
The instances a variable can reference are subject to type safety constraints:
- Class and interface references have polymorphism and abstraction constraints.
- Interface references to class instances have abstraction compatibility constraints.
For example, Device and Bus cannot be cross-referenced even if they have identical structure:
class Device {}
class Bus {}
func test() {
var a *Device = new(Device);
var b *Bus = new(Bus);
// a = b; // Error: Device reference cannot reference a Bus instance
}
References are nullable by default (can be nil). They can be marked as non-null (with !). Passing is one-way:
non-null → nullable.
For reverse passing, the variable must be explicitly checked for non-null (supported only for local variables, not
fields):
func f(a *!int, b *int) {
var x *int = a;
// var y *!int = b; // Error: cannot pass directly
if (b != nil) {
var y *!int = b; // Must explicitly check non-null; pass within the non-null branch
}
}
References can be marked as unmodifiable (with a # symbol), indicating that instances cannot be modified
through that reference. This is also a one-way transfer: modifiable → unmodifiable.
class Foo { var id int; }
func f(a *int, b *Foo) {
var x *#int = a; // Convert to unmodifiable reference
// *x = 1; // Error: cannot modify unmodifiable instance
var y *#Foo = b;
// y.id = 1; // Error: cannot modify unmodifiable instance
}
Unmodifiable references cannot be passed in reverse.
Except for array references, other references support the dereference operator *, which operates directly on the
referenced instance, both for reading and writing:
- Reading retrieves the instance's value and assigns it to a value type variable:
class Complex { var real, imag float; } func test(a &int, b *Complex) { var x int = *a; var y Complex = *b; } - Writing directly modifies the instance; unmodifiable references cannot be written to:
class Complex { var real, imag float; } func test(a &int, b *Complex, c &#Complex) { *a = 1; *b = {real=1.0, imag=-1.0}; // *c = {}; // ✖ Unmodifiable }
Strong references are denoted by * followed by a type symbol, e.g., var aDev *Device; declares a strong reference
variable aDev.
It can point to an instance of class Device or an instance of a subclass of Device:
func test() {
var b *Device = new(Device); // Initialized to point to a new Device instance
var a *Device = b; // Pass the instance referenced by b to a
a.speed = 10; // Modifications through a and b affect the same instance
printf("speed=%d", b.speed); // Prints: speed=10
}
Constant references declared with const must be initialized to point to an instance (or nil) and cannot change their
target:
const a *Bus = new(Bus);
// a = new(Bus); // ✖
// a = nil; // ✖
Variable-length arrays are also reference type variables; they can reference array instances of the same element type but any length.
Strong references indicate to the automatic memory manager whether an instance is in use:
- Instances referenced by strong references cannot be reclaimed.
- When an instance has no strong references, it should be reclaimed.
Phantom references do not affect memory deallocation. They can reference dynamically created instances or instances of value type variables, but only under certain conditions.
Phantom reference variables are constants and can only be declared with const:
func test() {
var gh Host;
const h1 &Host = gh;
}
Phantom references can only be local variables or parameters. They can be passed in the following scenarios:
- Value type variables within scope can be referenced by phantom references.
- Constant reference variables within scope can pass their instances to phantom references.
- Phantom references can pass instances to new phantom references.
- Local variables of strong reference type, after being passed to a phantom reference, cannot be modified within the phantom reference's scope.
- Within a class instance's phantom-reachable scope:
- Its value type fields can be phantom-referenced.
- Instances referenced by its constant fields can be phantom-referenced.
- Only phantom reference parameters can: They allow referencing temporary instances (i.e., instances that are about to be destroyed and released, including literals, initialization expressions, newly created instances, and return values).
Global variables can be referenced from any code:
var gDrv Driver;
const rDrv *Driver = new(Driver, {});
func use() {
const d1 &Driver = gDrv;
const d2 &Driver = rDrv;
}
Local variables must be used within scope for phantom references:
func sample1() {
var drv Driver;
const d1 &Driver = drv;
}
func sample2() {
const drv *Driver = new(Driver);
const d1 &Driver = drv;
}
func sample3() {
var drv *Driver = new(Driver);
{
const d1 &Driver = drv;
// drv = nil; // ✖
}
drv = nil;
}
Fields of instances can be phantom-referenced:
class Device {
const driver *Driver;
var disk Disk;
}
func sample1(dev Device) {
const drv &Driver = dev.driver;
const dk &Disk = dev.disk;
}
func sample2(dev *Device) {
const drv &Driver = dev.driver;
const dk &Disk = dev.disk;
}
Phantom reference parameters allow such usage:
func use1(a &int) {
}
func use2(a &Device) {
}
func use3(a [&]int) {
}
func sample() {
use1(0);
use1(new(int));
use1(get());
use2(Device{});
use2(new(Device));
use3([]int[1,2]);
use3(new([2]int));
}
func get() int {
return 0;
}
Refer to Enumerations for details.
Such a variable is either null or points to a function. Refer to Function Prototype for details.
Constants' immutability means the variable's value cannot change:
- For value type constants, all content is immutable:
- Each element of a constant array is constant.
- Fields of constant classes and structs cannot be modified.
- Reference type constants, once declared and initialized, can only point to a single instance until they go out of scope.
class Vector { var x,y,z int; }
class Data { var ve Vector; }
func test() {
const vec Vector = {x=1.0,y=2.0,z=3.0};
// vec.x = 4.0; // Error
const vecs [4]Vector = [{x=1.0,y=2.0,z=3.0}];
// vecs[1].x = 4.0; // Error
const data Data = {v={x=1.0,y=2.0,z=3.0}};
// data.ve.x = 4.0; // Error
}
Scope is the region where a variable is valid: a variable's lifetime begins at its declaration and ends when it leaves its scope. Scope is generally either local or global.
Local variables are declared within functions or methods:
- Their scope is the code block where they are declared and any nested inner blocks.
- Variables cannot be redeclared with the same name at the same level.
- When an inner block declares a variable with the same name (type may differ), the outer variable is shadowed and cannot be used.
func test() {
var v = "Hello"; // v's lifetime is within the current function
{
var s = "Fēng!"; // s's lifetime is within this block
printf("%s %s\n", v, s); // Can access outer variable v
}
// printf("%s %s\n", v, s); // Error: cannot access s from inner block
{
// printf("%s %s\n", v, s); // Error: cannot access s from another block
}
{
var v = "Dear Fēng"; // Inner redeclaration shadows outer v
printf("%s\n", v); // Prints: Dear Fēng
}
// var v = "Fēng"; // Error: cannot redeclare
}
Global variables must be placed at the topmost level of code, outside function and type definitions. Both variables and constants must be initialized. Their scope is global, and their lifetime is the entire runtime.
var count int = 0;
var qps int = 0;
var avg float64 = 0.0;
func doCount() int {
count+=1;
return count;
}
export can be used to make them available to other modules:
export const PI float64 = 3.1415926;
export var delay int = 0;
Here, the lifetime is defined as runtime.
bool literals are only true or false.
The null value is nil, representing the initial value of variables or fields—not pointing to any instance.
Applicable to reference type variables
and function prototype variables.
Strings are not belong to primitive type; the compiler encodes string literals. String literals are string constants and cannot be modified, so they can only be referenced by unmodifiable variables. String constants are not allocated on the function stack but are placed in a constant region:
func moduleName() [*#]byte {
var r [*#]byte = "test-module";
return r; // Still usable after leaving function moduleName
}
func test() {
printf("module: %s\n", moduleName());
}
This type of literal is only used to initialize fixed-length array types.
List array elements within square brackets: [1,2,3], ["Hello", "Good"], etc.
The array type can be placed explicitly before the literal; this is related to type inference — see Fixed-Length Array.
For example:
var ga1 = [4]int[1,2,3]; // Explicitly declare a length-4 int array
If the length is omitted, the length is inferred from the number of elements:
var ga2 = []int[1,2,3]; // Equivalent to: [3]int
Macros are code snippets with specific formats determined by their specific purpose. A specific purpose refers to some language feature; for example, when a macro is used to implement custom operations, the operation itself defines the code format. Currently, macros are only supported within classes and interfaces.
Macros are uniformly defined with the macro keyword. The main types are procedural macros and class macros.
Procedural macros resemble ordinary procedures (functions or methods), with a name, parameter list, and sequence of statements:
- Names do not interfere with other names; they can share names with other elements.
- Parameter lists differ from function parameters; they are context variables.
- The statement sequence is an ordinary sequence, optionally ending with an expression.
- Macros cannot be called.
For general grammar examples, please refer to Custom Operations.
Consist of a name, field table, and procedural macros, capable of preserving intermediate state. Example: implementation of iteration loops for derived types.
Generics are general, common types, similar to C++ templates, used to implement generic classes and functions. Unlike C++, generics here are checked before expansion, so no specific operations can be performed before expansion—only values can be passed.
Generics are marked with backticks (`).
Generic parameters are specified during definition:
class Box`T`{var t T;}
func save`T`(t T) {}
Concrete types are passed during use:
func f() {
var ib Box`int` = {t=100};
save`int`(100);
}
Multiple parameters can be specified:
class Pair`K,V`{
var k K;
var v V;
}
func make`S,T`(s S, t T) *Pair`S,T` {
return new(Pair`S,T`, {k=s,v=t});
}
func test(k int) {
var b Pair`int,bool` = {k=k, v=k%2==0};
var p *Pair`int,bool` = make`int,bool`(k, k%2==1);
}
Generic parameters can be defined for functions, interfaces, classes, and class methods.
Besides reducing boilerplate code, generics can solve self-dependency issues, e.g.:
var bb Box`Box`int``;
var bbb Box`Box`Box`int```;
Without generics, the Box class above would fall into recursive initialization and fail to compile:
class Box {
var t Box;
}
Generic parameters defined on functions can be used as types within the function body:
func go`R`(r R) {
var v R = r;
}
Concrete types can be passed arbitrarily:
func test(i int, b bool, a [16]byte, r *A) {
go`int`(i);
go`bool`(b);
go`[16]byte`(a);
go`*A`(r);
}
Within another generic function, a generic parameter can be passed:
func run`P`(p P) {
go`P`(p);
}
Generic parameters defined on classes can be used in fields, methods, and within method bodies:
class Box`E` {
var value E;
func set(v E) {
value = v;
}
func get() E {
return value;
}
}
The box class defined above can hold any instance:
func use() {
var box Box`[*]int`;
box.set(new[15]int);
box.get()[0] = 100;
}
Class methods can also have their own generic parameters, like functions:
class Box`E` {
var value E;
func set(v E) {
value = v;
}
func get() E {
return value;
}
func map`R`(f func(E)R) Box`R` {
return {value=f(value)};
}
}
func positive(i int) bool {
return i > 0;
}
func use() {
var b1 Box`int`;
b1.set(-100);
var b2 = b1.map`bool`(positive);
}
Methods with generics do not support polymorphism; they cannot be overridden or override other methods.
When inheriting a generic class, you can pass concrete types or generic parameters:
class Pair`K,V` {
var k K;
var v V;
}
class MyPair`V` : Pair`int,V` {
// This class actually has only one generic parameter 'V'
}
func use() {
var p1 MyPair`*int` = {k=1,v=new(int)};
}
Interface implementation follows the same pattern:
class Node`T` (Inode`T`) {
}
Generic parameters for interfaces can only be defined on the type itself; methods do not support them:
interface Box`V` {
set(V);
get()V;
}
Example implementing class:
class MyBox`E` (Box`E`) {
var value E;
func set(v E) {
value = v;
}
func get() E {
return value;
}
}
Generic inference is now supported, where the type parameters of the provider are inferred based on the receiver's type parameters.
The supported inference scenarios are listed below.
Generic functions can infer type parameters based on the actual argument types at the call site:
func gen`T`(t T) T {
return t;
}
func use(n int) {
var v int = gen(n); // Argument is int, so T is inferred as int, return type is int
}
Inference can also be based on the return type:
func empty`T`() T {
var t T;
return t;
}
func use(n int) {
var v int = empty(); // Left-hand type is int, so T is inferred as int
}
Type inference supports composite types such as tuples, arrays, generic classes, etc.:
func gen1`T1,T2`(a (T1,T2)) T1 {
return a.0;
}
class A`T1,T2` {
var t1 T1;
var t2 T2;
}
func gen2`T1,T2`(a &A`T1,T2`) T1 {
return a.t1;
}
func gen3`T`(a [&]T) T {
return a[0];
}
func use() {
{
var a (int, bool) = (11,true);
var v int = gen1(a);
}
{
var a A`int,bool` = {};
var v int = gen2(a);
}
{
var a [4]int;
var v int = gen3(a);
}
}
However, for initialization expressions (array expressions, field expressions, and tuple expressions), type annotations must be present for inference to work:
func use() {
{
// var v int = gen1((11,true)); // ✖ Cannot infer
var v int = gen1((11:int,true:bool));
}
{
// var v int = gen2({t1=1,t2=false}); // ✖ Cannot infer
var v int = gen2(A`int,bool`{});
}
{
// var v int = gen3([2]); // ✖ Cannot infer
var v int = gen3([]int[2]);
}
}
Class methods support generic parameters; inference works the same as for functions.
When new is used with a generic class, type parameters can be inferred from the left-hand side:
class Box`T` {
var v T;
}
class BigBox`S`:Box`S`{}
func makeInt() *Box`int` {
return new(Box);
}
func makeBigInt() *Box`int` {
return new(BigBox);
}
func make`E`() *Box`E` {
return new(Box);
}
func makeBig`E`() *Box`E` {
return new(BigBox);
}
When a function prototype points to a generic function, type parameters can also be inferred:
func filter`T`(t T) T {
return t;
}
func test() {
var f func(int)int = filter;
}
The exception type that can be thrown must be the built-in Exception class or one of its subclasses. The definition of Exception is as follows
(built-in):
class Exception {
var fn uint64;
var line uint32;
func trace(fnAddr uint64, lineNum uint32) {
fn = fnAddr;
line = lineNum;
}
}
In addition to the built-in Exception, there are two more built-in exception classes. They are built-in because they are thrown dynamically at
runtime:
NilException: thrown when a reference used at runtime isnil.OutOfBoundsException: thrown when an array index access exceeds the actual length.
Custom exceptions can be defined:
class MyException : Exception {
}
func run() {
throw new(MyException);
}
Compile-time constants are constants whose values can be deduced and calculated at compile time. The following are compile-time constants:
- Literals.
- Variables declared with
constwhose type is a primitive type or a string literal. - All expressions composed entirely of compile-time constants are also compile-time constants, because their results can be computed at compile time.
In test mode, the compiler only compiles unit tests.
Test case functions must be annotated with @Test:
func far() {}
@Test
func testFar() {
far();
}
Test case functions can be placed anywhere in the module, mixed with regular code. The compiler distinguishes them automatically.
For example, when combined with a main function, test mode skips main and only compiles test cases; compilation mode ignores all test cases:
import std$os;
@Test
func testFar() {
os$printf("This is testcase\n");
}
func main() {
os$printf("This is main\n");
}
Test case functions must not have parameters or return values. Counter-examples:
@Test
func test1() int { return 0; } // ✖: must not have a return value
@Test
func test2(a int) {} // ✖: must not have parameters
Test case functions cannot be called by other functions or methods. Counter-examples:
@Test
func test1() {}
@Test
func test2() {
test1(); // ✖: cannot be called by another test case
}
func far() {
test1(); // ✖: cannot be called by a function
}
func main() {
test1(); // ✖: cannot be called by the main function
}
class Tr {
func run() {
test1(); // ✖: cannot be called by a method
}
}
Fēng supports direct use of C functions and types.
Fēng's struct/union share not only the same keywords as C, but also the same memory layout, so they can be passed directly.
Fēng's primitive types correspond exactly to C in both bit width and signedness, and can be passed by value directly; the bool type also corresponds to C's bool type.
Pointer handling: Fēng cannot directly use C pointers — the compiler converts all pointers to the uint64 type during parsing.
Fēng's regular reference type variables can be converted to uint64 (only to uint64), allowing them to be passed to C pointer parameters.
Note: reverse conversion is strictly prohibited — reference values can only be obtained through new (see new Expression), so there are no safety concerns at the Fēng level.
Array references cannot be directly converted to uint64; instead, use the built-in field values to obtain the pointer (also of type uint64) for passing to C.
For example:
func use(a *int) {
var p uint64 = uint64(a);
// var r *int = p; // ✖ reverse conversion from uint64 to reference is not allowed
}
Modules can be written in pure C, but mixing C and Fēng code within a single module is not supported.
C module headers declare the symbols to be referenced (functions, types, or global variables). The compiler parses them, adds the module path prefix, and the module can then be imported and used like any ordinary Fēng module.
For example, a C module with the path jjj$mm has the following directory structure:
jjj/mm/
├── mm.h ← header: struct and function declarations
└── mm.c ← implementation: function bodies
Header mm.h:
struct Complex { float r, i; };
struct Complex add(struct Complex a, struct Complex b);Implementation mm.c:
#include "mm.h"
struct Complex add(struct Complex a, struct Complex b) {
return (struct Complex){.r = a.r + b.r, .i = a.i + b.i};
}It can then be used directly in Fēng:
import jjj$mm;
func test() {
var a mm$Complex = {r = 1.1}; // i defaults to 0
var b mm$Complex = {i = 1.1}; // r defaults to 0
var c = mm$add(a, b);
}
To call an external C library (libc or third-party), simply place a header file in the module directory that includes the target library's header and declares the symbols you need.
libc is linked automatically — no extra configuration required. See the std/os module: the header file.h includes <stdio.h>, allowing file.feng to directly call libc functions such as fopen and fwrite.
Third-party libraries (e.g. pthread, m, dl) are not auto-linked. Create a feng.cfg file in the module directory to declare the required libraries:
# feng.cfg — declare libraries to link, comma-separated (without -l prefix)
link=pthreadMultiple libraries are comma-separated: link = pthread,m.