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
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:
| Construct | Expression Form | Notes | Statement Form | Notes |
|---|---|---|---|---|
| if | if (cond) expr else expr | Returns value; else required | if cond { ... } | Executes block; else optional |
| let | let x = val; expr | Binds x in following expr | let x = val | Binds x in current block of code |
| for | for (x in items) expr | Returns array of results | for x in items { ... } | Executes block for each |
| fn | fn name(x) => expr | Expr as function body | fn 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:
| Type | Description | Allowed In |
|---|---|---|
| Functional | Flow control or compute values (let, if, for, return) | Both fn and pn |
| Procedural | Carry 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 (
fooandFooare 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
- Lambda_Data.md — Literals, arrays, maps, elements
- Lambda_Type.md — Type system and type annotations
- Lambda_Expr_Stam.md — Expressions and statements
- Lambda_Func.md — Functions and closures