Skip to content

Modules and Functions

A Skein file contains a single module declaration:

module Hello {
fn greet(name: String) -> String {
"Hello, ${name}!"
}
}

Skein modules compile to BEAM modules with Elixir-compatible names:

Skein NameBEAM Module Name
HelloElixir.Skein.User.Hello
UserServiceElixir.Skein.User.UserService
MathElixir.Skein.User.Math

The Elixir.Skein.User. prefix:

  • Prevents naming collisions with Elixir/Erlang standard modules
  • Enables natural Elixir interop: Skein.User.Hello.greet("World")
  • Follows Elixir’s Elixir. prefix convention for BEAM module atoms

Every compiled module includes an __info__/1 function that makes it behave like an Elixir module:

mod.__info__(:module) #=> Elixir.Skein.User.Hello
mod.__info__(:functions) #=> [greet: 1, add: 2, classify: 1]

This is generated automatically by the code generator. It supports the :module and :functions queries that Elixir tooling expects.

Functions require:

  • A name (lowercase identifier)
  • Typed parameters (may be empty)
  • A return type
  • A body block
fn add(a: Int, b: Int) -> Int {
a + b
}
fn get_version() -> String {
"0.1.0"
}

Parameters use name: Type syntax, separated by commas:

fn create_user(name: String, email: String, age: Int) -> String {
"Created ${name}"
}

Each parameter compiles to a Core Erlang function variable. The parameter name is transformed from snake_case to CamelCase (e.g., first_name becomes FirstName).

The return type follows the -> arrow. It is required on all functions:

fn compute(x: Int) -> Int { ... }
fn greet(name: String) -> String { ... }
fn is_valid(n: Int) -> Bool { ... }

The function body is always a block ({ ... }). The last expression is the return value:

fn process(x: Int) -> Int {
let doubled = x * 2 -- intermediate binding
let adjusted = doubled + 1
adjusted -- this is the return value
}

Modules typically contain multiple functions:

module Calculator {
fn add(a: Int, b: Int) -> Int {
a + b
}
fn subtract(a: Int, b: Int) -> Int {
a - b
}
fn multiply(a: Int, b: Int) -> Int {
a * b
}
fn is_positive(n: Int) -> Bool {
n > 0
}
}

All functions are exported from the compiled BEAM module for Elixir interop and runtime dispatch. There is no visibility modifier — but note that from Skein code, functions are module-private: another Skein module cannot call them (see Module Boundaries below).

Tools are the only cross-module seam. A module’s functions, types, and enums are private to that module. There is no import, no use, and no qualified cross-module function call — OtherModule.fn_name(args) is not Skein. To invoke code in another module, that module must expose a tool, and the caller must declare the grant:

-- Module A: declare the grant, then call
capability tool.use(Billing.CreateRefund)
let result = tool.call(Billing.CreateRefund, { ticket_id: id })

Dotted calls with an uppercase head (String.upcase(s)) are reserved for the standard library, which is ambient — available everywhere with no declaration.

This is a deliberate design decision: tool boundaries keep capability grants explicit at the call site, validate every cross-module payload against a schema, and produce a trace span for every call. The full rationale lives in the spec’s Module Boundaries section.

Unit tests are co-located in the module body (test, scenario, golden declarations), where the functions under test are in scope. Cross-module integration tests go through tool.call, exactly as production callers do.

A Skein function:

fn add(a: Int, b: Int) -> Int {
a + b
}

Compiles to a Core Erlang function definition:

'add'/2 = fun (A, B) ->
call 'erlang':'+'(A, B)

The code generator:

  1. Creates a Core Erlang function name via :cerl.c_fname(name_atom, arity)
  2. Creates parameter variables with CamelCase names via :cerl.c_var/1
  3. Generates the body expression recursively
  4. Wraps it all in :cerl.c_fun/2
  5. Adds the function to the module’s export list and definition list
Source text
|
v
Lexer: tokens = [{:module, {1,1}}, {:upper_ident, {1,8}, "Hello"}, {:lbrace, {1,14}}, ...]
|
v
Parser: %AST.Module{name: "Hello", declarations: [%AST.Fn{...}], meta: %{...}}
|
v
Analyzer: type, capability, transition, and guard checking; rewrites named arguments to positional
|
v
CodeGen: :cerl.c_module(...) -> :compile.forms(mod, [:from_core, :binary]) -> beam_binary
|
v
Load: :code.load_binary(module_atom, ~c"nofile", beam_binary) -> {:module, module}