Modules

As projects grow beyond a handful of files, you need a way to organize code into separate units that can coexist without name collisions. Maggie's module system is built on namespaces, file conventions, and a project manifest. It keeps small scripts simple -- a single file with no declarations works as it always has -- while giving larger projects the structure they need.

This chapter covers namespace declarations, imports, fully-qualified names, the directory-as-namespace convention, file loading and serialization, and when to use (or skip) namespaces entirely.

Test
Object name >>> #Object
Array name >>> #Array

Namespace Declarations

A namespace groups related classes under a shared prefix. Declare a namespace at the top of a source file with the namespace: directive. It must appear before any class or trait definitions. Every class defined in that file is registered under the namespace prefix.

Example
namespace: 'Widgets'

Button subclass: Object
  instanceVars: label
  method: label [ ^label ]

In the example above, the class is registered in Globals as Widgets::Button, not as bare Button. This means another file can define its own Button in a different namespace without conflict.

Rules for namespace:: - At most one per file. - Must appear before any class, trait, or method definitions. - The value is a string with :: separating segments. - Files without namespace: define classes in the root namespace.

Namespaces can be nested to any depth. Each :: segment represents a level of nesting.

Example
namespace: 'Yutani::Widgets::Forms'

FormField subclass: Object
  instanceVars: name value
  method: name [ ^name ]

This registers the class as Yutani::Widgets::Forms::FormField.

Import Directive

The import: directive lets you reference classes from another namespace without writing the full path every time. Place imports after the namespace: declaration (if any) and before class definitions.

Example
namespace: 'Yutani::Widgets'

import: 'Yutani'
import: 'Yutani::Events'

YutaniButton subclass: YutaniWidget
  instanceVars: label onClick
  method: label [ ^label ]

In the file above, YutaniWidget is resolved by searching: 1. The current namespace (Yutani::Widgets::YutaniWidget) 2. Each import in order (Yutani::YutaniWidget, then Yutani::Events::YutaniWidget) 3. The root namespace (YutaniWidget)

The first match wins. If no match is found, the compiler reports an error.

You can have zero or more import: declarations per file. Import order matters when two imported namespaces define a class with the same short name -- the first import in file order takes priority.

Example
import: 'Collections'
import: 'UI'

# If both Collections and UI define "List", Collections::List wins
# because its import appears first.

Fully-Qualified Names

The :: separator creates fully-qualified names (FQNs). You can always reference a class by its full path, whether or not you have imported its namespace. This is useful for disambiguation or one-off references.

Example
# Explicit FQN -- no import needed
btn := Widgets::Button new.
evt := Yutani::Events::ClickEvent new.

# Inside a method body, the compiler resolves FQNs at compile time.
# There is no runtime cost compared to a bare class name.

FQN syntax works everywhere a class name is valid: superclass references, subclass: declarations, message sends, and Compiler evaluate: strings.

The :: token is handled by the lexer as a single unit. Widgets::Button is one token, not three. This means you cannot have spaces around ::.

Example
'Widgets::Button' includes: '::'   "=> true"

When you use an FQN in a method body inside a file that has a namespace context, the compiler still resolves it correctly. FQN references take priority over the import resolution chain -- they are absolute paths.

Resolution Order

When the compiler encounters a bare class name in a method body, it resolves it using a three-step search:

1. Current namespace -- If the file declares namespace: 'MyApp', the compiler first looks for MyApp::ClassName.

2. Imports in order -- Each import: namespace is searched next, in the order they appear in the file. The compiler looks for Import1::ClassName, then Import2::ClassName, and so on.

3. Root namespace -- Finally, the compiler looks for the bare name in the root (global) namespace. Core classes like Object, Array, String, and Channel live here.

The first match wins and the compiler emits the resolved FQN directly in the bytecode. This means resolution happens once at compile time, not on every method call.

Example
namespace: 'MyApp::Views'

import: 'MyApp::Models'
import: 'Widgets'

# When this method references "User":
#   1. Checks MyApp::Views::User  (current namespace)
#   2. Checks MyApp::Models::User (first import)
#   3. Checks Widgets::User       (second import)
#   4. Checks User                (root namespace)
#
# If MyApp::Models::User exists, that is what the compiler uses.

Dashboard subclass: Object
  method: loadUser [
    ^User find: 1   "Resolves to MyApp::Models::User"
  ]

Because core classes (Object, Array, String, etc.) live in the root namespace, they are always available as a fallback. You never need to import them.

Test
Object name >>> #Object
Array name >>> #Array
String name >>> #String

Directory-as-Namespace Convention

When loading files from a directory tree, Maggie derives namespaces automatically from the directory structure. Path segments relative to the base directory become PascalCase namespace segments joined by ::.

Example
src/
  Helper.mag                -> (root namespace, no prefix)
  myapp/
    Main.mag                -> MyApp
    models/
      User.mag              -> MyApp::Models
      Account.mag           -> MyApp::Models
    views/
      Dashboard.mag         -> MyApp::Views
      widgets/
        Chart.mag           -> MyApp::Views::Widgets

Rules: - Directory names are converted to PascalCase (my_app -> MyApp, http-client -> HttpClient). - Files directly in the base directory have no namespace (root). - An explicit namespace: declaration in a file always overrides the directory-derived namespace.

This convention means that well-organized directory structures automatically produce well-organized namespaces with no extra declarations needed.

Example
# Given the layout above, loading the project:
mag ./src/...

# Registers:
#   Helper           (root)
#   MyApp::Main
#   MyApp::Models::User
#   MyApp::Models::Account
#   MyApp::Views::Dashboard
#   MyApp::Views::Widgets::Chart

The two-pass loading pipeline ensures that all class skeletons are registered before any method bodies are compiled. This means classes can reference each other freely, regardless of file ordering on disk. Pass 1a registers skeletons, pass 1b resolves superclass pointers, and pass 2 compiles method bodies with full FQN resolution context.

File Loading and Serialization

Maggie provides primitives for loading source files into a running VM and writing classes back out to files.

Compiler fileIn: loads a single .mag file (single-pass). Compiler fileInAll: loads all .mag files from a directory using the two-pass batch pipeline, so cross-file references just work.

Example
Compiler fileIn: 'lib/helpers/StringUtils.mag'.
Compiler fileInAll: 'src/'.

Compiler fileOut:to: reconstructs a class's source and writes it to a file. For namespaced classes, a namespace: declaration is included. Compiler fileOutNamespace:to: writes all classes in a namespace to a directory, one file per class.

Example
Compiler fileOut: 'MyApp::Models::User' to: '/tmp/User.mag'.
Compiler fileOutNamespace: 'MyApp::Models' to: '/tmp/models/'.

Note: fileOut reconstructs from stored method text. Methods from .mag files preserve their source; methods from evaluate: produce a stub. Import declarations are not preserved.

For faster startup, save the entire VM state to an image file:

Example
Compiler saveImage: 'my-app.image'.

# CLI equivalents:
#   mag ./src/... --save-image my-app.image
#   mag --image my-app.image -m Main.start

Project Manifest

A maggie.toml file in the project root configures the project name, namespace, source directories, entry point, dependencies, and image settings. When mag is invoked without explicit paths and a maggie.toml exists, the project is loaded automatically.

Example
# maggie.toml
[project]
name = "my-app"
namespace = "MyApp"
version = "0.1.0"

[source]
dirs = ["src"]
entry = "Main.start"

[dependencies]
yutani = { git = "https://github.com/chazu/yutani-mag", tag = "v0.5.0" }
local-lib = { path = "../shared-lib" }

[image]
output = "my-app.image"
include-source = true

Key sections:

- [project] -- name, namespace, version, and optional metadata (description, license, authors, repository, maggie version constraint). - [source] -- which directories to compile, entry point, and exclude patterns for filtering files. - [dependencies] -- external libraries (git with tag/branch/commit, or local paths). - [dev-dependencies] -- same format, loaded only for test/dev. - [image] -- output image file name and whether to preserve method source text in the image. - [test] -- test directories, entry point, and timeout. - [scripts] -- lifecycle hooks (prebuild, postbuild, pretest, posttest). - [[target]] -- multiple build targets with per-target entry points, source dirs, and go-wrap configuration.

Running the project:

Example
# Detects maggie.toml, resolves deps, loads src/, runs entry point
mag -m Main.start

# Same, but also saves an image for faster future startup
mag --save-image app.image

Dependencies

Dependencies are declared in maggie.toml and managed with the mag deps subcommand. Two types are supported: git (cloned to .maggie/deps/<name>/) and path (local filesystem reference).

Example
[dependencies]
yutani = { git = "https://github.com/chazu/yutani-mag", tag = "v0.5.0" }
local-lib = { path = "../shared-lib" }
ui-toolkit = { path = "../ui-toolkit", namespace = "CustomUI" }
bleeding = { git = "https://github.com/example/lib", branch = "main" }

Git dependencies use exactly one ref specifier: tag, branch, or commit (mutually exclusive). The lock file always records the exact commit hash regardless of which specifier is used.

Each dependency maps to a namespace, resolved in order: 1. Consumer override -- explicit namespace in the dependency entry. 2. Producer manifest -- the dep's own maggie.toml namespace. 3. PascalCase fallback -- dep name as PascalCase (my-lib -> MyLib).

All classes from a dependency are prefixed with its namespace. A dep with namespace Yutani containing src/widgets/Button.mag registers as Yutani::Widgets::Button.

Example
mag deps           # Resolve and fetch all dependencies
mag deps update    # Re-resolve, ignoring the lock file
mag deps list      # Show the dependency tree

Dependencies load in topological order (transitive first). The lock file (.maggie/lock.toml) records exact commit hashes for reproducible builds. Two deps mapping to the same namespace cause a hard error. Core VM names (Object, Array, String, etc.) are reserved and cannot be used as a dependency's root namespace segment.

Globals Registration

A class in a namespace is registered only under its FQN. A class Button in namespace Widgets is stored as Globals["Widgets::Button"] -- there is no entry for bare Button. A root-namespace class is stored under its bare name.

This means two namespaces can each define Button without conflict:

Example
# File: src/widgets/Button.mag
namespace: 'Widgets'
Button subclass: Object
  method: render [ 'I am a UI button' ]

# File: src/controls/Button.mag
namespace: 'Controls'
Button subclass: Object
  method: render [ 'I am a form control' ]

# Globals["Widgets::Button"] and Globals["Controls::Button"] coexist.

Core classes (Object, Array, String, etc.) live in the root namespace.

Test
Object name >>> #Object
SmallInteger name >>> #SmallInteger

When to Use Namespaces

Not every project needs namespaces. Here are guidelines for when they help and when they add unnecessary ceremony.

Skip namespaces when:

- Your project is a single file or a small collection of files. - You are writing a quick script or experiment. - All your class names are unique and unlikely to collide with anything else. - You are working in the REPL.

A file with no namespace: or import: declarations works exactly as it always has. The module system is fully backward compatible.

Example
# simple-script.mag -- no namespace needed
Counter subclass: Object
  instanceVars: count
  method: init [ count := 0 ]
  method: increment [ count := count + 1 ]
  method: count [ ^count ]

Use namespaces when:

Example
# A well-structured library
namespace: 'HttpClient'

import: 'HttpClient::Parsers'

Client subclass: Object
  instanceVars: baseUrl headers
  method: get: path [
    req := Request new: baseUrl , path.
    req headers: headers.
    ^req execute
  ]

Naming conventions:

- Namespace segments use PascalCase: MyApp, HttpClient, Yutani. - Keep namespace depth reasonable -- two or three levels is typical. - The top-level namespace usually matches the project name. - Choose names that are unlikely to collide: Yutani is better than Utils.

The directory convention reduces ceremony. If you organize your source tree into directories that match your desired namespace structure, you often do not need explicit namespace: declarations at all. The loading pipeline derives them automatically.

Putting It Together

Here is a small multi-namespace project showing how the pieces fit.

Example
# todo-app/
#   maggie.toml          (namespace = "TodoApp", entry = "Main.start")
#   src/
#     Main.mag            -> TodoApp
#     models/
#       Task.mag          -> TodoApp::Models
#     views/
#       TaskView.mag      -> TodoApp::Views

# --- src/Main.mag ---
import: 'TodoApp::Models'
import: 'TodoApp::Views'

Main subclass: Object
  classMethod: start [
    task := Task new: 'Write guide chapter'.
    TaskView render: task.
  ]

# --- src/models/Task.mag ---
Task subclass: Object
  instanceVars: title done
  method: init: aTitle [ title := aTitle. done := false. ]
  method: title [ ^title ]
  method: complete [ done := true ]

# --- src/views/TaskView.mag ---
import: 'TodoApp::Models'

TaskView subclass: Object
  classMethod: render: task [ task title printString ]

Key observations: - No explicit namespace: needed -- directory structure handles it. - Main uses Task and TaskView by short name via imports. - TaskView imports TodoApp::Models to reference Task. - mag -m Main.start auto-detects the manifest and calls TodoApp::Main.start.

Example
'TodoApp::Models::Task' includes: 'Models'   "=> true"
'TodoApp::Views' includes: '::'              "=> true"

Summary

Maggie's module system provides namespaces and imports without breaking backward compatibility. Files without declarations work as they always have. For larger projects, a few conventions and directives keep code organized.

Key points:

- namespace: sets the namespace for all classes in a file. - import: allows unqualified references to classes in other namespaces. - FQN syntax (Widgets::Button) works anywhere a class name is valid. - Resolution order: current namespace, imports (in order), root. - Directory names automatically map to PascalCase namespaces. - fileIn: / fileInAll: load source; fileOut:to: serializes. - maggie.toml configures projects, dependencies, and images. - Dependencies map to importable namespaces with collision detection. - Use namespaces when publishing libraries or working on larger projects. Skip them for scripts and experiments.

Test
Object name >>> #Object
SmallInteger name >>> #SmallInteger