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 — the host class — and 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 / struct / enum / union / interface emits into the full namespace under its own
name (Acme.Billing.Invoicing.Invoice). The host class itself is named after the namespace’s last
segment (a CLR type name cannot contain .), so namespace A.B.C produces host class A.B.C.C.
What lives on the host class
Section titled “What lives on the host class”- Free functions become static methods (a function with a receiver block
is a method on its receiver type instead; a
staticmember is a static method on that class). - Namespace-scope state —
let/var/constwith no enclosingfuncor type — becomes static members (Declarations).let/varwith=are fields; their=>and{ }forms are static properties;constis an inlined literal. - A
staticemits a second host class — a sibling in the same namespace, not a nested type — holding its ownconst, fields, and functions.
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 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 Geometrystruct 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}Method attachment is namespace-local: a receiver block attaches its method to the receiver type only when that type is declared in the method’s own namespace. A type’s whole method set therefore lives in one namespace (across files is fine — namespaces span files).
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 and always. An E# namespace Acme compiles to a host 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
doubled 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?” error, because the single using "Acme" has already
pulled both the namespace and its host into scope. (The double name is visible only 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
(using TTimer = "System.Timers.Timer").
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, … — the full list is in CLR mapping), searched last, soDictionary<…>,StringBuilder, andFormatExceptionresolve with nousing. - Nested-type readings — a dotted name re-read with one dot taken as CLR nesting instead of a
namespace boundary.
Grpc.Core.Service.ServiceBaseis tried asGrpc.Core.Service+ServiceBase, then asGrpc.Core+Service+ServiceBase: deepest split first, because a nested type is usually nested once. Source spells nesting and namespace qualification the same way, and the text alone does not say which dot is which. This tier runs only after the three above find nothing, so it can never change the meaning of a name that already resolves.
A type alias short-circuits the whole search. The implicit tier is 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 that
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), and the
compiler will not silently pick one. Resolve by qualifying (A.Widget) or aliasing one
(using W = "A.Widget"). The same rule applies to external BCL types: a bare Timer under both
using "System.Threading" and using "System.Timers" is ES2151, not a silent bind to 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" (bare Timer → System.Timers.Timer) and file
B’s using "System.Threading" (bare Timer → System.Threading.Timer) coexist in the same namespace
with no cross-pollination. Both the binder and the IL backend enforce per-file scope.