Lambda Script Syntax

March 20, 2026 · View on GitHub

This document covers the fundamental syntax elements of Lambda Script: comments, basic structure, identifiers, names, symbols, and namespaces.

Table of Contents

  1. Comments
  2. Basic Structure
  3. Whitespace and Line Breaks
  4. Identifiers
  5. Names and Symbols
  6. Namespaces

Comments

Lambda supports both single-line and multi-line comments:

// Single-line comment

/*
   Multi-line comment
   can span multiple lines
*/

Comments are ignored by the compiler and used for documentation purposes.


Basic Structure

Lambda Script files contain expressions and statements:

// Expressions (return values)
42
"hello world"
[1, 2, 3]

// Statements (declarations and control flow)
let x = 10;
pn main() {
	if (x > 5) {
	    print("x is greater than 5")
	}
}

Expressions vs Statements

  • Expressions evaluate to a value and can be used anywhere a value is expected
  • Statements compute values or perform actions
  • Most constructs in Lambda are expressions, enabling functional composition

Expression vs Statement Forms

Several key constructs have both expression and statement forms:

ConstructExpression FormNotesStatement FormNotes
ifif (cond) expr else exprReturns value; else requiredif cond { ... }Executes block; else optional
letlet x = val; exprBinds x in following exprlet x = valBinds x in current block of code
forfor (x in items) exprReturns array of resultsfor x in items { ... }Executes block for each
fnfn name(x) => exprExpr as function bodyfn name(x) { ... }Statements as function body
// Expression forms (return values)
let result = if (x > 0) "positive" else "negative"
let doubled = (for (n in nums) n * 2)
let add = fn(a, b) => a + b

// Statement forms (execute actions)
if x > 0 { print("positive") }
for n in nums { print(n) }
pn greet(name) { print("Hello, " ++ name) }

Statement Types

Lambda has two categories of statements:

TypeDescriptionAllowed In
FunctionalFlow control or compute values (let, if, for, return)Both fn and pn
ProceduralCarry out side-effect actions (var, assignment, while, I/O operations)Only pn

Functional statements are pure — they compute values without side effects:

// Allowed in both fn and pn
let x = compute(data)
if x > 0 { "positive" } else { "negative" }
for item in items { transform(item) }

Procedural statements perform actions that modify state or interact with the outside world:

// Only allowed in pn (procedural functions)
var counter = 0
counter = counter + 1
data |> "/tmp/output.json"
io.mkdir("./output")

This distinction enforces functional purity in fn functions while allowing controlled side effects in pn procedures.


Whitespace and Line Breaks

  • Whitespace is generally ignored except in strings
  • Line breaks can separate statements
  • Semicolons (;) terminate statements but are optional when followed by a line break
// These are equivalent:
let a = 1; let b = 2;

let a = 1
let b = 2

Identifiers

Identifiers are names used for variables, functions, types, and other declarations.

Rules

  • Must start with a letter (a-z, A-Z) or underscore (_)
  • Can contain letters, digits (0-9), and underscores
  • Are case-sensitive (foo and Foo are different)
  • Cannot be reserved keywords
// Valid identifiers
let name = "Alice"
let _private = 42
let camelCase = true
let snake_case = false
let PascalCase = "type"

// Invalid identifiers
// let 123abc = 1      // Cannot start with digit
// let my-var = 1      // Hyphens not allowed (use my_var)
// let let = 1         // Reserved keyword

Reserved Keywords

The following are reserved and cannot be used as identifiers:

let   pub   fn    pn    if    else   for   while
in    to    by    where order  group  limit offset
and   or    not   is    as    true   false null
type  import raise  var   break  continue  return

Reserved Type Names

Built-in type names are also reserved:

int    int64   float   decimal  bool   string  symbol  binary
array   map     element  range  path    type    any
null   error   datetime

Names and Symbols

Lambda distinguishes between strings, symbols, and names — each serving different purposes in the language.

Strings

Strings are UTF-8 text values enclosed in double quotes:

"hello world"
"line1\nline2"        // with escape sequences
"unicode: 你好"       // Unicode supported

Symbols

Symbols are interned identifiers enclosed in single quotes. They are used for:

  • Attribute keys in maps, objects and elements
  • Tag names in elements
  • Enumeration-like values
// Symbol literals
'hello'
'json'
'content-type'

// Common uses
let format = 'json'
let tag = 'div'
{name: "Alice", type: 'user'}

Key differences from strings:

  • Symbols are interned (only one copy exists in memory)
  • Faster equality comparison (pointer comparison)
  • Used for structural identifiers, not arbitrary text

Symbol Operations

// Equality
'hello' == 'hello'     // true (same symbol)
'hello' == 'world'     // false

// Type checking
type('hello')          // symbol
'hello' is symbol      // true
'hello' is string      // false

// Conversion
string('hello')        // "hello"
symbol("hello")        // 'hello'

// String operations work on symbols
len('hello')           // 5
'hello' ++ 'world'     // 'helloworld'
starts_with('hello', 'hel')  // true

Namespaces

Namespaces solve name collisions when mixing markup vocabularies (e.g., SVG inside HTML, MathML inside XHTML). Lambda uses . (dot) as the namespace separator, consistent with member access syntax.

Namespace Import

Namespaces are declared using the standard import statement with a bare URI (a symbol literal):

import svg: 'http://www.w3.org/2000/svg'
import xlink: 'http://www.w3.org/1999/xlink'

Syntax: import prefix : 'url'

Bare URI imports register a namespace prefix without loading any code. They use the same import alias: module syntax as module imports, but with a symbol literal (single-quoted URL) instead of a module path.

Namespace Prefixes as Reserved Names

Once declared, namespace prefixes are reserved — no variable, function, type, or field name may use the same name:

import svg: 'http://www.w3.org/2000/svg'

let svg = 123       // ERROR: 'svg' conflicts with namespace prefix
fn svg() => ...     // ERROR: 'svg' conflicts with namespace prefix

Namespaced Element Tags

Use ns.name form for namespaced element tags:

import svg: 'http://www.w3.org/2000/svg'

// Namespaced element tags
<svg.rect>
<svg.circle>
<svg.path>

// Unqualified tags work as before
<div>
<span>

Namespaced Attributes (Sub-Map Desugaring)

Attribute names can use ns.attr syntax. At compile time, dotted attribute keys are desugared into sub-maps — the namespace prefix becomes a map-valued attribute containing the local keys:

import svg: 'http://www.w3.org/2000/svg'
import xlink: 'http://www.w3.org/1999/xlink'

// Dotted syntax (what you write)
<svg.rect svg.width: 100, svg.height: 50>
<svg.a xlink.href: "https://example.com"; "Click">

// Desugared form (what actually gets stored)
// <svg.rect svg: {width: 100, height: 50}>
// <svg.a xlink: {href: "https://example.com"}; "Click">

// Mixed namespaced and regular attributes
<svg.rect id: "myRect", svg.width: 100>
// desugars to: <svg.rect id: "myRect", svg: {width: 100}>

Multiple attributes sharing the same namespace prefix are merged into a single sub-map.

Namespaced Member Access

Since namespaced attributes are stored as sub-maps, accessing them uses standard chained member access — e.ns returns the sub-map, .attr accesses the key within it:

import svg: 'http://www.w3.org/2000/svg'

let elem = <svg.rect svg.width: 100, svg.height: 50>

// Access namespaced attributes (chained through sub-map)
elem.svg           // {width: 100, height: 50}
elem.svg.width     // 100
elem.svg.height    // 50

Namespaced Symbols

The ns.name form in expression context creates a qualified symbol:

import svg: 'http://www.w3.org/2000/svg'

// Qualified symbols
svg.rect           // 'svg.rect' (with namespace target attached)
svg.circle         // 'svg.circle'

// Comparison
svg.rect == svg.rect     // true (same namespace and name)
svg.rect == svg.circle   // false (different name)

// String representation
string(svg.rect)         // "svg.rect"

Dynamic Namespaced Symbol Construction

For programmatic construction of namespaced symbols, use the two-argument symbol() function:

// Construct namespaced symbol dynamically
let s = symbol("href", 'http://www.w3.org/1999/xlink')

// Use for dynamic attribute access
let url = elem[s]

// The symbol carries namespace identity
type(s)            // symbol

Namespace Scope

Namespaces are file-local — they cannot be imported or exported. Each file declares its own namespace prefixes independently:

// file_a.ls
import svg: 'http://www.w3.org/2000/svg'
pub elem = <svg.rect svg.width: 100>

// file_b.ls
import a: .file_a
// 'svg' prefix is NOT available here
// but a.elem still has correct namespace data

import s: 'http://www.w3.org/2000/svg'  // can use different prefix
<s.circle>  // works fine

Comparison Semantics

Namespaced symbols are compared by both local name AND namespace URL (semantic comparison):

import svg: 'http://www.w3.org/2000/svg'
import s: 'http://www.w3.org/2000/svg'  // same URL, different prefix

svg.rect == s.rect    // true (same namespace URL)
svg.rect == 'svg.rect'  // false (one has namespace, one doesn't)

Example: SVG Document

import svg: 'http://www.w3.org/2000/svg'
import xlink: 'http://www.w3.org/1999/xlink'

let drawing = <svg.svg svg.width: 200, svg.height: 200;
    <svg.rect svg.x: 10, svg.y: 10, svg.width: 80, svg.height: 80, fill: "blue">
    <svg.circle svg.cx: 150, svg.cy: 50, svg.r: 40, fill: "red">
    <svg.a xlink.href: "https://example.com";
        <svg.text svg.x: 100, svg.y: 150; "Click me">
    >
>

See Also