Functions
Hybrix functions are like reusable recipes. They allow you to perform an action multiple times. A loop statement also provides a way to do something multiple times, but loop actions always happen one after another, and always in the same way. A func is much more flexible: You can do the action from any part of your program. And you can use parameters to change how the code behaves.
Math functions
Functions also appear in mathematics, but the concept is a bit different: Math functions represent relationships between inputs and outputs, whereas computer functions represent a step-by-step procedure that produces the output. Hybrix's functions are kept very simple to help you see how a
funcgets converted into Chombit CPU instructions. Other programming languages like TypeScript or Haskell offer more sophisticated functions with math-like qualities. These language features enable powerful abstractions, but if misused they can lead to inefficient or awkward programs. The reason is that mathematics focuses on the relationship between inputs and outputs, ignoring the calculation steps. In fact, math generally considers two functions to be equivalent if their outputs are the same, which is not true at all for computer programs. The calculation details directly determine the speed, memory usage, and maintainability of your code—the actual measures of success or failure in software engineering.
Here's a Hybrix function that calculates the maximum of two numbers. The maximum of 1 and 5 is 5, the maximum of 7 and -3 is 7, and so forth.
module main
# Define a function called "get_max":
func get_max(x: int, y: int): int
# If x is bigger, then x is the result
if x > y
do return x
# Otherwise y is the result
return y
end func
func start()
var n: int
# Call the function get_max() and put its return value
# into the variable "n":
n <- main::get_max(1, 5)
# (n is now 5)
end func
end module
Important points about functions:
- Functions are always defined using
func. - Hybrix functions always have a name like
get_max. The name is made of letters, numbers, and underscore (_). The name must not start with a number. - Functions must be defined inside
moduleorclass. - Other code can call a function (
main::get_max(1, 5)) - Hybrix programs always have a function
main::start(). The system calls this function automatically to start your program. Ifmain::start()returns, then your program is finished. - Functions can have parameters such as
xandyin the example above. - The caller's arguments
(1, 5)become the values of the parameters (1goes intox,5goes intoy). - Function parameters can specify a data type. For example,
x: intmeans thatxwill be a 4-byte integer (int). - If the parameter type is omitted, then the compiler will assume that it is
int. For example, we could have writtenfunc get_max(x, y): intinstead offunc get_max(x: int, y: int): int. - A
returnstatement ends the function and optionally sends a return value back to the caller. - The data type of the return value, called the return type, is specified in the function signature (for example
: intat the end offunc get_max(x: int, y: int): int). - If the return type is omitted, then there is no return value. Such functions do not produce a value that can be stored or used in an expression. For example, if we wrote
func get_max(x: int, y: int), then we could callmain::get_max(1, 5)as a standalone statement but we cannot assign it withn <-. - If the return type is omitted, the
returnstatement can still be used to end the function, but it must not include a return value.
Module functions
When referring to functions that belong to a module, you must always use :: to indicate the module name. In the above example, the module name is main and the function name is get_max, therefore we must write main::get_max. The same rule applies to variables defined inside a module. In this way, the same function name can appear in different modules (for example console::think(), sound::think(), engine::think()).
Here's another example, showing how to use get_max() to compute the maximum of 3 numbers. get_max3() works by computing the maximum of x and y, then computing the maximum of that result and z.
module main
func get_max(x: int, y: int): int
if x > y
do return x
return y
end func
func get_max3(x: int, y: int, z: int): int
var result: int
result <- main::get_max(main::get_max(x,y), z)
return result
end func
func start()
var n: int
n <- main::get_max3(7,11,4)
# n is now 11
end func
end module
Class functions
When referring to functions that belong to a class, you must always use . to indicate the object. In the example below, the object is the variable named p, so we write p.greet():
class person
var name: string
func greet(color: bool)
if color then
console::print("{6}")
else
console::print("{1}")
end if
console::print("Hello, ")
console::print(.name)
console::print("!{n}")
end func
end class
module main
func start()
# Minimal initialization of the Hybrix framework console
console::init(io::tilemap_a)
var font_layer: tile_layer
font_layer <- new tile_layer(io::tilemap_a, io::tileset_a_addresses)
font_layer.load_tileset(art::fonts[0])
var p: person
p <- new person()
p.name <- "Cameron"
# Prints "Hello, Cameron!" with color
p.greet(true)
p.name <- "Taylor"
# Prints "Hello, Taylor!" without color
p.greet(false)
end func
end module
Classes also support two other special kinds of member functions:
-
Virtual functions use the
hookkeyword instead offunc, but otherwise look like regular functions. They are used with class inheritance. -
Class constructors are declared using
constructorinstead offunc. They initialize newly created class instances.
Out parameters
Above, we said that a function can use return to pass an output value back to its caller. But what if you need multiple outputs? You could define a class to hold them, but it can be inefficient to allocate and free a class for one function return. As an alternative, the out modifier enables function parameters to return outputs. For example:
module main
var location_table: int[]
# If item_id is not found in the table, then "false" is returned
func try_get_location(item_id: int, out item_x: int, out item_y: int): bool
var i: int
i <- 0
loop
if i >= main::location_table.size
do drop
if main::location_table[i] = item_id then
item_x <- main::location_table[i + 1]
item_y <- main::location_table[i + 2]
return true
end if
i <- i + 3
end loop
return false
end func
end module
data main::location_table
[
10, 7, -1, # item #10 is at (7, -1)
20, 5, 0 # item #20 is at (5, 0)
]
end data
Unlike a return value, the item_x and item_y outputs can be read and written anywhere inside the function body. Corresponding out modifiers must be included when calling the function, for example try_get_location(10, out x, out y):
module main
func start()
# Set up the console
engine::init()
io::paint_mode <- 0 # (show printed output immediately)
console::init(io::tilemap_a)
engine::tile_layer_a.load_tileset(art::fonts[0])
var x: int, y: int
if main::try_get_location(10, out x, out y) then
# prints "7,-1"
console::print_int(x)
console::print_char(',')
console::print_int(y)
console::print_char('{n}')
end if
# item_x and item_y are initialized to 0 at the start of the call...
if main::try_get_location(30, out x, out y) then
# (this never runs because item #30 is not found)
console::print_int(x)
end if
# ...therefore this line prints 0
console::print_int(x)
end func
. . .
end module
Important points for out:
- The
outparameters must come last. For example,func f(out a: int, out b: int, c: int): boolis not allowed. - The calling convention ensures that
outparameters always start with a value of 0 ornull. - The function isn't required to assign to them before returning.
- After the call completes, the final value of each
outparameter is copied into its corresponding target, for exampleitem_xis copied intox. - This copying goes right-to-left, which affects the results if an argument is duplicated. For example,
try_get_location(10, out z, out z)will end withitem_xinz(overwritingitem_y). - The
outargument must be a simple local variable such asx; in the future, more complex expressions may be supported. - A class constructor cannot have
outparameters. - Often
outcan be avoided by simpler designs: For example,func find_index_of(list: string[], item: string, out index: int): boolcan be simplified tofunc find_index_of(list: string[], item: string): intby returning -1 to represent "not found". (This trick wasn't an option fortry_get_location()because special values like -1 are possible X and Y coordinates.)
Assembly functions
If you look at the system library code, you will find some func definitions whose bodies say chombit or intrinsic.
Chombit functions
Here is an example of a chombit function:
module kernel
. . .
func copy_memory_bytes(target: int, source: int, num_bytes: int)
chombit
end func
. . .
end module
chombit means that the function has been written using hand-coded assembly language, not the Hybrix language. The implementation of the kernel::copy_memory_bytes() function is shown below (slightly simplified):
# -----------------------------------------------------------------------------
@kernel.kernel.copy_memory_bytes:
push fp
move fp, sp
add sp, 16
# i:-20 arg_target
# i:-16 arg_source
# i:-12 arg_num_bytes
# i:-8 return ip
# i:-4 fp
# i:4 target
# i:8 source
# i:12 source_end
move i:4, i:-20
move i:8, i:-16
move i:12, i:8
add i:12, i:-12
# Avoid overshooting the end
add i:12, -15
# Copy blocks of 16 bytes using an unrolled loop
@kernel.kernel.copy_memory_bytes.l_0:
compare i:8, i:12
if not less
jump @kernel.kernel.copy_memory_bytes.l_1
load i:0, [i:8]
add i:8, 4
store [i:4], i:0
add i:4, 4
load i:0, [i:8]
add i:8, 4
store [i:4], i:0
add i:4, 4
load i:0, [i:8]
add i:8, 4
store [i:4], i:0
add i:4, 4
load i:0, [i:8]
add i:8, 4
store [i:4], i:0
add i:4, 4
jump @kernel.kernel.copy_memory_bytes.l_0
@kernel.kernel.copy_memory_bytes.l_1:
# Undo the adjustment
add i:12, 15
# Copy the remainder
@kernel.kernel.copy_memory_bytes.l_2:
compare i:8, i:12
if not less
jump @kernel.kernel.copy_memory_bytes.l_3
load b:0, [i:8]
add i:8, 1
store [i:4], b:0
add i:4, 1
jump @kernel.kernel.copy_memory_bytes.l_2
@kernel.kernel.copy_memory_bytes.l_3:
add sp, -16
pop fp
pop ip
fill 4
Hand-coded functions are often much more efficient than what the compiler produces. kernel::copy_memory_bytes() is implemented this way because it is an important system function that needs to run as fast as possible.
You cannot write your own chombit functions yet, but in the future, Hybrix will support this.
Intrinsic functions
Here is an example of an intrinsic function:
module math
. . .
func abs(x: int): int
intrinsic
end func
. . .
end module
The math::abs() function calculates the absolute value of a number, which converts negative numbers to positive numbers. For example, math::abs(-6) is 6, math::abs(1) is 1, and so forth.
intrinsic means that this function's implementation is generated directly by the Hybrix compiler. For example, when you write x <- math::abs(-3), the compiler generates assembly code that puts 3 directly into x. It does not even emit a function call, nor does it emit instructions to negate -3. For very small functions, this is a big savings.
You cannot implement your own intrinsic functions. They are an internal feature of the Hybrix compiler.