Blog › ICP guides

Nemerle developer on retainer: compile-time macros, quasi-quotation, typed splices, type inference, and .NET metaprogramming on monthly retainer

October 1, 2026 · ~15 min read

A Nemerle developer was building a code-generation macro to eliminate boilerplate property accessors on domain model classes. The macro was invoked at the class level and used quasi-quotation to generate def bindings for each property. The generated code compiled without error, the tests passed, and the macro was integrated into the build. Several sprints later, a downstream service began throwing type mismatch errors when it deserialized objects produced by one of the macro-generated classes. The error was at a call site two layers removed from the macro invocation: a method that accepted a string parameter was receiving an object. The developer searched the explicit code for any place where an object might be assigned where a string was expected and found nothing. The source of the error was inside the macro quasi-quotation template: one of the generated def bindings used an untyped splice $(propValue) where the macro author intended a string. Nemerle’s type inference filled in the type of the splice from the surrounding context at the macro call site, and at two call sites that context pushed the inferred type to object rather than string. Adding $(propValue : string) — a typed splice that constrains the splice position to string — caused ncc to immediately catch the two call sites where the generated binding would have had the wrong type. Type mismatches at downstream call sites: 2 → 0.

The work log said “fixed type error in downstream service, 6h.” It cannot explain that the error originated not at the call site that failed but inside a macro quasi-quotation template authored months earlier, that the root cause was a single missing type annotation in a splice position, or that the fix surfaced two additional compile-time errors that had been silently propagating as wrong-type assignments through generated code. A client reading that entry sees six hours of work for what appears to be a one-word change: $(propValue) → $(propValue : string). What is invisible is the diagnostic cost: identifying that the error was macro-generated rather than hand-written, tracing the generated binding’s type inference chain through the quasi-quotation template, understanding how Nemerle’s type inference fills in untyped splice positions from call-site context rather than from the template structure, locating the two call sites where context produced the wrong inference, and verifying that the typed splice fix did not break any existing correct call sites. The six hours were not spent typing five extra characters; they were spent diagnosing a class of latent type error that no amount of testing the happy path would have surfaced before production.

Nemerle language overview: statically typed .NET with first-class compile-time macros

Nemerle was created at the University of Wrocław, Poland, by Michał Moskal, Kamil Skalski, Paweł Olszta, and Marek Żabka, with the first public release around 2003. The central design insight is that metaprogramming should be a first-class feature of the language, not an add-on or preprocessor layer. Nemerle compiles to standard .NET IL via the ncc compiler and runs on both the .NET Framework and Mono. The resulting assemblies are fully interoperable with C# and other .NET languages. Nemerle found its most active industrial use in game development tooling and in research contexts that required compile-time code generation for domain-specific patterns.

The Nemerle type system is statically typed with pervasive type inference. Type inference follows an extended Hindley-Milner algorithm adapted for .NET’s object-oriented type hierarchy. A def binding is immutable and type-inferred from its initialization expression: def name = "Alice" gives name the type string; def count = 42 gives count the type int. A mutable binding is mutable and similarly type-inferred: mutable total = 0 gives total the type int and allows subsequent total = total + n assignments. Explicit type annotations are written after the binding name: def name : string = getValue(). Method signatures require explicit parameter and return type annotations in most contexts, though the compiler will infer return types from the method body when possible. The Nemerle compiler performs type inference globally within a compilation unit, meaning that the inferred type of a binding can be influenced by how the binding is used later in the same file — a property that makes macro-generated bindings particularly sensitive to call-site context.

Nemerle’s standard library provides functional data structures common to ML-family languages. option[T] is the option type with cases Some(value) and None(), used throughout the standard library and macro system. list[T] is a functional singly-linked list with head and tail, pattern-matched as x :: xs (cons cell) or [] (empty). array[T] is .NET’s mutable array type. when condition { body } executes the body when the condition is true; unless condition { body } executes when false. Pattern matching uses match (val) { | pattern1 => expr1 | pattern2 => expr2 | _ => fallback }. The compiler enforces exhaustiveness for matches on variant (discriminated union) types: if a case is missing, ncc emits a warning or error depending on the variant’s exhaustiveness annotation. Variant types are declared with the variant keyword and enumerated cases: variant Shape { | Circle { mutable Radius : float } | Rectangle { mutable Width : float; mutable Height : float } }.

Nemerle’s macro system: the macro keyword and quasi-quotation with <[ ]>

A Nemerle macro is declared with the macro keyword, a name, a parameter list of type PExpr (parsed expression, the compiler’s AST node type for expressions), and a body that returns a PExpr:

macro LogCall(methodName : PExpr, args : PExpr)
{
    <[ Console.WriteLine("Calling " + $(methodName.ToString())); $args ]>
}

The quasi-quotation operator <[ expr ]> (angle-bracket-square-bracket) takes a Nemerle expression written as source text and produces a PExpr AST node representing that expression. The Nemerle compiler parses the content of <[ ]> as Nemerle syntax but instead of compiling it to IL, it generates an AST node. Dollar-sign interpolation $(subExpr) splices a computed PExpr sub-node into the template at that position. The resulting PExpr represents the quasi-quotation structure with the spliced sub-node embedded. The macro returns this PExpr, and the compiler substitutes it at the macro’s invocation site.

The critical addition to the plain dollar-sign splice is the typed splice: $(name : SomeType). A typed splice adds a type constraint to the splice position: it instructs the compiler that the AST node spliced in at that position must have type SomeType after type-checking. If the spliced expression’s type does not match, ncc emits a compile error at the macro call site explaining that the expected type was SomeType but the expression had a different type. This is the mechanism that the property-generation macro was missing. The quasi-quotation template:

macro GenerateProperty(propName : PExpr, propValue : PExpr)
{
    <[ def $(propName : dyn) = $(propValue) ]>
}

left the $(propValue) splice untyped. Nemerle’s inference engine filled in the type of propValue from the context at the call site — specifically, from how the generated def binding was subsequently used in the code that followed the macro invocation. At two call sites, the code following the macro invocation passed the generated binding into a method accepting object, and Nemerle inferred object as the type of the splice. The fix was to add the explicit type constraint:

macro GenerateProperty(propName : PExpr, propValue : PExpr)
{
    <[ def $(propName : dyn) = $(propValue : string) ]>
}

With this change, the compiler enforced at every macro call site that the propValue expression had type string. The two call sites where object was being passed immediately produced ncc errors, surfacing the root cause: at those two sites, the expression passed as propValue was a method returning object rather than one returning string. These were genuine bugs in the calling code that the untyped splice had silently allowed to compile.

Nemerle macro pattern matching on AST nodes

More complex macros use pattern matching on PExpr nodes to inspect the structure of the macro’s arguments. The match expression can match against PExpr patterns using the quasi-quotation syntax:

macro IfSome(optExpr : PExpr, body : PExpr)
{
    match (optExpr)
    {
        | <[ Some($val) ]> =>
            <[ def $val = $optExpr.Value; $body ]>
        | _ =>
            <[ match ($optExpr) { | Some($val) => $body | None() => () } ]>
    }
}

In this pattern, <[ Some($val) ]> is a quasi-quotation pattern that matches a PExpr node representing a call to Some with a single argument, binding that argument to val. This is pattern matching on the AST structure of the expression, not on its runtime value. The macro can then use val — the extracted AST node representing the argument to Some — in the generated code. This kind of AST pattern matching is more powerful than simple parameter passing: the macro can inspect the syntactic structure of its arguments and generate different code depending on what that structure is. Retainer work involving complex macros regularly involves debugging these match cases: determining whether a pattern is too specific (fails to match valid invocations) or too broad (matches invocations it should not), and whether the binding variables extracted from the pattern have the expected types in the generated code.

The Nemerle standard library includes a number of macros that demonstrate the range of what the system can express. The using macro implements IDisposable resource management, equivalent to C#’s using statement. The lock macro implements thread-synchronized critical sections via System.Threading.Monitor. The assert macro generates an assertion check that includes the source text of the condition in the assertion failure message, obtained by calling optExpr.ToString() on the matched PExpr node. The try ... catch ... finally pattern for exception handling is also macro-implemented in some Nemerle versions. These macros are ordinary Nemerle code compiled into an assembly that ncc loads; any developer can write macros with identical capabilities. The macro assembly must be compiled before the code that uses it, and referenced with the -r:MacroAssembly.dll flag to ncc.

Nemerle type inference and the def / mutable binding system

Nemerle’s type inference is bidirectional in the sense that both the initialization expression and the downstream uses of a binding contribute to resolving its type. This is more permissive than simple left-to-right inference: if a def x = getValue() binding is subsequently passed to a method accepting string, Nemerle can infer that x has type string and that getValue() must return string. When this inference chain involves a macro-generated binding, the binding’s type is not determined by the macro template alone but by the combination of the template’s splice expression and the uses of the generated binding in the code following the macro invocation. This is the mechanism that caused the property-generation macro to produce bindings of type object at two call sites: the generated binding was used in a context that forced object, and the untyped splice allowed the inference to flow.

The practical implication for Nemerle retainer work is that macro templates must be audited for all splice positions that have a semantically intended type. Any splice that should produce a value of a specific type must use the typed splice syntax $(expr : T) rather than plain $(expr). This audit is not a one-time activity; it recurs whenever a macro’s template is extended or when new call sites are added that use the generated code in contexts the original author did not anticipate. A retainer engagement that includes ongoing macro library maintenance will typically encounter this class of issue multiple times over the course of a year, with each incident requiring the same diagnostic sequence: trace the type error from the call site back through the generated code to the macro template, identify the untyped splice position, add the typed splice, re-run ncc to expose any additional call sites with wrong types, and verify that the fix does not break existing correct call sites.

Nemerle’s option type and exhaustive pattern matching

Nemerle’s option[T] type is fundamental to the standard library and to well-written Nemerle code. A value of type option[T] is either Some(v) where v has type T, or None(). The idiomatic way to consume an option[T] is pattern matching:

match (lookup(key))
{
    | Some(value) => processValue(value)
    | None()      => handleMissing()
}

Calling .Value on an option[T] without first checking for None() throws an exception at runtime. The pattern match is both safer and more readable. The Nemerle compiler enforces exhaustiveness on matches over variant types: if a case is missing from a match on option[T], ncc emits a warning. In practice, retainer work often involves match expressions over richer variant types with more cases, and the exhaustiveness enforcement is the primary value of the variant system — adding a new case to a variant causes ncc to warn at every match that does not handle the new case, directing the developer to every site where the new case must be handled.

The interaction between option[T] and .NET interop is a recurring topic in retainer work. .NET methods that can return null are typically wrapped in Nemerle code that converts the result to option[T] at the boundary: def result = if (netMethod() != null) Some(netMethod()) else None(). Going in the other direction, .NET APIs that accept nullable parameters (declared as Nullable<T> in C# or as a reference type that can be null) require the Nemerle caller to unwrap the option[T] and convert to null when None(). This wrapping and unwrapping at interop boundaries is invisible in a work log that says “integrated with new API endpoint, 3h” and is the source of a non-trivial portion of type errors in Nemerle codebases that interface extensively with C# libraries.

Typical Nemerle retainer work and what it looks like in a work log

A Nemerle retainer typically covers three recurring categories. The first is macro system work: authoring new macros, extending existing macro templates, debugging type errors that originate in quasi-quotation templates, and auditing splice positions for missing type constraints. This work produces no visible runtime artifact — it shifts error detection from runtime to compile time, or from one compile-time error message to a more useful one. A work log entry that says “updated GenerateProperty macro to add typed splice, found 2 type errors at call sites, 6h” is auditable: the macro file changed, the typed splice is visible, the two call sites that produced errors are identifiable. Without that level of detail, the entry reads as six hours of work for a trivially small diff.

The second category is type inference diagnosis. Nemerle’s bidirectional type inference occasionally produces surprising results in large codebases where inference chains cross method and file boundaries. A binding that appears correctly typed in isolation may be inferred to a different type when a new method is added elsewhere that uses the binding in a different context. Diagnosing this requires understanding the inference chain: tracing which uses of the binding contributed to the conflicting inference, determining which use has the wrong type expectation, and deciding whether the fix is an explicit annotation on the binding, a typed splice in the macro template, or a type annotation on the method that created the wrong expectation. This diagnostic work is entirely invisible in a work log that says “resolved type error in inference chain, 4h.”

The third category is .NET interop maintenance. Nemerle codebases that reference C# libraries encounter all the standard .NET interop challenges: namespace conflicts, generic type parameter variance, interface implementation across assembly boundaries, and the null-versus-option[T] boundary. Retainer work in this category is driven by external changes — a new version of a referenced C# library changes method signatures, a .NET Framework upgrade changes how some types behave — rather than by bugs in Nemerle code. The work is investigative: determining exactly which assembly change broke the Nemerle code, finding the minimal fix, and verifying that no other Nemerle code depends on the old behavior. The work log entry reads “updated interop layer for library v2.3 upgrade, 5h” and the hours are justified only by documenting what changed in the library and what the Nemerle code had to change to match.

Track Nemerle developer retainer hours without the status emails

When a six-hour session traces a type error back to a missing typed splice in a macro quasi-quotation template and surfaces two additional hidden type mismatches in the process, the work log needs to say that — not just “fixed type error.” HourTab gives your Nemerle retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the macro, the splice position that was missing a type annotation, and the call sites that were caught. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks Nemerle developer retainer hours

Nemerle retainer work is particularly invisible because the primary output of a successful session is a stronger type constraint in a macro template, not a new feature or bug fix in the user-visible behavior. A client who sees “6h — updated macro template” cannot assess whether six hours was a reasonable cost for adding five characters to a quasi-quotation template. The work log needs to say: the macro was GenerateProperty; the splice was $(propValue) (untyped, inferred from call-site context); it was changed to $(propValue : string) (typed, enforced at every call site); ncc immediately caught two call sites where object was being passed where string was required; those two call sites were genuine bugs that would have produced type errors in downstream services; both were fixed. The six hours are justified by the diagnostic trail, not by the size of the diff.

HourTab gives Nemerle developers a public retainer-hours URL they send to clients — typically .NET platform teams using Nemerle’s macro system to build compile-time code generation for domain models, game studios maintaining Nemerle-based tooling pipelines, and research groups that adopted Nemerle for its metaprogramming expressiveness. For Nemerle retainers, each work log entry should name the mechanism at the level of the type analysis: which macro template was changed, which splice position received a type annotation, how many call sites were caught, and what the before and after type error count was. Comparative context for scope discussions: Nemerle retainer work has conceptual overlap with retainer work on other macro-capable .NET languages and on languages with first-class compile-time metaprogramming. Teams evaluating Nemerle alongside Rust’s procedural macros, F#’s type providers, or Template Haskell will find that Nemerle’s quasi-quotation system has a specific profile — tightly integrated with the .NET type system and Hindley-Milner inference — that requires expertise not transferable from those other systems. The retainer rate structure reflects this: senior Nemerle expertise commands $145 to $260 per hour because the combination of macro system depth, .NET type system mastery, and type inference diagnostic skill is rare.

FAQ: Nemerle developer retainers

What does a Nemerle developer on retainer typically do?

A Nemerle developer on monthly retainer covers Nemerle’s compile-time macro system (the macro keyword defines a compile-time transformation receiving the invocation site’s AST and returning a new AST node; macros are compiled separately and loaded by ncc during compilation; macros can call arbitrary Nemerle code at compile time and access the type environment); quasi-quotation and typed splices (<[ expr ]> is the quasi-quotation operator producing an AST node from a template; $(name) splices a computed sub-node; $(name : T) is a typed splice constraining the splice position’s type to T at expansion time — the critical tool for preventing wrong-type inference from call-site context); type inference and def / mutable bindings (def x = expr immutable type-inferred binding; mutable y = expr mutable binding; Nemerle’s bidirectional inference can fill in binding types from downstream uses, making macro-generated bindings sensitive to call-site context); pattern matching (match (val) { | Pattern => expr | _ => fallback }; exhaustiveness enforcement for variant types; quasi-quotation patterns <[ Pattern ]> for matching AST structures in macro bodies); and .NET interop (option[T] / list[T] / array[T]; using Namespace imports; all .NET Framework types accessible).

What Nemerle work is most commonly underlogged in a retainer?

Type mismatch diagnosis from macro expansion (tracing a call-site type error back to an untyped splice in a quasi-quotation template; adding the typed splice to enforce the correct type; 4 to 8 hrs per incident, invisible in a log that says “fixed type error”); macro quasi-quotation design (determining which splice positions need type annotations; designing templates to minimize unintended inference from context; handling optional macro arguments via AST pattern matching; 5 to 10 hrs invisible); match exhaustiveness analysis (adding missing cases to variant type matches; determining whether catch-all cases mask genuine unhandled patterns; 3 to 6 hrs invisible); .NET assembly integration (resolving namespace conflicts; handling Nemerle type aliases vs .NET types at interop boundaries; macro assembly loading order in multi-project builds; 3 to 6 hrs invisible); and option type boundary management (converting .NET null to option[T] at interop layers; unwrapping option[T] back to null for .NET API calls; 2 to 5 hrs invisible).

What are typical Nemerle developer retainer rates?

Entry-level Nemerle developers with 1 to 2 years of experience covering Nemerle syntax, basic .NET interop, def and mutable bindings, match expressions, and standard library use typically bill at $60 to $110 per hour. Mid-level Nemerle programmers with 2 to 4 years of experience covering compile-time macro authoring, quasi-quotation and typed splice design, option[T] propagation, match exhaustiveness, and .NET assembly integration typically bill at $95 to $175 per hour. Senior Nemerle language developers with 4 or more years of experience covering advanced macro system work including type-directed code generation, deep AST pattern matching, macro interaction with Nemerle’s type inference engine, and complex multi-level quasi-quotation template design typically bill at $145 to $260 per hour. Monthly retainer ranges: $1,800 to $3,500 per month for advisory engagements (roughly 15 to 25 hours per month); $4,000 to $11,000 per month for full engagement Nemerle development covering ongoing macro library authoring and .NET interop maintenance.

What should a Nemerle developer retainer agreement include?

A Nemerle developer retainer agreement should specify: macro system scope (which macros are in scope for authoring or debugging; whether the engagement includes typed splice audits of existing templates; whether the two-pass macro assembly build is in scope); quasi-quotation design scope (whether existing templates need typed splice additions; whether macro expansion testing via IL inspection is in scope); type inference scope (which bindings need explicit annotations; whether inference chain diagnosis is in scope; whether match exhaustiveness reviews are included); .NET platform scope (target .NET or Mono version; cross-assembly macro loading; Nullable<T> to option[T] interop); and hour logging format (macro: template file, splice position, typed constraint added, call sites caught; binding: inferred type before and after; match: variant type, cases added; interop: assembly, type mismatch resolved).

How should Nemerle developer retainer hours be logged?

Log each Nemerle retainer session with: macro type mismatch category (macro name; quasi-quotation template; splice position before: $(propValue); splice position after: $(propValue : string); type inferred from context before fix; call sites with wrong type: 2; call sites corrected: 2; downstream type errors before fix: how many; after fix: 0); binding inference category (binding name; def or mutable; type inferred before; type intended; how wrong type propagated to call sites; fix applied; before and after error count); match exhaustiveness category (variant type; cases present before; cases missing per ncc warning; cases added; catch-all cases evaluated; warnings before and after); .NET interop category (assembly; namespace; Nemerle alias vs .NET type mismatch; option[T] conversion applied; before and after error count); and for all categories, whether the work produced a visible artifact or an invisible type-safety improvement, since the latter is the defining characteristic of Nemerle macro retainer work.