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); // 15FLIN 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) // 15The 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
| Method | Description | Regex 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 format | Standard UUID pattern | |
is_ipv4() | IPv4 address | Octet-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 match | User-provided | |
replace_pattern(p, r) | Regex replace all | User-provided | |
split_pattern(p) | Split by regex | User-provided | |
find_all(p) | Find all matches | User-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:
0x5Bthrough0x5F: IsEmail, IsPhone, IsUrl, IsUuid, IsIpv40x69through0x6F: 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