Modules GitHub issue
- Status
- The Problem
- The Approach
- Implicit Top-Level Module
- Invoking a File
- Syntax
- What &foo Means Inside a Module
- Nesting
- Relationship to Classes
- Why Not Just Use a Class?
- Open Questions
Status GitHub issue
vibecode
{"vibecode": { "section": "status", "design_status": "provisional", "rationale": "adopted_to_solve_mutual_function_reference_problem" }}
Provisional design. The approach is adopted for now to solve the mutual function reference problem. Further experience with the language may refine or replace it.
The Problem GitHub issue
vibecode
{"vibecode": { "section": "the_problem", "problem": "functions_cannot_call_each_other_at_same_scope_level", "cause": "functions_do_not_capture_outer_scope" }}
Functions in Caspian do not capture outer scope. This means two functions defined at the same level cannot call each other:
$foo = function() do
end
$bar = function() do
&foo # $foo is invisible here — functions don't see outer scope
end
Passing functions as explicit parameters works but becomes obnoxious at scale. A shared context is needed — something both functions can see without threading it through every call.
The Approach GitHub issue
vibecode
{"vibecode": { "section": "the_approach", "concept": "module_is_object_functions_become_methods", "bare_call_resolution": "&foo inside module == self.foo", "effect": "mutual_function_calls_work_as_plain_method_calls_on_self" }}
A module is an object. Functions defined inside it are methods on that object. &foo inside a module method is syntactic sugar for self.foo — the same rule that already exists inside class method bodies.
#module
function &foo
end
function &bar
&foo # same as self.foo
end
end
The module instantiates an anonymous object. Every function defined inside becomes a method on that object. When &bar calls &foo, it is calling self.foo — a plain method call on the current object. No new lookup rules are introduced; the class method semantics already handle it.
The mutual-call problem dissolves. It was always just a method call on self.
Implicit Top-Level Module GitHub issue
vibecode
{"vibecode": { "section": "implicit_top_level_module", "concept": "every_program_wrapped_in_implicit_top_level_module", "effect": "top_level_functions_can_call_each_other", "notes": ["not_special_same_rules_as_any_module", "no_explicit_module_block_needed"] }}
Every Caspian program is implicitly wrapped in a top-level module. Functions defined at the top level of a file are methods on the top-level module object. This means top-level functions can call each other without any explicit module declaration:
function &foo
end
function &bar
&foo # works — both are methods on the implicit top-level module object
end
&bar
The implicit top-level module is not special in any way. It is a module like any other. It just has no #module ... end written by the programmer. All the same rules apply.
Invoking a File GitHub issue
vibecode
{"vibecode": { "section": "invoking_a_file", "model": "file_invoked_like_a_function_call", "scope": "own_implicit_top_level_module", "return_value": "last_evaluated_expression", "early_exit": "%call.return", "vocabulary": "invoke_distinct_from_load_which_means_slurp_bytes" }}
A file is invoked by being called, just like a function. The file's body runs in its own scope (its own implicit top-level module — same rules as any module). The return value of the invocation is the value of the last expression evaluated, the same rule that applies to closures and functions everywhere else in Caspian. %call.return works inside file bodies too, for early exit.
"Invoke" is the term of art. It is intentionally distinct from "load." Loading a file means slurping its bytes into memory — a lower-level operation. Invoking a file runs it as a function and captures its return value. The two are different operations, and the documentation uses them precisely.
# my-page.casp — the file's return value is the class
class < %puck['https://puck.uno/dogberry/page']
method &process($request)
response.new(200, {content_type: 'text/html'}, '...')
end
end
The caller receives whatever the file returned and decides what to do with it. A page-handling system like Robinson expects a class. A configuration consumer might expect a hash. A helper file might return a function or an object. The invocation mechanism is the same; the convention is set by the caller.
No new concepts are introduced — file invocation reuses what closures, functions, and modules already provide:
- Scope isolation: each invocation gets its own implicit top-level module (no scope leakage between files).
- Last-value return: the same default that applies to every other callable in Caspian.
%call.return: for early exit, same as in closures and functions.
Open: the exact API surface (system method name, argument-passing semantics, caching, relative-vs-absolute paths) is TBD.
Syntax GitHub issue
vibecode
{"vibecode": { "section": "syntax", "sigil": "#module", "behavior": "creates_and_instantiates_anonymous_object", "body": "same_structure_as_class_body", "available_inside": ["fields", "helpers", "properties", "class_machinery"] }}
#module
function &foo
end
function &bar
&foo
end
end
A #module block creates and immediately instantiates an anonymous object. The block is its definition body, following the same structure as a class body. Fields, helpers, properties, and other class machinery are all available inside a module.
The # sigil distinguishes modules from class definitions (class) and variables ($name).
What &foo Means Inside a Module GitHub issue
vibecode
{"vibecode": { "section": "what_ampfoo_means_inside_a_module", "resolution": ["1_look_up_foo_as_method_on_self", "2_call_if_found", "3_raise_if_not_found"], "notes": ["same_as_inside_class_method_body", "dollar_foo_variable_still_invisible", "function_isolation_rule_intact"] }}
Inside any module method, a bare function call &foo resolves as follows:
- Look up
fooas a method onself(the module object) - If found, call it
- If not found, raise an error
This is identical to how &foo works inside a class method body. The module object is self for the duration of any method call on it.
$foo (the variable) is still invisible inside a function. Only &foo (method call form) gets the self lookup. This keeps the function isolation rule intact — functions don't see outer variables, but they can call sibling methods through self.
Nesting GitHub issue
vibecode
{"vibecode": { "section": "nesting", "behavior": "inner_module_is_own_object_with_own_self", "cross_boundary_calls": "not_possible_via_ampfoo_must_pass_reference_explicitly" }}
Modules can be nested. An inner module is its own object with its own self. Functions inside the inner module cannot see the outer module's methods via &name — self is the inner module:
#module
function &outer_foo
end
#module
function &inner_bar
&outer_foo # error — self is the inner module, not the outer
end
end
end
To call across module boundaries, pass a reference explicitly as a parameter.
Relationship to Classes GitHub issue
vibecode
{"vibecode": { "section": "relationship_to_classes", "module_is": "sugar_for_anonymous_class_defined_and_instantiated_in_one_step", "use_formal_class_when": ["uns_name_needed", "schema_needed", "multiple_instances_needed", "inheritance_needed"], "use_module_when": "local_grouping_of_functions_single_instance_no_schema" }}
A module is syntactic sugar for an anonymous class that is defined and instantiated in one step and never named. The two are equivalent in the object system:
# these are conceptually equivalent:
#module
function &foo
end
end
# ...is roughly:
class
method &foo
end
end
$anon = %puck['<anonymous>'].new
Formal classes (class ... end) are for objects that need: - A UNS name and a place in the Puckverse - A schema (fields, types, validation) - Multiple instances - Inheritance
Modules are for local grouping of functions that need to call each other. They are single-instance, anonymous, and have no schema requirements.
Why Not Just Use a Class? GitHub issue
vibecode
{"vibecode": { "section": "why_not_just_use_a_class", "answer": "module_is_lighter_syntax_signals_lighter_intent", "signals": "grouping_functions_not_modeling_data" }}
You could. A module is just a lighter syntax for the same thing. The #module form exists because the common case — grouping functions, not modeling data — doesn't need field declarations, UNS names, or the full class apparatus. The lighter syntax signals the lighter intent.
Open Questions GitHub issue
- Can a module be assigned to a variable and passed around as an object from outside?
- Does a module's anonymous class inherit from
puck.uno/objectdirectly? - Can modules be named for later reference, or are they always anonymous?
- How does the implicit top-level module interact with
%engineand the bootstrapping sequence?