Skip to main content

Project Structure

Organizing Sector9 projects: file extensions, module layout, and test organization.

File Extensions

Sector9 uses two file extensions for source code:

ExtensionDescription
.sr9Canonical Sector9 source file (preferred)
.moLegacy Motoko extension (still supported)

Both extensions feed the same parser and verifier. New projects should use .sr9; .mo remains a legacy-compatible suffix. Import resolution prefers .sr9 when both suffixes are present.

Modules vs Actors

Sector9 distinguishes between modules (stateless logic) and actors (stateful entry points):

Module Files

Modules contain reusable logic, types, and helper functions. They should keep runtime state out unless that state is intentionally owned by an actor or another verified root:

math.sr9
// math.sr9 - Reusable stateless logic.

module {
public func double(n : Nat) : Nat
ensures result == n * 2;
{
n * 2
};
}

Actor Files

Actors are persistent entry points with mutable state:

counter.sr9
// counter.sr9 - Actor using a local module.

import Math "./math";

persistent actor Counter {
var count : Nat = 0;

public func increment() : async Nat
modifies count
ensures count == old(count) + 1;
ensures result == count;
{
count += 1;
count
};

public func doubled() : async Nat
reads count
ensures result == count * 2;
{
Math.double(count)
};
}

When to use which:

  • Use modules for reusable logic, types, and pure functions
  • Use actors for stateful services with public entry points

Import Syntax

Sector9 supports local and system imports:

// Local imports (relative path, no extension)
import Math "./math";
import Types "./types";

// Selective imports
import { double; triple } "./math";

// Core library imports
import Debug "mo:core/Debug";
import Nat "mo:core/Nat";

Imports must appear at the top of the file before any definitions.

A typical Sector9 project follows this structure:

my-project/
├── src/
│ ├── main.sr9 # Main actor (entry point)
│ ├── types.sr9 # Shared type definitions
│ ├── lib/
│ │ ├── math.sr9 # Math utilities
│ │ └── utils.sr9 # General helpers
│ └── modules/
│ ├── pool.sr9 # Business logic module
│ └── logs.sr9 # Logging module
├── test/
│ ├── math_sat.sr9 # Tests that should verify
│ ├── math_unsat.sr9 # Tests that should fail
│ └── ok/ # Golden output files
└── dfx.json # Project configuration

Test File Naming

In this repository, verifier tests encode their expected outcome in the filename suffix:

SuffixMeaningExample
_satVerification succeedsincrement_sat.sr9
_unsatVerification fails (intentional)overflow_unsat.sr9
_assert_unsatAn assert failsbounds_assert_unsat.sr9
_ensures_unsatA postcondition failscontract_ensures_unsat.sr9
_rejectType-check rejectioninvalid_syntax_reject.sr9

These suffixes allow repo test runners to automatically validate expected outcomes. Application projects can use the same convention, but it is not required by the compiler.

Golden Files

Repo test directories include an ok/ subdirectory containing expected outputs:

test/
├── increment_sat.sr9
├── increment_unsat.sr9
└── ok/
├── increment_sat.vpr # Generated Viper code
├── increment_sat.vpr.stderr # Translation diagnostics
├── increment_sat.silicon # Verification result (JSON)
└── increment_sat.silicon.ret # Exit code

Golden files serve as compiler regression tests. When verifier behavior intentionally changes in this repository, maintainers run ./update_goldens.sh test/path/file.sr9 to accept new outputs.

Multi-File Projects

Larger projects separate concerns across files:

types.sr9
// types.sr9 - Shared type definitions.

module {
public type Balance = Nat;
public type TransferResult = { remaining : Balance };
}
pool.sr9
// pool.sr9 - Business logic over shared types.

import T "./types";

module {
public func transfer(from : T.Balance, amount : Nat) : T.TransferResult
requires amount <= from;
ensures result.remaining == from - amount;
{
{ remaining = from - amount }
};
}
main.sr9
// main.sr9 - Actor state delegates arithmetic to modules.

import T "./types";
import Pool "./pool";

persistent actor TokenService {
var balance : T.Balance = 0;

public func withdraw(amount : Nat) : async Nat
modifies balance
entry_requires amount <= balance;
requires amount <= balance;
ensures balance == old(balance) - amount;
ensures result == balance;
{
let next = Pool.transfer(balance, amount);
balance := next.remaining;
balance
};
}

This pattern enables:

  • Separation of concerns: Logic vs. state vs. types
  • Code reuse: Multiple actors can import the same modules
  • Independent verification: Standalone modules can be verified as runtime-capable roots, while imported callers reason from their written contracts

Interface and Implementation Separation

For complex modules, separate interface definitions from implementations:

// pool_interface.sr9 - Types only
module {
public type Pool = { reserveX : Nat; reserveY : Nat };
public type SwapResult = { amountOut : Nat; newPool : Pool };
}
// pool_impl.sr9 - Implementation
import I "./pool_interface";

module {
public func swap(pool : I.Pool, amountIn : Nat) : I.SwapResult
requires amountIn > 0;
ensures result.amountOut > 0;
{
// Implementation
};
}

Version Patterns

When evolving data structures, use versioned modules:

// memory_v1.sr9
module {
public type State = { count : Nat };
}
// memory_v2.sr9
module {
public type State = { count : Nat; lastUpdate : Nat };

public func migrate(old : V1.State) : State {
{ count = old.count; lastUpdate = 0 }
};
}

This allows smooth upgrades while maintaining verification contracts.

Summary

  • Use .sr9 extension for new files (.mo still works for compatibility)
  • Modules contain stateless logic; actors contain stateful entry points
  • Imports use relative paths without file extensions
  • Repo test suffixes (_sat, _unsat, _reject) encode expected outcomes
  • Golden files in repo ok/ directories provide compiler regression testing
  • Separate interfaces from implementations for complex modules, and write contracts on exported APIs that callers should rely on