Skip to main content

Variables

When your program code works with data types, it always accesses them using variables. You can think of variables like little boxes that store some information, such as a number. The kind of information is the variable's data type. Variables always have a name (the identifier) such as x or number_of_coins. If you are familiar with the Chombit CPU, you can also think of a variable as a name for a memory address that stores some data.

The Hybrix language has four basic kinds of variables:

1. Local variables

Local variables are declared inside a func or hook definition, using the var keyword:

module main
func start()
# "x" is a local variable inside the function "start()"
# which is a member of the module "main".
var x: int

123 -> x

# Define two variables "y" and "z".
var y: byte, z: byte

# This is equivalent to "var i: int"
var i
end func
end module

The var definition must come before any statements that reference its variable, in the current block or a parent block. For example:

module main
func start()
if true then
1 -> x # ⚠️ ERROR: "var" does not precede "x"
var x: int
if true then
2 -> x # this is okay
end if
else
3 -> x # ⚠️ ERROR: "var" is in a sibling block
end if
end func
end module

The data type (int, byte, etc.) comes after the colon (:). If you omit the colon and type, the compiler makes it an int.

Local variable memory storage

Local variables are stored as registers on the Chombit CPU stack. This way, they do not consume any memory unless the function is being called. If the function is called again before it finishes (for example, if the func calls itself recursively), then the same variable can have multiple allocations at once. In other words, each function call gets its own copy of the local variables.

You must initialize a variable (for example, 123 -> x) before you can read its value (for example, x + 1 -> x).

2. Function parameters

Function parameters appear inside the ( ) of a func definition, without the var prefix. In the example below, width and height are function parameters:

module rectangle
# "width" and "height" are function parameters:
func get_area(width: int, height: int): int
var area: int
width * height -> area
return area
end func
end module

Function parameters behave exactly like local variables, except that their initial value is provided by the caller of the function. For example, if the function call is rectangle::get_area(2,3), then the function will start with width equal to 2, and height equal to 3.

You are allowed to change the value of a parameter like width inside the function, using it like a local variable:

module rectangle
func get_area(width: int, height: int): int
# Store the result back into "width", overwriting the old value:
width * height -> width
return width
end func

func get_square_area(side: int): int
var area
# Although the "side" value goes into "width" above,
# overwriting "width" will not affect "side"
rectangle::get_area(side, side) -> area
return area
end func
end module

Function parameter memory storage

Function parameters are also stored as registers on the Chombit CPU stack, just like local variables. The only difference is that the function's caller always initializes them.

3. Module member variables

Variables can also go inside a module definition, for example:

module game_board
# The full name will be "game_board::initialized"
var initialized: bool

func init()
# If init() was already called, don't initialize again
if game_board::initialized
do return

true -> game_board::initialized

# (more code here)
end func
end module

In the above example, game_board::initialized is a module member variable, sometimes called a global variable. Unlike local variables, module variables always exist and cannot have multiple copies. When you access them, you must use the :: operator to indicate the module. As a result, you could also have a local variable called initialized without confusion. The same variable name can also be reused in other modules without confusion:

module game_board
# The full name will be "game_board::initialized"
var initialized: bool

func init()
var initialized: bool

# "initialized" and "game_board::initialized" are two different variables:
true -> game_board::initialized
false -> initialized
end func
end module

module music_library
# The full name will be "music_library::initialized"
var initialized: bool
end module

Any func can access module variables, even if the func belongs to a different module or a class. If that was not your intention—if the variable is meant to be private to its module—you can add an _ prefix to indicate that. For example:

module game_board
# Other modules should not read or write this member:
var _initialized: bool

# Other modules can assign "visible" to control whether the
# game board will be rendered:
var visible: bool
end module

Module variable memory storage

Module member variables are stored in the Chombit heap memory, in a special memory block that holds all the members of all modules. When the program starts up, this memory is reset to all zeroes: bool variables will start out as false, int variables will be 0, pointers will be null, and so forth.

4. Class member variables

Variables can also go inside a class definition, for example:

class rectangle
# "x" and "y" are member variables of "rectangle":
var x: int, y: int

func get_area(): int
var area: int

# When accessing them inside "rectangle", write ".x" or ".y"
.x * .y -> area

return area
end func
end class

module main
func start()
var my_rect: rectangle, area: int
new rectangle() -> my_rect

# When accessing them from another class or module,
# use the variable name followed by ".x" or ".y":
10 -> my_rect.x
20 -> my_rect.y

my_rect.get_area() -> area
end func
end module

Class members must always be accessed using the . prefix. For example, .x (for code inside the class) or thing.x (for code outside the class). This means that you can have a local variable called x and a member variable called .x without confusing them.

Class variable memory storage

Class member variables are stored in the Chombit heap memory, in a normal heap block that is allocated by new. In the above example, new rectangle() -> my_rect creates a new instance of the rectangle class, allocating a heap block. Thus, you can have many instances of a class, and each instance will get its own separate .x and .y storage.

Newly allocated memory is reset to all zeroes: bool variables will start out as false, int variables will be 0, pointers will be null, and so forth.

The Hybrix garbage collector automatically frees the heap block when it is no longer used, for example if null is assigned to my_rect.

View var

A variable can be declared as view var to prevent other classes and modules from modifying it, while still allowing them to read it.

Suppose we are tracking a range of selected characters in a text string:

class selection
var left: int
var right: int
var length: int

var color: byte

func set_range(left: int, right: int)
if left > right
do kernel::fail("Invalid selection")

.left <- left
.right <- right
.length <- right - left
end func
end class

The set_range() function checks that left and right are valid, then computes the length. Other code can set color to any pixel color code, but to avoid mistakes, the other variables should only be modified via set_range().

Hybrix has a naming convention where an underscore (_) prefix indicates that an identifier is "private"; other code should not access it. So, one solution is to use functions to ensure read-only access:

class selection
# These are private (naming convention):
var _left: int
var _right: int
var _length: int

# Whereas "color" remains public:
var color: byte

func set_range(left: int, right: int)
if left > right
do kernel::fail("Invalid selection")

._left <- left
._right <- right
._length <- right - left
end func

func get_left(): int
return ._left
end func

func get_right(): int
return ._right
end func

func get_length(): int
return ._length
end func
end class

This works, but it's a bit awkward to write selection.get_left() instead of selection.left. More importantly, those function calls aren't free. In the debugger, the selection.get_left() statement will be something like 31 CPU cycles versus 4 cycles for selection.left. Using view var provides a better solution:

class selection
# These are read-only outside the "selection" class
view var left: int
view var right: int
view var length: int

# Whereas "color" remains public:
var color: byte

func set_range(left: int, right: int)
if left > right
do kernel::fail("Invalid selection")

.left <- left
.right <- right
.length <- right - left
end func
end class

module main
func start()
var s: selection, x: int
s <- new selection()

# This is okay
x <- s.left

# ⚠️ ERROR: This assignment is not allowed because the target is a "view var"
s.left <- 123
end func
end module

Important points:

  • view var can be used for member variables of a module or class (but not local variables)
  • The view vars can be read by other classes and modules...
  • ...but they can only be modified by their containing module or class or its subclasses.
  • Unlike the _ naming convention, the compiler verifies view var access and will report an error for invalid access.
  • The runtime code is identical for var and view var, so there is no performance penalty.
  • For classes and array objects, view does not apply to the nested state. For example, view var items: int[] prevents reassigning the items pointer, but does not prevent modification of array elements.