Skip to content

Names & resolution

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 Auth

The name may be dotted — nested namespaces in flattened form, as in C#:

namespace Acme.Billing.Invoicing

A 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.

  • Free functions become static methods (a function with a receiver block is a method on its receiver type instead; a static member is a static method on that class).
  • Namespace-scope statelet / var / const with no enclosing func or type — becomes static members (Declarations). let / var with = are fields; their => and { } forms are static properties; const is an inlined literal.
  • A static emits a second host class — a sibling in the same namespace, not a nested type — holding its own const, fields, and functions.

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 — quoted
using static "System.Math" // quoted
using SB = "System.Text.StringBuilder" // alias target — quoted

In short: declare unquoted, import quoted. A qualifier inside code (Acme.Billing.Invoice, Geometry.Point) is likewise unquoted — quotes appear only on a using operand.

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 — partial spans 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.Type in type position, NS.fn(args) in call position).
  • A bare cross-namespace reference with neither is a hard errorES2150 for a type, ES2152 for a free function, each naming the namespace to import or qualify.
namespace Geometry
struct Point { x: int, y: int }
namespace App
using "Geometry" // Point now bare
func 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).

FormMeaning
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 aliasSB() 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").

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:

  1. Exact / already-qualified — a fully-qualified name (System.Text.Json.JsonElement) or an exact top-level type name.
  2. Explicit using imports — namespaces brought in with using "NS", in import order; always win over the implicit set.
  3. 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, so Dictionary<…>, StringBuilder, and FormatException resolve with no using.
  4. Nested-type readings — a dotted name re-read with one dot taken as CLR nesting instead of a namespace boundary. Grpc.Core.Service.ServiceBase is tried as Grpc.Core.Service+ServiceBase, then as Grpc.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.

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.

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 TimerSystem.Timers.Timer) and file B’s using "System.Threading" (bare TimerSystem.Threading.Timer) coexist in the same namespace with no cross-pollination. Both the binder and the IL backend enforce per-file scope.