Back to flin
flin

Named Arguments and the Elvis Operator

Named arguments for readable function calls and the Elvis operator for null coalescing.

Thales & Claude | March 25, 2026 11 min flin
flinnamed-argumentselvis-operatorsyntaxnull-coalescing

Two features define whether a language feels pleasant to write in: how easily you can express intent, and how gracefully you can handle absence. Named arguments address the first. The Elvis operator addresses the second. Neither is strictly necessary -- you can write any program without them. But their presence transforms code from something that works into something that communicates.

Sessions 097 and 099 added both features to FLIN. The Elvis operator took forty-five minutes. Named arguments took about an hour. Together, they represent less than two hours of compiler work, yet they affect how every FLIN program reads.

The Elvis Operator: Why ?: Matters

JavaScript has the nullish coalescing operator ??, which returns the right-hand side when the left-hand side is null or undefined. FLIN already supported ?? with identical semantics (except FLIN has no undefined -- only none).

But ?? only checks for nullity. Sometimes you want to check for "truthiness" -- whether a value is meaningful, not just present. An empty string is not null, but it is often not useful. Zero is not null, but it might indicate "no value was provided." The boolean false is not null, but it might mean "the user did not answer this question."

The Elvis operator ?: fills this gap. It returns the right-hand side when the left-hand side is falsy:

// Elvis operator returns first truthy value
name = user.name ?: "Anonymous"
displayName = firstName ?: lastName ?: "Guest"

// Compare with nullish coalescing value1 = 0 ?? 42 // 0 (0 is not none, so ?? keeps it) value2 = 0 ?: 42 // 42 (0 is falsy, so ?: replaces it) ```

The distinction matters in practice. Consider a form where a user can optionally enter their display name. If they leave it blank, you want a default. With ??, an empty string passes through because it is not none. With ?:, an empty string is replaced:

// User left the field blank: displayName is ""
greeting = displayName ?? "Friend"   // "" -- not what you want
greeting = displayName ?: "Friend"   // "Friend" -- correct

Falsy vs. Null

The difference between ?? and ?: is precisely defined:

Expression`??` Result`?:` Result
0 ?? 42 / 0 ?: 42042
false ?? true / false ?: truefalsetrue
"" ?? "default" / "" ?: "default""""default"
none ?? "default" / none ?: "default""default""default"

Falsy values in FLIN are: none, false, 0, and "". Everything else is truthy. This matches JavaScript's falsy set (minus undefined, NaN, and -0, which FLIN does not have).

Implementation

The Elvis operator was implemented across seven files in the compiler, each change minimal:

Lexer: A new QuestionColon token was added to src/lexer/token.rs. The scanner's ? handler was updated to check for a : lookahead -- if the character after ? is :, emit QuestionColon; if it is ., emit QuestionDot (optional chaining); otherwise emit Question (ternary).

Parser: A new BinaryOp::Elvis variant was added to the AST. The parser handles it at the same precedence level as || (logical or), which means Elvis participates correctly in operator precedence chains.

Codegen: The emitter generates the same bytecode as the Or operator -- short-circuit evaluation that returns the first truthy value. This is not a coincidence; the Elvis operator is semantically identical to logical OR in FLIN's truthiness model. The difference is syntactic: ?: reads as "use this default if the value is not useful" while || reads as "logical disjunction."

Type Checker: The type of an Elvis expression is the union of the types of its two operands. If the left side is text and the right side is text, the result is text. This mirrors the type behavior of ?? and ||.

Formatter: Pretty printing outputs the ?: operator with appropriate spacing.

Renderer: Six patterns in the template renderer were updated to evaluate Elvis expressions in template interpolation.

The entire implementation was forty-five minutes, including tests. The lexer test verifies that ?: produces the correct token. The parser test verifies that name ?: "default" produces the correct AST node. All existing tests continued to pass -- 1,017 out of 1,017.

Why a Separate Operator

We considered making || behave like Elvis (returning the first truthy operand rather than a boolean). This is how JavaScript's || works, and it is what enables patterns like name = user.name || "Anonymous" in JavaScript.

We rejected this because FLIN's || should be a logical operator that returns a boolean. Overloading it with value-selection semantics creates confusion about what type a || b returns. Is it bool? Is it the type of a? It depends on context, and context-dependent typing is a source of bugs.

The Elvis operator makes the intent explicit. || is for boolean logic. ?: is for default values. ?? is for null replacement. Three operators, three purposes, zero ambiguity.

Named Arguments: Self-Documenting Calls

Consider this function call:

draw(100, 200, true, false)

What do those arguments mean? Is 100 the x-coordinate or the width? Is true the fill flag or the visibility flag? Without reading the function signature, the call is opaque.

Named arguments solve this:

draw(100, 200, color: "red", filled: true)

Now the intent is clear. The first two arguments are positional (x and y coordinates, presumably), while color and filled are explicitly labeled. The code documents itself.

Syntax

FLIN's named argument syntax uses the name: value pattern, identical to how fields are declared in entity literals:

// Simple named arguments
greet(name: "Alice", greeting: "Hello")

// Mixed positional and named draw(100, 200, color: "red", filled: true)

// Named arguments with expressions calculate(x: a + 1, y: b * 2) ```

Positional arguments must come before named arguments. This is the same rule that Python, Kotlin, and Swift enforce. It prevents ambiguity about which parameter each argument maps to.

Parser Implementation

The parser needed to distinguish between a named argument name: value and a map literal entry "key": value or a ternary expression condition ? a : b. The key insight is the lookahead pattern: if the parser sees an identifier followed immediately by a colon (with no question mark before it), and the context is a function call argument list, it is a named argument.

The is_named_argument() function performs this lookahead:

fn is_named_argument(&self) -> bool {
    // Check: identifier followed by ':'
    // Must be in a call argument context
    // Must not be a ternary expression
}

Once detected, parse_named_argument() consumes the name, the colon, and the value expression, producing a CallArg::Named AST node:

pub enum CallArg {
    Positional(Expr),
    Spread(Expr),
    Named {
        name: String,
        value: Expr,
        span: Span,
    },
}

Type Checker

The type checker handles named arguments by matching them against the function's parameter names. Each named argument's value is type-checked against the corresponding parameter's type. If a named argument references a parameter that does not exist, the type checker reports an error.

Codegen

The emitter generates code that places named arguments in the correct parameter positions. The function emit_call_with_named() emits positional arguments first, then resolves named arguments to their parameter positions and emits them in order.

Test Coverage

Ten new tests were added for named arguments:

Six parser tests verify the parsing of simple named arguments, multiple named arguments, mixed positional and named arguments, named arguments with expressions, string values, and method call contexts.

Four end-to-end tests verify that named arguments compile correctly, that mixed arguments compile, that named arguments execute with correct results at runtime, and that string parameters work.

After implementation, the test suite went from 1,026 library tests and 84 end-to-end tests to 1,032 library tests and 88 end-to-end tests. All passing with zero warnings.

Example File

An example file examples/named_arguments.flin was created to demonstrate the feature:

fn greet(name: text, greeting: text) {
    log(greeting + ", " + name + "!")
}

// Positional -- order matters greet("Alice", "Hello")

// Named -- order does not matter greet(greeting: "Bonjour", name: "Alice")

// Mixed greet("Alice", greeting: "Good morning") ```

The third call is the most interesting: the first argument is positional (mapped to name), and the second is named (explicitly mapped to greeting). The developer can use positional arguments for the parameters they remember and named arguments for the ones they do not.

The Destructuring Foundation

Session 097 also laid the groundwork for destructuring, though this feature was not completed until later sessions. The foundation included a Pattern enum in the AST supporting five pattern types:

pub enum Pattern {
    Identifier { name: String, span: Span },
    List { patterns: Vec<Pattern>, span: Span },
    Rest { name: String, span: Span },
    Map { entries: Vec<(String, Pattern)>, span: Span },
    WithDefault { pattern: Box<Pattern>, default: Expr, span: Span },
}

This recursive structure supports nested destructuring patterns like [a, [b, c]] and default values like [x = 10, y = 20]. The decision to create a separate Stmt::DestructuringDecl rather than modifying the existing Stmt::VarDecl was deliberate -- VarDecl was used in approximately 190 places across the codebase, and modifying it would have required changes to every one of those sites. A separate statement type kept the change isolated.

Stub implementations were added to the type checker, emitter, and formatter, ensuring that all code compiled and all tests passed even before the parser could produce destructuring statements. This stub-first approach is a pattern we use throughout FLIN's development: lay the foundation in all compiler stages, verify that nothing breaks, then connect the pieces.

Syntax Sugar and Language Identity

The Elvis operator and named arguments are, in compiler parlance, "syntax sugar." They do not enable programs that were previously impossible. The Elvis operator is equivalent to an if-then-else expression. Named arguments are equivalent to positional arguments in the correct order.

But syntax sugar matters enormously for language adoption and code quality. The difference between:

name = if user.name != none && user.name != "" then user.name else "Anonymous"

and:

name = user.name ?: "Anonymous"

is not just brevity. It is clarity. The second version communicates intent -- "use this name, or fall back to Anonymous" -- without requiring the reader to parse a conditional expression and reason about edge cases.

Similarly, the difference between:

create_user("Alice", "[email protected]", true, "admin", "en")

and:

create_user("Alice", email: "[email protected]", active: true, role: "admin", lang: "en")

is the difference between code that compiles and code that communicates.

FLIN's syntax sugar is chosen carefully. Each addition must pass three tests: Does it make common patterns shorter? Does it make code more readable? Does it align with what JavaScript and TypeScript developers already know? The Elvis operator passes all three. Named arguments pass all three. Destructuring, when completed, would pass all three.

These are not flashy features. They do not appear in marketing slides or conference keynotes. But they are the features that make developers choose one language over another for their next project -- the features that turn a first impression into a daily habit.

The Phase 1 Completion Arc

With the Elvis operator completed in Session 097, FLIN's "Phase 1" feature set -- the core operators and expressions that make a language feel complete -- reached 85 percent. The checklist read:

  • Arrow functions: complete
  • Nullish coalescing ??: complete
  • Optional chaining ?.: complete
  • Bitwise operators: complete
  • Exponentiation **: complete
  • Spread/rest ...: complete
  • Ranges .. and ..=: complete
  • Ternary if-then-else: complete
  • Compound assignment: complete
  • Increment ++ / decrement --: complete
  • Elvis operator ?:: complete (Session 097)
  • Destructuring: 70 percent (Session 097, completed later)

Named arguments, added in Session 099, were categorized as a Phase 2 feature -- not strictly necessary for a minimal viable language, but essential for developer experience.

Each of these features was implemented in a session lasting between forty-five minutes and three hours. The cumulative effect is a language that a JavaScript developer can sit down and write without consulting documentation for basic operations. The syntax is familiar. The operators work as expected. The defaults are sensible. And where FLIN departs from JavaScript -- maps with square brackets, text instead of string, none instead of null -- the differences are small enough to learn in a day and beneficial enough to appreciate within a week.

---

This is Part 195 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.

Series Navigation: - [194] Regex Support and Rest Parameters - [195] Named Arguments and the Elvis Operator (you are here) - Next arc: Arc 19

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles