Skip to content

Names & resolution

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 Auth

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

namespace Acme.Billing.Invoicing

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

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 — 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
data 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
}
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, 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.

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, … — see the interop namespace-search list), searched last, so Dictionary<…>, StringBuilder, and FormatException resolve with no using.

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.

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.

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