Nikki stdlib

The nikki standard library (stdlib) contains many (WIP) libraries to create programs. Calling functions in the stdlib is done the same as regular functions, though many of the functions in the stdblib are actually implemented in zig.

Calling functions

To call a function you must declare it in the constants sections. Here, we declare the std::print function:

(def "my-module"
  (let print (function "std" "print"))

  ; other declarations
  ...)

The form (function <module> <name>) declares a function in the constants table. Modules are organized using double semicolons, for example, std, std::math, std::fs, std::fmt. Then <name> is the name of the function to declare.

So the above declaration (function "std" "print") creates a reference to the print function that lives inside the std module. Its full name would be std::print.

To call a function use the .calli opcode, like this:

(defun "<entrypoint>"
  (.calli print)
  ; more code)

This will call the print function we declared before. Here’s a more complete example:

(def "my-module"
  ; Here we declare the function
  (let print (function "std" "print"))

  (defun "<entrypoint>"
    ; Here we call the function
    (.calli print)

    ; more code...
    (.ret)))

Sending data

Functions may receive parameters & return data. Each function defines what data it expects to receive and return.

To send data you use registers 11 to 19, by copying the data to that register, then calling the function. In the next example we do that:

  ; std::print expects a String at rf11
  (let print (function "std" "print"))
  (let name (string "John Doe\n"))

  (defun "<entrypoint>"
    ; Here we load the string `name` into rf11
    (.lr rf11 name)
    ; register 11 is now set up, so the call will be able to use its data
    (.calli print)

    ; more code

As another example, std::math::mod_u64 expects 2 parameters: a u64 in rv11 and a u64 in rv12. So we would set up those 2 registers before calling with .calli.

As the programmer, it is your job to properly set up any register the function expects. And function implementors must document what registers & values are needed for a function to run.

Receiving data

Functions return data also through registers 11 to 19. So after calling a function, any returned data will be stored in those registers.

The next example defines and calls std::math::mod_u64:

  ; std::math::mod_u64 expects:
  ;   - a: u64 in rv11
  ;   - b: u64 in rv12
  ; and returns:
  ;   - the modulo `a % b` in rv11
  (let mod_u64 (function "std::math" "mod_u64"))
  (let a (u64 16))
  (let b (u64 5))

  (defun "<entrypoint>"
    ; Here we set up rv11 and rv12
    (.lv rv11 a)
    (.lv rv12 b)

    ; now we call the function
    (.calli mod_u64)
    ; after the function runs, our result is in rv11
    ; so we use it for other operations:

    (.mod_u64 __ rv11 __)
    ; more code

Error handling (TBD)

Nikki (and the nara vm) support errors as values. When a function may fail, it will return a error value in register rv10/rf10, and the caller should check those to see.

For instance, say the function std::math::div_u64 may fail if b == 0. So that function:

Furthermore, std::math::div_u64 specifies these values for rv10:

So, after calling std::math::div_u64 the caller should check the value of rf10, and then decide if its safe to use rv11.

Error convention

Registers 10 to 19 are caller saved.

When a function may error it uses rv10 to signal the error. In such functions:

The .calli-assert opcode is provided for convenience, it will call a function and halt execution if rv10 is not set to 0.