Modules and Functions
Module Declaration
Section titled “Module Declaration”A Skein file contains a single module declaration:
module Hello { fn greet(name: String) -> String { "Hello, ${name}!" }}Module Naming and Elixir Interop
Section titled “Module Naming and Elixir Interop”Skein modules compile to BEAM modules with Elixir-compatible names:
| Skein Name | BEAM Module Name |
|---|---|
Hello | Elixir.Skein.User.Hello |
UserService | Elixir.Skein.User.UserService |
Math | Elixir.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
__info__/1 for Elixir Compatibility
Section titled “__info__/1 for Elixir Compatibility”Every compiled module includes an __info__/1 function that makes it behave like an Elixir module:
mod.__info__(:module) #=> Elixir.Skein.User.Hellomod.__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.
Function Declarations
Section titled “Function Declarations”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
Section titled “Parameters”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).
Return Type
Section titled “Return Type”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}Multi-Function Modules
Section titled “Multi-Function Modules”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).
Module Boundaries
Section titled “Module Boundaries”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 callcapability 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.
How Functions Compile
Section titled “How Functions Compile”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:
- Creates a Core Erlang function name via
:cerl.c_fname(name_atom, arity) - Creates parameter variables with CamelCase names via
:cerl.c_var/1 - Generates the body expression recursively
- Wraps it all in
:cerl.c_fun/2 - Adds the function to the module’s export list and definition list
Compilation Pipeline for a Module
Section titled “Compilation Pipeline for a Module”Source text | vLexer: tokens = [{:module, {1,1}}, {:upper_ident, {1,8}, "Hello"}, {:lbrace, {1,14}}, ...] | vParser: %AST.Module{name: "Hello", declarations: [%AST.Fn{...}], meta: %{...}} | vAnalyzer: type, capability, transition, and guard checking; rewrites named arguments to positional | vCodeGen: :cerl.c_module(...) -> :compile.forms(mod, [:from_core, :binary]) -> beam_binary | vLoad: :code.load_binary(module_atom, ~c"nofile", beam_binary) -> {:module, module}