CEP-0064: Inline Namespace Expressions¶
Draft
Draft proposal for a namespace { ... } source form that returns an inline namespace value. V1 uses file-backed namespaces through module(...) and include(...) only.
Summary¶
Catalyst should consider an inline namespace expression:
const json = module("json")
const helpers = namespace {
pub fn validate(payload: *const json.Value) void {
// ...
}
pub const ValueType = json.Value
}
helpers.validate(&payload)
The expression would produce a Namespace value whose public declarations are selected, destructured, imported, and re-exported like a namespace returned by include(...), but without requiring a separate source file for small helper groups.
Motivation¶
File-backed namespaces are the right default for module APIs, dependency graph boundaries, and separately documented implementation chunks. Some declarations are still too local to justify another .ct file, but too broad to leave as ungrouped names in the current namespace.
Inline namespace expressions give those declarations a qualified home:
- helper groups can stay near the code that owns them;
- private top-level names in the containing namespace do not need artificial prefixes;
- examples and tests can define small namespaces without creating supporting files;
- later refactors can move the inline body into a file-backed namespace with minimal call-site churn.
Proposed Direction¶
namespace { ... } is a declaration-bearing expression that evaluates at comptime to a Namespace value.
The body uses namespace declaration grammar, not ordinary statement-block grammar. It may contain declarations, imports, impls, module doc comments, attributes on declarations, and declaration guards if those guards are otherwise allowed for file namespaces. Runtime statements are invalid directly in the body.
The body is a nested namespace scope. It may refer to comptime-visible declarations from the containing declaration scope, but public declarations that mention private outer names must pass the normal public API validation rules. It cannot capture runtime locals because the resulting Namespace is not a runtime closure.
const parsing = namespace {
const json = module("json")
pub const Value = json.Value
pub fn validate(payload: *const Value) void {
// ...
}
}
The resulting value has type Namespace and follows the existing namespace operation rules:
- member selection uses compiler-defined namespace lookup;
- destructuring binds visible namespace declarations;
import parsingimports public declarations and public impls from the inline namespace;pub const parsing = namespace { ... }re-exports the inline namespace as a public subnamespace;- the value remains comptime-only and is invalid in runtime storage.
An inline namespace does not create a source-file dependency edge. Direct module(...) and include(...) calls inside its body are still dependency graph edges of the containing source file and should be collected with the surrounding declaration-scope dependency wiring.
Example¶
Possible future accepted helper namespace:
const json = module("json")
const helpers = namespace {
pub fn validate(payload: *const json.Value) void {
// ...
}
pub const ValueType = json.Value
}
helpers.validate(&payload)
const value: helpers.ValueType = parse_value(payload)
Possible future public subnamespace:
pub const testing = namespace {
pub fn assert_valid(payload: *const json.Value) void {
// ...
}
}
Possible future import:
const std = module("std")
const adapters = namespace {
pub impl LocalValue as std.debug.Debug {
// ...
}
}
import adapters
Public Surface¶
The include-equivalent direction keeps the existing namespace visibility model: declarations are private by default, and only pub declarations are visible through the returned namespace value.
That means this form:
const json = module("json")
const helpers = namespace {
fn validate(payload: *const json.Value) void {
}
}
helpers.validate(&payload)
would be invalid because validate is private to the inline namespace body.
The main alternative is making declarations inside namespace { ... } public by default. That matches the shortest motivating sketch, but it would make inline namespace bodies differ from file namespaces and could make later extraction to include(...) change visibility. The proposal currently prefers include-equivalent visibility and leaves public-by-default inline namespace bodies as an open question.
Identity and Reflection¶
Each inline namespace has a compiler-assigned namespace identity rooted in the containing source-file namespace and source location. Repeated evaluation of the same declaration-scope inline namespace initializer should return the same namespace identity for the current build context, just as repeated include(...) calls return the same file namespace identity.
Moving an inline namespace body to another source location changes declaration identity unless tooling provides an explicit migration mechanism. This matches ordinary declaration movement and avoids pretending inline namespaces are stable package-level API boundaries by default.
V1 excludes public namespace member enumeration, and this proposal does not change that boundary. Future namespace reflection should be able to report that a namespace was inline and point tooling at the source range that produced it, but ordinary source should not gain general enumeration merely because inline namespaces exist.
Parser and Keyword Boundary¶
namespace would need to be parsed as a declaration-bearing block expression. An ordinary prelude function spelling cannot express the body cleanly because the body contains declarations rather than runtime values.
If namespace becomes a keyword after V1 source compatibility is promised, existing code that uses namespace as an identifier would need migration. The design should either reserve namespace before that compatibility point or accept the compatibility cost explicitly.
Open Questions¶
- whether inline namespace declarations should stay private-by-default or use a public-by-default shorthand;
- whether
namespace { ... }is allowed only in declaration-scope initializers and import operands, or also inside local comptime blocks when all dependency edges are already explicit; - whether inline namespace bodies should prefer direct
module(...)andinclude(...)calls or lint toward binding those dependencies outside the body; - whether module doc comments inside an inline namespace document the inline namespace value, the binding that receives it, or both;
- whether the formatter should require blank lines between declarations inside inline namespace bodies exactly as it does for source files.
V1 Compatibility¶
V1 rejects namespace { ... }. Code should use a separate .ct file and include(...):
const helpers = include("helpers.ct")
Accepting inline namespace expressions later is source-additive only if namespace is already reserved before stable user code may bind it as an identifier. Without that reservation, accepting the keyword is a compatibility break for code that declares a value named namespace.