Names & resolution
Namespace declaration
Section titled “Namespace declaration”Every file begins with a namespace declaration. It emits as a public static partial class of the same
name; top-level functions become static methods on it (unless promoted to instance methods, or nested in
a static func). partial lets multiple files contribute to one namespace.
namespace AuthThe name may be dotted — nested namespaces in flattened form, as in C#:
namespace Acme.Billing.InvoicingA static func / data / enum / choice emits into the full namespace under its own name
(Acme.Billing.Invoicing.Invoice). Bare top-level functions land on the module class, named after
the namespace’s last segment (a CLR type name can’t contain .): namespace A.B.C → module class
A.B.C.C.
Quoting
Section titled “Quoting”A namespace declaration takes a bare, dotted identifier — never quoted:
NamespaceDecl = "namespace" QualifiedName . // namespace Acme.Billing (no quotes)QualifiedName = identifier { "." identifier } .A using (and using static, and an alias target) takes a quoted string literal, because its
operand is an arbitrary external path the E# grammar does not otherwise parse as an identifier chain:
using "System.Net.Http" // import target — quotedusing static "System.Math" // quotedusing SB = "System.Text.StringBuilder" // alias target — quotedIn short: declare unquoted, import quoted. A qualifier inside code (Acme.Billing.Invoice,
Geometry.Point) is likewise unquoted — quotes appear only on a using operand.
Resolution is namespace-scoped
Section titled “Resolution is namespace-scoped”Multiple namespaces compile into one assembly, but resolution follows C#, not a flat global registry:
- Same namespace → bare. A type or free function in the current namespace (any file —
partialspans files) is referenced by bare name. - Cross namespace → import or qualify. Reach a name from another namespace by importing it
(
using "NS") or qualifying it (NS.Typein type position,NS.fn(args)in call position). - A bare cross-namespace reference with neither is a hard error — ES2150 for a type, ES2152 for a free function, each naming the namespace to import or qualify.
namespace Geometrydata Point { x: int, y: int }
namespace Appusing "Geometry" // Point now barefunc go() -> int { let p = Point { x: 3, y: 4 } // bare — Geometry imported let q = Geometry.Point { x: 1, y: 2 } // qualified — works with or without the using return p.x + q.y}The using forms
Section titled “The using forms”| Form | Meaning |
|---|---|
using "System.Net.Http" | import an external .NET namespace — name its types unqualified |
using "Geometry" | import an internal E# namespace — its types and free functions |
using static "System.Math" | import a type so its static methods are callable bare (Max(2.5, 4.5)) |
using SB = "System.Text.StringBuilder" | a type alias — SB() everywhere a bare type name is legal |
For an external namespace, using emits as a C# using directive — it adds the directive, not the
assembly reference (the consuming project must still reference the assembly).
For an internal E# namespace, using "Acme" expands to both using Acme and
using static Acme.Acme — implicitly, always. An E# namespace Acme compiles to a host static class
also named Acme (so its module-level free functions live at Acme.Acme.fn); importing the namespace
static-imports that host class in the same step. So using "Acme" brings the namespace’s types and
its free functions into bare scope together — total(...), never Acme.total(...) and never the
double-segment Acme.Acme.total(...).
This is what makes the namespace/host-class double-name (Acme.Acme) a non-issue from E#: you never
write the doubled path, and you never hit a “which Acme?” resolution error, because the single
using "Acme" has already pulled both the namespace and its static host into scope. (The double name is
only visible to a C# consumer reaching in without the static import — see CLR mapping.)
A type alias is pure bind-time substitution (no runtime cost) and short-circuits the whole search — the aliased path is taken verbatim. It is the cleanest fix for a cross-namespace collision.
Resolution precedence
Section titled “Resolution precedence”An unqualified type name resolves in a fixed precedence; each tier is searched across all loaded assemblies before the next is tried, so a match in one assembly never shadows a better-tier match in another:
- Exact / already-qualified — a fully-qualified name (
System.Text.Json.JsonElement) or an exact top-level type name. - Explicit
usingimports — namespaces brought in withusing "NS", in import order. Always win over the implicit set. - Implicit standard namespaces — a built-in set of common BCL namespaces (
System,System.Collections.Generic,System.Text,System.Threading, … — see the interop namespace-search list), searched last, soDictionary<…>,StringBuilder, andFormatExceptionresolve with nousing.
A type alias short-circuits all three. The implicit tier can be dropped per-project with
<ImplicitUsings>disable</ImplicitUsings> in the .esproj, after which only exact names, explicit
usings, and aliases resolve.
Ambiguity
Section titled “Ambiguity”The same simple name may be declared in more than one namespace; the two are distinct CLR types and
coexist in one assembly. A bare reference visible from two in-scope namespaces at once — the current
namespace plus an import, or two imports — is ambiguous (ES2151).
Resolve by qualifying (A.Widget) or aliasing one (using W = "A.Widget"). The same rule applies to
external BCL types: Timer under both using "System.Threading" and using "System.Timers" is
ES2151 — the compiler will not silently bind whichever assembly the search hits first.
Per-file scope, one assembly
Section titled “Per-file scope, one assembly”Multiple files may declare the same namespace (all emit into one partial class) or different namespaces
(one assembly, C#-scoped resolution as above). Each file’s usings are scoped to that file — a bare
external name in file A resolves only through file A’s imports, never file B’s, even though both compile
into one assembly. So file A’s using "System.Timers" (Timer → System.Timers.Timer) and file B’s
using "System.Threading" (Timer → System.Threading.Timer) coexist with no cross-pollination. Both
the binder and the IL backend enforce per-file scope.