Back to flin
flin

Regex Support and Rest Parameters

Built-in regex support and rest parameter syntax in FLIN.

Thales & Claude | March 25, 2026 10 min flin
flinregexrest-parameterssyntaxpattern-matching

Session 053 was one of those rare days where two significant features went from "not started" to "fully implemented" in under an hour. Rest parameters -- the ...args syntax that JavaScript developers use constantly -- and regex-powered validation methods -- the foundation of FLIN's built-in data validation -- were both completed, tested, and committed by the end of a forty-five minute session.

This speed was not accidental. It was the result of a compiler architecture designed for extensibility. Adding a new operator means adding a token to the lexer, a node to the AST, a case to the parser, an opcode to the bytecode, an emitter rule, a type checker rule, and a VM handler. When that pipeline is clean and well-tested, each new feature follows the same pattern. The difficulty is not in the implementation but in the design -- deciding what the feature should do and how it should interact with everything else.

Rest Parameters: The Design

JavaScript's rest parameter syntax is one of the language's best features. It allows a function to accept any number of arguments, collected into an array:

// JavaScript
function sum(...nums) {
    return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4, 5); // 15

FLIN adopted the same syntax with one addition: the rest parameter carries a type annotation indicating the element type of the collected list:

fn sum(...nums: [int]): int {
    total = 0
    for n in nums { total += n }
    return total
}
result = sum(1, 2, 3, 4, 5)  // 15

The type annotation [int] tells the compiler that nums will be a list of integers. This enables the type checker to verify that every argument passed to the rest position is an integer. In JavaScript, rest parameters are untyped by default and TypeScript's ...nums: number[] annotation is checked at compile time but not at runtime. FLIN checks both.

The Implementation Path

Adding rest parameters required changes to five compiler stages. Each change was small because the infrastructure was already in place.

Stage 1: AST

The Param struct needed a single boolean field:

pub struct Param {
    pub name: String,
    pub type_ann: Option<TypeAnnotation>,
    pub default: Option<Expr>,
    pub is_rest: bool,    // NEW
    pub span: Span,
}

A Param::rest() constructor was added for convenience. The boolean flag is all the AST needs to distinguish a rest parameter from a regular one.

Stage 2: Parser

The parser already recognized the ... token as DotDotDot for spread operations. For rest parameters, the parser needed to recognize ... before a parameter name in a function signature:

Three validation rules were enforced at parse time: 1. A rest parameter must be the last parameter in the function signature. 2. Only one rest parameter is allowed per function. 3. Rest parameters cannot have default values.

These rules are checked during parsing, not during type checking. If a developer writes fn foo(...a, ...b), they get an immediate parse error rather than a deferred type error. Early errors with clear messages are a consistent design principle in FLIN.

Stage 3: Type Checker

The type checker needed to understand that a function with a rest parameter accepts a variable number of arguments. The FlinType::Function variant was extended with two new fields:

Function {
    params: Vec<FlinType>,
    ret: Box<FlinType>,
    min_arity: usize,  // Required params before rest
    has_rest: bool,     // Last param is rest
}

At call sites, the arity check changes based on has_rest:

if *has_rest {
    // Only check minimum arity
    if arg_types.len() < *min_arity {
        return Err("Expected at least N arguments...");
    }
} else {
    // Exact arity match required
    if params.len() != arg_types.len() { ... }
}

Each extra argument beyond the minimum arity is unified with the list element type. If the rest parameter is ...nums: [int], then every extra argument must be unifiable with int. This catches type mismatches at compile time: sum(1, 2, "three") produces a type error.

The min_arity and has_rest fields were propagated to every constructor of FlinType::Function -- lambda expressions, all twelve built-in functions, all eighteen string methods, and the unification and substitution helpers. This was the most tedious part of the implementation, but it ensured that rest parameter semantics are consistent everywhere functions appear in the type system.

Stage 4: Bytecode and Emitter

A rest parameter function needs the runtime to collect "extra" arguments into a list. The emitter generates code that takes all arguments beyond the minimum arity and packages them into a list value before the function body executes.

Stage 5: VM

The VM's function call handler was updated to detect rest parameters and collect the excess arguments. When calling sum(1, 2, 3, 4, 5) on a function with min_arity = 0 and has_rest = true, the VM collects all five arguments into a list and binds it to the nums parameter.

Regex Validation Methods

The second feature of Session 053 was more impactful for end users: twelve string validation methods powered by the regex crate.

FLIN's philosophy is that common operations should be built in, not imported. Email validation, URL checking, phone number formatting -- these are operations that virtually every web application needs. In JavaScript, each requires a library (validator.js, is-email, etc.) or a hand-written regex that is inevitably wrong in edge cases.

FLIN provides them as methods on the text type:

email = "[email protected]"
if email.is_email() {
    log("Valid email")
}

phone = "+1-555-0123" if phone.is_phone() { log("Valid phone") }

url = "https://flin.sh" if url.is_url() { log("Valid URL") }

// Custom regex code = "ABC-123" if code.matches("[A-Z]{3}-[0-9]{3}") { log("Valid code format") } ```

The Twelve Methods

MethodDescriptionRegex Pattern
is_email()RFC-compliant email check^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
is_phone()International phone format^\+?[0-9\s\-().]{7,20}$
is_url()HTTP/HTTPS URL^https?://[^\s/$.?#].[^\s]*$
is_uuid()UUID v4 formatStandard UUID pattern
is_ipv4()IPv4 addressOctet-validated pattern
is_hex_color()Hex color (#FFF or #FFFFFF)`^#?([0-9a-fA-F]{3}\[0-9a-fA-F]{6})$`
is_credit_card()13-19 digit format^[0-9]{13,19}$
is_slug()URL slug format^[a-z0-9]+(?:-[a-z0-9]+)*$
matches(pattern)Custom regex matchUser-provided
replace_pattern(p, r)Regex replace allUser-provided
split_pattern(p)Split by regexUser-provided
find_all(p)Find all matchesUser-provided

Rust Implementation

The validation methods are implemented in a new module, src/vm/builtins/validation.rs. Each method compiles its regex pattern once using lazy_static and reuses it for every call:

lazy_static! {
    static ref EMAIL_RE: Regex = Regex::new(
        r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    ).unwrap();

static ref PHONE_RE: Regex = Regex::new( r"^\+?[0-9\s\-().]{7,20}$" ).unwrap();

static ref URL_RE: Regex = Regex::new( r"^https?://[^\s/$.?#].[^\s]*$" ).unwrap(); }

pub fn is_email(s: &str) -> bool { EMAIL_RE.is_match(s) }

pub fn is_phone(s: &str) -> bool { PHONE_RE.is_match(s) } ```

The lazy_static macro ensures that each regex is compiled exactly once, on first use. Subsequent calls are pure matching operations with no compilation overhead. This matters because validation methods are called frequently -- on every form submission, every API request, every save operation.

The matches, replace_pattern, split_pattern, and find_all methods accept user-provided regex patterns. These are compiled at call time and not cached, since the patterns are dynamic. For performance-critical applications, developers should prefer the built-in methods (which are cached) over custom patterns.

Twelve New Opcodes

Each validation method has its own opcode in the bytecode format:

  • 0x5B through 0x5F: IsEmail, IsPhone, IsUrl, IsUuid, IsIpv4
  • 0x69 through 0x6F: IsHexColor, IsCreditCard, IsSlug, MatchesPattern, ReplacePattern, SplitPattern, FindAllPattern

Dedicated opcodes mean that validation is a single instruction in the compiled bytecode. There is no function call overhead, no method dispatch, no dynamic lookup. The VM reads the opcode, pops the string from the stack, calls the validation function, and pushes the boolean result.

Twenty-Three Tests

The validation module shipped with twenty-three unit tests covering valid inputs, invalid inputs, and edge cases for each method:

#[test]
fn test_is_email_valid() {
    assert!(is_email("[email protected]"));
    assert!(is_email("[email protected]"));
}

#[test] fn test_is_email_invalid() { assert!(!is_email("not-an-email")); assert!(!is_email("@missing-local.com")); assert!(!is_email("missing-domain@")); } ```

The tests are intentionally comprehensive because validation correctness is critical. A false positive (accepting an invalid email) creates bad data. A false negative (rejecting a valid email) blocks legitimate users. Both are bugs that reach production quickly and are noticed by every user.

The Connection Between Rest Parameters and Validation

Rest parameters and validation methods seem unrelated, but they share a common purpose: making FLIN feel like a batteries-included language for web development.

Rest parameters enable utility functions that accept variable arguments:

fn log_all(...messages: [text]) {
    for msg in messages {
        log(msg)
    }
}

fn max(...nums: [int]): int { result = nums[0] for n in nums { if n > result { result = n } } return result } ```

Validation methods enable data quality checks without external dependencies:

fn validate_contact(email: text, phone: text, website: text) {
    errors = []
    if !email.is_email() { push(errors, "Invalid email") }
    if !phone.is_phone() { push(errors, "Invalid phone") }
    if !website.is_url() { push(errors, "Invalid website") }
    return errors
}

Together, they allow FLIN code to be both flexible (accepting any number of arguments) and strict (validating every piece of data). This combination is what web applications need: flexibility at the API boundary, strictness at the data layer.

From Analysis to Implementation

Session 053 began with an analysis phase. Before writing any code, we examined the codebase to understand what already existed and what was missing. The spread operator was already ninety percent complete -- the lexer, parser, AST, bytecode, emitter, type checker, and VM all handled spread in lists, maps, and function calls. Only rest parameters (the receiving side of spread) were missing.

For regex, nothing existed yet. The regex crate was not in Cargo.toml. No validation module existed. No opcodes were allocated.

The analysis took about ten minutes. It produced a clear list of what needed to be done, in what files, at what lines. The implementation that followed was mechanical -- filling in the gaps identified by the analysis.

This workflow -- analyze first, then implement -- is one of the patterns that emerged from building FLIN as a two-person team (one human, one AI). The analysis phase is where the AI contributes the most, scanning thousands of lines of code to find exactly where changes are needed. The implementation phase is collaborative, with design decisions made together and code generated to precise specifications.

By the end of Session 053, FLIN had 939 tests passing, 23 new validation unit tests, and two features that JavaScript developers would immediately recognize and appreciate. The rest parameter syntax is identical to JavaScript. The validation methods are cleaner than any JavaScript library. And both are built into the language, available without imports, without configuration, without npm install.

---

This is Part 194 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: - [193] The FLIN Showcase App - [194] Regex Support and Rest Parameters (you are here) - [195] Named Arguments and the Elvis Operator

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles