SCUA

Manual

Modules

Once a program outgrows a single file, you split it into modules. A module is an ordinary .scua file that hands back some values for other files to use. You load one with import, and you reach into it with field access.

#A module is a file that returns its exports

Any .scua file whose top-level code ends in a return is a module. Whatever it returns is what other files see when they import it. The idiom is to return a record of functions.

Put this in dice.scua:

-- dice.scua — a small module. Its exports are the record it returns.
fn roll(sides, n)
  let total = 0
  let i = 0
  while i < n do
    total = total + (i % sides) + 1   -- deterministic stand-in for a real roll
    i = i + 1
  end
  return total
end

fn label(total)
  if total >= 18 then return "great" else return "ok" end
end

return { roll = roll, label = label }

The two functions are private to the file until you name them in the returned record. The record is the public surface; anything you leave out stays internal.

#Importing a module

import dice looks for dice.scua next to the file doing the import, runs it once, and binds its exports to the name dice. You then call its functions as fields, with dice.member(...).

Put this in game.scua, in the same folder as dice.scua:

import dice

let total = dice.roll(6, 4)
print(`rolled {total} ({dice.label(total)})`)

Run it:

$ scua game.scua
rolled 10 (ok)

The name after import is both the file to load (without the .scua) and the name you use to reach the exports. Imports resolve relative to the importing file, so import dice always means the dice.scua sitting beside game.scua, wherever you run the command from.

#Keeping modules in another folder

When shared modules live somewhere other than next to the importing file, point the runner at the extra folder with --mod-path:

$ scua --mod-path libs game.scua

Now import dice is tried next to game.scua first, then in libs/, and uses the first match. Repeat --mod-path for several folders; they're searched in the order given. The built-in modules (math, list, …) always resolve first. See the CLI reference for details.

Coming from Lua: where modules come from, vs. what they can touch

If you've used Lua's require, the --mod-path directories above are SCUA's analog of package.path: an ordered list of places a module name is looked up, first match wins. Lua keeps that list in a mutable global you can rewrite at runtime; SCUA keeps it per-program (and, for isolated partitions, per-partition), and today you set it on the command line. A future release adds setting it in code for trusted programs — closer to Lua's dynamic feel, but scoped to the partition rather than the whole process, so one tenant can't change another's lookups.

There's one place SCUA deliberately parts ways with Lua, and it's worth understanding up front. Lua's path system does two jobs: it finds your .lua files and, via package.cpath, it dynamically loads native .so/.dll C libraries — arbitrary native code with full machine access. SCUA splits those two jobs apart on purpose:

  • Finding code is what import and the search path do. What they load is always SCUA source — never a native binary. There is no cpath, and no "drop a compiled library on the path and load it."
  • Reaching native power — the network, the filesystem, a database, the clock — is a separate system: capabilities. You don't load a library to get network access; the host running your program grants a net (or fs, http, …) capability to the partition, off by default. The granted capability arrives as an ordinary import (import net) that simply isn't there unless it was granted.

The short version: the search path decides where your code comes from; capabilities decide what that code is allowed to touch. Lua bundles both into the dynamic path, which is convenient but is exactly why Lua code is hard to sandbox — any module can pull in a native library and do anything. Keeping them separate is what lets SCUA run untrusted code safely. (Genuine native extensions — your own C compiled against SCUA — are a possible future via a stable host ABI, but a deliberate, gated one, not a path you load binaries off of.)

#Renaming an import

If the module name clashes with something else, or you just want something shorter, rename it with as:

import dice as d

print(d.roll(20, 1))
$ scua game2.scua
1

d now refers to the same exports dice would have.

#What an import binds

import name gives you exactly the value the module returned, under the name name. If a module returns a record, you get a record and reach members with a dot. A module can return anything, though: a single function, a number, a record of records. The convention is a record of functions because it reads well at the call site (geom.add(...), dice.roll(...)) and keeps related code together.

A module's top-level code runs once, the first time it's imported. If two files import the same module, they share the one result.